Проблема с алгоритмом лабиринта

У меня проблема с алгоритмом, предназначенным для решения лабиринтов.

Я использовал алгоритм отсюда. http://www.cs.bu.edu/teaching/alg/maze/

НАЙТИ-ПУТЬ (x, y)

  1. if (x, y вне лабиринта) вернуть false
  2. если (x, y - цель) вернуть true
  3. если (x, y не открыт) вернуть false
  4. отметьте x, y как часть пути решения
  5. если (НАЙТИ-ПУТЬ (к северу от x, y) == true) вернуть true
  6. если (НАЙТИ-ПУТЬ (к востоку от x, y) == true) вернуть true
  7. если (НАЙТИ-ПУТЬ (к югу от x, y) == true) вернуть true
  8. если (НАЙТИ-ПУТЬ (к западу от x, y) == true) вернуть true
  9. unmark x,y as part of solution path
    1. return false

Это рекурсивное решение, я модифицировал его таким образом, чтобы он продолжал работать даже после нахождения выхода, чтобы он мог найти и другие решения. Кажется, это работает, просто он, кажется, находит половину от общего числа возможных решений, которые я знаю.

  1. if (x, y is target) return true заменяется на return false.

Кто-нибудь знает, в чем может быть проблема с таким алгоритмом, который дает половину общего числа возможных решений? У меня также есть проблема с определением общего количества тупиковых путей, есть предложения по этому поводу?


person Community    schedule 10.08.2009    source источник
comment
Можете ли вы создать небольшой лабиринт, в котором есть несколько таких решений, и разместить его здесь в виде диаграммы ASCII, чтобы мы могли попытаться понять, почему он не может найти их все?   -  person Lasse V. Karlsen    schedule 10.08.2009
comment
Или, кроме этого, не могли бы вы опубликовать свой реальный код? Алгоритм выглядит добротным, может, глючит его реализация?   -  person Lasse V. Karlsen    schedule 10.08.2009
comment
Обычно для решения лабиринтов вам нужно найти кратчайший путь, а не все пути. Это можно сделать, посчитав количество рекурсий и вернув это значение, а не «истина / ложь». Для чего это (если не домашнее задание?)   -  person Adriaan    schedule 11.08.2009


Ответы (5)


кажется, что отсутствует проверка, отмечена ли уже X&Y как часть решения, и если да, мы прерываем выполнение. (это должно быть где-то в пункте 3.5)

Если бы не лабиринт с возможным циклом где-нибудь, работал бы бесконечно и взорвал стек

кстати, из того, что я читал, алгоритм основан на лабиринте с одним решением

R

person Toad    schedule 10.08.2009
comment
Думаю, 3. тоже проверяет. - person Erich Kitzmueller; 10.08.2009

Вместо того, чтобы пытаться найти один путь через лабиринт, вам нужно найти (и, следовательно, нанести на карту) несколько путей через лабиринт.

Для этого вам нужно отметить, где вы были (на определенном пути). Если вы достигли точки, в которой уже были, вам нужно пометить этот маршрут как тупик.

Рекурсивная функция по-прежнему подходит, но убедитесь, что вы передаете структуру (placesIhaveBeen) через рекурсивную функцию.

Рекурсивный цикл должен прерваться, когда вы дойдете до точки, где все N, S, E, W заблокированы. (Вы бывали там раньше, вы не можете идти в этом направлении, это за пределами лабиринта) Рекурсивный цикл также должен прерваться, когда вы достигнете своей цели.

Если вы добились своей цели - увеличьте глобальную переменную на единицу. Если деваться некуда - увеличивайте свои тупики на единицу.

Я не могу написать для этого pcode (это займет слишком много времени), но я считаю, что секрет кроется в функции, которая возвращает истину, если N, S, E и W все заблокированы.

Дополнение к моему первоначальному ответу

Что касается того, почему я отношусь к участкам, в которых побывал, как к "заблокированным", и почему я отношусь к ним как к тупикам ....

      ########
      #      #  
      # #### #
####### #  # #
        #  # #
####### #  # #
      # #### #
      #      #  
      ########

Я бы классифицировал вышеупомянутую часть лабиринта как тупик, и я не понимаю, как я могу идентифицировать его как таковой, не рассматривая места, в которых я был, как заблокированные области.

Я понимаю, что это приведет к тому, что следующее также покажет тупики, но я не вижу способа обойти это.

      #######
      #     #  
      # ### #
####### #G# #
        # # #
####### #   #
      # ### #
      #     #  
      #######
person seanyboy    schedule 10.08.2009
comment
если n, s, e и w заблокированы, это НЕ тупик. Поскольку вы только что пришли с какого-то направления, это по определению не заблокировано. Если вы сейчас скажете: да, но я имел в виду, заблокировано ИЛИ уже было, то это также неверно, поскольку вы можете натолкнуться (через цикл) на себя. И тогда это НЕ тупик. - person Toad; 10.08.2009
comment
Рейнир - вы правы, но для упрощения кода я сделал предположение, что если вы уже были в точке на карте, то эта точка заблокирована. - person seanyboy; 10.08.2009
comment
комментировать ваши новые издания. Может быть, вы могли бы классифицировать их как 1 тупик при возврате. Итак, в тот момент, когда вы совершаете новый поворот, который не допускает никаких новых выборов, а только один принудительный маршрут (без цели). Вы можете классифицировать это как тупик - person Toad; 10.08.2009
comment
Я думаю, что причина 50% решений действительно в том, как передается «placeihave been» и как это проверяется. Потому что это не очевидно из псевдокода. - person Adriaan; 11.08.2009

По количеству тупиков нужно что-то вроде этого:

3.5 если (к северу от x, y не открыт) и (к югу от x, y не открыт) и (к западу от x, y не открыт) и (к востоку от x, y не открыт) deadends ++

person Erich Kitzmueller    schedule 10.08.2009
comment
тот же комментарий, что и у seanyboy: если n, s, e и w заблокированы, это НЕ тупик. Поскольку вы только что пришли с какого-то направления, это по определению не заблокировано. Если вы сейчас скажете: да, но я имел в виду, заблокировано ИЛИ уже было, то это также неверно, поскольку вы можете натолкнуться (через цикл) на себя. И тогда это НЕ тупик - person Toad; 10.08.2009

Это образец лабиринта

####################
#S #  #       #    #
#  # ##  ##  ### ###
#     #   #   #    #
## #  #   #     ## #
#     ### #####    #
# #   #   #  #   ###
# ### ### ## # #####
#  #      #       E#
####################
person Community    schedule 10.08.2009
comment
Я получаю 62720 возможных решений этого лабиринта, но кратчайший путь составляет 49 шагов, исключая s и e. У кого-нибудь есть что-нибудь получше? - person ; 10.08.2009

Я попробовал сделать простую реализацию алгоритма на java. Я пришел к выводу, что описанный вами алгоритм работает даже для поиска нескольких путей. Или, возможно, вам удалось придумать более умный тестовый пример, чем я. (Пожалуйста, опубликуйте свой лабиринт, чтобы я мог опробовать на нем свой алгоритм)

Моя реализация тупикового счетчика, вероятно, не самая эффективная, но она выполняет свою работу. Для каждого посещаемого текущего ОТКРЫТОГО узла он проверяет 4 окружающих узла:

  • Если хотя бы один сосед ОТКРЫТ, текущий узел не тупик
  • Если более одного соседа посещено, текущий узел не является тупиком
  • Если только один узел посещен (тот, из которого мы пришли на предыдущем шаге) и ни один другой сосед не открыт, текущий узел является тупиком.

Это код Java, который я написал (осторожно! Довольно долго). Альтернативой может быть, если вы хотите, сохранить путь в стеке, нажимая узел каждый раз, когда он установлен в состояние VISITED, и выталкивая узел каждый раз, когда он возвращается в состояние OPEN. Каждый раз, когда достигается ЦЕЛЬ, стек, содержащий текущий путь, должен быть скопирован и сохранен.

Если блок if, помеченный комментарием как "dead-end-research-step", удален, эта реализация почти полностью совпадает с той, которая описана в вопросе.

package test;

import java.util.HashSet;
import java.util.Set;

public class MazeSolver {

final static int OPEN = 0;
final static int WALL = 1;
final static int GOAL = 2;
final static int VISITED = 3;

static int[][] field = { { 0, 0, 0, 0, 0, 1 }, { 1, 0, 1, 1, 0, 1 },
        { 1, 0, 1, 0, 0, 0 }, { 0, 0, 0, 0, 1, 2 }, { 1, 0, 1, 0, 0, 0 } };

// This is what the field looks like:
//  
// 0 1 1 0 1
// 0 0 0 0 0
// 0 1 1 0 1
// 0 1 0 0 0
// 0 0 0 1 0
// 1 1 0 2 0

static int width = field.length;
static int height = field[0].length;
static int xStart = 0;
static int yStart = 0; // Initiated to start position: (x = 0, y = 0)
static int nrSolutions = 0; // Records number of solutions

// Used for storing id:s of dead end nodes.
// The integer id is (x + y * width)
static Set<Integer> deadEnds = new HashSet<Integer>();

public static void main(String[] arg) {
    System.out.println("Initial maze:");
    printField();

    findPath(xStart, yStart);

    System.out.println("Number of solutions: " + nrSolutions);
    System.out.println("Number of dead ends: " + deadEnds.size());
}

private static void findPath(int x, int y) {

    if (x < 0 || y < 0 || x >= width || y >= height) { // Step 1
        return;
    } else if (field[x][y] == GOAL) { // Step 2
        nrSolutions++;
        System.out.println("Solution nr " + nrSolutions + ":");
        printField();
        return;
    } else if (field[x][y] != OPEN) { // Step 3
        return;
    } else if (isDeadEnd(x, y)) { // Extra dead-end-investigation-step
        int uniqueNodeId = x + y * width;
        deadEnds.add(uniqueNodeId); // Report as dead end
        return;
    }

    field[x][y] = VISITED; // Step 4

    findPath(x, y - 1); // Step 5, go north
    findPath(x + 1, y); // Step 6, go east
    findPath(x, y + 1); // Step 7, go south
    findPath(x - 1, y); // Step 8, go west

    field[x][y] = OPEN; // Step 9

    // Step 10 is not really needed, since the boolean is intended to
    // display only whether or not a solution was found. This implementation
    // uses an int to record the number of solutions instead.
    // The boolean return value would be (nrSolutions != 0)
}

private static boolean isDeadEnd(int x, int y) {
    int nrVisitedNeighbours = 0;

    if (y > 0) { // If northern neighbour exists
        if (field[x][y - 1] == VISITED) {
            nrVisitedNeighbours++;
        } else if (field[x][y - 1] != WALL) {
            return false;
        }
    }
    if (x < width - 1) { // If eastern neighbour exists
        if (field[x + 1][y] == VISITED) {
            nrVisitedNeighbours++;
        } else if (field[x + 1][y] != WALL) {
            return false;
        }
    }
    if (y < height - 1) { // If southern neighbour exists
        if (field[x][y + 1] == VISITED) {
            nrVisitedNeighbours++;
        } else if (field[x][y + 1] != WALL) {
            return false;
        }
    }
    if (x > 0) { // If western neighbour exists
        if (field[x - 1][y] == VISITED) {
            nrVisitedNeighbours++;
        } else if (field[x - 1][y] != WALL) {
            return false;
        }
    }

    if (nrVisitedNeighbours > 1) { // Circular path scenario
        return false;
    }

    return true; // The exhaustive result of this check: this is a dead
    // end
}

private static void printField() {
    for (int yy = 0; yy < field[0].length; yy++) {
        for (int xx = 0; xx < field.length; xx++) {

            System.out.print(field[xx][yy] + " ");
        }
        System.out.println();
    }
    System.out.println();
}
}

Вышеупомянутый алгоритм сообщает о четырех различных путях решения и двух тупиках к примеру лабиринта, который жестко запрограммирован в коде.


‹EDIT› Может быть причиной того, что вы получаете слишком мало путей решения, является неправильное толкование того, что такое путь решения? Например, рассмотрим этот лабиринт:

######
##   #
## # #
#S   #
##E###
######

В этом лабиринте есть только один допустимый путь решения. Это связано с тем, что вам разрешено посещать каждый узел только один раз, поэтому обход кругового пути не является допустимым путем решения, поскольку это приведет к посещению узла к востоку от S и к северу от E дважды. Это определение пути решения подразумевается используемым алгоритмом.

Если бы кто-то разрешил посещать один и тот же узел несколько раз, было бы бесконечно много решений, так как вы могли бы пройти по кругу 1, 2, 3 ... до бесконечного количества раз. ‹/EDIT›

‹EDIT2›

Точно так, как вы говорите, я увеличиваю pathLength каждый раз, когда устанавливаю узел в состояние VISITED, и уменьшаю длину пути каждый раз, когда устанавливаю узел VISITED обратно в положение OPEN.

Чтобы записать длину кратчайшего пути, у меня также есть значение shorttestPath int, которое я инициирую в Integer.MAX_VALUE. Затем каждый раз, когда я достигаю цели, я делаю следующее:

if(pathLength < shortestPath){
    shortestPath = pathLength;
}

Что касается тупиков ... Я попытался пересчитать их вручную и подумал, что 9 мне кажется правильным. Вот лабиринт, опубликованный Сарин, но с тупиками, отмеченными ( рука) с D:

####################
#S # D#      D#D  D#
#  # ##  ##  ### ###
#     #   #   #    #
## #  #   #     ## #
#     ### #####    #
# #   #D  #D #   ###
# ### ### ## # #####
# D#D     #D      E#
####################

Вы можете найти еще что-нибудь? Или я неправильно понял, что вы имели в виду под тупиком? Я думал, что тупик означает: узел, к которому можно подойти только с одного направления.

Пример 1:

######
## ###
## ### 
## ### 
#S  E#
######

В приведенном выше лабиринте один тупик.

Пример 2:

######
##  ##
##  ## 
##  ## 
#S  E#
######

Вышеупомянутый лабиринт не имеет тупиков. Даже если вы находитесь в одном из наиболее удаленных к северу доступных узлов, по-прежнему есть два соседних квадрата, не являющихся стенами.

У вас есть другое определение тупика? ‹/EDIT2›

person Alderath    schedule 10.08.2009
comment
Используя эту реализацию в образце лабиринта, опубликованном Сарином (с добавлением записи кратчайшего пути), я получаю: Количество решений: 62720 Количество тупиков: 9 Кратчайший путь: 41 (исключая S и E) Кратчайший путь с 41 шагом: довольно легко найти вручную, без алгоритма, а это точно 41. - person Alderath; 11.08.2009
comment
Мне удалось получить результат прямо сейчас по вашему алгоритму, проблема была в моей реализации. Но можете ли вы объяснить, как считать шаги? Я изначально делаю шаги ++, когда отмечаю путь, и шаги - когда возвращаюсь назад. Но это явно не очень хорошо работает. Однако тупиковая цифра 9 не кажется правильной! - person ; 11.08.2009
comment
Я ответил на ваш комментарий, отредактировав свой пост, где можно было улучшить форматирование. Проверьте раздел с пометкой EDIT2. - person Alderath; 13.08.2009
comment
На самом деле я имел в виду количество возможных тупиковых путей, я получаю цифру примерно в 500 тысяч возможных тупиковых путей. Не уверен, что это правда. Мне удалось решить проблему с шагами. - person ; 13.08.2009
comment
У вас номер 4973375? (т.е. почти 5000к, а не 500к). Если это так, вы на самом деле не подсчитывали количество тупиков, вы считали каждый раз, когда вы врезались в стену (или посещенный узел). - person Alderath; 13.08.2009