C++实现简单的广度优先搜索(BFS)及路径查询(无向图)
广度优先搜索可以用来寻找在一张连通图上通往某一点的所需边最少的路径,广度优先搜索就是按照顶点与起点的距离的顺序来遍历所有顶点。代码参考《算法》第四版。
一.构造一个无向图
具体可参考构造方法:无向图
定义一个无向图对象:
#include "Graph_practise.hpp"
path = "graph_initial.txt"
undirect_Graph uGraph(path);
本次用来测试的图跟上篇深度优先搜索的图一样:
二.如何使用BFS来搜索一副连通无向图
上篇学习的深度优先搜索通过隐式的栈(先进后出)的顺序来调用DFS函数,但本篇记录的BFS不使用递归方法来搜索图,而是通过一个while循环和一个队列来实现。这个队列会在搜索的时候更新元素,元素为顶点还未被标记的所有邻居顶点。
下面是BFS的定义,和DFS比较像:
class BreadthFirstPaths
{
private:
vector<bool> marked;
map<int, int> EdgeTo;
int startPoint;
public:
bool isMarked(int v);
BreadthFirstPaths(undirect_Graph uGraph, int s);
void bfs(undirect_Graph uGraph, int s);
bool hasPathTo(int v);
void printPaths();
int getEdgeStart(int v);
stack<int> pathStore(int v);
int getStartPoint();
};
比如从0顶点(起点)出发去搜索的时候,首先标记0,接着发现0顶点有三个邻居顶点(5,1, 2),因为现在刚开始搜索,这三个邻居顶点肯定还没有被标记过,因此将这三个邻居顶点的标记状态设置为true,并依次加入队列当中,此时队列的元素为[5,1,2],然后在每次while循环过程当中,会提取队列第一个元素并pop,然后重复上面类似“寻找0顶点的未标记邻居顶点”的过程。
具体代码如下所示:
void BreadthFirstPaths::bfs(undirect_Graph uGraph, int s)
{
queue<int> NeighbourNotMarked;
startPoint = s;
marked[s] = true;
NeighbourNotMarked.push(s);
while(!NeighbourNotMarked.empty())
{
int v = NeighbourNotMarked.front();
NeighbourNotMarked.pop();
list<Vertex*> V_uGraphVertexSet = uGraph.getGraphVertexSet()[s]->neighbours;
list<Vertex*>::iterator iter = V_uGraphVertexSet.begin();
for(iter; iter != V_uGraphVertexSet.end(); iter++)
{
if(!isMarked((*iter)->value))
{
marked[(*iter)->value] = true;
NeighbourNotMarked.push((*iter)->value);
cout << (*iter)->value << "has been pushed into the Queue we created " << endl;
}
}
}
}
这个BFS函数体的执行过程相比于迭代地调用DFS可能更好理解。但以上代码还没有写出寻找从起点到某点的最短路径是什么,而是使用BFS算法(即按照顶点到起点的距离的顺序来搜索图)来搜索一张连通图,最后每个顶点都会被搜索到,这为后面实现寻找具体的最短路径作了铺垫。下面其他ismarked
和hasPathTo
等方法的实现:
bool BreadthFirstPaths::hasPathTo(int v)
{
return isMarked(v);
}
bool BreadthFirstPaths::isMarked(int v)
{
return marked[v];
}
//其实只用其中一个函数就够了
显而易见的是,以上的BFS函数的做法只是以广度优先搜索的方式遍历了一遍图结构,没有做任何记录,也就是说目前这个算法只是走了一遍迷宫,但没有记下来到底怎么走的,只是知道自己把所有的岔路口都走过一遍了。
二.如何使用BFS来搜索一副连通无向图并得到路径
下面的工作也不难,将我们走过的某点到起点的最短路径记录下来就行,我在这里定义了一个map容器,名字叫EdgeTo,最开始这个容器啥也没有,我们需要通过记录路径,往这个map容器新增元素,每个元素是pair<int, int>
形式,第一个int代表A点(泛指某个点),第二个int代表在最短路径上,A点的前一个点。具体记录路径的方法很简单,如下所示:
void BreadthFirstPaths::bfs(undirect_Graph uGraph, int s)
{
queue<int> NeighbourNotMarked;
//将起点先加入队列
marked[s] = true;
NeighbourNotMarked.push(s);
//开始广度优先搜索
while(!NeighbourNotMarked.empty()) //队列为空就意味着,已经连通图没有未被标记的顶点了
{
int v = NeighbourNotMarked.front();//获得队列当中第一个顶点
cout << "-------------the first Vertex " << v << " in Queue will be deleted-------------" << endl;
NeighbourNotMarked.pop();//把第一个顶点从队列删除,因为它已经被标注,并且它的邻接点马上要被check
list<Vertex*> V_uGraphVertexSet = uGraph.getGraphVertexSet()[v]->neighbours;
//获得v顶点的邻接表
list<Vertex*>::iterator iter = V_uGraphVertexSet.begin();
for(iter; iter != V_uGraphVertexSet.end(); iter++)
{
if(!isMarked((*iter)->value))
{
EdgeTo.insert(pair<int, int>((*iter)->value, v));
marked[(*iter)->value] = true;
NeighbourNotMarked.push((*iter)->value);
cout << (*iter)->value << "has been pushed into the Queue we created " << endl;
}
}
}
cout << "Queue is empty!" << endl;
}
可以看到这是一个bfs算法,唯一和之前写的bfs不同的是,在搜索某个A点的邻居顶点的时候,会为EdgeTo容器新增pair键值对(key是邻居顶点,value是A顶点)。最后运行算法,可以得到一个新的名为EdgeTo的map容器,可以用它来代表一棵树,如下所示:
这棵🌲较好的代表了从起点开始到达所有顶点的路径,而且都是所谓最短路径。
将EdgeTo容器表达成树:
最后,我们想要获取从某个起点出发,到达特定的节点的最短路径,可以基于这个EdgeTo容器来给出解,这里使用pathStore来保存一个路径栈,具体代码如下所示:
stack<int> BreadthFirstPaths::pathStore(int v)
{
stack<int> pathstore;
if(!isMarked(v)) return stack<int>();
for(int x = v; x != getStartPoint(); x = getEdgeStart(x))
{
pathstore.push(x);
}
pathstore.push(getStartPoint());
return pathstore;
}
int BreadthFirstPaths::getStartPoint()
{
return startPoint;
}
int BreadthFirstPaths::getEdgeStart(int v)
{
auto iter = EdgeTo.find(v);
if(iter != EdgeTo.end())
{
return iter->second;
}
return -1;
}
测试用例:
//测试广度优先搜索
string path = "graph_initial_1.txt";
undirect_Graph graph(path);
graph.print_allVertexAndEdge();
int startPoint = 0;
BreadthFirstPaths bfs(graph, startPoint);
bfs.printPaths();
for(int v = 5; v < graph.countOfVertex(); v++)
{
auto x = bfs.pathStore(v);
while(!x.empty())
{
cout << x.top() << endl;
x.pop();
}
break;
}
运行结果如下:
可以看到这里想要获得从起点0出发通往4的路径,结果很简单,从0开始只需要经过2就到达了4,而基于同样的图定义方式(顶点的顺序等),使用DFS算法,得到的路径是 0 - 5 - 3 - 2 - 4。
全部代码已上传,可查看BFS部分内容graph_practise.cpp和graph_practise.hpp