Что я должен сделать, чтобы улучшить реализацию DFS Java, чтобы решить проблему?

Я пытаюсь решить проблему под названием «Стены и ворота».

Вот ссылка на проблему. https://leetcode.com/problems/walls-and-gates/description/

Вам дана сетка m X n, комнаты, инициализированные этими тремя возможными значениями.

  • -1 Стена или препятствие.

  • 0 Ворота.

  • INF Бесконечность означает пустую комнату.

    Мы используем значение 231 - 1 = 2147483647 для представления INF, поскольку вы можете предположить, что расстояние до ворот меньше 2147483647.

Заполните каждую пустую комнату расстоянием до ближайших ворот. Если невозможно добраться до ворот, их следует заполнить INF.

Я пытаюсь использовать DFS для решения вышеуказанной проблемы.

Мой подход заключается в

  • Пройдите по сетке, используя два индекса i и j.

  • И везде, где я встречаю значение ячейки 0, я запускаю DFS, так как это Gate в соответствии с определением проблемы.

  • В алгоритме DFS я сначала проверяю, удовлетворяются границы или нет.

    • Если за границу, то возвращаюсь.
    • Если значение ячейки равно -1, то я также возвращаюсь, так как по условию задачи это стена.
    • Если ячейка уже пройдена, то и возвращаюсь.

    После всех вышеперечисленных случаев я узнаю минимальное значение и количество ячеек и обновляю текущую ячейку с минимальным значением.

  • Отметьте ячейку как посещенную. Для чего я использую другую логическую сетку.

  • И затем я прохожу всю ячейку, делая: ряд + 1, ряд - 1, ячейка + 1, ячейка -1.

Вот код:

class Solution {
    public void wallsAndGates(int[][] rooms) {
        boolean[][] roomsBool = new boolean[rooms.length][rooms[0].length];
        for(int i = 0; i < rooms.length; ++i){
            for(int j = 0; j < rooms[i].length; ++j){
                if (rooms[i][j] == 0){
                    //if it is a gate we will do a DFS and fill the cells with the appropriate values.
                    visitRommsDFS(rooms, i, j, 0, roomsBool);
                    roomsBool = new boolean[rooms.length][rooms[i].length];
                }
            }
        }
    }

    private void visitRommsDFS(int[][] rooms, int row, int col, int count, boolean[][] roomsBool){
        if (row < 0 || row >= rooms.length || col < 0 || col >= rooms[row].length || rooms[row][col] == -1 || roomsBool[row][col] == true || rooms[row][col] == 0 && count > 0) return;
        if (rooms[row][col] > 0){
            rooms[row][col] = Math.min(rooms[row][col], count);
        }
        roomsBool[row][col] = true;
        visitRommsDFS(rooms, row-1, col, count + 1, roomsBool);
        visitRommsDFS(rooms, row+1, col, count + 1, roomsBool);
        visitRommsDFS(rooms, row, col+1, count + 1, roomsBool);
        visitRommsDFS(rooms, row, col-1, count + 1, roomsBool);
    }
}

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

Вот пример ввода для проблемы.

[
    [0, 2147483647, -1, 2147483647, 2147483647, -1, -1, 0, 0, -1, 2147483647, 2147483647, 0, -1, 2147483647, 2147483647, 2147483647, 2147483647, 0, 2147483647, 0, -1, -1, -1, -1, 2147483647, -1, -1, 2147483647, 2147483647, -1, -1, 0, 0, -1, 0, 0, 0, 2147483647, 0, 2147483647, -1, -1, 0, -1, 0, 0, 0, 2147483647],
    [2147483647, 0, -1, 2147483647, 0, -1, -1, -1, -1, 0, 0, 2147483647, 2147483647, -1, -1, 2147483647, -1, -1, 2147483647, 2147483647, -1, 0, -1, 2147483647, 0, 2147483647, -1, 2147483647, 0, 2147483647, 0, 2147483647, -1, 2147483647, 0, 2147483647, -1, 2147483647, 0, 2147483647, 2147483647, 0, -1, 2147483647, -1, -1, -1, 0, 2147483647]
]

и вот мой вывод.

[
    [0, 1, -1, 2, 1, -1, -1, 0, 0, -1, 1, 2, 0, -1, 4, 3, 2, 1, 0, 1, 0, -1, -1, -1, -1, 2, -1, -1, 1, 2, -1, -1, 0, 0, -1, 0, 0, 0, 1, 0, 2, -1, -1, 0, -1, 0, 0, 0, 1],
    [1, 0, -1, 3, 0, -1, -1, -1, -1, 0, 0, 2, 1, -1, -1, 4, -1, -1, 1, 2, -1, 0, -1, 1, 0, 1, -1, 1, 0, 1, 0, 1, -1, 1, 0, 1, -1, 1, 0, 1, 1, 0, -1, 1, -1, -1, -1, 0, 1]
]

а вот ожидаемый результат.

[
    [0, 1, -1, 2, 1, -1, -1, 0, 0, -1, 1, 1, 0, -1, 4, 3, 2, 1, 0, 1, 0, -1, -1, -1, -1, 2, -1, -1, 1, 2, -1, -1, 0, 0, -1, 0, 0, 0, 1, 0, 1, -1, -1, 0, -1, 0, 0, 0, 1],
    [1, 0, -1, 1, 0, -1, -1, -1, -1, 0, 0, 1, 1, -1, -1, 4, -1, -1, 1, 2, -1, 0, -1, 1, 0, 1, -1, 1, 0, 1, 0, 1, -1, 1, 0, 1, -1, 1, 0, 1, 1, 0, -1, 1, -1, -1, -1, 0, 1]
]

Еще несколько примеров входных данных и их ожидаемые результаты:

  1. Пример ввода
[
    [2147483647, -1, 0, 2147483647],
    [2147483647, 2147483647, 2147483647, -1],
    [2147483647, -1, 2147483647, -1],
    [0, -1, 2147483647, 2147483647]
]

Ожидаемый результат примера вывода 1.

 [
    [3, -1, 0, 1],
    [2, 2, 1, -1],
    [1, -1, 2, -1],
    [0, -1, 3, 4]
]

Пример ввода 2.

[
    [-1]
]

Ожидаемый результат для примера ввода 2.

[
    [-1]
]

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

Michael 11.04.2023 17:47

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

John Doe 11.04.2023 17:53

Пожалуйста, смотрите минимальный воспроизводимый пример, акцент на минимальный.

kaya3 11.04.2023 18:08

Обратите внимание, что связанная проблема находится за стеной подписки.

trincot 11.04.2023 18:18

DFS не подходит для этой проблемы. Вы должны использовать BFS, чтобы найти расстояние до каждой ячейки от ворот.

Matt Timmermans 11.04.2023 18:37

@MattTimmermans, это было бы проще, да, я согласен, но мы можем использовать DFS, я имею в виду, что в принципе это может быть не очень эффективно, значение счетчика будет уменьшаться, когда мы возвращаемся в DFS, и ячейки должны иметь соответствующее значение !! Пожалуйста, поправьте меня, если я ошибаюсь!!

John Doe 11.04.2023 18:47

Да, вы ошибаетесь. «Расстояние» — это длина кратчайшего пути. BFS следует кратчайшим путям. ДФС нет.

Matt Timmermans 11.04.2023 18:50

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

John Doe 11.04.2023 18:53
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
8
231
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

    private void visitRommsDFS(int[][] rooms, int row, int col, int count, boolean[][] roomsBool){
        if (... roomsBool[row][col] == true ...) {
            return;
        }
        if (rooms[row][col] > 0){
            rooms[row][col] = Math.min(rooms[row][col], count);
        }
        roomsBool[row][col] = true;
        // 1
        visitRommsDFS(rooms, row-1, col, count + 1, roomsBool);
        // 2
        visitRommsDFS(rooms, row+1, col, count + 1, roomsBool);
        // 3
        visitRommsDFS(rooms, row, col+1, count + 1, roomsBool);
        // 4
        visitRommsDFS(rooms, row, col-1, count + 1, roomsBool);
    }

то есть вызов (1) отравляет все достижимые ячейки в roomsBool значением true, поэтому вызовы (2), (3) и (4) могут не иметь никакого эффекта.

Быстрое решение состоит в том, чтобы вернуть roomsBool состояние обратно в конце visitRommsDFS вызова:

roomsBool[row][col] = true;
visitRommsDFS(rooms, row - 1, col, count + 1, roomsBool);
visitRommsDFS(rooms, row + 1, col, count + 1, roomsBool);
visitRommsDFS(rooms, row, col + 1, count + 1, roomsBool);
visitRommsDFS(rooms, row, col - 1, count + 1, roomsBool);
roomsBool[row][col] = false;

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

ваш текущий алгоритм: пусть найдут все комнаты с воротами и пройдут все комнаты, до которых можно добраться от определенных ворот, временная сложность для такого наивного алгоритма явно хуже, чем O(MxNxG), лучшее, что вы можете здесь получить, это прекратить обход комнат, если комната ближе к «другой». ворота, а не к текущему, что-то вроде:

public static final int[][] DIRS = new int[][]{new int[]{-1, 0}, new int[]{1, 0}, new int[]{0, -1}, new int[]{0, 1}};

public void wallsAndGates(int[][] rooms) {
    for (int i = 0; i < rooms.length; ++i) {
        for (int j = 0; j < rooms[i].length; ++j) {
            if (rooms[i][j] == 0) {
                visitRoomsDFS(rooms, i, j);
            }
        }
    }
}

private void visitRoomsDFS(int[][] rooms, int row, int col) {
    int distance = rooms[row][col] + 1;
    for (int[] dir : DIRS) {
        int r = row + dir[0];
        int c = col + dir[1];
        if (r < 0 || c < 0 || r >= rooms.length || c >= rooms[row].length) {
            continue;
        }
        // optimization: "have seen better" or not a room
        if (rooms[r][c] <= distance) {
            continue;
        }
        rooms[r][c] = distance;
        visitRoomsDFS(rooms, r, c);
    }

}

Однако я считаю, что даже при такой оптимизации временная сложность все равно не лучше O(MxNxG), с другой стороны теоретическая временная сложность для этой головоломки не должна быть лучше O(MxN) - нам все равно нужно обойти все комнаты, а проблема в том, что алгоритм с временной сложностью O(MxN) существует:

основная идея состоит в том, чтобы проходить комнаты в очень определенном порядке:

  • сначала проходим через ворота
  • через секунду пересекаем соседей ворот - такие комнаты либо имеют расстояние 1 до каких-то ворот, либо являются стенами, либо мы их уже видели
  • в третьем обходим всех соседей комнат, находящихся на расстоянии 1 до каких-то ворот - такие комнаты либо имеют расстояние 2 до каких-то ворот, либо являются стенами, либо мы их уже видели.
  • и т. д.
public static final int[][] DIRS = new int[][]{new int[]{-1, 0}, new int[]{1, 0}, new int[]{0, -1}, new int[]{0, 1}};

public void wallsAndGates(int[][] rooms) {
    int rows = rooms.length;
    int cols = rooms[0].length;
    Deque<int[]> deque = new LinkedList<>();
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            if (rooms[i][j] == 0) {
                deque.add(new int[]{i, j});
            }
        }
    }
    int[] room;
    while ((room = deque.pollFirst()) != null) {
        int distance = rooms[room[0]][room[1]] + 1;
        for (int[] dir : DIRS) {
            int r = room[0] + dir[0];
            int c = room[1] + dir[1];
            if (r < 0 || c < 0 || r >= rows || c >= cols) {
                continue;
            }
            // ignoring walls and rooms
            // seen previously
            if (rooms[r][c] > distance) {
                rooms[r][c] = distance;
                deque.addLast(new int[]{r, c});
            }
        }
    }
}

Это, то есть отравление всех достижимых ячеек, является особенностью рекурсии. Не так ли? Пожалуйста, объясните что-нибудь еще об этом. Можно ли что-то сделать, чтобы это выполнялось немного быстрее?

John Doe 17.04.2023 09:27

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

Andrey B. Panfilov 17.04.2023 09:28

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

John Doe 17.04.2023 09:36

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

John Doe 17.04.2023 19:44

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

roomsBool[row][col] = false;

Этот выше шаг имеет решающее значение, чтобы вернуться.

Кроме того, всего одна проверка, что room[row][col] меньше count :-

if (rooms[row][col] < count) {
  rooms[row][col] = count;
}

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

roomsBool[row][col] = false;

после обхода во всех 4-х направлениях.

И предложенная оптимизация может быть легко включена в мое решение. Вот полное рабочее решение.

  class Solution {
    public void wallsAndGates(int[][] rooms) {

        boolean[][] roomsBool = new boolean[rooms.length][rooms[0].length];

        for(int i = 0; i < rooms.length; ++i){
            for(int j = 0; j < rooms[i].length; ++j){
                if (rooms[i][j] == 0){
                    //if it is a gate we will do a DFS and fill the cells with the appropriate values.
                    visitRommsDFS(rooms, i, j, 0, roomsBool);
                }
            }
        }
    }

    private void visitRommsDFS(int[][] rooms, int row, int col, int count, boolean[][] roomsBool){
        if (row < 0 || row >= rooms.length || col < 0 || col >= rooms[row].length || rooms[row][col] == -1 || rooms[row][col] == 0 && count > 0) return;

        rooms[row][col] =  count;
        roomsBool[row][col] = true;

        if (row > 0 && rooms[row-1][col] > count + 1)
          visitRommsDFS(rooms, row-1, col, count + 1, roomsBool);

        if (row + 1 < rooms.length && rooms[row+1][col] > count + 1)  
          visitRommsDFS(rooms, row+1, col, count + 1, roomsBool);

        if (col + 1 < rooms[row].length && rooms[row][col + 1] > count + 1)  
          visitRommsDFS(rooms, row, col+1, count + 1, roomsBool);

        if (col > 0 && rooms[row][col - 1] > count + 1) 
          visitRommsDFS(rooms, row, col-1, count + 1, roomsBool);

        roomsBool[row][col] = false;
    }
 }

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

Анализ временной сложности Поскольку пространство поиска представляет собой сетку m X n, сложность DFS, вторая вспомогательная функция равна O (m X n), так как мы покрываем все ячейки сетки, а их m X n. А обрезка, когда мы делаем рекурсивный вызов, гарантирует, что при выполнении DFS больше не будет повторяться обход ячейки.

Если бы я сделал это в BFS, то и временная сложность осталась бы такой же, как O (m X n), здесь выбор BFS и DFS не имел бы значения для временной сложности, так как размер сетки фиксируется заранее и мы проходим каждую ячейку только один раз, если только значение расстояния не меньше.

Если значение расстояния в следующем вызове меньше, чем текущее значение в ячейке, то, очевидно, мы обходим ячейку более 1 раза.

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

Почему?

Поскольку количество ячеек в заданной сетке ограничено, и даже если DFS движется в одном направлении, она вынуждена вернуться из этого направления из-за краев и стенок сетки. И продолжить исследование в другом направлении, и в итоге DFS покроет все ячейки. Кроме того, благодаря логике сокращения ненужного обхода мы эффективны при обходе. Как и BFS, стиль обхода. Таким образом, временная сложность обоих обходов асимптотически одинакова с приведенным выше сокращением нежелательных рекурсивных вызовов.

Условие roomsBool[row][col] == true лишнее, вы можете разбить свой длинный оператор if на несколько if, поместить отладчик на if (roomsBool[row][col] == true) и обнаружить, что код никогда не достигает этого if.

Andrey B. Panfilov 19.04.2023 09:17

Конечно, позвольте мне проверить. Но можем ли мы объяснить, почему он не собирается достигать этого заявления.

John Doe 19.04.2023 09:55

утверждение «мы видели эту комнату раньше» дополняется утверждением «если предложенное расстояние больше или равно текущему, то комната либо является воротами в стене, либо мы видели ее раньше». Итак, если вы хотите доказать правильность алгоритма, вам нужно доказать правильность rooms[row][col - 1] > count + 1 оптимизации, без этой оптимизации алгоритм имеет экспоненциальную временную сложность.

Andrey B. Panfilov 19.04.2023 10:12

Другие вопросы по теме