【问题标题】:How to specify tie-breaking logic in boost dijkstra_shortest_paths (dijkstra)如何在 boost dijkstra_shortest_paths (dijkstra) 中指定平局逻辑
【发布时间】:2021-10-20 15:55:57
【问题描述】:

我的问题的背景在这个 SO question 中得到了很好的解释 Is Dijkstra's algorithm deterministic?

具体来说,@Wyck 的回答说明了我想要解决的用例。 图片由@Wyck 提供

我的要求是,当被要求生成从 0 到 5 的路径时,boost 的 dijkstra_shortest_paths(基于提供的图表)总是生成:0->1->5,因为打破平局的逻辑应该使用最低的节点 ID 的总和。

【问题讨论】:

    标签: boost


    【解决方案1】:

    因此,由于缺少代码,这是您使用 BGL 编码的示例:

    Live On Coliru

    #include <boost/graph/adjacency_list.hpp>
    #include <boost/graph/dijkstra_shortest_paths.hpp>
    #include <boost/graph/graphviz.hpp>
    
    struct VertexProps { };
    struct EdgeProps { double weight = 1; };
    using Graph = boost::adjacency_list<boost::vecS, boost::vecS, boost::directedS,
                                        VertexProps, EdgeProps>;
    using V = Graph::vertex_descriptor;
    using E = Graph::edge_descriptor;
    
    Graph make_graph();
    
    int main() {
        auto g = make_graph();
        //write_graphviz(std::cout, g);
    
        std::vector<V> predecessors(num_vertices(g));
        boost::dijkstra_shortest_paths(g, 0,
                                       boost::weight_map(get(&EdgeProps::weight, g))
                                           .predecessor_map(predecessors.data()));
    
        std::cout << "Path: ";
        for (V v = 5; ; v = predecessors[v]) {
            std::cout << " " << v;
            if (v == predecessors[v])
                break;
        }
    }
    
    Graph make_graph() {
        // sample from https://stackoverflow.com/questions/69649312/how-to-specify-tie-breaking-logic-in-boost-dijkstra-shortest-paths-dijkstra
        Graph g;
        add_edge(0, 1, g);
        add_edge(0, 2, g);
        add_edge(0, 3, g);
        add_edge(0, 4, g);
        add_edge(1, 5, g);
        add_edge(2, 5, g);
        add_edge(3, 5, g);
        add_edge(4, 5, g);
    
        return g;
    }
    

    打印出来的

    Path:  5 1 0
    

    但是如果我们颠倒添加边的顺序:

    Graph make_graph() {
        Graph g;
        add_edge(4, 5, g);
        add_edge(3, 5, g);
        add_edge(2, 5, g);
        add_edge(1, 5, g);
        add_edge(0, 4, g);
        add_edge(0, 3, g);
        add_edge(0, 2, g);
        add_edge(0, 1, g);
    
        return g;
    }
    

    现在打印 Live

    Path:  5 1 0
    

    图模型似乎决定了检查顺序。让我们使用有序容器选择器(如setS)进行边缘存储:

    using Graph = boost::adjacency_list<boost::setS, boost::vecS, boost::directedS,
                                        VertexProps, EdgeProps>;
    

    现在我们可以有一个随机的插入顺序:

    #include <random>
    #include <algorithm>
    Graph make_graph() {
        // sample from https://stackoverflow.com/questions/69649312/how-to-specify-tie-breaking-logic-in-boost-dijkstra-shortest-paths-dijkstra
        using namespace std;
        vector ee{pair{0, 1}, {0, 2}, {0, 3}, {0, 4},
                  {1, 5},     {2, 5}, {3, 5}, {4, 5}};
        shuffle(ee.begin(), ee.end(), mt19937{random_device{}()});
    
        Graph g;
        for (auto [s, t] : ee)
            add_edge(s, t, g);
    
        return g;
    }
    

    而且仍然总是得到 (Live):

    Path:  5 1 0
    

    验证解决方案!

    没有测试的代码被破坏了。让我们开始工作吧:

    Graph make_graph() {
        vector ee{
            tuple //
            {0, 1, 1.0},
            {0, 2, 1},
            {0, 3, 1},
            {0, 4, 1},
            {1, 5, 2.0},
            {2, 5, 1},
            {3, 5, 1},
            {4, 5, 1},
        };
        // shuffle(ee.begin(), ee.end(), mt19937{random_device{}()});
    
        Graph g;
        for (auto [s, t, w] : ee)
            add_edge(s, t, EdgeProps{w}, g);
    
        return g;
    }
    

    请注意我们如何单独增加 1 -&gt; 5 边缘的权重。现在我们得到 Live

    Path:  5 4 0
    

    太好了...我们预计这里是 5 2 0。我决定录制一个 BFS 搜索实际进度的动画:Code On Coliru

    现在很明显,中间队列有利于以后的发现。我们需要调整优先级比较。

    自定义权重类型

    让我们尝试使用自定义 Weight 类型而不是 double 来破解它:

    struct Weight {
        double magnitude = 0;
    
        bool operator<(Weight const& rhs) const { return magnitude < rhs.magnitude; }
        bool operator==(Weight const& rhs) const { return magnitude == rhs.magnitude; }
        bool operator!=(Weight const& rhs) const { return magnitude != rhs.magnitude; }
        Weight operator+(Weight const& rhs) const {
            return {magnitude + rhs.magnitude};
        }
        friend std::ostream& operator<<(std::ostream& os, Weight const& w) {
            return os << w.magnitude;
        }
        static Weight Inf() { return {std::numeric_limits<double>::infinity()}; }
    };
    

    比照,这仍然有效:Live On Coliru

    当然,现在的挑战变成了将“累积节点 ID 总和”包含到等式中:

    struct Weight {
        double magnitude = 0;
        size_t cumulative_node_id_sum = 0;
    
        auto both() const { return std::tie(magnitude, cumulative_node_id_sum); }
    
        bool operator<(Weight const& rhs) const { return both() < rhs.both(); }
        bool operator==(Weight const& rhs) const { return both() == rhs.both(); }
        bool operator!=(Weight const& rhs) const { return both() != rhs.both(); }
        Weight operator+(Weight const& rhs) const {
            return Weight{magnitude + rhs.magnitude,
                          cumulative_node_id_sum + rhs.cumulative_node_id_sum};
        }
        friend std::ostream& operator<<(std::ostream& os, Weight const& w) {
            return os << w.magnitude;
        }
        static Weight Inf() {
            return Weight{std::numeric_limits<double>::infinity(), 0};
        }
    };
    

    还是一样(现场)。为什么?因为没有初始权重实际上知道节点id:

    让我们在make_graph中初始化:

    for (auto e : boost::make_iterator_range(edges(g))) {
        g[e].weight.cumulative_node_id_sum = target(e, g);
    }
    

    这会将初始节点 ID 和设置为每条边的目标的顶点 ID。有了它,一切都点击了:

    确实,路径回到了预期:

    Path:  5 2 0
    

    简化/清理

    了解了这一切之后,我们或许可以对该诊断代码进行吸脂手术。我们撒了一点点魔法,让default expression for distance_inf 起作用:

    Live On Coliru

    #include <boost/graph/adjacency_list.hpp>
    #include <boost/graph/dijkstra_shortest_paths.hpp>
    #include <iostream>
    using namespace std::literals;
    
    using Traits = boost::adjacency_list_traits<boost::setS, boost::vecS, boost::directedS>;
    using V      = Traits::vertex_descriptor;
    using E      = Traits::edge_descriptor;
    
    struct Weight {
        double magnitude              = 0;
        size_t cumulative_node_id_sum = 0;
    
        Weight(double magnitude = 0, size_t cumulative_node_id_sum = 0)
            : magnitude(magnitude)
            , cumulative_node_id_sum(cumulative_node_id_sum)
        { }
    
      private:
        auto both() const { return std::tie(magnitude, cumulative_node_id_sum); }
    
      public:
        bool   operator<(Weight const& rhs) const { return both() < rhs.both(); }
        bool   operator==(Weight const& rhs) const { return both() == rhs.both(); }
        bool   operator!=(Weight const& rhs) const { return both() != rhs.both(); }
        Weight operator+(Weight const& rhs) const {
            return Weight{magnitude + rhs.magnitude,
                          cumulative_node_id_sum + rhs.cumulative_node_id_sum};
        }
    };
    
    namespace std {
        template <> struct numeric_limits<Weight> : numeric_limits<double> {
        };
    } // namespace std
    
    struct EdgeProps {
        Weight weight;
    };
    
    using Graph = boost::adjacency_list<boost::setS, boost::vecS, boost::directedS,
                                        boost::no_property, EdgeProps>;
    
    Graph make_graph();
    
    int main()
    {
        auto g = make_graph();
    
        std::vector<V> predecessors(num_vertices(g));
        boost::dijkstra_shortest_paths( //
            g, 0,
            boost::predecessor_map(predecessors.data())
                .weight_map(get(&EdgeProps::weight, g)));
    
        std::cout << "Path: ";
        for (V v = 5;; v = predecessors[v]) {
            std::cout << " " << v;
            if (v == predecessors[v])
                break;
        }
        std::cout << "\n";
    }
    
    #include <random>
    #include <algorithm>
    Graph make_graph()
    {
        // sample from
        // https://stackoverflow.com/questions/69649312/how-to-specify-tie-breaking-logic-in-boost-dijkstra-shortest-paths-dijkstra
        using namespace std;
        vector ee{
            tuple //
            {0, 1, 1.0},
            {0, 2, 1},
            {0, 3, 1},
            {0, 4, 1},
            {1, 5, 2.0},
            {2, 5, 1},
            {3, 5, 1},
            {4, 5, 1},
        };
        shuffle(ee.begin(), ee.end(), mt19937{random_device{}()});
    
        Graph g;
        for (auto [s, t, w] : ee)
            add_edge(s, t, EdgeProps{w}, g);
    
        for (auto e : boost::make_iterator_range(edges(g))) {
            g[e].weight.cumulative_node_id_sum = target(e, g);
        }
    
        return g;
    }
    

    打印信任

    Path:  5 2 0
    

    ¹ 使用 gifsicle -l -O9 -k32 -d 100 frame{0..31}.gif &gt; test.gif 组合的图像

    【讨论】:

    • 代码的动画渲染版本的完整代码包含在 XML 注释中(anti-bitrot)。点击here直接访问
    • 非常感谢您提供如此详细的回答。我希望有一天,我能像你在这里一样清楚地解释一些事情。
    • 其中大部分可能是对工具的掌握 - 我什至欺骗了一些“最终老板”(例如,算法的命名参数重载似乎完全被打破了),或者我继承numeric_limits&lt;Weight的方式@(这巧妙地取决于double 的转换可构建性)。我很高兴它帮助你理解了!
    • 让我借此机会提一下我忘了明确说的话:最终解决方案无论边缘容器如何都有效(例如vecS works just the same)。请注意,更改顶点容器可能会产生雪崩效应:coliru.stacked-crooked.com/a/0b90470d4ac38a14coliru.stacked-crooked.com/a/709c73f5e4a37998。这是因为隐含的顶点索引消失了。
    猜你喜欢
    • 2015-04-30
    • 2012-12-03
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 2011-10-06
    • 2021-09-20
    • 2012-06-23
    • 1970-01-01
    相关资源
    最近更新 更多