最小生成树:在无向带权图中选择择一些边,在保证联通性的情况下,边的总权值最小。
最小生成树可能不只一棵,只要保证边的总权值最小,就都是正确的最小生成树。
如果无向带权图有n个点,那么最小生成树一定有n-1条边。
扩展:最小生成树一定是最小瓶颈树(题目5)
Kruskal算法(最常用)
1.把所有的边,根据权值从小到大排序,从权值小的边开始考虑。
2.如果连接当前的边不会形成环,就选择当前的边。
3.如果连接当前的边会形成环,就不要当前的边。
4.考察完所有边之后,最小生成树的也就得到了。
证明略,其中,判断是否形成环可以使用并查集,也就是说Kruskal算法并不需要建图。
时间复杂度O(m * log m) + O(n) + O(m)
Prim算法(不算常用)
1.解锁的点的集合叫set(普通集合)、解锁的边的集合叫heap(小根堆)。set和heap都为空。
2.可从任意点开始,开始点加入到set,开始点的所有边加入到heap。
3.从heap中弹出权值最小的边e,查看边e所去往的点x
A.如果x已经在set中,边e舍弃,重复步骤3。
B.如果x不在set中,边e属于最小生成树,把x加入set,重复步骤3。
4.当heap为空,最小生成树的也就得到了。
证明略!
时间复杂度O(n + m) + O(m * log m)
Prim算法的优化(比较难,不感兴趣可以跳过)请一定要对堆很熟悉
1.小根堆里放(节点,到达节点的花费),根据到达节点的花费来组织小根堆。
2.小根堆弹出(u节点,到达u节点的花费y),y累加到总权重上去,然后考察u出发的每一条边
假设,u出发的边,去往v节点,权重w
A.如果v已经弹出过了(发现过),忽略该边。
B.如果v从来没有进入过堆,向堆里加入记录(v, w)。
C. 如果v在堆里,且记录为(v, x)。
1)如果w < x,则记录更新成(v, w),然后调整该记录在堆中的位置(维持小根堆)。
2)如果w >= x,忽略该边。
3.重复步骤2,直到小根堆为空。
时间复杂度O(n+m) + O((m+n) * log n)
下面通过一些题目加深对最小生成树的理解。
题目一
测试链接:https://www.luogu.com.cn/problem/P3366
分析:这个就是一个最小生成树模板代码。代码如下。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int N, M;
int father[200002] = {0};
vector<vector<int>> b;
int find(int i){if(i != father[i]){father[i] = find(father[i]);}return father[i];
}
bool Union(int a, int b){int behalf_a = find(a);int behalf_b = find(b);if(behalf_a != behalf_b){father[behalf_a] = behalf_b;return true;}else{return false;}
}
int main(void){int temp1, temp2, temp3;int ans = 0;int num = 0;scanf("%d%d", &N, &M);for(int i = 0;i < M;++i){vector<int> temp;b.push_back(temp);scanf("%d%d%d", &temp1, &temp2, &temp3);b[i].push_back(temp1);b[i].push_back(temp2);b[i].push_back(temp3);}sort(b.begin(), b.end(), [](vector<int> v1, vector<int> v2)->bool{return v1[2] < v2[2];});for(int i = 1;i <= N;++i){father[i] = i;}for(int i = 0;i < M;++i){if(Union(b[i][0], b[i][1])){ans += b[i][2];++num;}}if(num == N-1){printf("%d", ans);}else{printf("orz");}
}
其中,采用并查集可以知道两个节点是否属于同一个集合,故不需要建图。
题目二
测试链接:https://www.luogu.com.cn/problem/P3366
分析:这和题目一一样,不过题目一采用Kruskal算法,题目二采用Prim算法。代码如下。
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<vector<vector<int>>> graph;
vector<bool> visited;
int main(void){int N, M, ans = 0;int temp1, temp2, temp3;int num = 0;scanf("%d%d", &N, &M);visited.assign(N+1, false);vector<vector<int>> temp;for(int i = 0;i <= N;++i){graph.push_back(temp);}for(int i = 0;i < M;++i){scanf("%d%d%d", &temp1, &temp2, &temp3);vector<int> tmp1;tmp1.push_back(temp2);tmp1.push_back(temp3);graph[temp1].push_back(tmp1);vector<int> tmp2;tmp2.push_back(temp1);tmp2.push_back(temp3);graph[temp2].push_back(tmp2);}visited[1] = true;++num;auto cmp = [](vector<int> v1, vector<int> v2)->bool{return v1[1] > v2[1];};priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> q(cmp);for(int i = 0;i < graph[1].size();++i){q.push(graph[1][i]);}while (!q.empty()){int cur = (q.top())[0];int weigth = (q.top())[1];q.pop();if(visited[cur] == false){ans += weigth;visited[cur] = true;++num;for(int i = 0;i < graph[cur].size();++i){q.push(graph[cur][i]);}}}if(num == N){printf("%d", ans);}else{printf("orz");}
}
其中,采用邻接表方式建图;使用优先队列辅助。
题目三
测试链接:https://leetcode.cn/problems/checking-existence-of-edge-length-limited-paths/
分析:可以先将queries数组按limit从小到大排序,将边数组按权值从小到大排序。这样在生成最小生成树的时候,遍历边数组,对于每一个queries,如果边的权值大于limit,则停止,查询queries的两个节点是否在同一个集合,在则为true,不在则为false。代码如下。
class Solution {
public:vector<int> father;int find(int i){if(i != father[i]){father[i] = find(father[i]);}return father[i];}void Union(int a, int b){int behalf_a = find(a);int behalf_b = find(b);if(behalf_a != behalf_b){father[behalf_a] = behalf_b;}}void build(int n){for(int i = 0;i < n;++i){father[i] = i;}}vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>>& edgeList, vector<vector<int>>& queries) {vector<bool> ans;father.assign(n, 0);sort(edgeList.begin(), edgeList.end(), [](vector<int> v1, vector<int> v2)->bool{return v1[2] < v2[2];});int legnth1 = edgeList.size();int length2 = queries.size();ans.assign(length2, false);for(int i = 0;i < length2;++i){queries[i].push_back(i);}sort(queries.begin(), queries.end(), [](vector<int> v1, vector<int> v2)->bool{return v1[2] < v2[2];});build(n);for(int i = 0, j = 0;i < length2;++i){for(;j < legnth1 && edgeList[j][2] < queries[i][2];++j){Union(edgeList[j][0], edgeList[j][1]);}ans[queries[i][3]] = (find(queries[i][0]) == find(queries[i][1]));}return ans;}
};
其中,采用Kruskal算法生成最小生成树。
题目四
测试链接:https://www.luogu.com.cn/problem/P2330
分析:题目挺花里胡哨的,其实就是一个最小生成树。代码如下。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n, m;
vector<vector<int>> road;
int father[301] = {0};
int find(int i){if(i != father[i]){father[i] = find(father[i]);}return father[i];
}
bool Union(int a, int b){int behalf_a = find(a);int behalf_b = find(b);if(behalf_a != behalf_b){father[behalf_a] = behalf_b;return true;}else{return false;}
}
int main(void){int u, v, c;int s = 0, max_score = 0;scanf("%d%d", &n, &m);for(int i = 0;i < m;++i){scanf("%d%d%d", &u, &v, &c);vector<int> temp;temp.push_back(u);temp.push_back(v);temp.push_back(c);road.push_back(temp);}for(int i = 1;i <= n;++i){father[i] = i;}sort(road.begin(), road.end(), [](vector<int> v1, vector<int> v2)->bool{return v1[2] < v2[2];});for(int i = 0;i < m;++i){if(Union(road[i][0], road[i][1])){++s;max_score = road[i][2];}}printf("%d %d", s, max_score);
}
其中,主体和题目一差不多,只是求的东西不一样。