Как найти дыру в 2D-матрице?
Я знаю, что название кажется неоднозначным, поэтому я добавил изображение, которое будет полезно для понимания проблемы. Мне нужно найти дыры внутри белого региона. Отверстие определяется как одна или несколько ячеек со значением "0" внутри белого региона. Я имею в виду, что он должен быть полностью закрыт ячейкой со значением "1" (например, здесь мы можем увидеть три отверстия, отмеченные как 1, 2 и 3). Я придумал довольно наивное решение:
1. Искать всю матрицу для ячеек со значением '0'
2. Запустите DFS (Flood-Fill), когда встречается такая ячейка (черная) и проверяем, можем ли мы коснуться границы основной прямоугольной области
3. Если мы коснемся границы во время DFS, то это не отверстие, и если мы не сможем достичь границы, тогда это будет рассматриваться как дыра
Теперь это решение работает, но мне было интересно, есть ли какое-нибудь другое эффективное/быстрое решение этой проблемы.
Пожалуйста, дайте мне знать ваши мысли. Спасибо.
![enter image description here]()
Ответы
Ответ 1
С наводнением, которое у вас уже есть: бегите вдоль границы вашей матрицы и заливайте его, т.е.
изменить все ноль (черный) на 2 (заполненный черный) и один на 3 (заполненный белый); игнорировать 2 и 3, что
происходят из более позднего наводнения.
Например, с вашей матрицей, вы начинаете с левого верхнего угла, а заливка черным - зона с областью
11. Затем вы двигаетесь вправо и найдите черную ячейку, которую вы только что заполнили. Снова вернитесь и найдите
белая область, очень большая (на самом деле все белые в вашей матрице). Налейте его. Затем вы двигаетесь вправо
снова, еще одна новая черная область, которая проходит вдоль всех верхних и правых границ. Передвигаться,
вы теперь найдете две белые ячейки, которые вы заполнили ранее, и пропустите их. И, наконец, вы найдете черную область вдоль нижней границы.
Затем сканируйте матрицу: все области, которые вы находите, которые все еще имеют цвет 0, являются отверстиями в черном. У вас также могут быть отверстия в белом.
Другой метод, вроде "арестованного наводнения"
Запустите все вокруг границы первой матрицы. Где вы находите "0", вы устанавливаете
до "2". Где вы находите "1", вы устанавливаете значение "3".
Теперь запустите новую внутреннюю границу (те ячейки, которые касаются только что отсканированной границы).
К нулевым ячейкам, касающимся 2, становятся 2, 1 клетки, трогающие 3, становятся 3.
Вам придется сканировать дважды, один раз по часовой стрелке, один раз против часовой стрелки, проверяя ячейки "наружу" и "до" текущей ячейки. Это потому, что вы можете найти что-то вроде этого:
22222222222333333
2AB11111111C
31
Ячейка A на самом деле 1. Вы проверяете своих соседей, и вы находите 1 (но бесполезно проверять это, так как вы еще не обработали его, поэтому вы не можете знать, является ли оно 1 или должно быть 3 - это, кстати, случай), 2 и 2. A 2 не может изменить 1, поэтому ячейка A остается 1. То же самое происходит с ячейкой B, которая снова равна 1 и т.д. Когда вы придете в ячейку C, вы обнаружите, что это 1 и имеет 3 соседних, поэтому он переключается на 3... но теперь все ячейки от A до C должны переключаться.
Самый простой, хотя и не самый эффективный способ справиться с этим - сканировать ячейки по часовой стрелке, что дает неверный ответ (между прочим, C и D - 1)
22222222222333333
211111111DC333333
33
а затем снова сканируйте их против часовой стрелки. Теперь, когда вы прибудете в ячейку C, он имеет 3-соседний и переключается на 3. Затем вы проверяете ячейку D, чей предыдущий сосед - C, теперь 3, поэтому D снова переключается на 3. В итоге вы получите правильный ответ
22222222222333333
23333333333333333
33
и для каждой ячейки вы исследовали двух соседей, идущих по часовой стрелке, один идет против часовой стрелки. Более того, один из соседей на самом деле является ячейкой, которую вы проверили раньше, поэтому вы можете сохранить ее в готовой переменной и сохранить один доступ к матрице.
Если вы обнаружите, что вы сканировали целую рамку, даже не переключая одну ячейку, вы можете остановить ее. Проверка этого будет стоить вам 2 (W * H) операций, поэтому это действительно стоит, если есть много дыр.
В большинстве шагов W * H * 2 вы должны сделать.
Вы также можете проверить Алгоритм Перколяции и попытаться адаптировать этот.
Ответ 2
Сделайте своего рода класс LinkedCells, который будет хранить ячейки, которые связаны друг с другом. Затем проверяйте ячейки поочередно в порядке слева направо и сверху вниз, делая следующую проверку для каждой ячейки: если соседняя ячейка черная - добавьте эту ячейку в эту группу ячеек. Иначе вы должны создать новую группу для этой ячейки. Вы должны проверить только верхний и левый сосед.
UPD: Извините, я забыл о слиянии групп: если обе соседние ячейки черные и принадлежат к разным группам - вы должны merege tha группы в одном.
В вашем классе LinkedCells должен быть флаг, если он подключен к краю. Он по умолчанию является ложным и может быть изменен на true, если вы добавляете к нему эту ячейку. В случае слияния двух групп вы должны установить новый флаг как ||
предыдущих флагов.
В итоге у вас будет набор групп, и каждая группа с фальшивым флагом связи будет "дырой".
Этот алгоритм будет O (x * y).
Ответ 3
Вы можете представить сетку в виде графика с отдельными ячейками в виде вершин и ребер, встречающихся между соседними вершинами. Затем вы можете использовать первый поиск или глубину первого поиска для начала в каждой из ячеек по бокам. Поскольку вы найдете только компоненты, связанные с боками, черные ячейки, которые не были посещены, являются отверстиями. Вы можете снова использовать алгоритм поиска, чтобы разделить отверстия на отдельные компоненты.
РЕДАКТИРОВАТЬ: Наихудшая сложность случая должна быть линейной по отношению к числу ячеек, в противном случае дать некоторый вклад в алгоритм, проверить, какие ячейки (поскольку вы сублинейные, будут большие невидимые точки), алгоритм не изучал и положил туда дыру. Теперь у вас есть вход, для которого алгоритм не находит одно из отверстий.
Ответ 4
Ваш алгоритм глобально Хорошо. Это просто вопрос оптимизации его путем слияния разведки заливки с помощью сканирования ячеек. Это просто минимизирует тесты.
Общая идея состоит в том, чтобы выполнить исследование заливки заливки по очереди при сканировании таблицы. Таким образом, у вас будет несколько параллельных заливок заливки, которые вы должны отслеживать.
Затем таблица обрабатывается по строкам сверху вниз, а каждая строка обрабатывается справа налево. Порядок произвольный, может быть наоборот, если вы предпочитаете.
Пусть сегменты идентифицируют последовательность последовательных ячеек со значением 0 в строке. Для определения сегмента вам нужен только индекс первой и последней ячейки со значением 0.
Как вы можете догадаться, сегмент также заполняет поток. Поэтому мы добавим идентификационный номер в сегменты, чтобы различать различные заливки.
Хорошо, что этот алгоритм состоит в том, что вам нужно только отслеживать сегменты и их идентификационный номер в строке я и i-1. Так что, когда вы обрабатываете строку i, у вас есть список сегментов, найденных в строке i-1 и связанный с ними идентификационный номер.
Затем вам необходимо обработать сегментное соединение в строке я и строке i-1. Ниже я объясню, как это можно сделать эффективным.
Теперь вам нужно рассмотреть три случая:
-
найден сегмент в строке i, не связанный с сегментом в строке i-1. Назначьте ему новую идентификацию отверстия (приращение целого числа). Если он подключен к границе таблицы, сделайте это число отрицательным.
-
найден сегмент в строке i-1, не связанный с сегментом в строке i-1. Вы нашли нижний сегмент отверстия. Если у него есть отрицательный идентификационный номер, он подключен к границе, и вы можете его игнорировать. В противном случае, поздравив, вы нашли дыру.
-
нашел сегмент в строке i, подключенный к одному или нескольким сегментам в строке i-1. Установите идентификационный номер всех этих подключенных сегментов на наименьший идентификационный номер. См. Следующий возможный вариант использования.
row i-1: 2 333 444 111
row i : **** *** ***
Сегменты в строке я должны получить значение 1, идентифицирующее ту же заливку заливки.
Соответствие сегментов в строках я и строке i-1 может быть выполнено эффективно, сохраняя их в порядке слева направо и сравнивая индексы сегментов.
Сначала обрабатывать сегменты с наименьшим начальным индексом. Затем проверьте, подключен ли он к сегменту с наименьшим индексом запуска другой строки. Если нет, процесс 1 или 2. В противном случае продолжить идентификацию связанных сегментов, отслеживая наименьший идентификационный номер. Когда не будет найдено больше сегментов, установите идентификационный номер всех подключенных сегментов, найденных в строке i, наименьшему идентификационному значению.
Сравнение индексов для теста подключения может быть оптимизировано путем хранения (первое-1, последнее) в качестве определения сегмента, так как сегменты могут быть связаны их углами. Затем вы можете напрямую сравнивать значение индексов и определять перекрывающиеся сегменты.
Правило для выбора наименьшего идентификационного номера гарантирует, что вы автоматически получите отрицательное число для подключенных сегментов и, по крайней мере, одно подключенное к границе. Он распространяется на другие сегменты и наводнения.
Это хорошее упражнение для программирования. Вы не указали точный результат, который вам нужен. Таким образом, это также остается упражнением.
Ответ 5
Алгоритм грубой силы, описанный ниже, здесь.
Предположим теперь, что мы можем записать в ячейках значение, отличное от 0 или 1.
Вам нужны функции заливки заливки, получающие координаты ячейки для начала и целочисленное значение для записи во все связанные ячейки, содержащие значение 0.
Поскольку вам нужно рассматривать только дыры (ячейки со значением 0, окруженные ячейками со значением 1), вы должны использовать два прохода.
Первый проход посещает только ячейки, которые касаются границы. Для каждой ячейки, содержащей значение 0, вы заполняете заливку значением -1. Это говорит о том, что эта ячейка имеет значение, отличное от 1, и имеет соединение с границей. После этого сканирования все ячейки со значением 0 принадлежат одному или нескольким отверстиям.
Чтобы различать различные отверстия, вам нужно выполнить второе сканирование. Затем вы просматриваете оставшиеся ячейки в прямоугольнике (1,1) x (n-2, n-2), который вы еще не просматривали. Всякий раз, когда ваше сканирование попадает в ячейку со значением 0, вы обнаружили новое отверстие. Затем вы заливаете это отверстие целым по вашему выбору, чтобы отличить его от других. После этого вы продолжаете сканирование до тех пор, пока не будут посещены все ячейки.
Когда это будет сделано, вы можете заменить значения -1 на 0, потому что не должно быть 0 слева.
Этот алгоритм работает, но не так эффективен, как другой предлагаемый мной алгоритм. Его преимущество заключается в том, что оно простое и не требует дополнительного хранилища данных для хранения сегментов, идентификации отверстий и возможной ссылки на целые сегменты.