2

I am trying to modify the example code available at https://www.boost.org/doc/libs/1_74_0/libs/graph/example/weighted_matching_example.cpp to include floating point edge weights with maximum_weighted_matching algorithm in boost graph library. However, the code seems to be running forever for certain floating point edge weights.

The relevant code modifications are mentioned below.

const int n_vertices = 4;
add_edge(0, 2, EdgeProperty(0.5), g);
add_edge(0, 3, EdgeProperty(0.1), g);
add_edge(1, 2, EdgeProperty(0.4), g);
add_edge(1, 3, EdgeProperty(0.1), g);

I am new to cpp boost library. Can someone please help me understand why the above modifications doesn't work whereas the algorithm works fine for interger edge weights (and for some floating point edge weights as well).

Ram
  • 21
  • 2

1 Answers1

3

The original graph looks like

                              +----+  5   +----+  1   +----+ 6
                              | 1  | ---- | 2  | ---- | 6  |--------------------+
                              +----+      +----+      +----+                    |
                                |                       |                       |
  +-----------------------------+-----------------------+                       |
  |                             |                                               |
  |   +----+  1   +----+  7   +----+  5   +----+  4   +----+  6   +----+  7   +----+
  |   | 0  | ---- | 4  | ---- | 5  | ---- | 9  | ---- | 14 | ---- | 15 | ---- | 10 |
  |   +----+      +----+      +----+      +----+      +----+      +----+      +----+
  |                 |                                                           |
  |                 | 2                                                         |
  |                 |                                                           |
  | 5 +----+      +----+  4   +----+  2   +----+                                |
  |   | 3  |      | 8  | ---- | 13 | ---- | 12 |                                |
  |   +----+      +----+      +----+      +----+                                |
  |     |                4                                                      |
  |     | 4         +-----------------------------------------------------------+
  |     |           |
  |   +----+  5   +----+  6   +----+  5   +----+
  +-- | 7  | ---- | 11 | ---- | 16 | ---- | 17 |
      +----+      +----+      +----+      +----+

Your graph on the other hand looks like

          0.1
      +--------------------------------------+
      |                                      |
    +---+  0.5   +---+  0.4   +---+  0.1   +---+
    | 0 | ------ | 2 | ------ | 1 | ------ | 3 |
    +---+        +---+        +---+        +---+

I note that the bruteforce method works: Live On Coliru

====== brute_force_maximum_weighted_matching ======
Found a matching:
Matching size is 2, total weight is 0.6

The matching is:
{0, 2}
{1, 3}

Indeed as you mentioned, changing the weights to integers did allow it to work:

add_edge(0, 2, EdgeProperty(/*0.*/5), g);
add_edge(0, 3, EdgeProperty(/*0.*/1), g);
add_edge(1, 2, EdgeProperty(/*0.*/4), g);
add_edge(1, 3, EdgeProperty(/*0.*/1), g);

Live On Coliru printing:

In the following graph:
              0.1
          +--------------------------------------+
          |                                      |
        +---+  0.5   +---+  0.4   +---+  0.1   +---+
        | 0 | ------ | 2 | ------ | 1 | ------ | 3 |
        +---+        +---+        +---+        +---+ 
        

====== maximum_weighted_matching ======
Found a matching:
Matching size is 2, total weight is 6

The matching is:
{0, 2}
{1, 3}

====== brute_force_maximum_weighted_matching ======
Found a matching:
Matching size is 2, total weight is 6

The matching is:
{0, 2}
{1, 3}

Why, Though

The $1m question.

I noticed:

  • the weight is float (tried double)

  • there's a declared edge_index property that seems unused. I tried

    • initializing to unique numbers [0..4) for the edges
    • dropping the property

None of this made any difference.

Then I suspected floating point accuracy issues. So I started scaling the weights by factors of 10 (nEx for X = 1, 2, 3, ... so that the total weights would be 6, 60, 600, ...). E.g.

add_edge(0, 2, EdgeProperty(5e5), g);
add_edge(0, 3, EdgeProperty(1e5), g);
add_edge(1, 2, EdgeProperty(4e5), g);
add_edge(1, 3, EdgeProperty(1e5), g);

In this approach X = -1 is identical to your question weights

This started failing at X = 10, again running indefinitely. At that point I selected double instead of float again, and lo and behold: Live On Coliru:

====== maximum_weighted_matching ======
Found a matching:
Matching size is 2, total weight is 6e+10

The matching is:
{0, 2}
{1, 3}

So... Floating Point Strikes Again?

Before jumping to conclusions, I read the documentation on maximum_weighted_matching. First off

Both maximum_weighted_matching and brute_force_maximum_weighted_matching find a maximum weighted matching in any undirected graph.

This is good, because I was wondering since your graph doesn't "look like" the examples used.

The maximum weighted matching problem was solved by Edmonds in [74]. The implementation of maximum_weighted_matching followed Chapter 6, Section 10 of [20] and was written in a consistent style with edmonds_maximum_cardinality_matching because of their algorithmic similarity. In addition, a brute-force verifier brute_force_maximum_weighted_matching simply searches all possible matchings in any graph and selects one with the maximum weight sum.

Ah. This increases the likelihood that there is an implementation specific bug/undocumented limitation the edmonds_maximum_cardinality_matching implementation, which is not present in the brute_force_maximum_weighted_matching variant.

WORKAROUND #1: Use the brute_force_maximum_weighted_matching algorithm instead

Now since

For maximum_weighted_matching, the management of blossoms is much more involved than in the case of max_cardinality_matching

I thought to also apply max_cardinality_matching itself:

    //maximum_weighted_matching(g, &mate[0]);
    boost::edmonds_maximum_cardinality_matching(g, mate.data());

Yes, that works without a hitch for weights that hang up maximum_weighted_matching (obviously, since the weights aren't used). So far, so good.

More Docs...

Why is a verification algorithm needed? Edmonds' algorithm is fairly complex, and it's nearly impossible for a human without a few days of spare time to figure out if the matching produced by edmonds_matching on a graph with, say, 100 vertices and 500 edges is indeed a maximum cardinality matching

Oh. Wow. This doesn't immediately inspire confidence.

But nothing in the docs really gives me any indication why it could hang/suffer extreme worst case behaviour on certain weights.

Since floating point accuracy is at fault/involved, let me present secondary workardounds:

  • Workaround #2: Use long double
  • Workaround #2: Use decimal floats

Workaround #2: Use long double

Annoyingly, this Just Works™

Since this kind of "guess-work" solution feels bad to me, let's be a little bit more methodical:

Workaround #3: Use decimal floats

Boost Multiprecision has our backs. But just

using Weight = boost::multiprecision::cpp_dec_float_50;

doesn't cut it, because BGL uses std::min to get the minimum of two expressions, and due to expression templates the template argument cannot be deduced¹.

So short of fixing that¹ lets disable expression templates:

using Weight = // boost::multiprecision::cpp_dec_float_50;
    boost::multiprecision::number<
        boost::multiprecision::cpp_dec_float<50>,
        boost::multiprecision::et_off >;

This also does the trick: Live On Coliru.

SUMMARY

These are three workarounds. I suggest workaround #1 because it is reliable. It might not suit your performance needs, however.

In that case I'd consider Workaround #3 while also reporting the current test case as a bug with the library developers.

Listing

Anti-bitrot listing of the Workaround #3:

#include <boost/graph/adjacency_list.hpp>
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <boost/graph/maximum_weighted_matching.hpp>
#include <iostream>
#include <string>
#include <vector>

using Weight = // boost::multiprecision::cpp_dec_float_50;
    boost::multiprecision::number<
        boost::multiprecision::cpp_dec_float<50>,
        boost::multiprecision::et_off >;

using EdgeProperty =
    boost::property<boost::edge_weight_t, Weight>;
using my_graph =
    boost::adjacency_list<
        boost::vecS,
        boost::vecS,
        boost::undirectedS,
        boost::no_property,
        EdgeProperty>;

using V = boost::graph_traits<my_graph>::vertex_descriptor;
using E = boost::graph_traits<my_graph>::edge_descriptor;

static auto report(my_graph const& g, std::vector<V> const& mate) {
    auto sum = matching_weight_sum(g, &mate[0]);
    std::cout << "Found a matching:" << std::endl;
    std::cout << "Matching size is " << matching_size(g, &mate[0])
        << ", total weight is " << sum
        << std::endl;
    std::cout << std::endl;

    std::cout << "The matching is:" << std::endl;
    for (V v : boost::make_iterator_range(vertices(g))) {
        if (mate[v] != g.null_vertex() && v < mate[v]) {
            std::cout << "{" << v << ", " << mate[v] << "}" << std::endl;
        }
    }
    std::cout << std::endl;
    return sum;
}

int main() {
    // vertices can be refered by integers because my_graph use vector to store
    // them
    my_graph g(4);
    add_edge(0, 2, EdgeProperty(5e-1), g);
    add_edge(0, 3, EdgeProperty(1e-1), g);
    add_edge(1, 2, EdgeProperty(4e-1), g);
    add_edge(1, 3, EdgeProperty(1e-1), g);

    // print the ascii graph into terminal (better to use fixed-width font)
    std::cout << R"(In the following graph:
              0.1
          +--------------------------------------+
          |                                      |
        +---+  0.5   +---+  0.4   +---+  0.1   +---+
        | 0 | ------ | 2 | ------ | 1 | ------ | 3 |
        +---+        +---+        +---+        +---+ 
        )" << std::endl << std::endl;

    Weight sum1 = 0, sum2 = 0;
    if (1) {
        std::cout << "====== maximum_weighted_matching ======\n";
        std::vector<V> mate(num_vertices(g));
        maximum_weighted_matching(g, &mate[0]);

        sum1 = report(g, mate);
    }

    // now we check the correctness by compare the weight sum to a brute-force
    // matching result note that two matchings may be different because of
    // multiple optimal solutions
    if (1) {
        std::cout << "====== brute_force_maximum_weighted_matching ======\n";
        std::vector<V> mate(num_vertices(g));
        brute_force_maximum_weighted_matching(g, &mate[0]);

        sum2 = report(g, mate);
    }

    assert(sum1 == sum2);
}

¹ this could easily be fixed in the implementation as well:

//delta3 = std::min(delta3, gamma[*vi] / 2);
delta3 = std::min<edge_property_t>(delta3, gamma[*vi] / 2); // SEHE WAS HERE

//delta2 = std::min(delta2, (*bi)->dual_var / 2);
delta2 = std::min<edge_property_t>(delta2, (*bi)->dual_var / 2); // SEHE WAS HERE

I might makemade a pull request for this one

sehe
  • 374,641
  • 47
  • 450
  • 633
  • This is a long read. Perhaps start with the **SUMMARY** section if you're short on time/patience. Cheers – sehe Dec 16 '20 at 21:10
  • [Added the pull request](https://github.com/boostorg/graph/pull/235) to support `cpp_dec_float` – sehe Dec 16 '20 at 21:22
  • 1
    Thank you @sehe for such a detailed explanation and prompt response. I would begin with workaround #1 and switch to workaround #3 in case of performance issues. Many thanks ! – Ram Dec 17 '20 at 12:17