图论_最小生成树(更新中)

[最小生成树]

[前导知识]:
1.连通图:对于无向图来说,任意两点之间都有路径连接的图叫做连通图。

2.树:没有圈的连通图称为树。(废话大师)

3.生成树(Spanning Tree):对于连通图来说,生成树必须满足以下2个条件
(1)包含连通图中所有的顶点;
(2)任意两顶点之间有且仅有一条通路;
推论:连通图的生成树具有这样的特征,即生成树中 e = v - 1;(其中e为边数,v为顶点数)
(注意:连通图中,由于任意两顶点之间可能含有多条通路,遍历连通图的方式有多种,往往一张连通图可能有多种不同的生成树与之对应)

[不相关小知识]:(搞人心态是吧,还弄个不相关小知识,不相关你别写啊)
1.生成森林:对于非连通图(可分解为多个连通分量,每个连通分量至少对应一棵生成树)来说,多个生成树称为生成森林。

[定义]:如果一棵生成树有边权值,那么使得边权值和最小的生成树称为最小生成树(MST,Minimum Spannning Tree)

[相关算法]:
1.Prim算法
(1)简述:Prim算法和Dijkstra算法十分相似,都是从某个顶点出发,不断添加边的算法。
(2)分析:
首先,我们假设一棵只包含一个顶点v的树T。然后贪心地选取T和其他顶点之间相连的最小权值的边,并把它加到T中。
不断进行这个操作,就可以得到一棵树了。接下来我们来证明通过这个方法得到的生成树就是最小生成树。
(注:这个顶点v可以任意选,不影响贪心结果,证明本蒻蒟不会)

//书本证明:
我们令V表示顶点的集合。假设现在已经求得的生成树的顶点的集合是X (X 包含于 V),并且存在在V上的最小生成树使得T是它的一个子图。
下面我们证明存在一棵最小生成树使得T是它的一个子图并且它包含了连接X和V\X 之间的边中权值最小的边。记连接X和V\X 的权值最小
的边为e,它连接着v(∈ X) 和 u(∈ V\X)。根据假设,存在一棵V上的最小生成树使得T是它的一个子图。如果e也在这棵最小生成树上,问题
就得到证明了,所以我们假设e不在这棵树上。因为生成树本质是一棵树,所以在添加了e之后就产生了圈。圈上的边中,必然存在一条和e
不同的边f连接着X和V\X。从e的定义可以知道f的权值不超过原来的生成树。因此可以说存在同时包含e和T的最小生成树。所以把e加入T中满足
最初的假设。可以这样不断地加入新的边,直到X = V。因为存在V上的最小生成树使得T是它的一个子图,而X = V,所以T就是V上最小的生成树。
(注:X表示X顶点集合, V\X表示在V集合中除外X的顶点集合)

如何查找最小权值的边?
把X和顶点V连接的边的最小权值记为mincost[v]。在向X里添加顶点u时,只需要查看和u相连的边就可以了。对于每条边,
更新mincost[v] = min(mincost[v], 边(u,v)的权值)即可。
如果每次都遍历未包含在X中的点的mincost[v],需要O(|V|*|V|)时间。不过和Dijkstra算法一样,如果使用堆来维护mincost
时间复杂度就是O(|E|log|V|)。

[prim算法 代码实现]
 1 int cost[maxn][maxn];//cost[u][v] 表示边e = (u,v)的权值(不存在时设置为INF)
 2 int mincost[maxn];//从集合X出发到各个顶点的最小权值
 3 bool used[maxn];//顶点i是否包含在集合X中
 4 int V;//顶点数
 5 
 6 int prim()
 7 {
 8     //初始化
 9     for (int i = 0; i < V; ++i)//编号 0~V - 1
10     {
11         mincost[i] = INF;
12         used[i] = false;
13     }
14     mincost[0] = 0;
15     int res = 0;//最小生成树的大小
16 
17     //更新
18     while (true)
19     {
20         int v = -1;//从不属于X的顶点中选取从X到其权值最小的顶点
21         for (int u = 0; u < V; ++u)
22             if (!used[u] && (v == -1 || mincost[u] < mincost[v]))
23                 v = u;
24         
25         if (v == -1)
26             break;
27         used[v] = true;//把顶点v加入X
28         res += mincost[v];//把边的长度加入结果中
29         for (int u = 0; u < V; ++u)
30             mincost[u] = min(mincost[u], cost[v][u]);
31     }
32     return res;
33 }

[进阶]:堆优化
 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 const int maxn = 4e5 + 10;
 4 typedef pair<int, int> pii;
 5 vector<pii> vec[maxn]; 
 6 int vis[maxn];//记录顶点i是否已经编入树
 7 int main() 
 8 {
 9     int n, m;
10     int u, v, val;
11     int ans = 0;
12     scanf("%d%d", &n, &m);
13     memset(vis, 0, sizeof(vis));
14     for (int i = 1;i <= m;++i) {
15         scanf("%d%d%d", &u, &v, &val);//读入边
16         vec[u].push_back(pii(val, v));//顶点u到v距离为val
17         vec[v].push_back(pii(val, u));//顶点v到u距离为val
18     }
19     priority_queue<pii, vector<pii>, greater<pii> > pq;//优先队列:小顶堆
20     vis[1] = 1;//记录1已编入树
21     for (int i = 0; i < (int)vec[1].size(); ++i) 
22         pq.push(vec[1][i]);//将与1相连的顶点入队
23     while (!pq.empty()) 
24     {
25         pii now = pq.top();
26         pq.pop();
27         if (!vis[now.second])//当前顶点没有被访问过
28         {
29             vis[now.second] = 1;//标记已经被访问过
30             ans += now.first;//将最短边添加入树(小顶堆已经帮我们排序好了,所以可以直接添加)
31         }
32         for (int i = 0;i < (int)vec[now.second].size();++i)//枚举与刚刚进树的这个顶点
33         {
34             if (!vis[vec[now.second][i].second])//相邻的顶点
35                 pq.push(vec[now.second][i]);//将其进队列 优先队列会自动排序
36         }
37     }
38     bool flag = true;
39     for (int i = 1; i <= n; ++i)//检查是否所有点已经编入树,如果没有则代表图不连通无法构成最小生成树
40         if (!vis[i])
41         {
42             flag = false;
43             break;
44         }
45     if (flag)
46         printf("%d\n", ans);
47     else 
48         printf("orz\n");
49     return 0;
50 }

2.Kruskal算法
Kruskal算法按照边的权值的顺序从小到大看一遍,如果不产生圈(重边等也算在内)
,就把当前这条边加入到生成树中。

如何判断是否产生圈?
    假设现在要把连接顶点u和顶点v的边e加入生成树中。如果加入之前u和v不在同一个连通分量内
,那么加入e也不会产生圈。反之,如果u和v在同一个连通分量内,那么一定会产生圈。可以使用并查集
高效地判断是否属于同一个连通分量。

Kruskal算法在边的排序上最费时,算法的复杂度是O(|E|log|V|)。

[代码]
 1 //并查集 模板
 2 const int MAX_V = ???;//顶点数上限
 3 int father[MAX_V];
 4 int find(int x)
 5 {
 6     if (x == father[x]) 
 7         return x;
 8     return father[x] = find(father[x]);
 9 }
10 void init_union_find(int v)
11 {
12     for (int i = 0; i < V; ++i)
13         father[i] = i;
14 }
15 bool same(int u, int v)
16 {
17     return find(u) == find(v);
18 }
19 void unite(int u, int v)
20 {
21     father[find(u)] = find(v);
22 }
23 //
24 struct edge
25 {
26     int u,v,cost;
27 };
28 bool com(const edge& e1, const edge& e2)
29 {
30     return e1.cost < e2.cost;
31 }
32 const int MAX_E = ???;//边数上限
33 edge es[MAX_E];
34 int V,E;//顶点数 边数
35 int kruskal()
36 {
37     sort(es, es + E, com);//按照edge.cost的顺序由小到大排序
38     //(注: sort函数中的比较函数不用填写括号)
39     init_union_find(V);//初始化并查集
40     int res = 0;
41     for (int i = 0; i < E; ++i)
42     {
43         edge e = es[i];
44         if (!same(e.u,e.v))
45         {
46             unite(e.u,e.v);
47             res += e.cost;
48         }  
49     }
50     return res;
51 }

 

posted @ 2021-07-20 19:28  QingHUIWuHan  阅读(151)  评论(0)    收藏  举报