Эффективный алгоритм для границы множества плиток


12

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

То есть я хочу получить список всех плиток, которые касаются одной из плиток на территории, но при этом не находятся на этой территории. Какой эффективный способ найти это?

Для дополнительной сложности, случается, что мои плитки являются гексами, но я подозреваю, что это не имеет большого значения, каждая плитка по-прежнему помечена целыми координатами x и y, и, учитывая плитку, я легко могу найти ее соседей. Ниже приведено несколько примеров: черный цвет - это территория, а синий - граница, которую я хочу найти. Пример территорий и границ Само по себе это не является сложной проблемой. Один простой алгоритм для этого в псевдопифонах:

def find_border_of_territory(territory):
    border = []
    for tile in territory:
        for neighbor in tile.neighbors():
            if neighbor not in territory and neighbor not in border:
                border.add(neighbor)

Однако это медленно, и я хотел бы что-то лучше. У меня есть цикл O (n) над территорией, другой цикл (короткий, но все же) по всем соседям, и затем я должен проверить членство по двум спискам, один из которых имеет размер n. Это дает ужасное масштабирование O (n ^ 2). Я могу уменьшить это до O (n), используя наборы вместо списков для границы и территории, чтобы быстро проверять членство, но все равно оно невелико. Я ожидаю, что будет много случаев, когда территория большая, но граница небольшая из-за простого масштабирования области против линии. Например, если территория представляет собой гекс с радиусом 5, она имеет размер 91, а граница - только размер 36.

Кто-нибудь может предложить что-то лучше?

Редактировать:

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

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

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

Что касается времени, в моем текущем коде у меня есть некоторые подпрограммы, которые должны проверить каждую плитку территории. Не на каждом шагу, но на создание и иногда после. Это занимает более 50% времени выполнения моего набора тестового кода, хотя это очень малая часть всей программы. Поэтому я стремился минимизировать любые повторения. ОДНАКО, тестовый код включает в себя гораздо больше создания объектов, чем нормальный запуск программы (естественно), поэтому я понимаю, что это может быть не очень актуально.


10
Я думаю, что если ничего не известно о форме, алгоритм O (N) кажется разумным. Все, что быстрее, потребовало бы не смотреть на каждый элемент территории, что сработало бы, только если вы знаете что-то о форме, я думаю.
amitp

3
Вам, вероятно, не нужно делать это очень часто. Также n не очень большое, намного меньше, чем общее количество плиток.
Триларион

1
Как создаются / изменяются эти области? И как часто они меняются? Если они выбраны по частям, то вы можете составлять список соседей по ходу работы, и, если они не меняются часто, вы можете сохранять массив территорий и их границ, а также добавлять или удалять их по мере необходимости (так что нет нужно постоянно пересчитывать их).
Дейв Мангуст

2
Важно: действительно ли это диагностированная и профилированная проблема производительности? С такой небольшой проблемой (всего несколько сотен элементов, правда?), Я не думаю, что это O (n ^ 2) или O (n) должно быть проблемой. Похоже, преждевременная оптимизация в системе, которая не будет запускаться каждый кадр.
Делиот

1
Простой алгоритм O (n), так как существует не более 6 соседей.
Эрик

Ответы:


11

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

В этом случае ваша территория.

Территория должна представлять собой неупорядоченный (O (1) хэш) набор границ и элементов.

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

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

Это требует O (1) работы всякий раз, когда вы добавляете или удаляете плитку на или с территории. Посещение границы занимает O (длина границы). Если вы хотите знать, что такое граница, значительно чаще, чем добавлять / удалять элементы с территории, это должно победить.


9

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

Но если вас интересует только поиск внешней границы (не внутренних отверстий), мы можем сделать это немного эффективнее:

  1. Найдите один край, разделяющий вашу территорию. Вы можете сделать это ...

    • (если вы знаете хотя бы один фрагмент территории и знаете, что у вас есть ровно один связанный блок территории на вашей карте)

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

      Это сканирование является линейным по диаметру карты.

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

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

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

  2. Начиная с начального края, найденного в шаге 1, следуйте по периметру вашего ландшафта, добавляя каждый не-рельефный фрагмент снаружи в вашу коллекцию границ, пока не вернетесь к начальному краю.

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

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


7

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

Потому что:

  • Этот запрос легко выполнить лениво. Например: вам не нужно искать границу на всей карте, только на том, что видно.

  • Этот запрос легко выполнить параллельно. На самом деле, я мог бы изобразить некоторый шейдерный код, который делает это. И если вам это нужно для чего-то другого, кроме визуализации, вы можете визуализировать текстуру и использовать ее.

  • Если тайл меняется, граница изменяется только локально, что означает, что вам не нужно снова вычислять все это.

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

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

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


2

Переместите свою область вверх на одну плитку, затем вверх-вправо, затем вниз вправо и т. Д. Затем удалите исходную область.

Объединение всех шести наборов должно быть O (n), сортировка O (n.log (n)), разность наборов O (n). Если исходные плитки хранятся в каком-то отсортированном формате, объединенный набор также можно отсортировать в O (n).

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


1

Я только что написал в блоге о том, как это сделать. При этом используется первый метод, упомянутый @DMGregory, начиная с краевой ячейки и маршируя по периметру. Это на C # вместо Python, но должно быть довольно легко адаптироваться.

https://dillonshook.com/hex-city-borders/


0

ОРИГИНАЛЬНАЯ ПОЧТА:

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

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

РЕДАКТИРОВАТЬ Есть гораздо более эффективные способы, чем простая итерация. Как я пытался указать в своем (теперь удаленном) ответе ниже, вы можете достичь O (1) в лучших случаях и O (n) в худшем случае.

Добавление плитки на территорию O (1) - O (N):

В случае отсутствия соседей вы просто создаете новую территорию.

В случае одного соседа вы добавляете новую плитку на существующую территорию.

В случае с 5 или 6 соседями вы знаете, что все это связано, поэтому вы добавляете новую плитку на существующую территорию. Это все операции O (1), и обновление новых пограничных территорий также является O (1), поскольку это простое слияние одного набора с другим.

В случае 2, 3 или 4 соседних территорий вам может потребоваться объединить до 3 уникальных территорий. Это O (N) на объединенном размере территории.

Удаление плитки с территории O (1) - O (N):

С нуля соседи стирают территорию. O (1)

С одним соседом убери плитку с территории. O (1)

С двумя или более соседями может быть создано до 3 новых территорий. Это O (N).

В последние несколько недель я проводил свободное время, разрабатывая демонстрационную программу, представляющую собой простую гекс-территориальную игру. Попробуйте поднять свой доход, разместив территории рядом друг с другом. 3 игрока, Red, Green и Blue, соревнуются за получение наибольшего дохода, стратегически размещая плитки на ограниченном игровом поле.

Вы можете скачать игру здесь (в формате .7z ) hex.7z

Простое управление мышью ЛКМ размещает плитку (может размещаться только там, где выделено наведением курсора). Оценка сверху, доход снизу. Посмотрите, сможете ли вы придумать эффективную стратегию.

Код можно найти здесь:

Eagle / EagleTest

Для сборки из исходного кода вам нужны Eagle и Allegro 5. Оба компилируются с помощью cmake. Шестнадцатеричная игра в настоящее время строится с использованием CB.

Переверните эти отрицательные голоса вверх ногами. :)


По сути, это то, что делает алгоритм в OP, хотя проверка соседних плиток перед включением выполняется немного быстрее, чем удаление их всех в конце.
ScienceSnake

По сути, это то же самое, но если вычесть их, как только это
станет

Я полностью обновил свой ответ и удалил посторонний ответ ниже.
BugSquasher
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.