0%

最小生成树算法总结

最小生成树的算法有 Prim 和 Kruskal,首先,先说明一个结论,定义图有 n 个点,m 条边, prim 的复杂度为 O(nlogm),kk 的复杂度为 O(mlogm),可见 prim 更适合稠密图,而 kk 更适合稀松图。

当连通图中各边权值不相等时,最小生成树唯一;当有相等的权值时最小生成树可能不唯一。

prim 算法的思想是,首先选取一个点,加入点集,然后将该点所连的边加入边集,重复以下步骤。
选取当前边集合中权值最小的边,如果该边所连接的点不在点集中,将该边的另一个端点加入点集合,同时将该端点所连接的边加入边的集合。
当选取了 n-1 条边后,得到最小生成树。使用堆优化后的时间复杂度为 O(nlogm)。

证明,假设 prim 生成的树 T* 不是最小生成树,设最小生成树为 T,T* 中 不属于 T ,那么一定有将 加入 T,T 中形成环(此时共有 n 条边,一定存在环),可以通过删除环上任意一条边(根据构建过程,是点所连边中权值最小的)得到更小的生成树,因此 T 不存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const int maxn = 1e4 + 7;
int vis[maxn];
int n, m;
struct edge
{
int from, to, w;
edge(int from, int to, int w)
{
this->from = from;
this->to = to;
this->w = w;
}
bool operator<(const edge b) const
{
return this->w > b.w;
}
};
vector<edge> gra[maxn];
priority_queue<edge> pq;
int prim()
{
int res = 0;
int cnt = 0;
memset(vis, 0, sizeof(vis));
vis[1] = 1;
for (int i = 0; i < gra[1].size(); i++)
{
edge tmp = gra[1][i];
pq.push(tmp);
}
while (cnt < n - 1 && !pq.empty())
{
edge top = pq.top();
pq.pop();
int to = top.to;
if (!vis[to])
{
res += top.w;
cnt++;
vis[to] = 1;
for (int i = 0; i < gra[to].size(); i++)
{
edge tmp = gra[to][i];
if (!vis[tmp.to])
{
pq.push(tmp);
}
}
}
}
if (cnt != n - 1)
{
return -1;
}
else
{
return res;
}
}
void addedge(int from, int to, int w)
{
gra[from].push_back(edge(from, to, w));
gra[to].push_back(edge(to, from, w));
}

次小生成树问题一般使用 prim 算法解决。设最小生成树的权值为 W,因为 prim 算法可以记录生成树路径,思想是在剩下的边集中尝试将每一条边 (u,v) 加入生成树中 ,此时必然可以构成一个环,此时要删除权值最大的一条边。可以用一个 max 数组记录路径 path(u,v) 上的权值最大值 max(u,v),那么最终的答案就是 min (W + w(u,v) - max(u,v) ),max(u,v)可以在循环过程中求得。

网上的算法都是 O(V^2)的,我自己尝试了,似乎写不出堆优化的后的次小生成树写法,记录路径要用到 LCA。下面是模板的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;

const int maxn = 1e4 + 7;
int n, m;
int pre[maxn];
int dis[maxn];
bool vis[maxn];
bool use[maxn][maxn]; // 最小生成树使用的边
int maxv[maxn][maxn]; // u->v中边权最大值
int cost[maxn][maxn]; // u->v的花费
inline int prim(int s)
{
memset(vis, 0, sizeof vis);
memset(use, 0, sizeof use);
memset(maxv, 0, sizeof maxv);
memset(dis, 0x3f, sizeof dis);
dis[s] = 0;
for (int i = 1; i <= n; i++)
pre[i] = s;
int res = 0;
for (int j = 1; j <= n; ++j)
{
int u, minw = INF;
for (int i = 1; i <= n; ++i)
{
if (!vis[i] && dis[i] < minw)
{
u = i, minw = dis[i];
}
}
if (minw == INF)
return -1;
// 不连通
res += dis[u];
vis[u] = 1;
use[u][pre[u]] = use[pre[u]][u] = 1;
for (int v = 1; v <= n; ++v)
{
if (vis[v])
{
maxv[u][v] = maxv[v][u] = max(maxv[v][pre[u]], dis[u]);
}
if (!vis[v] && dis[v] > cost[u][v])
{
dis[v] = cost[u][v], pre[v] = u;
}
}
}
return res;
}
inline int smst(int s)
{
// s:起点 ans:最小生成树的值
int res = INF, ans = prim(s);
if (ans == -1)
return -1;
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j)
if (cost[i][j] != INF && !use[i][j]) // i->j有边且不在最小生成数里
res = min(res, ans + cost[i][j] - maxv[i][j]);
return res == INF ? -1 : res;
}

int main()
{
freopen("data.in", "r", stdin);
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cost[i][j] = cost[j][i] = INF;
}
}
for (int i = 0; i < m; i++)
{
int from, to, w;
scanf("%d%d%d", &from, &to, &w);
cost[from][to] = min(cost[from][to], w);
cost[to][from] = min(cost[to][from], w);
}
cout << smst(1);
}

Kruskal 的算法思想是,对于一个有 n 个点和 m 条边的无向图来说,先对其边按照权值按照从小到大的顺序进行排序。初始化一个并查集。每次取排好序的边集中权值最小的一条边,若这条边与已经选取的边不构成回路,那么将这条边加入到最终的生成树中,最终得到的生成树一定是最小生成树。回路的判断用并查集来实现。时间复杂度为 O(mlogm)

假设 Kruskal 算法对 n≤k 阶图适用,那么,在 k+1 阶图 G 中,我们把最短边的两个端点 a 和 b 做一个合并操作,即把 u 与 v 合为一个点 v’,把原来接在 u 和 v 的边都接到 v’上去,这样就能够得到一个 k 阶图 G’(u,v 的合并是 k+1 少一条边),G’最小生成树 T’可以用 Kruskal 算法得到。

我们证明 T’+{}是 G 的最小生成树。

用反证法,如果 T’+{}不是最小生成树,最小生成树是 T,即 W(T)})。显然 T 应该包含,否则,可以用加入到 T 中,形成一个环,删除环上原有的任意一条边,形成一棵更小权值的生成树。而 T-{},是 G'的生成树。所以 W(T-{})=W(T’),也就是 W(T)=W(T’)+W()=W(T’+{}),产生了矛盾。于是假设不成立,T’+{}是 G 的最小生成树,Kruskal 算法对 k+1 阶图也适用。

由数学归纳法,Kruskal 算法得证。

算法的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct edge
{
int from, to, w;
} edges[maxn];
int cmp(edge a, edge b)
{
return a.w < b.w;
}
int n, m;
int u[maxn];
int find(int x)
{
return u[x] == x ? x : u[x] = find(u[x]);
}
void merge(int x, int y)
{
u[x] = y;
}

int kk()
{
int res = 0;
for (int i = 1; i <= n; i++)
{
u[i] = i;
}

sort(edges, edges + m, cmp);
for (int i = 0; i < m; i++)
{
int x = find(edges[i].from);
int y = find(edges[i].to);
if (x != y)
{
merge(x, y);
res += edges[i].w;
}
}
return res;
}

对于 KK 算法,求次小生成树的方法是维护一个子图集合和一个 maxw 数组,maxw 数据记录子图集之间的最大边权。开始每个点都属于一个子图。在进行并查集合并操作的时候合并子图。每次插入边的时候,更新边端点所在子图的最大边权为新加入的边的权值。显然根据 KK 算法的思想,每次加入的边的权值都是连接的两个子图中权值中最大的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;
const int maxn = 2e5 + 7;
vector<int> gra[maxn];
int maxw[5005][5005];
struct edge
{
int from, to, w, vis;
} edges[5005];
int cmp(edge &a, edge &b)
{
return a.w < b.w;
}
int n, m;
int u[maxn];
int find(int x)
{
return u[x] == x ? x : u[x] = find(u[x]);
}
void merge(int x, int y)
{
u[x] = y;
for (int i = 0; i < gra[x].size(); i++)
{
gra[y].push_back(gra[x][i]);
}
}

int kk()
{
int res = 0;
int cnt = 0;
for (int i = 1; i <= n; i++)
{
u[i] = i;
gra[i].push_back(i);
}

sort(edges, edges + m, cmp);
for (int i = 0; i < m; i++)
{
int x = find(edges[i].from);
int y = find(edges[i].to);
if (x != y)
{
for (int j = 0; j < gra[x].size(); j++)
{
for (int k = 0; k < gra[y].size(); k++)
{
maxw[gra[x][j]][gra[y][k]] = maxw[gra[y][k]][gra[x][j]] = edges[i].w;
}
}
edges[i].vis = 1;
merge(x, y);
cnt++;
res += edges[i].w;
}
}

if (cnt != n - 1)
{
return -1;
}
return res;
}

int seckk()
{
int ans = INF;
int res = kk();
if (res == -1)
{
return -1;
}
for (int i = 0; i < m; i++)
{
if (!edges[i].vis)
{
int w = edges[i].w;
int wmax = maxw[edges[i].from][edges[i].to];
ans = min(ans, res + w - wmax);
}
}
return ans;
}

int main()
{
freopen("data.in", "r", stdin);
cin >> n >> m;
for (int i = 0; i < m; i++)
{
scanf("%d%d%d", &edges[i].from, &edges[i].to, &edges[i].w);
edges[i].vis = 0;
}
int res = seckk();

if (res == -1)
{
cout << "orz";
}
else
{
cout << res;
}
}