图算法是个庞大的家族,其中大部分成员的主体框架都可以归结于图的遍历。图的遍历需要访问所有顶点一次且仅 一次,此外,图遍历同时还要访问所有的边一次且仅一次。经过一次遍历,树边的顶点共同构成了原图的一颗遍历树。
为防止对顶点的重复访问,在遍历的过程中,需要动态地设置各顶点的不同状态,并且随着遍历的进程不断地转换状态,直至最后的“访问完毕”,图的遍历更加强调对处于特定状态顶顶啊的甄别和查找,故也称作图搜索。
最基本而典型的图搜索算法包括:广度优先搜索(BFS),深度优先搜索(DFS),优先级搜索等(PFS),本文主要介绍图的广度优先搜索(breadth-first search,BFS),本文使用的图数据结构参见之前博客https://blog.csdn.net/qq_18108083/article/details/84870399
策略:越早被访问到的顶点,其邻居顶点越优先被选用。始自图中顶点s的BFS搜索,将首先访问顶点s,再依次访问顶点s所有未成访问过的邻居顶点,再按照后者被访问的先后次序,组个访问它们的邻居。这个过程很类似与树的层次遍历,具有先入先出的特点,故采用队列结构作为辅助容器。
实现:由于整个图可能具有多个连通域,从单个顶点开始的BFS可能不能遍历到图中的所有顶点,所以BFS函数能够遍历从顶点s开始的单个连通域,而bfs函数则对所有顶点进行检查,只要未曾被访问过,就从该点开始一次新的BFS搜索,这样就能保证所有的连通域都能够被遍历到。
template<typename Tv, typename Te> void graph<Tv, Te>::BFS(int v, int& clock) //遍历单个
{
queue<int> Q; //顶点缓存队列
status(v) = DISCOVERED; //标记顶点为已发现
Q.enqueue(v); //将当前顶点入队
while (!Q.empty()) //只要队列非空,则继续
{
v = Q.dequeue(); //每次选择一个顶点出队,遍历其所有邻居,检查是否存在关联边
dTime(v) = ++clock;
for (int u = firstNbr(v); u >= 0; u = nextNbr(v, u)) //对于顶点v,从顶点集V的最后一个元素开始寻找邻居(与v存在关联边)
{
if (status(u) == UNDISCOVERED) //如果此邻居顶点尚未发现
{ cout << "(v,u)" << "(" << v<<"," << u << ")" << endl;
status(u) = DISCOVERED; //设置该顶点为已被发现
type(v, u) = TREE; //设置边e(v,u)为TREE(遍历树)
parent(u) = v; //设置在遍历树中顶点u的父亲为v
Q.enqueue(u); //顶点u入队
}
else //如果此邻居顶点已经被发现
{
type(v, u) = CROSS; //设置边e(v,u)为CROSS(跨边),不是遍历树枝干
}
}
status(v) = VISITED; //设置顶点v为已遍历
}
}
template<typename Tv, typename Te> void graph<Tv, Te>::bfs(int s)
{
reset(); //复位所有顶点和已存在边的状态为未被发现,未确定
int clock = 0; //时间标签
int v = s;
do
{
if(status(v)==UNDISCOVERED)
BFS(v,clock); //对每个顶点都进行一次单连通域广度优先搜索
v++;
cout << "v----" << v << endl;
} while ((v = (++v%n)) != s);
}
效率:若图G=(V,E)中共有n个顶点和e条边,则BFS仅需O(n+e)时间。