Prim算法
算法描述
prim算法设一个集合Q表示待合并的顶点,每次在Q中选择一个与V-Q中顶点最近的顶点,从Q中去除它,直到Q为空。
输入:图
G
(
V
,
E
)
,
∣
V
∣
=
n
,
∣
E
∣
=
m
G(V,E),|V|=n,|E|=m
G(V,E),∣V∣=n,∣E∣=m
输出:图
G
G
G的最小生成树
M
S
T
(
m
i
n
i
m
u
m
−
s
p
a
n
n
i
n
g
−
t
r
e
e
)
MST(minimum-spanning-tree)
MST(minimum−spanning−tree)
伪代码
/*
设置集合Q:待合并入MST的顶点,A:MST的边集
则V-Q即为MST已有的顶点集合
初始:Q=V,A=空集
*/
while (Q不空)
{
从所有连接集合V-Q和集合Q的边中选出一条权值最小的边(u,v);
从Q中除去v;
(u,v)加入到A中;
}
算法正确性:设之前的MST的边都是按上述选法选出的。若当前不选连接V-Q和Q的权值最小的边(u,v),则在最终的最小生成树T中连接u和v的路径上至少有一条边权值大于(u,v)(如若不然,(u,v)便不是当前连接V-Q和Q的权值最小的边),在最小生成树中去掉该边,连接u和v,则仍为一棵树,但权值总和变小,从而T不是最小生成树,矛盾。从而这种选法可以保证得到最小生成树。
实现细节与复杂度
实现算法时用一个布尔数组维护集合Q,顶点下标即为在数组中的元素的下标,true表示该顶点在集合V-Q中,否则在Q中。
算法实现很重要的一点就是如何从所有连接集合V-Q和集合Q的边中选出一条权值最小的边(u,v),有以下两种方法:
1、建立数组
c
o
s
t
[
]
cost[]
cost[],存放Q中每个顶点到V-Q中所有顶点的最近距离,找出具有最小
c
o
s
t
cost
cost值的v。
2、把V中的顶点按“离V-Q中所有顶点的最近距离”排成优先队列,则每次只需取队头元素即得最小权值边(u,v)。
第一种方法复杂度高,第
k
(
k
⩾
1
)
k(k\geqslant1)
k(k⩾1)次从接下来
n
−
k
n-k
n−k个顶点中寻找
c
o
s
t
cost
cost值最小的结点,共寻找
n
−
1
n-1
n−1次,故算法复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
第二种方法的复杂度和优先队列的实现有关。我采用堆作为优先队列,堆的元素有三个属性:源点
s
s
s,目标点
t
t
t,代价
c
o
s
t
cost
cost,记为
(
s
,
t
,
c
o
s
t
)
(s,t,cost)
(s,t,cost),表示图中源点
s
s
s到目标点
t
t
t的代价为
c
o
s
t
cost
cost,如果不关心最终最小生成树有哪些边而只关心其代价则可以不要源点
s
s
s。这种方法具体步骤如下:
(1)取堆顶元素,出队,若堆顶元素的目标点不属于Q,则重复执行(1),否则转(2).
(2)将属于Q的目标点
t
t
t从Q中去除。
(3)更新Q中顶点到V-Q中所有顶点的最小距离,即更新
c
o
s
t
cost
cost数组中对应的值:对图
G
G
G中
t
t
t的每个邻居
u
u
u,若
u
∈
Q
u\in Q
u∈Q且
t
t
t到
u
u
u的距离
d
d
d小于
c
o
s
t
[
u
]
cost[u]
cost[u],则更新
c
o
s
t
[
u
]
=
d
cost[u]=d
cost[u]=d,新建堆元素
(
t
,
u
,
d
)
(t,u,d)
(t,u,d)入堆。
上述步骤重复执行n-1次,由于堆的调整复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn),而步骤(3)是对每条边进行的,实际上在n-1次中循环中共执行m次,从而总的时间复杂度为
O
(
n
l
o
g
n
+
m
l
o
g
n
)
=
O
(
m
l
o
g
n
)
O(nlogn+mlogn)=O(mlogn)
O(nlogn+mlogn)=O(mlogn)。
Kruskal算法
算法描述
kruskal算法维护一个边集Q,每次从Q中取出一条权值最小的边,加入到当前的树中,若该边的加入导致出现环,则取消加入,如此循环n-1次。
输入:图
G
(
V
,
E
)
,
∣
V
∣
=
n
,
∣
E
∣
=
m
G(V,E),|V|=n,|E|=m
G(V,E),∣V∣=n,∣E∣=m
输出:图
G
G
G的最小生成树
M
S
T
(
m
i
n
i
m
u
m
−
s
p
a
n
n
i
n
g
−
t
r
e
e
)
MST(minimum-spanning-tree)
MST(minimum−spanning−tree)
伪代码
/*
设置集合Q表示仍未合并入MST的边集,A表示MST的边集
初始Q=E,A=空集
*/
int count = m - 1;
while (count)
{
从Q中取出具有最小权值的边(u,v);
if ((u,v)加入MST不会导致出现环)
{
将(u,v)加入A;
count--;
}
}
实现细节与复杂度
算法实现的两个关键点:
1、如何从Q中取出最小权值边;
2、如何判断边的加入是否会导致环;
对于1,由于每次是从Q中取出最小权值的边,所以可以用优先队列实现Q,关键码为边的权值。优先队列可以采用堆的形式,堆的元素有三个属性:源点
s
s
s,目标点
t
t
t,代价
c
o
s
t
cost
cost,记为
(
s
,
t
,
c
o
s
t
)
(s,t,cost)
(s,t,cost),表示图中源点
s
s
s到目标点
t
t
t的代价为
c
o
s
t
cost
cost。
对于2,可以采用并查集。当前生成的图的每个极大连通分量对应一个集合,集合的元素是该连通分量的所有顶点,而每次判断环只需要判定新加入的边的两个端点是否则同一个集合中即可。边加入就是将两个端点所在的两个不同集合合并。
建堆复杂度为
O
(
m
)
O(m)
O(m),堆的调整复杂度为
O
(
l
o
g
m
)
O(logm)
O(logm),循环次数为
O
(
m
)
O(m)
O(m),故总时间复杂度为
O
(
m
l
o
g
m
)
O(mlogm)
O(mlogm),
代码
见github中函数prim和kruskal。