Эффективный способ найти перекрытие N прямоугольников

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

У меня есть два списка прямоугольников

rect = Rectangle(10, 12, 56, 15)
rect2 = Rectangle(0, 0,1, 15)
rect3 = Rectangle (10,  12, 56, 15)

listA = [rect, rect2]
listB = [rect3]

который создается из класса:

import numpy as np
import itertools as it

class  Rectangle(object):
    def __init__(self, left, right, bottom, top):
        self.left = left
        self.bottom = right
        self.right = bottom
        self.top = top

    def overlap(r1, r2):
        hoverlaps = True
        voverlaps = True
        if (r1.left > r2.right) or (r1.right < r2.left):
            hoverlaps = False
        if (r1.top < r2.bottom) or (r1.bottom > r2.top):
            voverlaps = False
        return hoverlaps and voverlaps

Мне нужно сравнить прямоугольник в listA для listB код выглядит следующим образом, который является крайне неэффективным - сравнение по одному

for a in it.combinations(listB):
    for b in it.combinations(listA):
        if a.overlap(b):

Любой эффективный метод решения этой проблемы?

Ответы

Ответ 1

Во-первых: Как и во многих проблемах из вычислительной геометрии, необходимо указать параметры для анализа порядка роста: вызывая длины списков m и n, наихудший случай только в этих параметрах - Ω (m × n), так как все области могут перекрываться (в этом отношении алгоритм из вопроса асимптотически оптимален). Обычно указывается размер вывода: t = f (m, n, o) (чувствительный к выходу алгоритм).
Тривиально, f ∈ Ω (m + n +o) для поставленной задачи.


Line Sweep - это парадигма для уменьшения геометрических задач одним измерением - в его первоначальном виде, от 2D до 1D, от плоскости к линии.

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


Существует много структур данных, поддерживающих вставку и удаление интервалов, и поиск всех интервалов, которые перекрывают интервал запроса. В настоящее время я думаю, что расширенные поисковые деревья могут быть проще всего понять, реализовать, протестировать, проанализировать...
Используя это, перечисляя все о пересекающихся пар оси выровнен прямоугольников (а, б) из listA и listB должно быть возможным в O (журнал (т + п) (M + N) +o) времени и O (M + N ) пространство. Для значительных экземпляров проблем избегайте структур данных, требующих больше, чем линейного пространства ((оригинальные) сегментные деревья, например, для перекрытия интервалов).


Другая парадигма в дизайне алгоритма - Divide & Conquer: с задачей вычислительной геометрии, выберите одно измерение, в котором проблему можно разделить на независимые части, а координата такая, что подзадачи для "координат ниже" и "координаты выше" близки ожидаемое время выполнения. Вполне возможно, что необходимо решить другую (и другую) подзадачу "включая координату". Это, как правило, выгодно, когда: а) время выполнения решения подсуммов является "супер-логарифмически линейным", и б) существует дешевый (линейный) способ построения общего решения из решений для подзадач,
Это поддается совместному решению проблем и может использоваться с любым другим подходом для подзадач, включая линейную развертку.


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

Ответ 2

Несколько возможных незначительных улучшений эффективности. Во-первых, исправьте функцию overlap(), она потенциально не требует вычислений:

def overlap(r1, r2):

    if r1.left > r2.right or r1.right < r2.left:
        return False

    if r1.top < r2.bottom or r1.bottom > r2.top:
        return False

    return True

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

def containing_rectangle(rectangles):
    return Rectangle(min(rectangles, key=lambda r: r.left).left,
        max(rectangles, key=lambda r: r.right).right,
        min(rectangles, key=lambda r: r.bottom).bottom,
        max(rectangles, key=lambda r: r.top).top
    )

c = containing_rectangle(listA)

for b in listB:
    if b.overlap(c):
        for a in listA:
            if b.overlap(a):

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

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

Мы можем разбить и запустить оба списка с прямоугольниками контейнеров:

listAA = [listA[x:x + 10] for x in range(0, len(listA), 10)]

for i, arrays in enumerate(listAA):
    listAA[i] = [containing_rectangle(arrays)] + arrays

listBB = [listB[x:x + 10] for x in range(0, len(listB), 10)]

for i, arrays in enumerate(listBB):
    listBB[i] = [containing_rectangle(arrays)] + arrays

for bb in listBB:
    for aa in listAA:
        if bb[0].overlap(aa[0]):
            for b in bb[1:]:
                if b.overlap(aa[0]):
                    for a in aa[1:]:
                        if b.overlap(a):

С моими случайными данными это уменьшало сравнение порядка 15% до 20%, даже считая сравнения прямоугольников контейнеров. Выделение прямоугольников выше произвольно, и вы, вероятно, сможете сделать лучше.

Ответ 3

Исключение, которое вы получаете, исходит из последней строки кода, который вы показываете. Список выражений list[rect] недопустим, поскольку list является классом, а синтаксис [] в этом контексте пытается его проиндексировать. Вероятно, вы хотите просто [rect] (который создает новый список, содержащий единственный элемент rect).

Есть еще несколько основных проблем с вашим кодом. Например, ваш метод Rect.__init__ не устанавливает left атрибут, который, как вы ожидаете, в методе тестирования столкновений. Вы также использовали разную капитализацию для r1 и r2 в разных частях метода overlap (Python не считает, что r1 будет таким же, как R1).

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

list_of_rects = [rect1, rect2, rect3, rect4] # assume these are defined elsewhere

for a, b in itertools.combinations(list_of_rects, 2):
    if a.overlap(b):
        # do whatever you want to do when two rectangles overlap here

Ответ 4

Очевидно, что если ваш список (по крайней мере listB) отсортирован по r2.xmin, вы можете выполнить поиск r1.xmax в listB и прекратить тестирование перекрытия r1 в этом спискеB (остальное будет направо). Это будет O (n · log (n)).

Сортированный вектор имеет более быстрый доступ, чем отсортированный список.

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

Также исправьте функцию overlap() как объяснил cdlane.

Ответ 5

Эта реализация с использованием numpy примерно в 35-40 раз быстрее в соответствии с тестом, который я сделал. Для 2 списков, каждый из которых содержит 10000 случайных прямоугольников, этот метод занял 2,5 секунды, а метод в вопросе занял ~ 90 секунд. С точки зрения сложности это еще O (N ^ 2), как метод в вопросе.

import numpy as np

rects1=[
    [0,10,0,10],
    [0,100,0,100],
]

rects2=[
    [20,50,20,50],
    [200,500,200,500],
    [0,12,0,12]
]

data=np.asarray(rects2)


def find_overlaps(rect,data):
    data=data[data[::,0]<rect[1]]
    data=data[data[::,1]>rect[0]]
    data=data[data[::,2]<rect[3]]
    data=data[data[::,3]>rect[2]]
    return data


for rect in rects1:
    overlaps = find_overlaps(rect,data)
    for overlap in overlaps:
        pass#do something here

Ответ 6

Если вы знаете верхний и нижний пределы для координат, вы можете сузить поиск, разделив координатное пространство на квадраты, например, 100x100.

  • Сделайте один "набор" на квадрат координаты.
  • Проходите через все квадраты, помещая их в "набор" любого квадрата, который они перекрывают.

См. Также Tiled Rendering, которая использует разделы для ускорения графических операций.

    // Stores rectangles which overlap (x, y)..(x+w-1, y+h-1)
    public class RectangleSet
    {
       private List<Rectangle> _overlaps;

       public RectangleSet(int x, int y, int w, int h);
    }

    // Partitions the coordinate space into squares
    public class CoordinateArea
    {
       private const int SquareSize = 100;

       public List<RectangleSet> Squares = new List<RectangleSet>();

       public CoordinateArea(int xmin, int ymin, int xmax, int ymax)
       {
          for (int x = xmin; x <= xmax; x += SquareSize)
          for (int y = ymin; y <= ymax; y += SquareSize)
          {
              Squares.Add(new RectangleSet(x, y, SquareSize, SquareSize);
          }
       }

       // Adds a list of rectangles to the coordinate space
       public void AddRectangles(IEnumerable<Rectangle> list)
       {
          foreach (Rectangle r in list)
          {
              foreach (RectangleSet set in Squares)
              {
                  if (r.Overlaps(set))
                      set.Add(r);
              }
          }
       }
    }

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

CoordinateArea A = new CoordinateArea(-500, -500, +1000, +1000);
CoordinateArea B = new CoordinateArea(-500, -500, +1000, +1000);  // same limits for A, B

A.AddRectangles(listA);
B.AddRectangles(listB);

for (int i = 0; i < listA.Squares.Count; i++)
{
    RectangleSet setA = A[i];
    RectangleSet setB = B[i];

    // *** small number of rectangles, which you can now check thoroghly for overlaps ***

}

Ответ 7

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

Смотрите также: