Compare commits

...

6 commits

Author SHA1 Message Date
Bruno BELANYI dfdd65cd27 posts: generic-flyweight: fix typo
All checks were successful
ci/woodpecker/push/deploy/2 Pipeline was successful
2024-06-27 10:32:27 +00:00
Bruno BELANYI 3226918995 posts: polymorphic-flyweight: fix typo 2024-06-27 10:32:26 +00:00
Bruno BELANYI eb2245db21 posts: union-find: fix typos 2024-06-27 10:32:26 +00:00
Bruno BELANYI fa2849bdba Add Union-Find post
All checks were successful
ci/woodpecker/push/deploy/2 Pipeline was successful
2024-06-26 11:13:58 +00:00
Bruno BELANYI 62c4506eef posts: union-find: add 'union' 2024-06-26 11:13:48 +00:00
Bruno BELANYI 11646adee2 posts: union-find: add 'find' 2024-06-26 11:13:48 +00:00
3 changed files with 76 additions and 2 deletions

View file

@ -16,7 +16,7 @@ favorite: false
The flyweight is a well-known
[GoF](https://en.wikipedia.org/wiki/Design_Patterns) design pattern.
It's intent is to minimize memory usage by reducing the number of instantiations
Its intent is to minimize memory usage by reducing the number of instantiations
of a given object.
I will show you how to implement a robust flyweight in C++, as well as a way to

View file

@ -68,7 +68,7 @@ public:
const std::type_index lhs_i(lhs);
const std::type_index rhs_i(rhs);
if (lhs_i != rhs_i)
returh lhs_i < rhs_i;
return lhs_i < rhs_i;
// We are now assured that both classes have the same type
return less_than(rhs);
}

View file

@ -78,3 +78,77 @@ each element to be a root and make it its own parent (`_parent[i] == i` for all
`i`).
The `_rank` field is an optimization which we will touch on in a later section.
### Find
A naive Implementation of `find(...)` is simple enough to write:
```python
def find(self, elem: int) -> int:
# If `elem` is its own parent, then it is the root of the tree
if (parent := self._parent[elem]) == elem:
return elem
# Otherwise, recurse on the parent
return self.find(parent)
```
However, going back up the chain of parents each time we want to find the root
node (an `O(n)` operation) would make for disastrous performance. Instead we can
do a small optimization called _path splitting_.
```python
def find(self, elem: int) -> int:
while (parent := self._parent[elem]) != elem:
# Replace each parent link by a link to the grand-parent
elem, self._parent[elem] = parent, self._parent[parent]
return elem
```
This flattens the links so that each node links directly to the root, making
each subsequent `find(...)` constant time.
Other compression schemes exist, along the spectrum between faster shortening
the chain faster earlier, or updating `_parent` fewer times per `find(...)`.
### Union
A naive implementation of `union(...)` is simple enough to write:
```python
def union(self, lhs: int, rhs: int) -> int:
# Replace both element by their root parent
lhs = self.find(lhs)
rhs = self.find(rhs)
# arbitrarily merge one into the other
self._parent[rhs] = lhs
# Return the new root
return lhs
```
Once again, improvements can be made. Depending on the order in which we call
`union(...)`, we might end up creating a long chain from the leaf of the tree to
the root node, leading to slower `find(...)` operations. If at all possible, we
would like to keep the trees as shallow as possible.
To do so, we want to avoid merging taller trees into smaller ones, so as to keep
them as balanced as possible. Since a higher tree will result in a slower
`find(...)`, keeping the trees balanced will lead to increased performance.
This is where the `_rank` field we mentioned earlier comes in: the _rank_ of an
element is an upper bound on its height in the tree. By keeping track of this
_approximate_ height, we can keep the trees balanced when merging them.
```python
def union(self, lhs: int, rhs: int) -> int:
lhs = self.find(lhs)
rhs = self.find(rhs)
# Always keep `lhs` as the taller tree
if (self._rank[lhs] < self._rank[rhs])
lhs, rhs = rhs, lhs
# Merge the smaller tree into the taller one
self._parent[rhs] = lhs
# Update the rank when merging trees of approximately the same size
if self._rank[lhs] == self._rank[rhs]:
self._rank[lhs] += 1
return lhs
```