最小生成树:在无向带权图中选择择一些边,在保证联通性的情况下,边的总权值最小。
最小生成树可能不只一棵,只要保证边的总权值最小,就都是正确的最小生成树。
如果无向带权图有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
分析:这个就是一个最小生成树模板代码。代码如下。
cpp
#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算法。代码如下。
cpp
#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。代码如下。
cpp
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
分析:题目挺花里胡哨的,其实就是一个最小生成树。代码如下。
cpp
#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);
}
其中,主体和题目一差不多,只是求的东西不一样。