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 provid
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.
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.
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(n2). In fact, this algorithm is for general graphs, but because your graph is rather special — comes from rectangles — you can do even better: it is actually possible to solve the problem in O(n log n) time total, using segment trees.
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.
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;
}
}
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;
}