数据结构(c++)学习笔记--图的应用


一、双连通分量

1.判定法则

1.1 关节点 + 双连通分量

  • 无向图的关节点(articulation point)其删除之后,原图的连通分量(connected components )增多

  • 无关节点的图,称作双(重)连通图(bi-connectivity )

  • 极大的双连通子图,称作双连通分量(Bi-Connected Components)

Yi6Rt.png

YiNpq.png

1.2 蛮力(如何确定关节点)

  • 蛮力: 对每一顶点v,通过遍历检查G{v}是否连通

  • 共需O( n * (n + e) )时间,太慢! 而且,即便找出关节点,各BCC仍需确定

  • 改进: 从任一顶点出发,构造DFS树 根据DFS留下的标记,甄别是否关节点

  • 比如,叶节点绝不可能是关节点

1.3 非叶节点

YiJpW.png

  • 根r:必须至少有2棵子树

YisPG.png

  • 内部节点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)的运行栈

3.实例

YiRTl.png

YiYe7.png

Yio62.png

二、优先级搜索

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之间的最短路径及长度

Yiab3.png

2.最短路径树

2.1 单调性 + 假想实验

  • u ∈ π u∈π uπ only if π ( u ) ⊆ π ( v ) π(u)⊆π(v) π(u)π(v)

Yik74.png

2.2 消除歧义

  • 各边权重均为正,否则有可能出现总权重非正的环路,以致最短路径无从定义

  • 有负权重的边时,即便所有环路总权重皆为正 以下将介绍的Dijkstra算法依然可能失效

  • 任意两点之间,最短路径唯一

    • 不影响计算结果的前提下 总可通过适当扰动予以保证

2.3 最短路径树

  • 所有最短路径的并,既连通亦无环
  • 于是, T = T n − 1 = ∪ 0 ≤ i ≤ n π ( u i ) T=T_{n-1}=∪_{0≤i≤n}π(u_i) T=Tn1=0inπ(ui)构成一棵树

YmSJw.png

3.实例

YmcJj.png

YmsoS.png

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)eFw(e)达到最小

YmZSz.png

1.2 MST

  • 重要性
    • 自身可有效计算
    • 众多优化问题的基本模型
    • 为许多NP问题提供足够好的近似解

1.3 MST≠SPT

pSUzWvT.png

1.4 负权 & 退化

  • 权值可以为零,为负数

  • 所有支撑树所含的边数,必相等,故可统一调整:increase(1 - findMin())

  • 合成数(composite number): (w(uv),min(u,v),max(u,v))

    • 5ab < 5ad < 5bc < 5bd < 5cd < 6ac

pSaSZqg.png

1.5 蛮力算法

  • 枚举出N的所有支撑树,从中找出代价最小者

pSaSnaj.png

  • Cayley公式:完全图 K n 有 n n − 2 K_n有n^{n-2} Knnn2棵支撑树

2.极短跨边

2.1 排除沿环的最长边

pSaSxS0.png

  • 任何环路C上的最长边f,都不会被MST采用

  • 在移除f之后,MST将分裂为两棵树,将其视作一个割,则C上必有该割的另一跨边e,既然|e|<|f|,那么只要用e替换f,就会得到一棵总权重更小的支撑树

  • 这也是Kruskal算法的依据

2.2 包括穿过割的最短边(是Prim算法的依据)

pSapkk9.png

  • 设(U;V\U)是N的一个割

  • 若uv是该割的一条极短跨边 则必存在一棵包含uv的MST

  • 反证:假设uv未被任何MST采用,任取一棵MST,将uv加入其中,于是将出现唯一的回路,且该回路必经过uv以及至少另一跨边st 接下来,摘除st后恢复为一棵支撑树,且总权重不致增加

  • 反之,任一MST都必然通过极短跨边联接每一割

2.3 递增式构造

pSapZSx.png

  • 首先,任选: 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 vVk
  • 由此前的分析:只需将($V_k; V $\ V k V_k Vk)视作原图的一个割,该割所有跨边中的极短者即是 v k + 1 u v_{k+1}u vk+1u

3.实例

pSapf74.png

pSap4AJ.png

4.正确性

4.1 似是而非

pSapz4A.png

  • 设Prim算法依次选取了边{ e2, e3, …, en }

  • 其中每一条边e ,的确都属于某棵MST

  • 但在MST不唯一时,由此并不能确认,最终的T必是MST(之一)

  • 由极短跨边构成的支撑树,未必就是一棵MST

4.2 可行的证明

  • 在不增加总权重的前提下,可以将任一MST转换为T,每一 T k T_k Tk都是某棵MST的子树,1 ≤ k ≤ n

pSa9CgP.png

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; //做松弛 
        } 
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值