本文翻译自"Manipulating C++ Graph Data Structures with the Boost Graph Library",原文请见:
http://www.informit.com/articles/article.asp?p=673259&seqNum=4
使用Boost创建一个简单的图
如果你不得不使用图算法,你要么自己构建图数据结构,或者使用第三方库。本文所使用的是Boost库。让我们通过第一个例子来展示如何使用Boost库以支持图算法。
一般地,在计算机中图可以表示为一组顶点和一组连接顶点的边的集合;边简单地表示为顶点对。进一步每一个顶点和每一条边可能还依附着额外的数据,比如对边来说会有一个数来表示权重。这就是Boost库如何工作的本质。
Boost Graph library是以模板的形式提供的,这意味着你可以在这些模板的基础上创建自己的类型。然而,这些模板的所有类型参数都有默认值,所以你无需任何定制,就可以创建一个图。
下面的一小段代码展示了创建一个图和插入顶点和边的过程,这段代码可以被完全编译,但它除了创建一个图外,并没有做其他什么事情。
#include <iostream>
#include <stdlib.h>
#include <boost/graph/adjacency_list.hpp>
using namespace boost;
int main(int argc, char *argv[])
{
adjacency_list<> mygraph;
add_edge(1, 2, mygraph);
add_edge(1, 3, mygraph);
add_edge(1, 4, mygraph);
add_edge(2, 4, mygraph);
return 0;
}
mygraph变量就是一个图,其类型是adjacency_list,它是图库中基本的图模板类型。在这里我们使用了默认参数,在<>中的参数列表是空的。
函数add_edge是一个模板协助函数,它用来给图增加顶点或边的。为了使用它,你需要传递两个顶点参数,如果在该图中这些顶点不存在,则它们会被加入;然后连接这两个顶点的边也会被加入。这样,上面的这段代码就创建了一个图,它具有四个顶点,编号是1,2,3,4,边有(1,2)、(1,3)、(1,4)、(2,4)。
这只是一个很基本的例子。为了创建更加复杂的图,你需要定义更多的数据关联到你的边和顶点,我们将在下一节讨论。
给顶点和边增加属性值
当你使用图,通常你会附加一些信息给顶点和边,图库使用了一个很有趣的方法来做到这一点。诸如名字或颜色信息作为一种属性来存储,一个属性由一个名字(或标识),如距离,和类型,如浮点数或字符串,组成。你可以给顶点和边增加属性。
提示
在图库中,有几个常用的,已经内建好了的属性标识,如颜色,名字和权重。所有的内建标识都列在在线帮助中,同样你可以在graph/properties.hpp这个头文件中找到它们。它们是用宏的方式来定义的,形成了枚举常量。比如,在properties.hpp中有一行属性定义:
BOOST_DEF_PROPERTY(edge, name);
这个宏的结果是产生了一个枚举常量edge_name,它的类型是edge_name_t。你还能看到其他的内建属性标识定义。
当你定义一个图时,你可以指定跟该图相关联的的属性类型,你也可以加入你很多想要的属性给顶点和边。为了定义属性,首先你需要为每一个属性类型创建一个模板类型,然后在你具现化图之前定义图类型。比如,假设你想每一条边增加距离属性,给每个顶点赋予城市名称,下面的代码就来展示如何定义这两种属性类型:
typedef property<edge_weight_t, int> EdgeWeightProperty;
typedef property<vertex_name_t, std::string> VertexNameProperty;
第一行定义了EdgeWeightProperty类型,模板的第一个参数是这个属性的标识,在这里我们使用的是预先定义好的标识edge_weight_t,它的意思是边的权重,第二个参数是int,意思是这个属性具有一个整型数值。第二行定义了第二个属性,它的标识是vertex_name_t,类型是string。
现在你已经定义好属性,你可以定义一个图类型了:
typedef adjacency_list<vecS, vecS, undirectedS,
VertexNameProperty, EdgeWeightProperty> Graph;
这个定义中的前三个模板参数是预先定义好的,第一和第二个参数(都是vecS)表示我们要求图使用vector来作为图的内部存储方式,第三个参数说明这是一个无向图,第四、五个参数是我们刚刚定义的属性类型,前者是顶点的属性类型,后者是边的属性类型。
这个时候,你可能会有疑问:如何给顶点和边指定不止一个属性类型呢?事实上,可以把属性串联在一起。基本类型模板包含了一个模板参数,它可以是你已经定义的属性类型。下面是顶点类型定义,不过,这时我们定义了为顶点定义了两个属性,一个是name,一个是index(下面的代码使用了预定义的标识vertex_index2_t):
typedef property<vertex_index2_t, int>
VertexIndexProperty;
typedef property<vertex_name_t, std::string,
VertexIndexProperty> VertexNameProperty;
注意:
这里使用的是vertex_index2_t,而不是vertex_index_t,这是因为后者有一些与它有关的内建模板已经做了我们正在做的。
我们首先定义了index属性,不过这个顺序无关紧要。属性VertexNameProperty就包含了VertexIndexProperty作为它的第三个参数,这样就创建了一个链。然后你也可以象前面一样将VertexNameProperty作为第四个参数传递给图定义。
你也可以精简你的代码,不过精简过的代码对于图库初学者来说可读性要差一些,虽然一旦你理解了属性的这种机制后,这种精简的过程也是十分的直接。
typedef property<vertex_name_t, std::string,
property<vertex_index2_t, int> > VertexProperties;
我们重命名了类型VertexProperties,这是由于它现在包含了两种不同的属性。有个地方请大家注意,C++标准要求在两个>之间要有空格,这是为了同>>运算符区分开来。别忘了这个空格,否则你将得到编译错误。
操纵属性
为了访问图中的属性,你需要从图库中获得一个协助对象,对于每一种属性类型你都需要一个协助对象。这些对象是模板对象,一旦你创建了一个图类型的实例后,你就可以获得它。下面是获得前一节定义的三个属性的代码:
property_map<Graph, vertex_name_t>::type
city_name = get(vertex_name, g);
property_map<Graph, vertex_index2_t>::type
city_index2 = get(vertex_index2, g);
property_map<Graph, edge_weight_t>::type
edge_distance = get(edge_weight, g);
别担心这里的函数名字是一个很通用的单词get,这个函数是得到很好定义的模板函数,它会编译的很好。
第一条语句创建了一个对象来获得vertex_name属性,仔细观察这条语句的参数,在最左边,传递给模板的参数Graph是前面定义过的,紧跟着是属性的标识vertex_name_t;右边,在get函数里,第一个参数并不是属性的标识名,而是一个与该属性相关的枚举类型vertex_name。每一个预定义的标识名都有一个枚举类型。get函数的第二个参数是图对象g。
请记住,这里的三个调用你可以用来获得顶点的属性和边的属性,虽然我们这里给出的代码片段是针对顶点来做的。下面的一行代码将来要用于获得属性:
std::cout << city_name[*vp.first];
这里的vp.first是一个指向vertex的指针。对象city_name象map一样地工作,它将一个顶点对象作为参数,然后返回该顶点合适的属性,在这个例子中,返回的是城市名。
加入顶点和边
在你指定了属性类型和定义了图类型之后,你已经可以去创建一个图实例,然后向图结构中加入顶点和边,这些顶点和边是具有你所定义的属性的。
让我们从一个新的图的开始,定义一些属性,然后加入一些带属性的顶点和边。我们将给出所有的代码,这样你不需要将我们前面给出的代码片段拼接起来。
// Property types
typedef property<edge_weight_t, int> EdgeWeightProperty;
typedef property<vertex_name_t, std::string,
property<vertex_index2_t, int> > VertexProperties;
// Graph type
typedef adjacency_list<vecS, vecS, undirectedS,
VertexProperties, EdgeWeightProperty> Graph;
// Graph instance
Graph g;
// Property accessors
property_map<Graph, vertex_name_t>::type
city_name = get(vertex_name, g);
property_map<Graph, vertex_index2_t>::type
city_index2 = get(vertex_index2, g);
property_map<Graph, edge_weight_t>::type
edge_distance = get(edge_weight, g);
// Create the vertices
typedef graph_traits<Graph>::vertex_descriptor Vertex;
Vertex u1;
u1 = add_vertex(g);
city_name[u1] = "Los Angeles";
city_index2[u1] = 3;
Vertex u2;
u2 = add_vertex(g);
city_name[u2] = "Bakersfield";
city_index2[u2] = 2;
Vertex u3;
u3 = add_vertex(g);
city_name[u3] = "New York";
city_index2[u3] = 1;
// Create the edges
typedef graph_traits<Graph>::edge_descriptor Edge;
Edge e1;
e1 = (add_edge(u1, u2, g)).first;
edge_distance[e1] = 100;
Edge e2;
e2 = add_edge(u1, u3, g).first;
edge_distance[e2] = 2500;
第一段代码中,我们定义了属性类型,接着定义了图类型,然后我们创建了图的一个实例g。下一步我们要获得对象的属性来存取对象,如同我们前面所说的。
我们开始增加顶点和边。为了简化代码,从vertex_descriptor创建了一个自己的类型Vertex,接着我们传递g给协助函数add_vertex,从而为g增加一个顶点。
注意
请记住,图中的顶点是没有位置的,所以我们不需要告诉图我们想要增加顶点的位置,我们只需增加顶点即可。
图增加一个顶点后返回该顶点的顶点描述,这样我们可以利用属性存取对象设置该顶点相应的属性:city_name 和city_index2。设置属性是容易的,我们只要将顶点象个索引似的放在[]中。
创建边也是类似的,除了add_edge函数返回的是pair类型,而不是边类型。所以我们给该函数的返回值加上.first得到需要的边类型。我们将结果(边类型)保存在一个变量中,接着我们使用该变量来设置属性。由于在数学上边无非就是两个顶点组成的集合,所以我们需要将它们(两个顶点)和图本身传递给函数add_edge。
遍历顶点和边
为了遍历边和顶点,你需要调用edges或vertices来获得相应的迭代子。这些函数的返回值类型是pair,你可以在你的遍历中使用它们。下面的两小块代码展示了如何遍历边和顶点。
// Iterate through the vertices and print them out
typedef graph_traits<Graph>::vertex_iterator vertex_iter;
std::pair<vertex_iter, vertex_iter> vp;
for (vp = vertices(g); vp.first != vp.second; ++vp.first)
std::cout << city_name[*vp.first] << " " << city_index2[*vp.first] << std::endl;
std::cout << std::endl;
// Iterate through the edges and print them out
Vertex v1, v2;
typedef graph_traits<Graph>::edge_iterator edge_iter;
std::pair<edge_iter, edge_iter> ep;
edge_iter ei, ei_end;
for (tie(ei, ei_end) = edges(g); ei != ei_end; ++ei)
std::cout << edge_distance[*ei] << endl;
上面的这两小块代码使用了两种不同的方法来演示edges和vertices函数的返回值类型pair的使用。当遍历顶点时,返回的是一个pair(我们为这个pair创建了一个typedef);为了访问顶点,我们使用vp.first,这是一个迭代子;象绝大多数C++中的迭代子一样,可以通过解引用(dereference)的方式来获得它指向的对象。这样我们可以写出城市名称和序号:
city_name[*vp.first]
city_index2[*vp.first]
如果你不想使用pair,标准库中有一个方便的协助函数tie,它可以将pair中的各个部分赋给tie函数中的实参。比如,edges函数返回一个pair,调用:
tie(ei, ei_end) = edges(g)
将保存pair的第一项给ei,第二项给ei_end。这样,在循环中,你可以简单地使用*ei来存取边,就像:
edge_distance[*ei]
使用图和boos解决问题
虽然游戏Six Degrees of Kevin Bacon很好玩,但事实上游戏中的问题就是最短路径问题,该问题有一些具体的应用,而超出了电影明星所考虑的问题。比方说,你叫一个货运公司帮你从Bakersfield , California到Lake Mary, Florida运送货物,运送方需要找到一条从出发地到目的地的最短路径。通常,这个包裹需要经过几个城市,每个城市就可以看作图的一个顶点,并且城市之间是用边连接起来的。这个图是带权重的,这是因为城市之间的距离是在变化的。在这一点上,这个问题有别于Six Degrees问题的。
数学家和计算机学家研究出了许许多多算法来求解图论问题,这其中就包括了最短路径问题,许多书全文讨论如何使用图论方法来解决问题。当然,许多大问题都涉及大量的顶点和边,这样的问题最适合让计算机来计算了。
Boost库包含许多著名的图算法,这样你就不需要自己编写代码实现这些算法了。比如它就包含了几种不同的最短路径算法。事实上,Kevin Bacon问题并不是图论真正关心的问题,因为它所有边的权重都是1,这意味着当两个演员出现在一本电影中,他们就连接起来了,而这连接并没有所谓的物理距离。
当所有的边的权重都是1时,算法就等价于广度优先算法(BFS),BFS是用于从根节点(顶点)出发遍历所有的顶点。其思想是从根顶点出发,访问与根顶点相连的顶点,并标记这些顶点为已经访问过的。接着你再用同样的方法继续访问还没访问过的顶点,直到所有的顶点都被访问,或者在你找到你想要的顶点上停止遍历,这也意味着你找到了你所要顶点的最短路径。
提示
请考虑:当你找到你所要寻找的顶点,你如何可以认为你找到了最短路径呢?这是因为,假如有比你现在找到的更短的路径,它应当在此之前就已经被找到了。这个理由听起来很平凡,但从数学上,你已经能过看到算法是如何找到最短路径的。
Boost库的文档包含了Kevin Bacon游戏的很好的示例,在这里就不重复这些类似的代码了,我们建议去查看这个文档。
对大多数程序员来说,比Kevin Bacon游戏更感兴趣的是文件依赖问题。象make和ant程序都需要这个算法,比方说,你在写一个电子表格软件,表中有许多单元格,它们包含了对其他单元格引用的公式;需要你给出这些公式的计算顺序。如果单元格A1的公式使用了单元格A2,并且A2也包含了一个公式,你就必须在计算A1之前先计算A2中的公式;这就是依赖问题。同样的道理也出现在编译程序(如make和ant)中,如果file1依赖于file2,就需要在编译file1之前编译file2。Boost Graph库的文档中有一个求解文件依赖问题的示例。