一、广度优先搜索
- 算法
- 始自顶点s的广度优先搜索Breadth-First_Search。先访问顶点s,依次访问s的所有尚未访问的邻接顶点,依次访问它们尚未访问的邻接顶点,如此反复,直到没有尚未访问的邻接顶点
- 通过广度优先遍历,实际上是划分等价类,得到原图的一棵支撑树(spanning tree),涵盖了原图的所有顶点。
- 相当于树的层次遍历
- 实现
//Graph::BFS()
template<typename Tv, typename Te>//顶点类型、边类型
void Graph<Tv, Te>::BFS(int v, int & clock){//遍历的起点为顶点v
Queue<int> Q; status(v) = DISCOVERED; Q.enqueue(v);//初始化
while(!Q.empty()){//反复地
int v = Q.dequeue();//取出队首的顶点,并且重新命名为v
dTime(v) = ++clock;//取出队首节点v,该操作可简单的代表对该节点所做的一些操作
for(int u = firstNbr(v); -1 < u; u = nextNbr(v, u))//考察v的每一邻居u
/*...视u的状态,分别处理... */
status(v) = VISITED;//至此,当前顶点访问完毕
}
}//经过遍历搜索,每一个顶点的状态都会由UNDISCOVERED转化为DISCOVERED转化为VISITED
- 对于枚举出的新邻居u,可能的处理方式
//每当有一个顶点入队,都会标记为DISCOVERED状态
//初始化时每一条边都是UNDETERMINED,每当该边被遍历过程采纳,将该边的状态改为TREE
while(! Q.empty()){//反复的
int v = Q.dequeue(); dTime(v) = ++clock;//取出队首顶点v,并
for(int u = firstNbr(v); -1 < u; u = nextNbr(v, u))//考察v的每一邻居u,for循环总共执行边的数目e次
if(UNDISCOVERED == status(u)){//若u尚未被发现,则
status(u) = DISCOVERED; Q.enqueue(u);//发现该顶点
status(v, u) = TREE; parent(u) = v;//引入树边
}else//若u已被发现(正在队列中), 或者甚至已访问完毕(已出队列),则
status(v, u) = CROSS;//将(v,u)归类于跨边(不属于同一个等价类)
status(v) = VISITED;//至此,当前顶点访问完毕
}//为什么不会把父节点与该点的连接改为cross呢
- 该图可能有多个连通域,改进如下
template <typename Tv, typename Te>//顶点类型、边类型
void Graph<Tv, Te>::bfs(int s){//s为起点类型
reset(); int clock = 0; int v = s;//初始化O(n + e)
do//逐一检查所有顶点,一旦遇到尚未发现的顶点
if( UNDISCOVERED == status(v) )//累计O(n)
BFS(v, clock);//即从该顶点出发启动一次BFS
while(s != ( v = ( ++v % n ) ));
//按序号访问,故不重不漏
}//无论多少连通分量,最终是每个节点访问一次
- 复杂度
- 理论上是O(n^2 + e), 实际运行中由于高速缓存等因素,更接近于O(n + e)
- 将邻接矩阵该为邻接表,可以直接得到O( n + e )
- 最短路径:BFS中节点到起点的路径,就是该节点的最短路径
二、深度优先搜索
- 算法
- 始自顶点s的深度优先搜索Depth - First Search。访问顶点s,若s尚有未被访问的邻居,则任取其一u, 递归执行DFS(u),否则,返回
- 会得到一棵支撑树,未被支撑树采纳的边也同样会被分类,而且这种分类更加细致
- 实现
template<typename Tv, typename Te>//顶点类型、边类型
void Graph<Te, Tv>::DFS(int v, int & clock){
dTime = ++clock; status(v) = DISCOVERED;//发现当前的顶点v,记录此时时间dTime
for(int u = firstNbr(v); -1 < u; u = nextNbr(v, u))//枚举v的每一邻居
/*视u的状态,分别处理*/
/*与BFS不同,含有递归*/
status(v) = VISITED; fTime(v) = ++clock;//至此,当前顶点v访问完毕,记录此时时间fTime
}
- 对于不同类型情况的处理
for(int u = firstNbr(v); -1 < u; u = nextNbr(v, u))//枚举v所有邻居u
switch( status(u) ){//并视其状态分别处理
case UNDISCOVERED://u尚未发现,意味着支撑树可在此拓展
status(v, u) = TRUE; parent(u) = v; DFS(u, clock); break;//递归
case DISCOVERED//:u已被发现但尚未访问完毕,应属被后代指向的祖先
status(v, u) = BACKWARD; break;//在遍历树中,我们试图从一个后代回连到它的祖先,故称之为backward,后向边
default://u已访问完毕(VISITED, 有向图),则视承袭关系分为前向边或跨边
status(v, u) = dTime(v) < dTime(u) ? FORWARD : CROSS; break;//这条边是由它的祖先节点指向其后代,称forward再合适不过了;在整个遍历树中,这两个顶点之间并没有任何的祖先或后代的直系血缘关系,称为cross,跨越边
}//switch
- 该算法在有向图中,可以采取类似BFS处理多连通区域的方法进行优化
- 括号引理 / 嵌套引理
- 顶点的活动期:active[u] = (dTime[u], fTime[u])
- 给定有向图G = (V, E)及其任一DFS森林,则
- 任何一对顶点存在血缘关系,当且仅当它们的活跃期存在包含与被包含的关系,且祖先的活跃期包含后代的活跃期
- 任何一对顶点之间没有任何直系血缘关系,那么它们的活跃期也是彼此互不相交的
- 可用来判断两个顶点之间是否存在直系血缘关系,在O(1)的时间内就可以实现
三、拓扑排序
- 定义
- 任给有向图G(不一定是DAG),尝试将所有的顶点排成一个线性序列,使其次序与原图相容(每一顶点都不会通过边指向前驱顶点)
- 接口要求:若原图存在回路(即并非DAG),检查并报告,否则,给出一个相容的线性序列
- 存在性
- 任何有向无环图G中,必有一个零入度的点m
- 无环图简称DAG,对任何一个DAG而言,必然有拓扑排序
- 实现
- 初步实现:找到一个零入度的点,将它记录并删除,接着寻找下一个零入度的点,以此类推。
- 策略:逆序输出零出度顶点
- 对图G做DFS,其间每当有顶点被标记为VISITED,则将其压入S。一旦发现有后向边,则报告"NOT_A_DAG"并退出。DFS结束后,顺序弹出S中的各个顶点。
- 各节点按fTime逆序排列,即是拓扑排序
- 复杂度与DFS相当,也是O(n + e)
Begin with the end in mind.
四、双连通分量
- 判定准则
- 无向图的关节点:其删除后,原图的连通分量增多
- 无关节点的图,称作双(重)连通图(bi - connectivity)
- 极大的双连通子图,称作双连通分量(Bi-Connected Components)
- 实现方式:从任一顶点出发,构造DFS树,根据DFS留下的标记,甄别是否关节点。
- 分析
- 叶子节点不可能是关节点,因为删除后连通分量数量不会增加
- 根,至少有2棵子树,则是关节点
- 中间的节点,通过DFS遍历得到一个指标hca(Highest Connected Ancestor),找到其最高的祖先是谁。
- 由括号引理得,dTime越小的祖先,辈分越高
- DFS过程中,一旦发现后向边(v, u),即取:hca(v) = min(hca(v), dTime(u)) (找辈分最高的祖先)
- DFS(u)完成并返回v时,若有hca(u) < dTime(v),即取:hca(v) = min( hca(v), hca(u))
- 否则,即可断定:v是关节点,且{ v } + subtree(u)即为一个BCC
# define hca(x) (fTime(x))//
template <typename Tv, typename Te>
void Graph<Tv, Te>::BCC(int v, int & clock, Stack<int> &S) {
hca(v) = dTime(v) = ++clock; status(v) = DISCOVERED; S.push(v);
for(int u = firstNbr(v); -1 < u ; u = nextNbr(v, u))
switch(status(u))
{/*...视u的状态分别处理...*/}
status(v) = VISITED;//对v的访问结束
}
#undef hca
- 树边
switch(status(u))
case UNDISCOVERED:
parent(u) = v; type(v, u) = TREE;//拓展树边
BCC(u, clock, S);//从u开始遍历,返回后...
if(hca(u) < dTime(v))//若u经后向边指向v的真祖先
hca(v) = min(hca(v), hca(u));//则v亦必如此
else//否则,以v为关节点(u以下即是一个BCC, 且其中顶点此时正集中于栈S的顶部)
while(u != S.pop());//弹出当前BCC中(除v外)的所有节点,即弹出节点到孩子节点被弹出,保留父亲节点在栈中。为了保证双连通量的完整,可打印父亲节点,但是不弹栈。
//可视需要做进一步的处理
break;
- 后向边
switch (status(u))
case DISCOVERED://回边
type(v, u) = BACKWARD;
if( u != parent(v))//无向图可以看成是双向边,可视为一次优化,避免该节点的回边为其父节点
hca(v) = min(hca(v), dTime(u));//更新hca[v],越小越高
break;
default://VISITED(digraphs only)
type(v, u) = dTime(v) < dTime(u) ? FOREARD : CROSS;
break;