一、双连通分量
1.判定法则
1.1 关节点 + 双连通分量
-
无向图的关节点(articulation point)其删除之后,原图的连通分量(connected components )增多
-
无关节点的图,称作双(重)连通图(bi-connectivity )
-
极大的双连通子图,称作双连通分量(Bi-Connected Components)
1.2 蛮力(如何确定关节点)
-
蛮力: 对每一顶点v,通过遍历检查G{v}是否连通
-
共需O( n * (n + e) )时间,太慢! 而且,即便找出关节点,各BCC仍需确定
-
改进: 从任一顶点出发,构造DFS树 根据DFS留下的标记,甄别是否关节点
-
比如,叶节点绝不可能是关节点
1.3 非叶节点
- 根r:必须至少有2棵子树
-
内部节点v: 有某个孩子u,而subtree(u)不能 经由BACKWARD边,联接到v的任何真祖先a
-
此时,{v} = BCC(u) BCC( parent(v) )
1.4 最高祖先(Highest Connected Ancestor)
-
hca(v) = subtree(v)经后向边能抵达的最高祖先
-
由括号引理: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
2.算法
#define hca(x) ( fTime(x) ) //利用此处闲置的fTime
template<typename Tv,typename Te>
void Graph::BCC( Rank v, int & clock, Stack & S ) {
hca(v) = dTime(v) = ++clock; status(v) = DISCOVERED; S.push(v);
for ( Rank u = firstNbr(v); -1 < u; u = nextNbr(v, u) )
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;
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) ? FORWARD : CROSS;
break;
}
status(v) = VISITED; //对v的访问结束
}
#undef hca
- 复杂度
- 运行时间与常规的DFS相同,也是O(n + e)
- 自行验证:栈操作的复杂度也不过如此
- 除原图本身,还需一个容量为O(e)的栈存放已访问的边 为支持递归,另需一个容量为O(n)的运行栈
- 运行时间与常规的DFS相同,也是O(n + e)
3.实例
二、优先级搜索
1.通用算法
-
各种遍历算法的区别,仅在于选取顶点进行访问的次序
- 广度/深度:优先访问与更早/更晚被发现的顶点相邻接者;...
-
不同的遍历算法,取决于顶点的选取策略
-
不同的顶点选取策略,取决于存放和提供顶点的数据结构——Bag
-
此类结构,为每个顶点v维护一个优先级数——priority(v)
- 每个顶点都有初始优先级数;并可能随算法的推进而调整
-
通常的习惯是,优先级数越大/小,优先级越低/高
- 特别地,priority(v) == INT_MAX,意味着v的优先级最低
2.算法
template <typename Tv, typename Te>
template <typename PU> //优先级更新器(函数对象)
void Graph<Tv, Te>::PFS( Rank v, PU prioUpdater ) { //PU的策略,因算法而异
priority(v) = 0; status(v) = VISITED; parent(v) = -1; //起点v加至PFS树中
while (1) { //将下一顶点和边加至PFS树中
for ( Rank u = firstNbr(v); -1 < u; u = nextNbr(v, u) ) //对v的每一个邻居u
prioUpdater( this, v, u ); //更新其优先级及其父亲
for ( int shortest = INT_MAX, u = 0; u < n; u++ )
if ( UNDISCOVERED == status(u) ) //从尚未加入遍历树的顶点中
if ( shortest > priority(u) ) //选出下一个 {
shortest = priority(u); v = u;
} //优先级最高的顶点v
if ( VISITED == status(v) ) break; //直至所有顶点均已加入
status(v) = VISITED; type( parent(v), v ) = TREE; //将v加入遍历树
}
}
3.复杂度
-
执行时间主要消耗于内、外两重循环;其中两个内循环前、后并列
-
前一内循环的累计执行时间:若采用邻接矩阵,为O();若采用邻接表,为O(n+e) 后一循环中,优先级更新的次数呈算术级数变化{ n, n - 1, ..., 2, 1 },累计为O(n2) 两项合计,为O()
-
后面将会看到:若采用优先级队列,以上两项将分别是O(e*)和O(n*)
- 两项合计,为O((e+n)*)
-
这是很大的改进——尽管对于稠密图而言,反而是倒退(已有接近于O(e + n*)的算法)
三、Dijkstra算法
1.最短路径
-
按照图的类型:无(等)权图(BFS);带权有向图
-
【E. Dijkstra, 1959】SSSP: Single-Source Shortest Path 给定顶点s,计算s到 其余各个顶点的最短路径及长度
-
【Floyd-Warshall, 1962】 APSP: All-Pairs Shortest Path 找出每对顶点u和v之间的最短路径及长度
2.最短路径树
2.1 单调性 + 假想实验
- only if
2.2 消除歧义
-
各边权重均为正,否则有可能出现总权重非正的环路,以致最短路径无从定义
-
有负权重的边时,即便所有环路总权重皆为正 以下将介绍的Dijkstra算法依然可能失效
-
任意两点之间,最短路径唯一
- 不影响计算结果的前提下 总可通过适当扰动予以保证
2.3 最短路径树
- 所有最短路径的并,既连通亦无环
- 于是,构成一棵树
3.实例
4.实现
4.1 完美前向保密 (Perfect Forward Secrecy)
-
, let priority(v) = ||s,v|| ≤∞
-
-
注意:优先级数随后可能改变(降低)的顶点,
-
因此,只需枚举的每一邻接顶点 ,并取 priority(v)=
-
以上完全符合PFS的框架,唯一要做的工作无非是按照prioUpdater()规范,编写一个优先级(数)更新器
4.2 算法
g->pfs( 0, DijkPU() ); //从顶点0出发,启动Dijkstra算法
template<typename Tv,typename Te> struct DijkPU { //Dijkstra算法的优先级更新器
virtual void operator()( Graph* g, Rank v, Rank u ) { //对v的每个
if ( UNDISCOVERED != g->status(u) ) return; //尚未被发现的邻居u,按
if ( g->priority(u) > g->priority(v) + g->weight(v, u) ) { //Dijkstra
g->priority(u) = g->priority(v) + g->weight(v, u); //策略
g->parent(u) = v; //做松弛
}
}
};
四、Prim算法
1.最小支撑树
1.1 最小 + 支撑 + 树
-
连通网络N=(V;E)的子图T=(V;F)
-
支撑(spanning) = 覆盖N中所有顶点
-
树= 连通且无环,|F| = |V| - 1
-
同一网络的支撑树,未必唯一
-
minimum = optimal:
- 总权重达到最小
1.2 MST
- 重要性
- 自身可有效计算
- 众多优化问题的基本模型
- 为许多NP问题提供足够好的近似解
1.3 MST≠SPT
1.4 负权 & 退化
-
权值可以为零,为负数
-
所有支撑树所含的边数,必相等,故可统一调整:increase(1 - findMin())
-
合成数(composite number): (w(uv),min(u,v),max(u,v))
- 5ab < 5ad < 5bc < 5bd < 5cd < 6ac
1.5 蛮力算法
- 枚举出N的所有支撑树,从中找出代价最小者
- Cayley公式:完全图棵支撑树
2.极短跨边
2.1 排除沿环的最长边
-
任何环路C上的最长边f,都不会被MST采用
-
在移除f之后,MST将分裂为两棵树,将其视作一个割,则C上必有该割的另一跨边e,既然|e|<|f|,那么只要用e替换f,就会得到一棵总权重更小的支撑树
-
这也是Kruskal算法的依据
2.2 包括穿过割的最短边(是Prim算法的依据)
-
设(U;V\U)是N的一个割
-
若uv是该割的一条极短跨边 则必存在一棵包含uv的MST
-
反证:假设uv未被任何MST采用,任取一棵MST,将uv加入其中,于是将出现唯一的回路,且该回路必经过uv以及至少另一跨边st 接下来,摘除st后恢复为一棵支撑树,且总权重不致增加
-
反之,任一MST都必然通过极短跨边联接每一割
2.3 递增式构造
- 首先,任选:
- 以下,不断地将{};{} ,其中,
- 由此前的分析:只需将(\ )视作原图的一个割,该割所有跨边中的极短者即是
3.实例
4.正确性
4.1 似是而非
-
设Prim算法依次选取了边{ e2, e3, ..., en }
-
其中每一条边e ,的确都属于某棵MST
-
但在MST不唯一时,由此并不能确认,最终的T必是MST(之一)
-
由极短跨边构成的支撑树,未必就是一棵MST
4.2 可行的证明
- 在不增加总权重的前提下,可以将任一MST转换为T,每一都是某棵MST的子树,1 ≤ k ≤ n
5.实例
5.1 PFS
-
, let priority(v) = ||s,v|| ≤∞
-
-
注意:优先级数随后可能改变(降低)的顶点,
-
因此,只需枚举的每一邻接顶点 ,并取 priority(v)=
-
以上完全符合PFS的框架,唯一要做的工作无非是按照prioUpdater()规范,编写一个优先级(数)更新器
5.2 算法
g->pfs( 0, PrimPU() ); //从顶点0出发,启动Prim算法
template<typename Tv,typename Te> struct PrimPU { //Prim算法的顶点优先级更新器
virtual void operator()( Graph* g, Rank v, Rank u ) { //对v的每个
if ( UNDISCOVERED != g->status(u) ) return; //尚未被发现的邻居u,按
if ( g->priority(u) > g->weight(v, u) ) {//Prim
g->priority(u) = g->weight(v, u); //策略
g->parent(u) = v; //做松弛
}
}
};