原文链接: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_property、record_distances、record_predecessors生成的三个事件访问器联合成一个列表,这个列表就是事件访问器列表。
适配器
适配器的作用就是把事件访问器列表转化成符合具体图算法的Visitor对象,Graph库中提供了下面这些适配器,从名字上就可以看出它们的用途:
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
前趋顶点: 陈留