1
0
Fork 0

Fix Algorithm.doc

This commit is contained in:
Joris van Rantwijk 2024-07-26 23:48:45 +02:00
parent 123be5a4f2
commit 5af13809b3
2 changed files with 43 additions and 45 deletions

View File

@ -7,7 +7,7 @@ For an edge-weighted graph, a _maximum weight matching_ is a matching that achie
the largest possible sum of weights of matched edges.
The code in this repository is based on a variant of the blossom algorithm that runs in
_O(n \* m \* log(n))_ steps.
_O(n m log n)_ steps.
See the file [Algorithm.md](doc/Algorithm.md) for a detailed description.
@ -44,7 +44,7 @@ The folder [cpp/](cpp/) contains a header-only C++ implementation of maximum wei
**NOTE:**
The C++ code currently implements a slower algorithm that runs in _O(n<sup>3</sup>)_ steps.
I plan to eventually update the C++ code to implement the faster _O(n*m*log(n))_ algorithm.
I plan to eventually update the C++ code to implement the faster _O(n m log n)_ algorithm.
The C++ code is self-contained and can easily be linked into an application.
It is also reasonably efficient.

View File

@ -82,7 +82,7 @@ It has also been shown to be quite fast in practice on several types of graphs
including random graphs [[7]](#mehlhorn_schafer2002).
This algorithm is more difficult to implement than the older _O(n<sup>3</sup>)_ algorithm.
In particular, it requires a specialized data structure to implement mergeable priority queues.
In particular, it requires a specialized data structure to implement concatenable priority queues.
This increases the size and complexity of the code quite a bit.
However, in my opinion the performance improvement is worth the extra effort.
@ -241,7 +241,7 @@ have _slack_.
An augmenting path that consists only of tight edges is _guaranteed_ to increase the weight
of the matching as much as possible.
While searching for an augmenting path, we simply restrict the search to tight edges,
While searching for an augmenting path, we restrict the search to tight edges,
ignoring all edges that have slack.
Certain explicit actions of the algorithm cause edges to become tight or slack.
How this works will be explained later.
@ -277,8 +277,8 @@ an odd-length alternating cycle.
The lowest common ancestor node in the alternating tree forms the beginning and end
of the alternating cycle.
In this case a new blossom must be created by shrinking the cycle.
If the two S-blossoms are in different alternating trees, the edge that links the blossoms
is part of an augmenting path between the roots of the two trees.
On the other hand, if the two S-blossoms are in different alternating trees,
the edge that links the blossoms is part of an augmenting path between the roots of the two trees.
![Figure 3](figures/graph3.png) <br>
*Figure 3: Growing alternating trees*
@ -457,9 +457,9 @@ $$ \pi_{x,y} = u_x + u_y + \sum_{(x,y) \in B} z_B - w_{x,y} $$
An edge is _tight_ if and only if its slack is zero.
Given the values of the dual variables, it is very easy to calculate the slack of an edge
which is not contained in any blossom: simply add the duals of its incident vertices and
which is not contained in any blossom: add the duals of its incident vertices and
subtract the weight.
To check whether an edge is tight, simply compute its slack and check whether it is zero.
To check whether an edge is tight, simply compute its slack and compare it to zero.
Calculating the slack of an edge that is contained in one or more blossoms is a little tricky,
but fortunately we don't need such calculations.
@ -492,7 +492,7 @@ At that point the maximum weight matching has been found.
When the matching algorithm is finished, the constraints can be checked to verify
that the matching is optimal.
This check is simpler and faster than the matching algorithm itself.
It can therefore be a useful way to guard against bugs in the matching algorithm.
It can therefore be a useful way to guard against bugs in the algorithm.
### Rules for updating dual variables
@ -522,7 +522,7 @@ It then changes dual variables as follows:
- _z<sub>B</sub> &larr; z<sub>B</sub> &#x2212; 2 * &delta;_ for every non-trivial T-blossom _B_
Dual variables of unlabeled blossoms and their vertices remain unchanged.
Dual variables _z<sub>B</sub>_ of non-trivial sub-blossoms also remain changed;
Dual variables _z<sub>B</sub>_ of non-trivial sub-blossoms also remain unchanged;
only top-level blossoms have their _z<sub>B</sub>_ updated.
Note that these rules ensure that no change occurs to the slack of any edge which is matched,
@ -566,21 +566,21 @@ to an alternating tree, or expanding a blossom) that allow the algorithm to make
In fact, it is convenient to let the dual update mechanism drive the entire process of discovering
tight edges and growing alternating trees.
In my description of the search algorithm above, I stated that a tight edge between
a newly labeled S-vertex and an unlabeled vertex or a different S-blossom should be used to
grow the alternating tree or to create a new blossom or to form an augmenting path.
In my description of the search algorithm above, I stated that upon discovery of a tight edge
between a newly labeled S-vertex and an unlabeled vertex or a different S-blossom, the edge should
be used to grow the alternating tree or to create a new blossom or to form an augmenting path.
However, it turns out to be easier to postpone the use of such edges until the next delta step.
While scanning newly labeled S-vertices, edges to unlabeled vertices or different S-blossoms
are discovered but not yet used.
Such edges will merely be indexed in a suitable data structure.
Even if the edge is tight, it will be indexed rather than used right away.
Such edges are merely registered in a suitable data structure.
Even if the edge is tight, it is registered rather than used right away.
Once the scan completes, a delta step will be done.
If any tight edges were discovered during the scan, the delta step will find that either
_&delta;<sub>2</sub> = 0_ or _&delta;<sub>3</sub> = 0_.
The corresponding step (growing the alternating tree, creating a blossom or augmenting
the matching) will occur at that point.
If no suitable tight edges exist, a real change of dual variables will occur.
If no suitable tight edges exist, a real (non-zero) change of dual variables will occur.
The search for an augmenting path becomes as follows:
@ -590,7 +590,7 @@ The search for an augmenting path becomes as follows:
Add all vertices inside such blossoms to _Q_.
- Repeat until either an augmenting path is found or _&delta;<sub>1</sub> = 0_:
- Scan all vertices in Q as described earlier.
Build an index of edges to unlabeled vertices or other S-blossoms.
Register edges to unlabeled vertices or other S-blossoms.
Do not yet use such edges to change the alternating tree, even if the edge is tight.
- Calculate _&delta;_ and update dual variables as described above.
- If _&delta; = &delta;<sub>1</sub>_, end the search.
@ -607,7 +607,7 @@ The search for an augmenting path becomes as follows:
- If _&delta; = &delta;<sub>4</sub>_, expand the corresponding T-blossom.
It may seem complicated, but this is actually easier.
The code that scans newly labeled S-vertices, no longer needs to treat tight edges specially.
The code that scans newly labeled S-vertices, no longer needs special treatment of tight edges.
In general, multiple updates of the dual variables are necessary during a single _stage_ of
the algorithm.
@ -665,7 +665,7 @@ All vertices of sub-blossoms that got label S are inserted into _Q_.
The algorithm often needs to find the top-level blossom _B(x)_ that contains a given vertex _x_.
A naive implementation may keep this information is an array where the element with
A naive implementation may keep this information in an array where the element with
index _x_ holds a pointer to blossom _B(x)_.
Lookup in this array would be fast, but keeping the array up-to-date takes too much time.
There can be _O(n)_ stages, and _O(n)_ blossoms can be created or expanded during a stage,
@ -684,8 +684,8 @@ for example by storing a pointer to the blossom inside the queue instance.
When a new blossom is created, the concatenable queues of its sub-blossoms are merged
to form one concatenable queue for the new blossom.
Concatenating two queues produces a new queue that contains all members of the original queues.
This operation takes time _O(log n)_.
The merged queue contains all vertices of the original queues.
Merging a pair of queues takes time _O(log n)_.
To merge the queues of _k_ sub-blossoms, the concatenation step is repeated _k-1_ times,
taking total time _O(k log n)_.
@ -693,7 +693,7 @@ When a blossom is expanded, its concatenable queue is un-concatenated to recover
for the sub-blossoms.
This also takes time _O(log n)_ for each sub-blossom.
Implementation details of a concatenable queue will be discussed later in this document.
Implementation details of concatenable queues are discussed later in this document.
### Lazy updating of dual variables
@ -775,8 +775,7 @@ _&delta;<sub>1</sub>_ is the minimum dual value of any S-vertex.
This value can be computed in constant time.
The dual value of an unmatched vertex is reduced by _&delta;_ during every delta step.
Since all vertex duals start with the same dual value _u<sub>start</sub>_,
all unmatched vertices have dual value _u<sub>start</sub> - &Delta;_,
which is the minimum dual value among all vertices.
all unmatched vertices have dual value _&delta;<sub>1</sub> = u<sub>start</sub> - &Delta;_.
_&delta;<sub>3</sub>_ is half of the minimum slack of any edge between two different S-blossoms.
To compute this efficiently, we keep edges between S-blossoms in a priority queue.
@ -836,15 +835,16 @@ This ensures that the priorities remain unchanged during delta steps.
The priorities also remain unchanged when the T-vertex becomes unlabeled or the unlabeled
vertex becomes a T-vertex.
At the middle level, every T-blossom or unlabeled top-level maintains a priority queue
At the middle level, every T-blossom or unlabeled top-level blossom maintains a priority queue
containing its vertices.
This is in fact the _concatenable priority queue_ instance that is maintained by every
top-level blossom as described earlier in this document.
The priority of each vertex in the queue is set to the minimum priority of any edge
This is in fact the _concatenable priority queue_ that is maintained by every top-level blossom
as was described earlier in this document.
The priority of each vertex in the mid-level queue is set to the minimum priority of any edge
in the low-level queue of that vertex.
If edges are added to (or removed from) the low-level queue, the priority of the corresponding
vertex in the mid-level queue may change.
If the low-level queue of a vertex is empty, that vertex has priority _Inf_ in the mid-level queue.
If the low-level queue of a vertex is empty, that vertex has priority _Infinity_
in the mid-level queue.
At the highest level, unlabeled top-level blossoms are tracked in one global priority queue.
The priority of each blossom in this queue is set to the minimum slack of any edge
@ -862,7 +862,7 @@ The whole thing is a bit tricky, but it works.
### Re-using alternating trees
According to [[5]], labels and alternating trees should be erased at the end of each stage.
According to [[5]](#galil1986), labels and alternating trees should be erased at the end of each stage.
However, the algorithm can be optimized by keeping some of the labels and re-using them
in the next stage.
The optimized algorithm erases _only_ the two alternating trees that are part of
@ -884,7 +884,7 @@ For S-blossoms that lose their labels, the modified vertex dual variables are up
The various priority queues also need updating.
Former T-blossoms must be removed from the priority queue for _&delta;<sub>4</sub>_.
Edges incident on former S-vertices must be removed from the priority queue for _&delta;<sub>3</sub>_.
Edges incident on former S-vertices must be removed from the priority queues for _&delta;<sub>3</sub>_ and _&delta;<sub>2</sub>_.
Finally, S-vertices that become unlabeled need to construct a proper priority queue
of incident edges to other S-vertices for _&delta;<sub>2</sub>_ tracking.
This involves visiting every incident edge of every vertex in each S-blossom that loses its label.
@ -894,8 +894,8 @@ This involves visiting every incident edge of every vertex in each S-blossom tha
Every stage of the algorithm either increases the number of matched vertices by 2 or
ends the matching.
Therefore the number of stages is at most _n/2_.
Every stage runs in _O((n + m) log n)_ steps, therefore the complete algorithm runs in
_O(n (n + m) log n)_ steps.
Every stage runs in time _O((n + m) log n)_, therefore the complete algorithm runs in
time _O(n (n + m) log n)_.
Creating a blossom reduces the number of top-level blossoms by at least 2,
thus limiting the number of simultaneously existing blossoms to _O(n)_.
@ -921,11 +921,11 @@ A blossom also becomes unlabeled at most once, at the end of the stage.
Changing the label of a blossom takes some simple bookkeeping, as well as operations
on priority queues (_&delta;<sub>4</sub>_ for T-blossoms, _&delta;<sub>2</sub>_ for unlabeled
blossoms) which take time _O(log n)_ per blossom.
Assigning label S or removing label S also involves some work per vertex in the blossom,
but I account for that time separately below so I can ignore it here.
Assigning label S or removing label S also involves work for the vertices in the blossom
and their edges, but I account for that time separately below so I can ignore it here.
Blossom labeling thus takes total time _O(n log n)_ per stage.
During each stage, an vertex becomes an S-vertex at most once, and an S-vertex becomes
During each stage, a vertex becomes an S-vertex at most once, and an S-vertex becomes
unlabeled at most once.
In both cases, the incident edges of the affected vertex are scanned and potentially
added to or removed from priority queues.
@ -946,7 +946,7 @@ Also in case of a T-blossom, some sub-blossoms will become S-blossoms and their
vertices become S-vertices, but I have already accounted for that cost above
so I can ignore it here.
Expanding a blossom thus takes time _O(k log n)_.
The number of blossom expansions during a stage is _O(n)_.
Any blossom is involved as a sub-blossom in an expanding blossom at most once per stage.
Blossom expansion thus takes total time _O(n log n)_ per stage.
The length of an augmenting path is _O(n)_.
@ -1017,8 +1017,8 @@ Priority queues are used for a number of purposes:
- a separate priority queue per vertex to find the least-slack edge between that vertex
and any S-vertex.
This type of queue is implemented as a binary heap.
It supports the following operations:
These queues are implemented as a binary heaps.
This type of queue supports the following operations:
- _insert_ a new element with specified priority in time _O(log n)_;
- find the element with _minimum_ priority in time _O(1)_;
@ -1029,11 +1029,11 @@ It supports the following operations:
Each top-level blossom maintains a concatenable priority queue containing its vertices.
We use a specific type of concatenable queue that supports the following operations
[[4]](#galil_micali_gabow1986) [[5]](#aho_hopcroft_ullman1974):
[[4]](#galil_micali_gabow1986) [[8]](#aho_hopcroft_ullman1974):
- _create_ a new queue containing 1 new element;
- find the element with _minimum_ priority in time _O(1)_;
- _change_ the priority of a given element;
- _change_ the priority of a given element in time _O(log n)_;
- _merge_ two queues into one new queue in time _O(log n)_;
- _split_ a queue, thus undoing the previous _merge_ step in time _O(log n)_.
@ -1052,7 +1052,7 @@ Each internal node also stores its height (distance to its leaf nodes).
Only leaf nodes have a priority.
However, each internal node maintains a pointer to the leaf node with minimum priority
within its subtree.
As a consequence, the root of the tree has a pointer to the element with minimum priority.
As a consequence, the root of the tree has a pointer to the least-priority element in the queue.
To keep this information consistent, any change in the priority of a leaf node must
be followed by updating the internal nodes along a path from the leaf node to the root.
The same must be done when the structure of the tree is adjusted.
@ -1060,7 +1060,7 @@ The same must be done when the structure of the tree is adjusted.
The left-to-right order of the leaf nodes is preserved during all operations, including _merge_
and _split_.
When trees _A_ and _B_ are merged, the sequence of leaf nodes in the merged tree will consist of
the leaf nodes _A_ followed by the leaf nodes of _B_.
the leaf nodes of _A_ followed by the leaf nodes of _B_.
Note that the left-to-right order of the leaf nodes is unrelated to the priorities of the elements.
To merge two trees, the root of the smaller tree is inserted as a child of an appropriate node
@ -1087,7 +1087,6 @@ To do this, we assign a _name_ to each concatenable queue instance, which is sim
a pointer to the top-level blossom that maintains the queue.
An extra operation is defined:
_find_ the name of the queue instance that contains a given element in time _O(log n)_.
Implementing the _find_ operation is easy:
Starting at the leaf node that represents the element, follow _parent_ pointers
to the root of the tree.
@ -1285,7 +1284,6 @@ changing all weights by the same amount doesn't change which of these matchings
7. <a id="mehlhorn_schafer2002"></a>
Kurt Mehlhorn, Guido Schäfer, "Implementation of O(nm log(n)) Weighted Matchings in General Graphs: The Power of Data Structures", _Journal of Experimental Algorithmics vol. 7_, 2002.
([link](https://dl.acm.org/doi/10.1145/944618.944622))
([pdf](https://sci-hub.se/https://doi.org/10.1145/944618.944622))
8. <a id="aho_hopcroft_ullman1974"></a>
Alfred V. Aho, John E. Hopcroft, Jeffrey D. Ullman,