Как я могу сгруппировать массив прямоугольников в острова связанных регионов?

Эта проблема

У меня есть массив java.awt.Rectangles. Для тех, кто не знаком с этим классом, важно знать, что они предоставляют функцию .intersects(Rectangle b).

Я хотел бы написать функцию, которая берет этот массив Rectangles и разбивает его на группы соединенных прямоугольников.

Скажем, например, что это мои прямоугольники (конструктор принимает аргументы x, y, width, height):

Rectangle[] rects = new Rectangle[]
{
    new Rectangle(0, 0, 4, 2), //A
    new Rectangle(1, 1, 2, 4), //B
    new Rectangle(0, 4, 8, 2), //C
    new Rectangle(6, 0, 2, 2) //D
}

Быстрый рисунок показывает, что A пересекает B, а B пересекает C. D ничего не пересекает. Утомительно нарисованный ascii-арт тоже справляется со своей задачей:

┌───────┐   ╔═══╗
│A╔═══╗ │   ║ D ║
└─╫───╫─┘   ╚═══╝
  ║ B ║                 
┌─╫───╫─────────┐
│ ╚═══╝ C       │
└───────────────┘

Поэтому вывод моей функции должен быть:

new Rectangle[][]{
    new Rectangle[] {A,B,C},
    new Rectangle[] {D}
}

Неудачный код

Это была моя попытка решить проблему:

public List<Rectangle> getIntersections(ArrayList<Rectangle> list, Rectangle r)
{
    List<Rectangle> intersections = new ArrayList<Rectangle>();
    for(Rectangle rect : list)
    {

        if(r.intersects(rect))
        {
            list.remove(rect);
            intersections.add(rect);
            intersections.addAll(getIntersections(list, rect));
        }
    }
    return intersections;
}

public List<List<Rectangle>> mergeIntersectingRects(Rectangle... rectArray)
{
    List<Rectangle> allRects = new ArrayList<Rectangle>(rectArray);
    List<List<Rectangle>> groups = new ArrayList<ArrayList<Rectangle>>();
    for(Rectangle rect : allRects)
    {
        allRects.remove(rect);
        ArrayList<Rectangle> group = getIntersections(allRects, rect);
        group.add(rect);
        groups.add(group);
    }
    return groups;
}

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

for(Rectangle rect : allRects)
{
    allRects.remove(rect);
    //...
}

Кто-нибудь может пролить свет на проблему?


person Eric    schedule 12.02.2010    source источник
comment
Небольшое отступление: обычно рекомендуется объявлять переменные списка как тип List, и только когда вы фактически создаете новый список, вам нужно указать тип (ArrayList или LinkedList или что-то еще).   -  person MatrixFrog    schedule 12.02.2010
comment
Спасибо. Я изменил код, чтобы отразить это.   -  person Eric    schedule 13.02.2010
comment
В качестве дополнения у меня есть пример апплета здесь, использующий код в < a href="http://stackoverflow.com/questions/2254697/how-can-i-group-an-array-of-rectangles-into-islands-of-connected-regions/2761225#2761225"> мой ответ < /а>.   -  person Eric    schedule 09.06.2010
comment
Это случайно не вопрос C CodeJam из раунда 2? :)   -  person Peter Alexander    schedule 09.06.2010
comment
Из Google Code Jam этого года? Я так не думаю. Посмотрите на исходную дату вопроса.   -  person MatrixFrog    schedule 09.06.2010
comment
Нет, это должно было быть применено к перекрывающимся прямоугольным каплям, полученным с камеры для проекта робототехники.   -  person Eric    schedule 09.06.2010


Ответы (8)


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

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

В псевдокоде (простое упражнение по его переводу на Java) это может выглядеть примерно так:

# r is the list of rectangles
n = length of r (number of rectangles)

#Determine "neighbors" of each vertex
neighbors = (array of n lists, initially empty)
for i in 1 to n:
    for j in 1 to n:
        if i!=j and r[i].intersects(r[j]):
            neighbors[i].append(j)

#Now find the connected components
components = (empty list of lists)
done = (array of n "False"s)
for i in 1 to n:
    if done[i]: continue
    curComponent = (empty list)
    queue = (list containing just i)
    while queue not empty:
        r = pop(head of queue)
        for s in neighbors[r]:
            if not done[s]:
                done[s] = True
                queue.push(s)
                curComponent.push(s)
    #Everything connected to i has been found
    components.push(curComponent)

return components

Я предварительно вычисляю соседей и использую метки «готово», чтобы сохранить коэффициент O (n) и сделать все это O (n2). На самом деле, этот алгоритм предназначен для обычных графов, но поскольку ваш граф довольно особенный — состоит из прямоугольников, — вы можете сделать еще лучше: на самом деле можно решить задачу за O(n log n) общего времени, используя деревья сегментов.

person ShreevatsaR    schedule 12.02.2010
comment
Я думаю, что заявленный алгоритм O(n log n ), вероятно, не соответствует действительности. Я думаю, что это больше похоже на O( n^2 log n ). Я думаю, что ваша ссылка на деревья сегментов эквивалентна эффективному алгоритму линии развертки (который использует деревья RB), и алгоритм линии развертки в этом случае будет работать O ( n ^ 2 log n ), потому что он должен останавливаться на всех пересечениях, которые могут быть быть O (n ^ 2) из. Однако даже в этом случае ожидаемое время выполнения алгоритма развертки лучше, чем у подхода O( n^2 ). - person ldog; 13.02.2010
comment
Алгоритм O(n log n) определенно возможен, Имаи и Асано показали алгоритм O(n log n) уже в 1983 году: dx.doi.org/10.1016/0196-6774(83)90012-3 Кроме того, алгоритм развертки не должен останавливаться на всех пересечениях, ему нужно для шага в каждой точке меняется линия развертки, которая представляет собой либо все западные и восточные стороны прямоугольников, либо все стороны прямоугольников с севера на юг, в зависимости от того, проходите ли вы по x или y. Что естественно приводит к 2*n событиям развертки. Хитрость заключается в том, чтобы обрабатывать все события за время O(log n). - person wich; 15.02.2010
comment
В документе говорится, что они находят компоненты связности, находя граф G=(V,E), где набор вершин V представляет собой прямоугольники, а ребро (u,v) тогда и только тогда, когда прямоугольники u и v пересекаются. Такой граф может иметь O (n ^ 2) ребер (где n - количество прямоугольников), поэтому, если они не представляют свой вывод по-разному, их утверждение не может быть верным. - person ldog; 15.02.2010
comment
поскольку просто запись их вывода потребует времени O (n ^ 2). Большинство алгоритмов развертки, имеющих дело с пересечениями, имеют время выполнения O(k log n), где k — количество пересечений (ребер в G), что по-прежнему является большим улучшением по сравнению с медленным алгоритмом O(n^2), поскольку k обычно На). В некотором смысле вы можете показать, что ожидаемое время равно O(n log n), но не время наихудшего случая. - person ldog; 15.02.2010
comment
@gmatt: Не стоит быть таким скептичным. :-) Это правда, что если вы остановитесь в каждой точке пересечения или если вы выпишете весь граф пересечений, вам понадобится Ω(n^2), но фокус в том, чтобы не этого делать. Авторы действительно решают проблему за O(n log n) в наихудшем случае. (Я не читал статьи внимательно, но рассмотрю более простую задачу нахождения, скажем, площади объединения: снова есть O(n^2) точек пересечения, но с деревом интервалов/сегментов вы просто проводите влево -вправо и добавить/удалить каждый вертикальный сегмент (при входе или выходе из прямоугольника) за O(log n), всего O(n log n). - person ShreevatsaR; 15.02.2010
comment
Не было места для ссылок на документы, доступные в Интернете: см. cs.duke.edu/~edels/Papers/1984-J-01-ConnectedComponents.pdf от Edelsbrunner et al. который цитируется для алгоритма O (n log n), а также эта статья, которая может сделать это O (n log u / log log u), где u — размер вселенной (наибольшие числа на входе): cs.uu.nl/research/techreps/repo/CS- 1986/1986-18.pdf - person ShreevatsaR; 15.02.2010
comment
Проблема ШривацаР с таким алгоритмом развертки заключается не в вставке и удалении вертикальных отрезков, а в том, что для объединения всех пересекающихся групп необходимо найти в дереве интервалов все интервалы, с которыми пересекается вновь вставленный интервал, что является запросом диапазона между у_низкий и у_высокий. Однако это операция O(log n + m), где m — количество зарегистрированных пересечений, которое в нашем случае может быть O(n). Результатом такого алгоритма будет алгоритм O (n log n + k), где k — количество пересечений, на которое ссылается gmatt. - person wich; 15.02.2010
comment
Однако дело в том, что после объединения группы интервалов не нужно сообщать их все каждый раз, когда их пересекает новый отрезок, как только вы узнаете, что новые отрезки пересекают один из них, вы можете забыть об остальных, т.е. хитрость заключается в том, чтобы сделать это правильно. - person wich; 15.02.2010
comment
что вы правильно поняли мою точку зрения об O (n log n + k), где k - количество пересечений. Ваша точка зрения, если я правильно понимаю, заключается в том, что, как только вы определите, что два или более прямоугольника пересекаются, вы можете рассматривать их как одну единицу (или связанный компонент), что упростит тот граф, на который я ссылался ранее. Я думаю, что в этом случае вы будете преобразовывать граф (группировать несколько узлов, чтобы они стали одним узлом), и поэтому вам придется повторно связать существующие ребра, чтобы общее количество операций не изменилось. Я прочитаю эти бумаги, опубликованные Шривацаром, и посмотрю, что они говорят. - person ldog; 15.02.2010
comment
Вы ничего не связываете заново, поскольку этого графа вообще не существует, это просто подразумеваемый граф. В любой момент времени нет физического представления графа пересечений. - person wich; 16.02.2010

Ладно, думаю, я понял. Этот алгоритм довольно неэффективен, O (n ^ 3) по расчету, но, похоже, он работает.

Я использовал Set вместо List в getIntersections(), чтобы не считать один и тот же прямоугольник дважды (хотя я не думаю, что это действительно необходимо). Я предполагаю, что ваш конечный результат может быть даже Set<Set<Rectangle>>, но алгоритм должен быть примерно таким же. Я также везде использовал Lists вместо массивов, потому что я думаю, что массивы уродливы, но их достаточно легко преобразовать обратно, если вам нужно. Набор newRectanglesToBeAdded позволяет нам решить, нужно ли нам продолжать цикл или нет, а также не дает нам добавлять в список, пока мы его итерируем (что так же плохо, как пытаться удалить что-то из списка во время итерации). над ним). Я не думаю, что это самое элегантное решение, но, похоже, оно работает (по крайней мере, для предоставленных вами тестовых данных).

  public static Set<Rectangle> getIntersections(List<Rectangle> list,
      Rectangle r) {
    Set<Rectangle> intersections = new HashSet<Rectangle>();
    intersections.add(r);

    Set<Rectangle> newIntersectionsToBeAdded = new HashSet<Rectangle>();

    do {
      newIntersectionsToBeAdded.clear();
      for (Rectangle r1 : list) {
        for (Rectangle r2 : intersections) {
          if (!intersections.contains(r1) && r2.intersects(r1)) {
            newIntersectionsToBeAdded.add(r1);
          }
        }
      }
      intersections.addAll(newIntersectionsToBeAdded);
    } while (!newIntersectionsToBeAdded.isEmpty());
    return intersections;
  }

  public static List<Set<Rectangle>> mergeIntersectingRects(List<Rectangle> allRects) {
    List<Set<Rectangle>> grouped = new ArrayList<Set<Rectangle>>();
    while (!allRects.isEmpty()) {
      Set<Rectangle> intersections = getIntersections(allRects, allRects.get(0));
      grouped.add(intersections);
      allRects.removeAll(intersections);
    }
    return grouped;
  }
person MatrixFrog    schedule 12.02.2010
comment
Если я правильно понимаю, этот алгоритм будет O (n ^ 3) ... вероятно, не очень хорошая идея. Один из двух худших примеров; список из n прямоугольников, где каждый прямоугольник r_i пересекается с r_{i-1} и r_{i+1}, но не с другими. Тогда do ... while будет добавлять только один прямоугольник на каждой итерации, то есть n итераций. for (r1) всегда проходит по всему списку, поэтому n итераций, for (r2) проходит по выбору до настоящего момента, в диапазоне от 0 до n или в среднем n/2. К счастью, while (!empty) запустится только один раз. Это приводит к O (n * n * n/2) = O (n ^ 3). - person wich; 13.02.2010
comment
К сожалению, я застрял со списками: версия java, которую я использую (LeJOS NXJ), является довольно ограниченным подмножеством и, следовательно, не включает класс Set. Однако я уверен, что смогу обойти это. Спасибо. Есть идеи, в чем недостаток моего алгоритма? - person Eric; 13.02.2010
comment
Помимо того, что вы не можете изменить список, пока вы повторяете его проблему, о которой упоминали люди, это кажется прекрасным. Установка/список не должна быть проблемой, если у вас есть какой-то способ гарантировать, что вы не добавите никаких дубликатов (с одной стороны, потому что вам не нужны дубликаты, а с другой, потому что это вызовет бесконечный цикл, если ваш алгоритм примерно похож на мой). - person MatrixFrog; 13.02.2010

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

Этот SO Question, похоже, подтверждает мои подозрения.

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

allRects.remove(rect);

вы должны использовать итератор, а затем использовать

rect_i.remove();

и то же самое для

list.remove(rect);

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

Моя версия:

ArrayList<Rectangle> rects = new ArrayList<Rectangle>(rectArray);
ArrayList<ArrayList<Rectangle>> groups = new ArrayList<ArrayList<Rectangle>>();
while (!rects.isEmpty)
{
    ArrayList<Rectangle> group = new ArrayList<Rectangle>();
    ArrayList<Rectangle> queue = new ArrayList<Rectangle>();
    queue.add(rects.remove(0));
    while (!queue.isEmpty)
    {
        rect_0 = queue.remove(0);
        rect_i = rects.iterator();
        while (rect_i.hasNext())
        {
            Rectangle rect_1 = rect_i.next();
            if (rect_0.intersects(rect_1))
            {
                queue.add(rect_1);
                rect_i.remove();
            }
        }
        group.add(rect_0);
    }
    groups.add(group);
}

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

Кроме того, этот тип наивного алгоритма хорош, если у вас есть небольшой список прямоугольников, которые вам нужно проверить, но если вы хотите сделать это для очень больших списков, вам нужно будет использовать гораздо более эффективный алгоритм. Этот наивный алгоритм — O(n^2), более умный алгоритм, который сначала сортирует все углы прямоугольника лексикографически, а затем выполняет развертку плоскости и выполняет проверку пересечения диапазонов на линии развертки, приведет к относительно простому алгоритму O(n log n).

person wich    schedule 12.02.2010
comment
rect сам по себе не итератор, это прямоугольник. Итератор находится как бы за кулисами, когда вы используете так называемый синтаксис for each. Но вы могли бы (я думаю) явно указать итератор со всеми его (ИМХО относительно уродливыми) методами hasNext() и getNext() и т.д. - person MatrixFrog; 12.02.2010
comment
ах, спасибо, обычно я не трогаю Java 10-футовым шестом, так что приходится все искать... Обновлю свой ответ. - person wich; 12.02.2010
comment
Ваш звонок groups.add(group) должен быть groups.addAll(group). Более того, использование ArrayList из ArrayLists для хранения подключенных компонентов довольно неуклюже. Возможно, лучше использовать ArrayList из Set с или даже Set из Set с, поскольку порядок не имеет значения. - person ntownsend; 13.02.2010
comment
@ntownsend нет, это не должно быть addAll, это приведет к тому, что группы станут списком, с которого мы начали, я хочу добавить список, я не хочу добавлять все элементы в список. Что касается всей сделки с ArrayList, я сохранил ее в том виде, в котором она была у OP, вероятно, LinkedLists здесь были бы лучше, учитывая, что элементы будут удалены из списка в значительной степени в случайном порядке. - person wich; 13.02.2010
comment
Упс! Вы совершенно правы. Я разорвал его на этом. LinkedLists также является хорошим вызовом. - person ntownsend; 13.02.2010

Если вам нужен алгоритм O(n log n), один из них был показан Имаи и Асано в Нахождение компонент связности и максимальной клики графа пересечений прямоугольников на плоскости.

Примечание. Я все еще работаю над своим собственным алгоритмом развертки плоскости, чтобы найти набор за время O (n log n).

person wich    schedule 15.02.2010

(это слишком долго для комментария)

Быстрый рисунок НЕ показывает, что A пересекается с B: A имеет высоту 4, а B начинается с позиции Y, равной 5, как они могут пересекаться!?

Вы можете проверить это следующим образом, который выводит «false»:

System.out.println( new Rectangle(0, 0, 2, 4).intersects( new Rectangle(1, 5, 4, 2) ) );

Тогда ваша подпись метода неполна, как и ваш пример кода.

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

person SyntaxT3rr0r    schedule 12.02.2010
comment
Алгоритм должен работать независимо от ошибочности входного примера, прямо сейчас алгоритм неверен, поэтому даже если A будет пересекаться с B, он все равно потерпит неудачу. - person wich; 13.02.2010
comment
Что именно вы имеете в виду, говоря, что сигнатуры моего метода неверны? Я также не понимаю, почему алгоритм неверен. Вроде должно работать. Основная проблема в том, что я не могу отследить источник бесконечного цикла. - person Eric; 13.02.2010
comment
@Eric ваш mergeIntersectingRects() говорит, что он возвращает Rectangle[] (массив прямоугольников), но вам нужно что-то больше похожее на локальную переменную groups: список списков прямоугольников. (Или, как в моем ответе, может быть, Список наборов прямоугольников или что-то в этом роде.) Когда вы приводите groups к Rectangle[], я не уверен, что это будет делать. - person MatrixFrog; 13.02.2010
comment
@Eric, вы не можете удалять элементы из списка, перебирая его, используя для каждого, см. stackoverflow.com/questions/1196586/ - person wich; 13.02.2010
comment
Этот актерский состав был не я. Кто-то другой, должно быть, отредактировал его. Спасибо, что заметили, что размеры не совпадают. Думаю, я изменю его на List‹List‹Rectangle››, так как преобразование в Rectangle[][] доставляет больше хлопот, чем оно того стоит. - person Eric; 13.02.2010

Это решение, к которому я пришел в конце концов. Кто-нибудь может предположить его эффективность?

пакет java.util;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;

public class RectGroup extends ArrayList<Rectangle> implements List<Rectangle>
{
    public RectGroup(Rectangle... rects)
    {
            super(rects);
    }

    public RectGroup()
    {
        super();
    }

    public boolean intersects(Rectangle rect)
    {
        for(Rectangle r : this)
            if(rect.intersects(r))
                return true;

        return false;
    }

    public List<RectGroup> getDistinctGroups()
    {
        List<RectGroup> groups = new ArrayList<RectGroup>();
        // Create a list of groups to hold grouped rectangles.

        for(Rectangle newRect : this)
        {
            List<RectGroup> newGroupings = new ArrayList<RectGroup>();
            // Create a list of groups that the current rectangle intersects.

            for(RectGroup group : groups)
                if(group.intersects(newRect))
                    newGroupings.add(group);
            // Find all intersecting groups

            RectGroup newGroup = new RectGroup(newRect);
            // Create a new group

            for(List<Rectangle> oldGroup : newGroupings)
            {
                groups.remove(oldGroup);
                newGroup.addAll(oldGroup);
            }
            // And merge all the intersecting groups into it

            groups.add(newGroup);
            // Add it to the original list of groups
        }
        return groups;
    }
}
person Eric    schedule 03.05.2010

Вы не можете удалить объект из списка, который вы итерируете, объект итератора или нет, вам нужно будет найти другой способ

person walnutmon    schedule 12.02.2010
comment
Это неправильно, у итераторов есть метод удаления, то есть если контейнер его поддерживает. - person wich; 12.02.2010

Подключенные компоненты.

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

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

person ldog    schedule 12.02.2010
comment
O (n log n) определенно возможно с алгоритмом развертки плоскости, я работаю над реализацией на C ++, которую опубликую завтра. - person wich; 13.02.2010
comment
Я с нетерпением жду этого алгоритма, если он существует. - person ldog; 13.02.2010
comment
Все еще изо всех сил пытаюсь управлять структурой линии развертки за время O (log n). У меня есть один пограничный случай, который вырождает мою структуру в линейное дерево глубины, которое, как ни странно, является не случаем пересечений O (n ^ 2), а серией вложенных Us. Я буду настойчив, хотя, слишком много удовольствия, чтобы делать этот проект. - person wich; 15.02.2010
comment
Что касается возможности алгоритма O(n log n), Имаи и Асано показали алгоритм O(n log n) уже в 1983 году: dx.doi.org/10.1016/0196-6774(83)90012-3 Дело в том, что прямоугольники ортогональны, поэтому можно использовать отсортированные x и y координаты более разумным способом. - person wich; 15.02.2010
comment
эта статья интересна, потому что они говорят, что могут найти граф за время O ( n log n ), но этот граф может иметь O ( n ^ 2) ребер, что означает, что они не хранят граф как G = (V, E) в противном случае их время O (n log n) нарушено, поскольку E = O (n ^ 2). В таком случае я подозреваю, что их алгоритм неполный, я хотел бы прочитать статью. - person ldog; 15.02.2010
comment
Я думаю, они хотели написать, что алгоритм O(k logn n), где k — количество пересечений, что для меня гораздо более правдоподобно. - person ldog; 15.02.2010
comment
Нет, это определенно O(n log n) с n количеством прямоугольников, вам вообще не нужно беспокоиться о количестве пересечений, вы не проверяете их все и не сообщаете о них всех, вы сообщаете только о подключенных компонентах. - person wich; 15.02.2010