可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
The problem
I have an array of java.awt.Rectangle
s. For those who are not familiar with this class, the important piece of information is that they provide an .intersects(Rectangle b)
function.
I would like to write a function that takes this array of Rectangle
s, and breaks it up into groups of connected rectangles.
Lets say for example, that these are my rectangles (constructor takes the arguments 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 quick drawing shows that A intersects B and B intersects C. D intersects nothing. A tediously drawn piece of ascii art does the job too:
Therefore, the output of my function should be:
new Rectangle[][]{ new Rectangle[] {A,B,C}, new Rectangle[] {D} }
The failed code
This was my attempt at solving the problem:
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; }
Unfortunately, there seems to be an infinite recursion loop going on here. My uneducated guess would be that java does not like me doing this:
for(Rectangle rect : allRects) { allRects.remove(rect); //... }
Can anyone shed some light on the issue?
回答1:
What you want is to find connected components. That is, imagine a graph whose vertices correspond to rectangles, and where there is an edge between two vertices if the corresponding rectangles intersect. Then, you want to find and label the connected components of this graph.
Just finding the edges (determining, for each pair of rectangles, whether they intersect) takes O(n2) time, after which you can use either depth-first search or breadth-first search to find all the components in an additional O(E) time, where E < n2.
In pseudocode (simple exercise to translate it to Java), it may look something like this:
# 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
I'm precomputing neighbors and using "done" labels to save the O(n) factor and make the whole thing O(n2segment trees.
回答2:
Alright, I think I got it. This algorithm is rather inefficient, O(n^3) by wich's calculation, but it does seem to work.
I used Set
instead of List
in getIntersections()
to keep from counting the same rectangle twice (although I don't think this is actually necessary). I guess your final result could even be a Set<Set<Rectangle>>
but the algorithm should be about the same. I also used List
s everywhere instead of arrays because I think arrays are ugly, but it's easy enough to convert back if you need to. The set newRectanglesToBeAdded
lets us decide whether we need to keep looping or not, and also keeps us from adding to a list while we're iterating over it (which is just as bad as trying to remove things from a list while we're iterating over it). I don't think it's the most elegant solution, but it seems to work (at least for the test data you provided).
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:
I'm not up on my java foo, but I guess the problem is that you're removing items from your list while iterating the list. Depending on the implementation of the container type this can have big problems. Someone with more Java knowledge may be able to confirm or deny this.
This SO Question seems to confirm my suspicions.
After googling a bit it seems that java iterators support a remove method, so instead of
allRects.remove(rect);
you should use an iterator and then use
rect_i.remove();
and the same for
list.remove(rect);
Although I think this will still get you in trouble since you are modifying the same list at a lower level in the call stack.
My version:
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); }
Note: I think the code's correct now, but I wrote this up just from reference docs and I'm no Java coder, so you may need to tweak.
As an aside, this type of naive algorithm is fine if you have a small list of rectangles that you need to check, but if you want to do this for very large lists, then you will want to use a much more efficient algorithm. This naive algorithm is O(n^2), a smarter algorithm that first sorts all rectangle corners lexicographically and then performs a plane sweep and does range intersection checking on the sweep line would result in a relatively straightforward O(n log n) algorithm.
回答4:
(this is too long for a comment)
A quick drawing does NOT show that A intersects B: A has an height of 4 while B start at an Y position of 5, how could they intersect!?
You can verify it with the following that prints out 'false':
System.out.println( new Rectangle(0, 0, 2, 4).intersects( new Rectangle(1, 5, 4, 2) ) );
Then your method signature are incomplete so is your code example.
If you clarify a bit your problem and give a working, correct, example, then I've got a very nice solution for you.
回答5:
If you desire an O(n log n) algorithm, one was shown by Imai and Asano in Finding the connected components and a maximum clique of an intersection graph of rectangles in the plane.
Note: I'm still working on my own plane sweep algorithm to find the set in O(n log n) time.
回答6:
This is the solution I went for in the end. Can anyone take a guess to its efficiency?
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; } }
回答7:
You cannot remove an object from a list your iterating through, iterator object or not, you will need to find another way
回答8:
Connected components.
Alternatively, because you only have rectangles you may be able to design an very efficient sweep line algorithm.
I would expect the best algorithm to take at least O( n^2 )
time because given n
rectangles there are O( n^2 )
possible intersections, and any algorithm to compute what you want would need to consider all intersections at some point.