Рекурсия лабиринта решает ошибку StackOverflow

Я пытаюсь решить лабиринт, используя рекурсию. Он объявлен Cell [][] maze.

public class Cell {
    private Wall left;
    private Wall right;
    private Wall up;
    private Wall down;
    private boolean end;

    // Setters and getters not shown
}

Если для какой-либо стороны ячейки нет Wall, то она имеет значение null, иначе она относится к объекту Wall. Wall ссылки непротиворечивы: обе ячейки, примыкающие к одной стене, ссылаются на нее с соответствующими полями. Если стены нет, то в обеих соседних ячейках есть соответствующие записи null. Вот поиск:

public boolean solveMaze(Cell[][] maze, int i, int j) {

    if (maze[i][j].isEnd()){
        System.out.println(maze[i][j].toString());
        return true;
    }
    if (maze[i][j].getDown() == null) {
        return solveMaze(maze, i, j + 1); 
    }
    if (maze[i][j].getUp() == null) {
        return solveMaze(maze, i, j - 1) ;
    }
    if (maze[i][j].getLeft() == null) {
        return solveMaze(maze, i - 1, j);
    }
    if (maze[i][j].getRight() == null) {
        return solveMaze(maze, i + 1, j) ;  
    }
    return false;
}

Я получаю сообщение об ошибке Stack Overflow. Что не так с моим условием остановки рекурсии?

Обновление:

С вашей высоко оцененной помощью я решил эту проблему: это правильное решение, которое работает безупречно:

public boolean solveMaze(Cell[][] maze, int i, int j){

    if (maze[i][j].isEnd()){
        System.out.println("Maze Exit :["+i+","+j+"]" );
        return true;
    }

    if (maze[i][j].isVisited()){
        return false;
    }

    maze[i][j].setVisited(true);

    if ((maze[i][j].getButtom() == null) ){
        if (solveMaze(maze,i,j+1)==true)
            return true;
    }

    if ((maze[i][j].getUp() == null) ){
    if ( solveMaze(maze,i,j-1) ==true )
        return true;
    }

    if ((maze[i][j].getLeft() == null)){
        if (solveMaze(maze,i-1,j))
            return true;
    }

    if ((maze[i][j].getRight() == null)){
        if (solveMaze(maze,i+1,j)) 
            return true;
    }

    maze[i][j].setVisited(false);
    return false;
} 

может быть, это будет полезно для любого тела в будущем.


person danny.lesnik    schedule 13.07.2012    source источник
comment
Должен ли getButton() быть getBottom()?   -  person DGH    schedule 14.07.2012
comment
@millimoose, видимо, это вопрос для интервью. Могут ли те, кто ответит на него, получить предложение о работе?   -  person MadProgrammer    schedule 14.07.2012
comment
@millimoose, я не понимаю, почему ты называешь это домашним заданием, я дал свое решение, которое не работает, мне нужна помощь. Извините, но ваш комментарий не по адресу!!!   -  person danny.lesnik    schedule 14.07.2012
comment
@user992484 user992484, это не вопрос для интервью, я отметил его как вопрос для интервью, потому что такие вопросы часто задают на собеседованиях. Во всяком случае, я удалил этот тег.   -  person danny.lesnik    schedule 14.07.2012
comment
;) не пробс. Хороший вопрос, но жду ответа   -  person MadProgrammer    schedule 14.07.2012
comment
@ danny.lesnik Я не говорил, что это один, я спросил, так ли это, потому что он выглядел как один. Я вполне готов поверить вам на слово. (Что бы это ни стоило для кого-либо.)   -  person millimoose    schedule 14.07.2012


Ответы (5)


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

Это можно сделать либо с помощью логического флага visited в каждой ячейке, изначально установленного в значение false, а затем установленного в значение true для каждого квадрата, который вы ищете, либо вы можете поддерживать отдельные Set из (i,j) пар, которые были найдены, которые изначально пусты.

NB: Ваше использование i и j нетрадиционно. Если бы кто-то другой написал код чтения лабиринта с обычным использованием, это могло бы вызвать проблему. В математике i обычно используется для номера строки и j для столбца. С этим соглашением ваши тесты стены не согласуются с вашими приращениями и уменьшениями. Отсутствие нижней стенки потребует, например, увеличения i.

person Gene    schedule 13.07.2012
comment
и что мне делать, если я попал в тупик и мне нужно вернуться в предыдущую ячейку, а она уже помечена как посещенная? - person danny.lesnik; 14.07.2012
comment
Привет @danny.lesnik. Этот алгоритм всегда найдет путь, если он существует. Он может не найти кратчайшего пути. На самом деле это вряд ли сделать. Это называется поиском в глубину. - person Gene; 14.07.2012

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

Если у вас есть некоторые «знания», эвристика, о том, как искать в лабиринте, вы также можете взглянуть на Поиск A-Star


В вашем случае BFS может сделать следующее: (Кстати, будьте любезны и используйте соответствующие конструкторы, геттеры и сеттеры)

public class Cell {
    public int x;
    public int y;
    public Cell parent;

    @Override
    public boolean equals(Object obj) {
        // TODO Override equals so it only incudes x and y coorinates, and not parent
        return true;
    }

    @Override
    public int hashCode() {
        // TODO Override hash code as well
        return 0;
    }
}

public Cell seachFor(Cell start, Cell finish) {
    Queue<Cell> open = new LinkedList<>();
    Set<Cell> closed = new HashSet<>();

    open.add(start);

    while (!open.isEmpty()) {
        Cell current = open.poll();
        if (current.equals(finish)) {
            return current;
        }

        closed.add(current);

        for (Cell neighbour : getNeighbours(current)) {
            if (!closed.contains(neighbour)) {
                open.add(neighbour);
            }
        }
    }

    return null;

}

private List<Cell> getNeighbours(Cell current) {
    /* TODO Set the neighbour's "parent"
     * Return valid adjacent neighbour cells
     */
    return null;
}

public Deque<Cell> pathfinder(Cell start) {
    Deque<Cell> path = new ArrayDeque<>();
    path.push(start);

    Cell current = start;

    while (current.parent != null) {
        current = current.parent;
        path.push(current);
    }

    return path;
}

public static void main(String[] args) {
    Cell start = maze.getStart();
    Cell finish = maze.getFinish();
    Deque<Cell> path = pathFinder(searchFor(start, finish))
    while (!path.isEmpty()) {
        Cell current = path.pop();
        maze.moveTo(current);
    }
}

Обратите внимание, что это фиктивный код, и вам нужно уточнить его, прежде чем он заработает.

person ioreskovic    schedule 13.07.2012
comment
Спасибо, я знаю этот алгоритм, но я хотел бы решить его с помощью рекурсии. - person danny.lesnik; 14.07.2012
comment
@ danny.lesnik A * или BFS, безусловно, можно закодировать рекурсивно, но BFS на графиках легче сделать правильно итеративно. Вы также можете попробовать сначала в глубину с итеративным углублением, которое, я думаю, на самом деле предпочтительнее реализовать рекурсивно, никогда не будет рекурсивно бесконечно и найдет оптимальное решение. - person millimoose; 14.07.2012
comment
Кроме того, если лабиринт находится на евклидовой плоскости и вы знаете, где находится целевая ячейка, у вас всегда есть эвристика для A*. (Должно работать манхэттенское расстояние или декартово расстояние.) - person millimoose; 14.07.2012

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

person DGH    schedule 13.07.2012

Сначала спускайтесь вниз, пока не дойдете до стены. Потом один вверх - потом снова вниз. И один вверх, и один вниз - пока java не обнаружит, что это довольно глупо, и не остановится с StackOverFlowError;)

person Andreas Dolk    schedule 13.07.2012

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

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

лабиринт Cell[][] maze

  public class Cell{
    boolean visited = false;
    Wall left;
    Wall right;
    Wall up;
    Wall bottom;
    boolean end;

    //Settters and Getters
    }

устанавливайте для этой посещаемой переменной значение true каждый раз, когда вы ее нажимаете, а затем изменяйте свой код на

public boolean solveMaze(Cell[][] maze, int i, int j){
        if (maze[i][j].isEnd()){
            System.out.println(maze[i][j].toString());
            return true;
        }
        if (maze[i][j].getButton() == null)&&(maze[i][j].visited==false)){
            return solveMaze(maze,i,j+1); 
        }

        if (maze[i][j].getUp() == null){
        return solveMaze(maze,i,j-1) ;
        }
        // ect 

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

person Frank Visaggio    schedule 13.07.2012
comment
и что мне делать, если я попал в тупик и мне нужно вернуться в предыдущую ячейку, а она уже помечена как посещенная? Нужно ли мне в этом случае помечать предыдущую ячейку как непосещенную? - person danny.lesnik; 14.07.2012
comment
Я хочу сказать нет, я верю, что в каждой ячейке вы видите, куда можно пойти, и рекурсивно проверяете все направления. Так что тот, кто это сделает, тот и закончит. Итак, вы начинаете с одной точки и идете в каждом направлении, чтобы найти выход. Если это игра, в которой вам разрешено проверять только один путь за раз, скажем, для ACSL, тогда да, вам придется пометить их как не посещенные или иметь своего рода систему тральщика, в которой вы указываете, сколько неисследованных путей находится в каждом узле и возвращаетесь. к одному с некоторыми неизведанными путями. - person Frank Visaggio; 17.07.2012