算法复习——图算法篇之最小生成树之Kruskal算法

算法复习——图算法篇之最小生成树之Kruskal算法

以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!

1. 问题回顾

​ 问题背景、问题定义和通用框架可见算法复习——图算法篇之最小生成树之Prim算法

2. Kruskal算法

Prim算法和Kruskal算法最大的不同是,Prim算法始终维持边集A一棵树,规避了无环图的判断,且整个过程中只有两个割集,每次都要去寻找这两个割集的轻边,并加入图中;而Kruskal算法中,每一步中可能有多个割集,按顺序加入边,但每次加入边都需要判断是否为无环图,只要不是无环图,就可以确保加入的边一定是轻边。

2.1 算法思想

  • 直接实现通用框架
    • 需保证边集A仍是一个无环图
      • 选边时避免成环
    • 需保证边集A仍是最小生成树的子集
      • 每次选择当前权重最小边

2.2 算法思想精炼

Generic-MST(G)

A ← 0
while 没有形成最小生成树 do
	寻找A的安全边(u, v)	// 不成环的最小边,是一种贪心策略
	A ← A ∪ (u, v)
end
return A

​ Kruskal算法每次寻找的都是当前所有可选边中的最小边,所以中间过程是一个森林(有若干棵子树),森林不断合并子树最终形成一棵树。

​ 全局最小边不能避免成环问题,因此需要判断所选边的顶点是否在一棵子树,如果在一棵子树,再添加这条边,就出现了环。所以如何寻找安全边就变成了算法正确性的关键。

3. 正确性证明

​ 安全边的定义可见算法复习——图算法篇之最小生成树之Prim算法

​ 若每次向边集 A A A中新增安全边,可保证边集 A A A是最小生成树的子集。因为问题就变成Kruskal算法选边策略能否保证每次都选择了安全边?

证明

​ 不妨设当前已选边集为 A A A(即图中的蓝线),下一条选择的边是 ( i , g ) (i, g) (i,g)

在这里插入图片描述

​ 已选边集 A A A把图分成为若干棵子树,其中 ( V ′ , E ′ ) (V', E') (V,E)是包含顶点 i i i的子树。

​ 构造割 ( V ′ , V − V ′ ) (V', V-V') (V,VV),割不妨害边集 A A A,换言之 A A A中的边不会横跨 ( V ′ , V − V ′ ) (V', V-V') (V,VV)(即边集 A A A不包含横跨边 ( b , c ) (b, c) (b,c) ( c , d ) (c, d) (c,d) ( c , f ) (c, f) (c,f) ( i , g ) (i, g) (i,g) ( i , h ) (i, h) (i,h))。 ( i , g ) (i, g) (i,g)是横跨割 ( V ′ , V − V ′ ) (V', V-V') (V,VV)的最小权重边,所以 ( i , g ) (i, g) (i,g)是关于割 ( V ′ , V − V ′ ) (V', V-V') (V,VV)的轻边;又由于割 ( V ′ , V − V ′ ) (V', V-V') (V,VV)不妨害边集 A A A,根据安全边辨识定理,所以轻边 ( i , g ) (i, g) (i,g)为安全边。

在这里插入图片描述

4. 伪代码

MST-Kruskal(G)

输入:图G

输出:最小生成树

把边按照权重升序排序
T ← {}
for (u, v) ∈ E do
	if u,v 不在同一子树 then
		T ← T ∪ {(u, v)}
		合并u, v所在子树
	end
end
return T

​ 那么问题在于如何高效判定和维护所选边的顶点是否在一棵子树?

5. 不相交集合

5.1 概念阐释

​ 不相交集合可以高效查找顶点所属子树,也可以高效合并顶点所在子树。

​ 不相交集合把每棵生成子树看作一个顶点集合,每个集合表示为一棵有向树,多个不相交集合构成不相交集合森林。集合元素表示为树结点,树边由子结点指向父结点,根结点有一条指向自身的边。

5.2 相关操作

  • 初始化集合:创建根结点,并设置一条指向自身的边。

    伪代码:

    Create-Set(x)

    输入:顶点x

    输出:并查集

    x.parent ← x
    return x
    

    时间复杂度是 O ( 1 ) O(1) O(1)

在这里插入图片描述

  • 判定顶点是否在同一集合:回溯查找树根,检查树根是否相同

    伪代码:

    Find-Set(x)

    输入:顶点x

    输出:所属连通分量

    while x.parent ≠ x do
    	x ← x.parent
    end
    return x
    

    时间复杂度是 O ( h ) O(h) O(h) h h h为树高

    在这里插入图片描述

  • 合并集合:合并两棵树,例如 { a , b , h , g , f } ∪ { c , i } \{a, b, h, g, f\} \cup \{c, i\} {a,b,h,g,f}{c,i}

    在这里插入图片描述

    合并集合的简单实现就如上图所示,找到两树根,任意连接两棵树。但是,这样有一个潜在问题,可能造成树深度过大,降低查找效率。

    在这里插入图片描述

    因此,我们要尽可能降低树高度,提高树根查找效率。所以高效的实现方法是树高小的树连接到树高大的树上。伪代码如下:

    Union-Set(x)

    输入:顶点 x x x y y y

    a ← Find-Set(x)
    b ← Find-Set(y)
    if a.height ≤ b.height then
    	if a.height = b.height then
    		a.height ← b.height + 1
    	end
    	a.parent ← b
    end
    else
    	b.parent ← a
    end
    

    时间复杂度是 O ( h ) O(h) O(h)

5.3 时间复杂度

​ 分析以上三个操作的时间复杂度,那么不禁会有一个疑问:树的高度 h h h和顶点规模 ∣ V ∣ |V| V有何关系?

∣ V ∣ ≥ 2 h |V| \geq 2^h V2h

​ 那我们用归纳法证明一下,以上结论为什么是对的。

  • 只有一个顶点,规模 ∣ V ∣ = 1 |V|=1 V=1,高度 h = 0 h = 0 h=0,显然 1 ≥ 2 0 1 \geq 2^0 120
  • 假设:任意不相交集合 m m m,高度 h m h_m hm和规模 V m V_m Vm满足 V m ≥ 2 h m V_m \geq 2^{h_m} Vm2hm
  • 归纳:两不相交结合 a , b a, b a,b拟做合并,设合并产生的新不相交集合为 c c c
    • h a ≠ h b h_a \neq h_b ha=hb V c = V a + V b ≥ 2 h a + 2 h b ≥ 2 m a x { h a , h b } = 2 h c V_c = V_a + V_b \geq 2^{h_a}+2^{h_b} \geq 2^{max\{h_a, h_b\}} = 2^{h_c} Vc=Va+Vb2ha+2hb2max{ha,hb}=2hc(因为树高小的树要接在树高大的树下)
    • h a = h b h_a = h_b ha=hb V c = V a + V b ≥ 2 h a + 2 h b = 2 h a + 1 = 2 h c V_c = V_a + V_b \geq 2^{h_a}+2^{h_b} = 2^{h_a+1} = 2^{h_c} Vc=Va+Vb2ha+2hb=2ha+1=2hc
  • 综上,所有不相交集合都满足 ∣ V ∣ ≥ 2 h |V| \geq 2^h V2h,即 h ≤ l o g ∣ V ∣ h \leq log|V| hlogV

​ 所以, C r e a t e − S e t ( x ) Create-Set(x) CreateSet(x)的时间复杂度是 O ( 1 ) O(1) O(1) F i n d − S e t ( x ) Find-Set(x) FindSet(x)的时间复杂度是 O ( h ) = O ( l o g ∣ V ∣ ) O(h)=O(log|V|) O(h)=O(logV) U n i o n − S e t ( x ) Union-Set(x) UnionSet(x)的时间复杂度是 O ( l o g ∣ V ∣ ) O(log|V|) O(logV)

5.4 Kruskal优化伪代码

MST-Kruskal(G)

输入:图G

输出:最小生成树

把边按照权重升序排序
为每个顶点建不相交集
T ← {}
for (u, v) ∈ E do
	if Find-Set(u) ≠ Find-Set(v) then
		T ← T ∪ {(u, v)}
		Union-Set(u, v)
	end
end
return T

时间复杂度分析

​ 排序的时间复杂度是 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(ElogE);建立不相交集的时间复杂度是 O ( ∣ V ∣ ) O(|V|) O(V);判断两个点是否在一个子树内和合并两个子树的时间复杂度是 O ( l o g ∣ V ∣ ) O(log|V|) O(logV),执行最小生成树算法的时间复杂度是 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV);所以最终整体算法的时间复杂度是 O ( ∣ E ∣ l o g ∣ E ∣ + ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|E|+|E|log|V|) O(ElogE+ElogV),假设 ∣ E ∣ = O ( ∣ V ∣ 2 ) |E|=O(|V|^2) E=O(V2),则整体的时间复杂度是 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

6. 小结

Prim算法与Kruskal算法比较

通用框架Prim算法Kruskal算法
成环判断始终保持一棵树,不断扩展森林合成一棵树,不相交集合
轻边发现优先队列全部边排序
求解视角微观视角,基于当前点选边宏观视角,基于全局顺序选边
算法思想都是采用贪心策略的图算法都是采用贪心策略的图算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值