Replace implementation of maximum_weighted_matching()#400
Replace implementation of maximum_weighted_matching()#400jeremy-murphy merged 18 commits intoboostorg:developfrom
Conversation
Without this check, the test program declares all tests passed if it fails to open the input file.
- Hand-picked graphs to explore basic functionality. - Graphs that are known to trigger bugs in the old implementation of maximum_weighted_matching(). - Random small graphs.
The new code runs in O(V^3). It also solves a number of known issues.
dbe033a to
5fa8626
Compare
|
Are the heap data structures in Boost.Heap not sufficient? They are mergeable and mutable. |
The mutable heap in Boost.Heap would work as the "plain" type of heap in the matching algo. But it looks like BGL currently does not use Boost.Heap and I don't know how you feel about adding that dependency. I also need a concatenable heap which does not currently exist in Boost. The merge feature of Boost.Heap is not sufficient. I need to merge heaps in O(log(n)) time with the option to unmerge them later in O(log(n)). The typical way to implement this is with a custom balanced binary tree. It's not rocket science but it adds another 800 lines or so. LEMON and LEDA implement the O(V E log(V)) matching algorithm. It is much faster than O(V^3) on certain classes of sparse graphs. The speedup on random sparse graphs is fairly modest in my experience. And it can be slower on dense graphs. The new code is already an order of magnitude faster than the previous version for graphs with V > 200. My feeling is that the faster algorithm adds a lot of code in exchange for little benefit. But I'm up for the challenge. If you want the best matching algorithm in BGL, I will be happy to work on it. |
|
Thanks for the explanation. There's no problem with adding Boost.Heap as a dependency, as Boost.Graph already depends on many other parts of Boost. Sounds like the efficient algorithm would require adding a new data structure to Boost.Heap to start with, which shouldn't be too difficult, although I'm aware that the maintainer is not all that active any more. Given that the new implementation is much faster anyway, let's defer the efficient algorithm to later. Ultimately it would be nice to have a top-level algorithm that uses a heuristic to pick the fastest algorithm but users are still free to call specific algorithms. (Best of both worlds.) |
I think the concatenable queue may be so special-purpose that it could just stay in BGL, but generalizing it is definitely also a valid option.
Agreed. It occurs to me that the O(V E log(V)) algorithm also needs an
I understand. There is no hurry from my side. Thanks for supporting this effort. |
|
What's the "nearest ancestor" problem referred to in the documentation for the fast Gabow algorithm? Is that LCA or something else? |
To be honest, I don't know. I kept this comment from the documentation by Yi Ji as I saw no reason to remove it. I know about the existence of that fast algorithm. I tried to read the paper by Gabow but I can not make heads or tails of it. Mehlhorn and Schaefer made an offhand remark that this algorithm may be unpractical (https://dl.acm.org/doi/10.1145/944618.944622 page 7) but that was a long time ago. I'm not aware of any public available implementation. |
|
Doesn't surprise me too much. Bender et. al. made a similar remark about the theoretically optimal algorithm for LCA: it just ain't worth it. |
|
Citation LCA remark: https://www.sciencedirect.com/science/article/abs/pii/S0196677405000854 |
jeremy-murphy
left a comment
There was a problem hiding this comment.
I haven't even got to the code proper yet but here are a few requests and questions to start with.
There was a problem hiding this comment.
Sorry, just a trivial request, but since the library is C++14 now can you please use using instead of typedef and remove spaces from in-between nested template <<....>> brackets? Thanks. I will enable clang-format one day...
PS. I mean, a template should be written as
template <typename Foo>, etc.
jeremy-murphy
left a comment
There was a problem hiding this comment.
Not quite finished...
| typedef typename property_traits< VertexIndexMap >::value_type index_t; | ||
| typedef typename std::make_unsigned<index_t>::type unsigned_index_t; | ||
| auto nv = num_vertices(g); | ||
| std::vector<bool> got_vertex(nv); |
There was a problem hiding this comment.
Make sure this is really what you want, as opposed to a bitset.
There was a problem hiding this comment.
I believe std::bitset<N> requires its size to be fixed at compile time. But I need an array which is sized at run time to match the number of vertices of the graph.
There was a problem hiding this comment.
Yes, sorry, I meant Boost's dynamic_bitset.
There was a problem hiding this comment.
Ah ok. vector<bool> and dynamic_bitset both provide the functionality I need. I don't see a specific reason to prefer one or the other.
There was a problem hiding this comment.
I forgot to address this one, whoops.
vector<bool> is generally avoided because of its unusual performance trying to satisfy the standard container interface (providing a reference to each element via iteration) whilst only using one bit per value, which requires the use of a proxy class, etc.
A dynamic bitset is just more honest about what it is and does.
It's not a big deal, might not need to be changed until someone makes some other changes in there.
| template < typename Func > | ||
| static void for_vertices_in_blossom(const blossom_t* blossom, Func func) | ||
| { | ||
| const nontrivial_blossom_t* ntb = blossom->nontrivial(); |
There was a problem hiding this comment.
I don't understand.
Do you mean auto func instead of template <typename Func> ? That was not allowed before C++20.
Or do you mean auto ntb instead of const nontrivial_blossom_t* ntb ?
There was a problem hiding this comment.
Ok. I changed it to auto for these and a few similar verbose declarations.
Is that what you had in mind, or do you want to push further towards the almost-alway-auto style?
There was a problem hiding this comment.
I personally use and recommend AAA style, but what you've done here is fine. I just felt those long typenames with qualifiers were not helping the readability.
Also remove unnecessary < > spaces around template arguments.
| blossom_label_t label; | ||
|
|
||
| /** True if this is an instance of nontrivial_blossom. */ | ||
| const bool is_nontrivial_blossom; |
There was a problem hiding this comment.
No const member variables either, same reason as references, but I'll review again on a proper screen to decide if it's worth changing.
There was a problem hiding this comment.
It's a good general principle, so yeah, please change it.
| typedef typename property_traits< VertexIndexMap >::value_type index_t; | ||
| typedef typename std::make_unsigned<index_t>::type unsigned_index_t; | ||
| auto nv = num_vertices(g); | ||
| std::vector<bool> got_vertex(nv); |
There was a problem hiding this comment.
Yes, sorry, I meant Boost's dynamic_bitset.
| template < typename Func > | ||
| static void for_vertices_in_blossom(const blossom_t* blossom, Func func) | ||
| { | ||
| const nontrivial_blossom_t* ntb = blossom->nontrivial(); |
| = sub_blossom->vertices(); | ||
| for (vertex_vec_iter_t v = sub_vertices.begin(); | ||
| v != sub_vertices.end(); ++v) | ||
| if ((! edge.has_value()) || (s < slack)) |
There was a problem hiding this comment.
I'm personally not in favour of a space between a unary operator such as ! and its operand, same as for other unary operators like dereference *, negation -, etc.
I don't think BGL has a history of it, but maybe I'm wrong?
There was a problem hiding this comment.
Oh, I probably came up with this. I don't see it anywhere else in BGL.
I removed the spaces now.
I really like the space, but this is not the time to start a discussion about code formatting.
|
There are some functions that take a |
|
That's all from me, once the last comments are resolved I'll merge it in. |
I now changed these into |
I know what you mean, sometimes you just have to pay a price for doing things correctly, but it also might mean that those other places using PS. And I realize that sounds hypocritical because I asked you to change the member variables from reference to pointer. Sometimes C++ is just ugly. |
This is a re-implementation of maximum_weighted_matching, based on a paper by Zvi Galil.
The new code runs in time O(V^3).
A new set of testcases are also added.
Resolves #199 #223 #399
The code has been tested extensively on large random graphs, using LEMON as a reference.
Faster algorithms are known for this problem. I initially planned to implement the O(VElog(V)) algorithm by Galil, Micali and Gabow. However it needs a mutable heap with features that are not readily available in the BGL, and it needs a special kind of mergeable priority queue. While possible, I feel that the amount of code would be disproportionate. So I decided to fall back to a simpler O(V^3) algorithm, essentially the same algorithm that inspired the previous implementation.
Feedback is very welcome. I will already mention a few points that may draw criticism:
brute_force_maximum_weighted_matching()unchanged. This function is not very useful in my opinion, but it was part of the public API and there is no need to change it.maximum_weighted_matching()is backwards compatible with the previous code. But I removed the classweighted_augmenting_path_finder, which was essentially an internal detail although it lived in the globalboostnamespace.