I have a series of rectangles stored in the following class:
struct rectangle
{
rectangle(int x_, int y_, int w_, int h_) :
x{ x_ },
y{ y_ },
w{ w_ },
h{ h_ }
{}
bool contains(int x_, int y_) const
{
return (x_ >= x) &&
(x_ <= (x + w)) &&
(y_ >= y) &&
(y_ <= (y + h));
}
int x{}, y{}, w{}, h{};
};
A fairly simple class that stores the coordinates of the top left corner of the rectangle and its width and height; also provides a function that indicates whether a given point is inside the rectangle.
I need to purge my collection of rectangles so that any rectangle that is contained in another rectangle is removed. For example my data is as follows (full data collection at the end of the thread):
[00] X: 5 Y: 2 W: 3 H: 9 [01] X: 5 Y: 3 W: 3 H: 8 [02] X: 5 Y: 4 W: 4 H: 7 [03] X: 7 Y: 5 W: 7 H: 7 ... [28] X: 7 Y: 9 W: 3 H: 3 [29] X: 6 Y: 10 W: 3 H: 3
And the visual representation of the first four rectangles would be as follows:
The rectangle 0
(filled with vertical lines) contains inside the rectangle 1
(filled with horizontal lines), but although both ( 0
and 1
) touch the rectangles 2
(filled with blue) and 3
(filled with yellow) they do not contain them.
So the only rectangle in the top 4 that should be removed would be the 1
. I had thought that to get the functionality I want a std::set
container would be the right one since it doesn't allow duplicates... I just have to modify what duplicate means in this context, for that I create a free rectangle comparison operator:
bool operator <(const rectangle &l, const rectangle &r)
{
return
r.contains(l.x, l.y) &&
r.contains(l.x + l.w, l.y) &&
r.contains(l.x, l.y + l.h) &&
r.contains(l.x + l.w, l.y + l.h);
}
He std::set
uses total preordering through the concept of comparison using the less-than operator ( <
), so the above implementation will return true if the rectangle to the right of the operation contains within it all the points of the rectangle to the left of the operation. , that is, the right rectangle is larger than the left one. With the sample data the rectangle 0
would not be less than the rectangle 1
but the 1
would be less than 0
.
But when executing the code, not only does it not eliminate the rectangles that are eaten by other rectangles, but it also eliminates rectangles that it should not eliminate. Of my collection of 30 rectangles, the std::set
only keeps the 0
and the 1
!
It's not clear to me if the problem is in my code or my understanding of the container std::set
(or both). I'm thinking of switching to a std::vector
and cleaning the content in a second pass instead of relying on container mechanisms that don't allow duplicates, but I'd rather not.
Is there a way to get the behavior I'm looking for?
[00] X: 5 Y: 2 W: 3 H: 9 [01] X: 5 Y: 3 W: 3 H: 8 [02] X: 5 Y: 4 W: 4 H: 7 [03] X: 7 Y: 5 W: 7 H: 7 [04] X: 8 Y: 5 W: 6 H: 6 [05] X: 9 Y: 5 W: 5 H: 5 [06] X: 10 Y: 5 W: 4 H: 4 [07] X: 11 Y: 5 W: 3 H: 4 [08] X: 5 Y: 6 W: 9 H: 5 [09] X: 6 Y: 6 W: 8 H: 7 [10] X: 7 Y: 6 W: 7 H: 6 [11] X: 8 Y: 6 W: 6 H: 5 [12] X: 9 Y: 6 W: 5 H: 4 [13] X: 10 Y: 6 W: 4 H: 3 [14] X: 11 Y: 6 W: 3 H: 3 [15] X: 5 Y: 7 W: 12 H: 4 [16] X: 6 Y: 7 W: 11 H: 6 [17] X: 7 Y: 7 W: 10 H: 5 [18] X: 8 Y: 7 W: 9 H: 4 [19] X: 9 Y: 7 W: 8 H: 3 [20] X: 13 Y: 7 W: 4 H: 3 [21] X: 14 Y: 7 W: 3 H: 4 [22] X: 5 Y: 8 W: 13 H: 3 [23] X: 6 Y: 8 W: 12 H: 5 [24] X: 7 Y: 8 W: 11 H: 4 [25] X: 8 Y: 8 W: 10 H: 3 [26] X: 14 Y: 8 W: 4 H: 3 [27] X: 6 Y: 9 W: 4 H: 4 [28] X: 7 Y: 9 W: 3 H: 3 [29] X: 6 Y: 10 W: 3 H: 3
std::set
(andstd::map
,std::sort
, etc), does not require the matcher (let's call itcomp
) to implement full preordering; in fact, neither does it require it to be preorder, nor does it require it to be total. What is required of comparators, when they implement<
, is that they implement a strict weak ordering , that is, a weak strict ordering.A couple of definitions before:
Two elements are comparable if
comp(a, b) == true
orcomp(b, a) == true
. Depending on the properties it hascomp
, it will implement<
,<=
,==
, etc.Two elements are equal if they occupy the same position in the range (if
it1 == it2
, then*it1
and*it2
are the same element). Being equivalent is something else. To be equal is to be the same element.If an STL algorithm expects you to
comp
define an equivalence relation (std::equal
), then two elements are equivalent if they are comparable, and therefore ,comp(*it, *it) == true
since two equal elements are assumed to be equal. Of course, different elements could also be equivalent, that's for you to decidecomp
, but it is expected thatcomp
at least be reflexive, since an equivalence relation is.Similarly, if it is expected to
comp
implement<
, then one element is less than another if they are comparable in only one direction. Therefore,comp(*it, *it) == false
, since it<
must filter us equivalent elements.And
std::set
you need me tocomp
implement<
.A preorder requires that every element be comparable with itself (we don't want that), while a strict order requires that no element be comparable with itself (we want that). Furthermore,
<
it can never be a total order. A total order requires that for every pair of elements,comp(a, b) || comp(b, a)
, regardless of who they area
andb
. Therefore, ifa == b
, thencomp(a, a) == true
, which also does not allow us to implement<
. That is why itstd::set
does not exist forcomp
it to be neither preorder nor total.comp
must be irreflexive (a preorder is reflexive), which explicitly prohibits any element from being comparable with itself, which further implies that the order cannot be total. That is why I think it is called a strict order , because it is strictly partial .And what is this weak strict order ? Note that, since the order is partial (not being able to require that every pair be comparable), the comparator can form a "tree" instead of a line of order, where a parent has several children (all younger than it, but not comparable to each other). So we have to add an extra condition to get our line (that is, get a total order except self-comparisons).
The extra condition is that the property of being equivalent (that is, not being comparable:
!comp(*it1, *it2) and !comp(*it2, *it1)
), must be transitive. Suppose the following tree:b and e are equivalent, because they cannot be compared, y and c also, therefore, b and c must be equivalent, and therefore not comparable. Which means that e must also be a parent of c, to break transitivity. And also father of d. And b, father of f, and of g. In this way, if two elements are equivalent, every element less than one must be less than the other, collapsing the tree as follows:
where each level of the original tree is now like a single node of equivalent elements, and we already have an order line. Therefore, although the definition seems "strange" or relaxed, the required comparator is the < usual relation.
In short, you need your comparator to form a line of rectangles, because that is exactly what is required
set
, and nothing less. You must invent an order relationship so that all your rectangles form an ordered line, even if they collide, except that when one is included in the other, they are equivalent, so that the STL takes it as a repeated element.Therefore, you need to first determine if two squares are equivalent (if one is included in the other, so that set doesn't remove them), and if they are, return false. If they are not, you should order them, for example, according to the lower left corner.
However, there are other solutions. It all depends on how you are going to use your structure. Do you need to search for them frequently, or do you need to always have them sorted, for example to delete squares as they come and go? If it is yes, then use a
std::set
, but be careful in what order you put the elements, and that of each group of contained squares, the first one that arrives is the largest. Or if the insert fails, delete and reinsert the new one if it's bigger:If you don't need to have the rectangles continuously sorted, and only want to collapse them once, simply pass a
std::sort
to the structure you use as final storage (for example,std::vector
), and then astd::unique
to delete repeated elements.NOTE: Internally,
std::set
it uses a tree internally, because it uses a red-black tree, a structure that allows you to implement full binary search trees efficiently, and not because your matcher forms a tree (hence perhaps you thought itcomp
was a preorder, mistaking it for partial order).NOTE2: I don't know if any of this will work. It's too late and I haven't compiled it :)
NOTE3:
std::unordered_set
it is also an option, but I have not recommended it because I at least don't know how to use them properly. Or rather, I don't know how to create a hash function properly. Perhaps pass the four attributes of the rectangle to string, and return thehash
one of the concatenated string, but perhaps it is a very inefficient solution.In a std::set it is determined that one element is equal to another using the operator<() when this returns false in both directions of comparison. That is, if you have two rectangles a and b, they will be equal if operator<(a, b) and operator<(b, a) are false.
In your initial case the comparison is false for all the rectangles between them, except precisely between the first two between which an order can be established given the comparison function.
Have you thought about using std::unordered_set redefining the compare function? In this case it would make more sense since you do not establish an order between rectangles but you want to know which ones have "equivalents".