Маркировка прогрессивных подключенных компонентов - PullRequest
2 голосов
/ 27 июня 2009

Я работаю с сеткой квадратов, которые имеют два состояния: «ВКЛ» и «ВЫКЛ». У меня есть довольно простой алгоритм маркировки подключенных компонентов , который находит все компоненты «ВКЛ». Обычно, но не всегда, есть только один компонент «ВКЛ».

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

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

Groups G;
Foreach changed cell C:
  Group U = emptygroup;
  U.add(C);
  Foreach Group S in G:
    if (S contains a cell which is adjacent to C)
      G.Remove(S);
      U.UnionWith(S);
  G.add(C);

Однако, если изменения содержат какие-либо отключенные ячейки, я не уверен, что делать. Имейте в виду, что все ячейки ON должны быть членами ровно одной группы. Таким образом, одним из решений было бы взять каждую ячейку, которая смежна с новой ячейкой OFF, и посмотреть, связаны ли они друг с другом (например, используя * pathfinding). Это приведет к 1-4 смежным группам (если только ячейка не была единственной ячейкой в ​​своей группе и, следовательно, имеет 0 соседних ячеек для проверки, и в этом случае она дает 0 групп). Тем не менее, это только немного лучше, чем начинать с нуля, так как обычно (но не всегда) соединять эти смежные квадраты примерно так же трудно, как просто найти смежную группу (если у кого-то нет предложений для умного способа сделать это). Кроме того, это немного страшно, если есть много измененных ячеек ... хотя я признаю, что обычно их нет.

Контекст, для тех, кто настаивает на знании, почему я делаю это: Единственное правило в загадках Nurikabe состоит в том, что у вас может быть только 1 непрерывная группа стен. Упрощение проблемы, которую я пытаюсь решить, чтобы получить увеличенную скорость (и играть с поиском пути) выше. По сути, я хочу проверить наличие смежных стен, не тратя впустую информацию, полученную в результате предыдущих таких испытаний. Я пытаюсь увидеть, сколько мест в моем решателе я могу использовать предыдущую информацию, чтобы повысить скорость, поскольку кажется довольно болезненным использовать алгоритм O (f (N)), когда O (f (Δ)) алгоритма будет достаточно (N - размер головоломки, а Δ - изменения, внесенные с момента последнего запуска алгоритма).

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

Примечание: Я пропустил объяснение моего текущего алгоритма, но в основном он работает, выполняя основанный на стеке алгоритм Flood Fill для первого найденного квадрата ON, затем проверяет, есть ли еще квадраты ON (что означает, что это более чем одна группа, которую он не удосуживается изучить).

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

токовая петля:

foreach (Square s in m.Neighbors[tmp.X][tmp.Y])    
{
    if (0 != ((byte)(s.RoomType) & match) && Retval.Add(s)) curStack.Push(s);
}

Идея улучшения:

foreach (Square s in m.NeighborsXX[tmp.X][tmp.Y])    
{
    if (Retval.Add(s)) curStack.Push(s);
}

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

Ответы [ 3 ]

1 голос
/ 27 июня 2009

Не полное решение, но здесь идет речь:

  • Для каждого подключенного компонента сохраняйте связующее дерево в памяти
    • Свойство дерева A: Наше связующее дерево имеет представление о том, какой узел «выше», какой (как в деревьях поиска). Выбор выше которого произвольный
  • Давайте обсудим удаление и добавление ребер
  • При добавлении ребра:
    • Проверьте, находятся ли два узла в одном компоненте, проверив, совпадают ли корни их деревьев
      • Свойство дерева B: дерево должно быть плотным, поэтому эта проверка будет O (log n)
    • Если в той же группе, то ничего не делать
    • Если они находятся в разных группах, то соедините деревья с новым ребром.
      • Для этого потребуется преобразовать "форму" (определение того, кто выше кого) одного из деревьев, чтобы наш новый край мог быть "над" ним
  • При удалении кромки:
    • Если этот край не участвует в остовном дереве группы, ничего не делать.
    • Если это так, нам нужно проверить, подключена ли группа
      • DFS из одной группы, чтобы попытаться связаться с другой
      • Лучше сделать это из меньшего из двух
        • Свойство дерева C: мы поддерживаем для каждого узла в дереве размер его поддерева
        • Используя свойство C, мы можем определить размеры обеих групп
      • Из-за свойства B: обычно меньшая группа будет очень маленькой, а большая группа будет очень большой
      • Если группы связаны, то мы действуем так, как если бы мы добавили соединительный край
      • Если группы не связаны, то нам нужно взобраться на дерево, чтобы сохранить свойство C (вычесть размер ранее подключенного поддерева из размеров поддерева предков)
  • Проблема: как нам сохранить свойство B (дерево плотное)?

Надеюсь, в этом есть смысл:)

1 голос
/ 27 июня 2009

это та же проблема, что и при расчете (при условии 4-связности на сетке) связанных цепочек камней в игре Го (Igo в Японии), и постепенное выполнение этого является одним из ключей к высокопроизводительному алгоритму игры в Го.

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

Исходя из моего ограниченного понимания проблемы, я бы порекомендовал вам использовать union-find, когда вы включаете элемент, чтобы объединить помеченные группы, и пересчитывать связанные группы с нуля, когда вы выключаете элемент сетки. , Чтобы оптимизировать это, всякий раз, когда вы включаете и выключаете элементы сетки, сначала обрабатывайте случай выключения, чтобы операции поиска объединения не были потрачены впустую. Если вы хотите иметь более продвинутый инкрементальный алгоритм, вы можете начать постепенно увеличивать данные о связности для каждого элемента, но, скорее всего, они не окупятся.

0 голосов
/ 27 июня 2009

Интересная проблема! Вот моя первоначальная мысль. Надеюсь, у меня будет больше и я буду обновлять этот ответ по мере их поступления ...

[Обновление 2] Поскольку вам нужна только одна группа, поиск A * кажется идеальным. Вы профилировали поиск A * против перемаркировки? Я должен думать, что хорошо написанный A * поиск будет быстрее, чем заливка. Если нет, то, возможно, вы можете опубликовать свой реальный код для помощи по оптимизации?

[Обновление 1] Если вы знаете, что новая ячейка ВЫКЛЮЧЕНА C находится в группе G, то вы можете перезапустить алгоритм CCL, но нужно только пометить ячейки в группе. G. Другие ячейки ON могут сохранять свои существующие метки. Вам не нужно будет изучать остальную часть сетки, что может быть значительной экономией по сравнению с первоначальным CCL всей сетки. (Как заядлый решатель Nurikabe сам, это должно быть как минимум на 33% экономии в разгаданной головоломке и очень значительной экономии в незавершенной головоломке, не так ли? "33%", исходя из моих догадок решаемые головоломки примерно 2/3 черного и 1/3 белого.)

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

...