如何将一组矩形分组到连接区域的“岛屿”中?

时间:2010-02-12 19:56:10

标签: java algorithm recursion geometry arraylist

问题

我有一个java.awt.Rectangle的数组。对于那些不熟悉这门课程的人来说,重要的信息是它们提供.intersects(Rectangle b)功能。

我想编写一个函数,它接受Rectangle的这个数组,并将其分解为连接的矩形组。

让我们举例说,这些是我的矩形(构造函数接受参数xywidthheight):

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);
    //...
}

有人能解释一下这个问题吗?

8 个答案:

答案 0 :(得分:3)

你想要的是找到connected components。也就是说,想象一个图形,其顶点对应于矩形,并且如果相应的矩形相交,则在两个顶点之间存在边缘。然后,您要查找并label此图表的已连接组件。

找到边缘(确定每对矩形是否相交)需要O(n 2 )时间,之后你可以使用depth-first search或{{3在额外的O(E)时间内找到所有组件,其中E 2

在伪代码(将其转换为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(n 2 )。事实上,这个算法适用于一般图形,但是因为你的图形非常特殊 - 来自矩形 - 你可以做得更好:实际上可以使用{{3来解决O(n log n)时间总计中的问题。 }}

答案 1 :(得分:2)

我不喜欢我的java foo,但我想问题是你在迭代列表时从列表中删除项目。根据容器类型的实现,这可能会有很大的问题。拥有更多Java知识的人可能能够确认或否认这一点。

SO Question似乎证实了我的怀疑。

在google搜索之后,似乎java迭代器支持remove方法,因此不是

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)算法。

答案 2 :(得分:2)

好吧,我想我明白了。这个算法效率很低,通过计算得到了O(n ^ 3),但似乎确实有效。

我在Set中使用了List而不是getIntersections()来防止对同一个矩形进行两次计数(尽管我认为这实际上并不是必需的)。我猜你的最终结果甚至可能是Set<Set<Rectangle>>,但算法应该大致相同。我还在任何地方使用List而不是数组,因为我认为数组是丑陋的,但如果你需要,它很容易转换回来。集合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;
  }

答案 3 :(得分:1)

(评论太长了)

快速绘图 NOT 显示A与B相交:A高度为4而B从Y位置开始为5,它们如何交叉!?

您可以使用打印出'false'的以下内容对其进行验证:

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

然后您的方法签名不完整,所以是您的代码示例。

如果你澄清一下你的问题并给出一个正常的,正确的例子,那么我有一个非常好的解决方案。

答案 4 :(得分:1)

如果你想要一个O(n log n)算法,Imai和Asano在Finding the connected components and a maximum clique of an intersection graph of rectangles in the plane中展示了一个算法。

注意:我仍在使用自己的平面扫描算法在O(n log n)时间内找到该集合。

答案 5 :(得分:1)

这是我最终采用的解决方案。任何人都可以猜测它的效率吗?

package 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;
    }
}

答案 6 :(得分:0)

你无法从迭代通过迭代器对象的列表中删除对象,你需要找到另一种方式

答案 7 :(得分:0)

Connected components

或者,因为您只有矩形,所以您可以设计出非常高效的sweep line algorithm

我希望最好的算法至少花费O( n^2 )时间,因为给定的n矩形有O( n^2 )个可能的交叉点,任何计算你想要的算法都需要考虑所有在某些时候交叉点。