文章目录
一、双连通分量
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( n 2 n^2 n2);若采用邻接表,为O(n+e) 后一循环中,优先级更新的次数呈算术级数变化{ n, n - 1, …, 2, 1 },累计为O(n2) 两项合计,为O( n 2 n^2 n2)
-
后面将会看到:若采用优先级队列,以上两项将分别是O(e* log n \log n logn)和O(n* log n \log n logn)
- 两项合计,为O((e+n)* log n \log n logn)
-
这是很大的改进——尽管对于稠密图而言,反而是倒退(已有接近于O(e + n* log n \log n logn)的算法)
三、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 单调性 + 假想实验
- u ∈ π u∈π u∈π only if π ( u ) ⊆ π ( v ) π(u)⊆π(v) π(u)⊆π(v)
2.2 消除歧义
-
各边权重均为正,否则有可能出现总权重非正的环路,以致最短路径无从定义
-
有负权重的边时,即便所有环路总权重皆为正 以下将介绍的Dijkstra算法依然可能失效
-
任意两点之间,最短路径唯一
- 不影响计算结果的前提下 总可通过适当扰动予以保证
2.3 最短路径树
- 所有最短路径的并,既连通亦无环
- 于是, T = T n − 1 = ∪ 0 ≤ i ≤ n π ( u i ) T=T_{n-1}=∪_{0≤i≤n}π(u_i) T=Tn−1=∪0≤i≤nπ(ui)构成一棵树
3.实例
4.实现
4.1 完美前向保密 (Perfect Forward Secrecy)
-
∀ v ∉ V k ∀v∉V_k ∀v∈/Vk, let priority(v) = ||s,v||
≤∞ -
于是套用 P F S 框架,为将 T k 扩充至 T k + 1 ,只需选出优先级最高的跨边 e k 及其对应顶点 v k ,并将其加入 T k ,随后,更新 V / V k + 1 中所有顶点的优先级(数) 于是套用PFS框架,为将T_k扩充至T_{k+1},只需选出优先级最高的跨边e_k及其对应顶点v_k,并将其加入T_k,随后,更新V/V_{k+1}中所有顶点的优先级(数) 于是套用PFS框架,为将Tk扩充至Tk+1,只需选出优先级最高的跨边ek及其对应顶点vk,并将其加入Tk,随后,更新V/Vk+1中所有顶点的优先级(数)
-
注意:优先级数随后可能改变(降低)的顶点, 必与 v k 邻接 必与v_k邻接 必与vk邻接
-
因此,只需枚举 v K v_K vK的每一邻接顶点 ,并取 priority(v)= m i n ( p r i o r i t y ( v ) , p r i o r i t y ( v k ) + ∣ ∣ v k , v ∣ ∣ ) min(priority(v),priority(v_k)+||v_k,v||) min(priority(v),priority(vk)+∣∣vk,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:
- 总权重 w ( T ) − ∑ e ∈ F w ( e ) w(T)-\sum_{e∈F}w(e) w(T)−∑e∈Fw(e)达到最小
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公式:完全图 K n 有 n n − 2 K_n有n^{n-2} Kn有nn−2棵支撑树
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 递增式构造
- 首先,任选: T 1 = ( v 1 ; Ø ) T_1=({v_1};Ø) T1=(v1;Ø)
- 以下,不断地将 T k 拓展为树 T k + 1 , T k + 1 = ( V k + 1 ; E k + 1 ) = ( V k ∪ T_k拓展为树T_{k+1},T_{k+1}=(V_{k+1};E_{k+1})=(V_k∪ Tk拓展为树Tk+1,Tk+1=(Vk+1;Ek+1)=(Vk∪{ V k + 1 V_{k+1} Vk+1}; E k ∪ E_k∪ Ek∪{ v k + 1 u v_{k+1}u vk+1u} ) ) ) ,其中 v ∈ V k v∈V_k v∈Vk,
- 由此前的分析:只需将($V_k; V $\ V k V_k Vk)视作原图的一个割,该割所有跨边中的极短者即是 v k + 1 u v_{k+1}u vk+1u
3.实例
4.正确性
4.1 似是而非
-
设Prim算法依次选取了边{ e2, e3, …, en }
-
其中每一条边e ,的确都属于某棵MST
-
但在MST不唯一时,由此并不能确认,最终的T必是MST(之一)
-
由极短跨边构成的支撑树,未必就是一棵MST
4.2 可行的证明
- 在不增加总权重的前提下,可以将任一MST转换为T,每一 T k T_k Tk都是某棵MST的子树,1 ≤ k ≤ n
5.实例
5.1 PFS
-
∀ v ∉ V k ∀v∉V_k ∀v∈/Vk, let priority(v) = ||s,v||
≤∞ -
于是套用 P F S 框架,为将 T k 扩充至 T k + 1 ,只需选出优先级最高的跨边 e k 及其对应顶点 v k ,并将其加入 T k ,随后,更新 V / V k + 1 中所有顶点的优先级(数) 于是套用PFS框架,为将T_k扩充至T_{k+1},只需选出优先级最高的跨边e_k及其对应顶点v_k,并将其加入T_k,随后,更新V/V_{k+1}中所有顶点的优先级(数) 于是套用PFS框架,为将Tk扩充至Tk+1,只需选出优先级最高的跨边ek及其对应顶点vk,并将其加入Tk,随后,更新V/Vk+1中所有顶点的优先级(数)
-
注意:优先级数随后可能改变(降低)的顶点, 必与 v k 邻接 必与v_k邻接 必与vk邻接
-
因此,只需枚举 v K v_K vK的每一邻接顶点 ,并取 priority(v)= m i n ( p r i o r i t y ( v ) , p r i o r i t y ( v k ) + ∣ ∣ v k , v ∣ ∣ ) min(priority(v),priority(v_k)+||v_k,v||) min(priority(v),priority(vk)+∣∣vk,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; //做松弛
}
}
};