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

Posted

技术标签:

【中文标题】如何将一组矩形分组为连接区域的“岛”?【英文标题】:How can I group an array of rectangles into "Islands" of connected regions? 【发布时间】:2011-01-16 07:36:03 【问题描述】:

问题

我有一个java.awt.Rectangles 数组。对于不熟悉这个类的人来说,重要的信息是它们提供了一个.intersects(Rectangle b) 函数。

我想编写一个函数,接收这个Rectangles 数组,并将其分解为相连的矩形组。

比如说,这些是我的矩形(构造函数接受参数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);
    //...

谁能解释一下这个问题?

【问题讨论】:

快速抛开:通常建议您将列表变量声明为 List 类型,并且仅当您实际创建一个全新的列表时才需要指明类型(ArrayList 或 LinkedList 或其他可能是)。 谢谢。我已经更改了代码以反映这一点。 作为后续,我有一个示例小程序here,使用my answer中的代码。 这不会发生在 CodeJam 第 2 轮的问题 C 中吗? :) 来自今年的 Google Code Jam?我不这么认为。查看问题的原始日期。 【参考方案1】:

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

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

在谷歌上搜索了一下,似乎 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) 算法。

【讨论】:

rect 本身不是迭代器,它是一个矩形。当您使用所谓的“for each”语法时,迭代器有点像“幕后”。但是你可以(我认为)用它的所有(恕我直言相对丑陋)hasNext() 和 getNext() 方法等显式公开迭代器。 啊,谢谢,通常我不会用 10 英尺长的杆子接触 Java,所以必须查找所有内容...会更新我的答案。 您对groups.add(group) 的呼叫应该是groups.addAll(group)。此外,使用ArrayLists 的ArrayList 来存储连接的组件有点笨拙。最好使用ArrayListSet s,甚至是SetSets,因为顺序似乎并不重要。 @ntownsend 不,它不应该是 addAll,这将导致组最终成为我们开始的列表,我想添加列表,我不想添加所有元素名单。至于整个 ArrayList 交易,我将其保留为 OP 所拥有的,可能 LinkedLists 在这里会更好,因为元素会以随机顺序从列表中删除。 哎呀!你完全正确。我把它放在那个上面。 LinkedLists 也是一个不错的选择。【参考方案2】:

你不能从你迭代的列表中删除一个对象,不管是否是迭代器对象,你需要找到另一种方法

【讨论】:

这样不对,迭代器有remove方法,就是容器支持的话。【参考方案3】:

(评论太长了)

快速绘图显示 A 与 B 相交:A 的高度为 4,而 B 从 Y 位置 5 开始,它们怎么可能相交!?

您可以使用以下打印出'false'的内容来验证它:

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

那么您的方法签名不完整,您的代码示例也是如此。

如果您稍微澄清一下您的问题并给出一个可行的、正确的示例,那么我为您提供了一个非常好的解决方案。

【讨论】:

无论输入示例是否有缺陷,该算法都应该可以工作,现在该算法不正确,因此即使 A 与 B 相交,它仍然会失败。 我的方法签名不正确是什么意思?我也看不出为什么算法不正确。似乎它应该工作。主要问题是我无法追踪无限循环的来源。 @Eric 你的 mergeIntersectingRects() 说它返回一个 Rectangle[] (矩形数组),但你想要的是更像局部变量 groups:矩形列表的列表。 (或者,就像我的回答一样,可能是矩形集列表,或类似的东西。)当您将groups 转换为 Rectangle[] 时,我不确定那会做什么。 @Eric,您不能在使用 for each 迭代列表时从列表中删除元素,请参阅 ***.com/questions/1196586/… 那个演员不是我。一定是其他人编辑了它。感谢您发现尺寸不匹配。我想我会将其更改为 List>,因为转换为 Rectangle[][] 比它的价值更麻烦。【参考方案4】:

好吧,我想我明白了。这个算法效率相当低,根据 wich 的计算 O(n^3),但它似乎确实有效。

我在getIntersections() 中使用Set 而不是List 来避免对同一个矩形进行两次计数(尽管我认为这实际上没有必要)。我猜你的最终结果甚至可能是Set&lt;Set&lt;Rectangle&gt;&gt;,但算法应该差不多。我还在任何地方都使用了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;
  

【讨论】:

如果我没看错,这个算法将是 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)。 不幸的是,我被列表困住了:我使用的 Java 版本 (LeJOS NXJ) 是一个相当有限的子集,因此不包括 Set 类。但是,我确信我可以解决这个问题。谢谢。知道我的算法有什么缺陷吗? 除了人们提到的“在迭代列表时无法修改列表”问题之外,它似乎还不错。只要您有某种方法可以确保不添加任何重复项(一方面,因为您不想要重复项,另一方面,因为这会导致如果您的算法与我的算法大致相似,则无限循环)。【参考方案5】:

你想要的是找到connected components。也就是说,想象一个图,其顶点对应于矩形,如果对应的矩形相交,则两个顶点之间有一条边。然后,你想找到和label这个图的连通分量。

仅查找边(确定每对矩形是否相交)需要 O(n2) 时间,之后您可以使用 depth-first search 或 breadth-first search 来查找额外 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(n2)。事实上,这个算法是针对一般图的,但是因为你的图比较特殊——来自矩形——你可以做得更好:实际上可以在 O(n log n) 时间内解决这个问题,使用segment trees .

【讨论】:

我认为声称的O(n log n ) 算法可能不正确。我认为它更像O( n^2 log n )。我认为您对分段树的引用等效于有效的扫描线算法(使用 RB 树),并且在这种情况下扫描线算法将运行 O( n^2 log n ),因为它必须在可能存在的所有交叉点处停止为 O(n^2) 的。然而,即便如此,扫描线算法的预期运行时间也比 O(n^2) 方法要好。 O(n log n) 算法肯定是可能的,Imai 和 Asano 在 1983 年就已经展示了 O(n log n) 算法:dx.doi.org/10.1016/0196-6774(83)90012-3 扫描线算法也不必停止在所有的交叉点上,它需要在每个点处扫描线的变化,这是矩形的所有东西边或矩形的所有南北边,这取决于一个扫过x还是y。这自然会导致 2*n 扫描事件。诀窍是在 O(log n) 时间内处理所有事件。 论文说他们通过找到图 G=(V,E) 来找到连通分量,其中顶点集 V 是矩形,边 (u,v) iff 矩形 u 和 v相交。这样的图可能有 O(n^2) 条边(其中 n 是矩形的数量),所以除非它们以不同的方式表示它们的输出,否则它们的声明不可能是真的。 因为只写他们的输出需要 O(n^2) 时间。大多数处理交叉点的扫描算法的运行时间为 O(k log n),其中 k 是交叉点(G 中的边)的数量,这仍然是慢速 O(n^2) 算法的一大改进,因为 k 是“通常”O(n)。从某种意义上说,您可以显示预期时间是 O(n log n),但不是最坏情况下的时间。 @gmatt:你不应该这么怀疑。 :-) 确实,如果您在每个交点处停下来,或者如果您写出整个交点图,则需要 Ω(n^2),但诀窍是 这样做。作者确实在 O(n log n) worst case 中解决了这个问题。 (我没有仔细阅读论文,但考虑寻找联合区域的更简单问题:再次存在 O(n^2) 交点,但使用区间/段树,您只需向左扫-to-right,并在 O(log n) 中添加/删除每个垂直段(当您进入或退出矩形时),总共 O(n log n)。【参考方案6】:

Connected components.

或者,因为你只有矩形,你可以设计一个非常有效的sweep line algorithm。

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

【讨论】:

O(n log n) 绝对可以通过平面扫描算法实现,我正在使用 C++ 进行实现,我将在明天发布。 我期待那个算法,如果它存在的话。 仍在努力在 O(log n) 时间内管理扫描线结构。我有一个边缘案例将我的结构退化为线性深度树,有趣的是,它不是 O(n^2) 交叉点案例,而是一系列嵌套的 Us。不过我会坚持下去,做这个项目太有趣了。 至于 O(n log n) 算法的可能性,Imai 和 Asano 在 1983 年就已经展示了 O(n log n) 算法:dx.doi.org/10.1016/0196-6774(83)90012-3 重点是矩形是正交的因此可以以更智能的方式使用已排序的 x 和 y 坐标。 那篇论文很有趣,因为他们说他们可以在 O(n log n) 时间内找到该图,但该图可以有 O(n^2) 条边,这意味着他们不会将图存储为G=(V,E) 否则他们的 O(n log n) 时间因为 E=O(n^2) 而被打破。在那种情况下,我怀疑他们的算法不完整,我希望我能阅读这篇论文。【参考方案7】:

如果您需要 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) 时间内找到集合。

【讨论】:

【参考方案8】:

这是我最终寻求的解决方案。谁能猜猜它的效率?

包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;
    

【讨论】:

以上是关于如何将一组矩形分组为连接区域的“岛”?的主要内容,如果未能解决你的问题,请参考以下文章

R语言使用treemap包中的treemap函数可视化treemap图:treemap将分层数据显示为一组嵌套矩形自定义设置各个分层分组的线条色彩线条的宽度

PyQt 将 graphicsScene 对象分组为一个对象

将一列的多个结果行连接成一个,按另一列分组[重复]

分组多个边界框

MYSQL 查询数据排序数据和分组数据

将一组 3d 点渲染为矩形,同时保持纵横比