使用Graph库的多种机制来简化BFS代码

原文链接:http://cpp-prog.com/2009/0705/131_2.html


本文首先从广度优先算法(Breadth First Search,BFS)入手,逐步介绍算法中会用到的一些机制如“外部属性”、“命令参数”、“访问器”等。最后再介绍一些其它的常用图算法。

使用Graph库的多种机制来简化BFS代码

           上例中我们把ColorMap类型直接加入到了图属性中(见main函数前四行),不过很多时候图的类型并不是想改就能改的,而且为了一个算法而加入一个属性几乎是不可接受的。

           Graph库提供了一种称为“外部属性”的机制,只要我们实现了“可读写PropertyMap”概念,就可以使用外部容器来代替图中的属性表,要实现这个概念很简单,只要写两个函数并提供一个类型提取类(traits)就行:

           比如我们可以让vector满足ColorMap概念:


    //get和put必须在包含Boost.Graph头文件之前定义
    template<class T>
    T& get(std::vector<T> &map, size_t s)
    {
        return map[s];
    }
     
    template<class T>
    void put(std::vector<T> &map, size_t s, T col)
    {
        map[s] = col;
    }
     
    //property_traits<vector<T> >必须在包含Boost.Graph头之后定义
    template <class T>
        struct property_traits<vector<T> > {
            typedef size_t key_type;
            typedef T value_type;
            typedef T& reference;
            typedef lvalue_property_map_tag category;
    };

           定义了这些以后,我们就可以这样写BFS算法了,使用vector作为vertex_color的外部属性( so不再需要往图中加入vertex_color属性了):

    vector<default_color_type> colormap(7);
    breadth_first_search(g, 新野,
        queue<unsigned>(), my_visitor(), colormap);

          Graph库里已经帮我们实现了针对原始指针的“可读写PropertyMap”概念方法,所以很多时候直接用指针就行,原代码可如下简化:

    typedef property<vertex_city_t, city_property_t,
        property<vertex_color_t, default_color_type>
    > VertexProperty;
    //把上行改回下面这行
    typedef property<vertex_city_t, city_property_t> VertexProperty;
     
    // BFS
    default_color_type colormap[7];
    breadth_first_search(g, 新野,
        queue<unsigned>(), my_visitor(), &colormap[0]);
     同时,库里还提供了几个property_map的适配器,可以让我们把随机迭代器、std::map转换到property_map接口。这里不再介绍,可以查看<a target=_blank href="http://libs/property_map/property_map.html#sec:property-map-types">官方文档</a>

使用命名参数(bgl_named_params)

          Graph库中的不少算法的参数都比较多,如上面的BFS算法中,一共有5个参数,这还算少的,看看dijkstra_shortest_paths算法,一共有12个参数,傻眼了吧?Graph的设计者考虑到了我们的(以及他们自己的)心理承受能力,就提供了一个称为命名参数的机制,利用它,我们就可以只输入我们关心的参数,而不用管其它参数鸟。

         上例中的BFS代码,我们其实不关心算法中应该使用哪种队列,ColorMap也不想管,我们要的是结果!利用命名参数,我们可以这样写:


    // BFS
    breadth_first_search(g, 新野, visitor(my_visitor()) );

         visitor(my_visitor())这个括号指出了my_visitor()是visitor参数,而不是color_map或Q之类的其它东西。

         它的实现原理简单的说就是在库里实现了一堆函数(如这里的visitor),返回值都是bgl_named_params类型。而这个bgl_named_params类型内部也实现了同样多的同名方法(当然也有visitor),可用于连续指定多个参数。

         所有的图算法都支持两种版本,一种是传统的N多参数的版本,一种是以bgl_named_params作为参数的版本(称为命名参数版本)。

         比如,我们还想关心一下color_map参数,就可以这样写:


    // BFS
    default_color_type colormap[7];
    breadth_first_search(g, 新野,
        visitor(my_visitor()).color_map((default_color_type*)colormap) );

     因为bgl_named_params在内部定义了所有和外部同名的方法,所以这个color_map和visitor的顺序可以随意调换:

    // BFS
    default_color_type colormap[7];
    breadth_first_search(g, 新野,
        color_map((default_color_type*)colormap).visitor(my_visitor()) );


          官方文档中的每个图算法都会指出命名参数版本中可使用的参数,比如我们的breadth_first_search可以在 这里查到有visitor、color_map、vertex_index_map和buffer四个。

事件访问器列表及其适配器(EventVisitorList Adaptor)

           前面的BFS例子中我们继承了 default_bfs_visitor来生成了一个BFS Visitor概念的访问者(Visitor)类。咳,前面的例子离这里有点远了,再抄一遍吧:


    // BFS访问者
    struct my_visitor : default_bfs_visitor {
      template <class Vertex, class Graph>
        void discover_vertex(Vertex u, Graph& g)
        {
            cout << get(vertex_name_t(), g)[u] << endl;
        };
    };


          除了我们自己编写一个Visitor类外,Graph库还提供了另一种的方法——使用事件访问器列表适配器(EventVisitorList Adaptor)。名字好长,可以把它拆分成三个部分:1.事件访问器:这是一个有点象函数对象的东东,它负责Visitor的实际工作;2.列表:把多个事件访问器组合起来;3.适配器:把事件访问器列表转化成符合图算法要求的Visitor对象。

          举个例子先,比如我们上面这个my_visitor,它的作用是当顶点首次被访问时,就输出这个顶点的name属性。用事件访问器列表适配器的方法可以这样写:


    make_bfs_visitor(
        write_property(city,  // city是图的vertex_name_t属性表对象
            std::ostream_iterator<string>(cout, " "),
            on_discover_vertex() )
        );


          这样就得到了一个和my_visitor()一样功能的Visitor对象,其中的write_property是一个事件访问器生成函数,make_bfs_visitor是适配器生成函数,这里没有用上列表,因为my_visitor功能实在是有点单一。

         于是,我们的BFS代码再次可以再次简化:

          删除my_visitor类的定义,把BFS搜索代码改写成:


    breadth_first_search(g, 新野,
        visitor(
            make_bfs_visitor(
                write_property(city,
                    std::ostream_iterator<string>(cout, " "),
                    on_discover_vertex() )
                )
            )
        );


事件访问器(Event Visitors)

          顾名思义,事件访问器列表适配器的基本单元就是事件访问器,上例中的write_property就是一个事件访问器的生成函数,这个函数生成一个叫做property_writer的事件访问器。事件访问器有点象函数对象,它重载operator()并在其中实现Visitor的主要功能。

          同时它需要一个对象来指定这个操作在何时启动,这个对象称为事件标识符,比如上例中的on_discover_vertex()就是一个事件标识符。

          在Graph库中,有这么几种事件标识符,它指出了事件访问器的使用时机:


    struct on_initialize_vertex { }; // 初始化顶点,vertex事件
    struct on_start_vertex { };   // 起始搜索顶点,vertex事件
    struct on_discover_vertex { };// 首次找到顶点,vertex事件
    struct on_examine_edge { }; // 查询当前顶点的边,edge事件
    struct on_tree_edge { };  // 边归入搜索树,即边的另一端顶点属于首次找到的顶点,edge事件
    struct on_cycle_edge { }; // ,edge事件
    struct on_finish_vertex { }; // 对当前顶点访问结束,vertex事件
    struct on_forward_or_cross_edge { }; // ,edge事件
    struct on_back_edge { }; // ,edge事件
    struct on_edge_relaxed { }; // ,edge事件
    struct on_edge_not_relaxed { }; // ,edge事件
    struct on_edge_minimized { }; // ,edge事件
    struct on_edge_not_minimized { }; // ,edge事件


         下面这个就是一个最简单的事件访问器:


    struct my_eventvisitor
    : public base_visitor<my_eventvisitor >
    {
        typedef on_finish_vertex event_filter;
     
        template <class T, class Graph>
        void operator()(T x, Graph&)
        {
            cout << "finished " << x << endl;
        }
    };


            其中的event_filter的类型定义决定了这个事件访问器的启动时机,本例中是在结束一个顶点的访问时启动。重载的operator()是访问器的主体,这里显示一行"finished 顶点"字符串。

            另外,事件访问器都应该从base_visitor继承,这样做的目的是便于统一接口,事实上我们也可以不从base_visitor上继承,比如把上例中的public base_visitor<my_eventvisitor >删除,也可编译通过。

           下面是Graph库提供的事件访问器和它的生成函数,第一个参数都是要求符合“可读写PropertyMap”概念的属性表(所以使用指针也可以啦),最后一个参数是事件标识符。

           记录前趋点(即当前点是从哪个点那里找过来的,搜索树的父节点),用于edge事件


    template <class PredecessorMap, class Tag>
    predecessor_recorder<PredecessorMap, Tag>
        record_predecessors(PredecessorMap pa, Tag);

记录距离(即搜索深度),用于edge事件

    template <class DistanceMap, class Tag>
    distance_recorder<DistanceMap, Tag>
        record_distances(DistanceMap pa, Tag); 

记录时间戳(边或顶点是在第几次被访问到的),可用于edge或vertex事件

    template <class TimeMap, class TimeT, class Tag>
    time_stamper<TimeMap, TimeT, Tag>
        stamp_times(TimeMap pa, TimeT& t, Tag);


把边或顶点的属性值写入到OutputIterator out里。可用于edge或vertex事件

    template <class PropertyMap, class OutputIterator, class Tag>
    property_writer<PropertyMap, OutputIterator, Tag>
        write_property(PropertyMap pa, OutputIterator out, Tag);

列表

            一个事件访问器只能关联一个遍历事件,比如我们刚才的例子,如果除了按搜索顺序输出城市名以外,还想同时记录前趋点、搜索深度等。这时必须得有个机制把多个事件访问器组合成一个对象。

           这个机制的具体方法就是用std::pair连续组合多个事件访问器形成一个列表,比如:


    unsigned order[city_count];
    unsigned pd[city_count];
    int dt[city_count]={0};
     
    make_pair(
        write_property(  // 当首次访问顶点时将序号记录至order数组
            get(vertex_index_t(),g),
            &order[0],
            on_discover_vertex()),
        make_pair(
            record_distances( &dt[0], on_tree_edge() ), //当归入搜索树时记录搜索深度
            record_predecessors( &pd[0], on_tree_edge())//当归入搜索树时记录前趋顶点
            )
        )


           这个例子使用两个std::pair(make_pair是pair的生成函数)把write_propertyrecord_distancesrecord_predecessors生成的三个事件访问器联合成一个列表,这个列表就是事件访问器列表。

适配器

           适配器的作用就是把事件访问器列表转化成符合具体图算法的Visitor对象,Graph库中提供了下面这些适配器,从名字上就可以看出它们的用途:

事件访问器列表转BFS算法Visitor的适配器生成函数

    template <class EventVisitorList>
    bfs_visitor<EventVisitorList>
        make_bfs_visitor(EventVisitorList ev_list); 


事件访问器列表转DFS算法Visitor的适配器生成函数

    template <class EventVisitorList>
    dfs_visitor<EventVisitorList>
    make_dfs_visitor(EventVisitorList ev_list);


事件访问器列表转Dijkstra算法Visitor的适配器生成函数

    template <class EventVisitorList>
    dijkstra_visitor<EventVisitorList>
    make_dijkstra_visitor(EventVisitorList ev_list);


事件访问器列表转Bellman Ford算法Visitor的适配器生成函数

    template <class EventVisitorList>
    bellman_visitor<EventVisitorList>
    make_bellman_visitor(EventVisitorList ev_list); 


事件访问器列表转A*算法Visitor的适配器生成函数

    template <class EventVisitorList>
    astar_visitor<EventVisitorList>
    make_astar_visitor(EventVisitorList ev_list); 


事件访问器、列表、适配器合到一起,代码示例

    // BFS
    unsigned order[city_count];
    unsigned pd[city_count];
    int dt[city_count]={0};
    breadth_first_search(g, 新野,
        visitor(
            make_bfs_visitor(
                make_pair(
                    write_property(  // 当首次访问顶点时将序号记录至order数组
                        get(vertex_index_t(),g),
                        &order[0],
                        on_discover_vertex()),
                    make_pair(
                        record_distances( &dt[0], on_tree_edge() ), //当归入搜索树时记录搜索深度
                        record_predecessors( &pd[0], on_tree_edge())//当归入搜索树时记录前趋顶点
                        )
                    )
                )
            )
        );
    //显示结果
    for(int i=0; i<city_count; i++)
    {
        unsigned cityIdx = order[i];
        cout << city[cityIdx] << endl;
        cout << "搜索深度: " << dt[cityIdx] << endl;
        if(dt[cityIdx] >0) cout << "前趋顶点: " << city[pd[cityIdx]] << endl;
        cout << endl;
    }


输出结果:
新野
搜索深度: 0

许昌
搜索深度: 1
前趋顶点: 新野

洛阳
搜索深度: 2
前趋顶点: 许昌

汝南
搜索深度: 2
前趋顶点: 许昌

陈留
搜索深度: 3
前趋顶点: 洛阳

寿春
搜索深度: 3
前趋顶点: 汝南

小沛
搜索深度: 4
前趋顶点: 陈留






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值