题意整理:
去除题目背景后,实际上就是需要对一个给定边权的图,求其最小生成树.
最小生成树:在一个具有N个顶点的带权连通图G中,如果存在某个子图G',其包含了图G中的所有顶点和一部分边,且不形成回路,并且子图G'的各边权值之和最小,则称G'为图G的最小生成树。注意到最小生成树并不唯一
最小生成树有两种常用的算法,既Kruskal算法和Prim算法
方法一:Kruskal算法
核心思想:
Kruskal算法的核心思想是:在带权连通图中,不断地在边集合中找到最小的边,如果该边满足得到最小生成树的条件,就将其加入,直到得到一颗最小生成树。
具体的执行步骤:
1:在带权连通图中,将边按权值排序,从权值最小的边开始执行2
2:判断是否能够选择这条边,依据是边的两个顶点是否已连通,如果不连通就选择这条边使其连通。
3:循环2,直到图中所有的顶点都在同一个连通分量中,即得到最小生成树。在这里,如果所有点在一个连通分量中,被选择的边数必为n - 1,故可以简单判断选择的边数判断
最小生成树采用并查集实现
对于下面的图,步骤为:
1.对边(1,3),符合条件,选择该边,此时集合为{1,3},{2},{4},{5}
2.对边(1,4),符合条件,选择该边,此时集合为{1,3,4},{2},{5}
3.对边(3,4),不符合条件,不选择该边
4.对边(1,2),符合条件,选择该边,此时集合为{1,2,3,4},{5}
5.对边(2,4),不符合条件,不选择该边
6.对边(2,5),符合条件,选择该边,此时集合为{1,2,3,4,5},全部点在同一个连通分量中,循环结束
核心代码:
class Solution { public: int father[50001]; //节点的父节点 void init(int n) { for (int i = 0; i < n; i++) { // 初始化父节点为自身 father[i] = i; } } int find(int x) { // 路径压缩和寻根 return x == father[x] ? x : father[x] = find(father[x]); } void join(int x, int y) { //合并节点 int rootX = find(x); int rootY = find(y); if (rootX != rootY) { father[rootX] = rootY; } } int miniSpanningTree(int n, int m, vector<vector<int> >& cost) { //按边权对边排序 sort(cost.begin(), cost.end(), [&](vector<int>& a, vector<int>& b) { return a[2] < b[2]; }); int p = n - 1;//所需边数 int ans = 0; init(n); for(vector<int>& it : cost) { if(find(it[0]) != find(it[1])) { //不在一个连通分量中,合并 join(it[0], it[1]); ans += it[2]; if(--p == 0) { //边数足够,不再循环 break; } } } return ans; } };
复杂度分析:
时间复杂度:。对边的排序复杂度为,合并操作对每个节点操作一次,复杂度为,故总的时间复杂度为
空间复杂度:,既并查集消耗空间
方法二:Prim算法
核心思想:
Prim算法的步骤是:初始化已加入集合U,图中点集为V,在带权连通图中,从图中某一顶点v开始,此时集合U={v},重复执行下述操作:在所有u∈U,w∈V-U的边(u,w)∈E中找到一条权值最小的边,将(u,w)这条边加入到已找到边的集合,并且将点w加入到集合U中,当U=V时,就找到了这颗最小生成树。
1.选择点1作为起始,加入可选择边(1,3),(1,4),(1,2),U={1}
2.选择点3,此时可选边为(1,4),(1,2),(3,4),U={1,3}
3.选择点4,此时可选边为(1,2),(4,2),(4,5),U={1,3,4}
4.选择点2,此时可选边为(4,5),(2,5),U={1,2,3,4}
5.选择点5,U={1,2,3,4,5},循环结束
核心代码:
class Solution { public: struct cmp { //用于按距离排序 bool operator()(pair<int, int> a, pair<int, int> b) { return a.first > b.first; } }; int miniSpanningTree(int n, int m, vector<vector<int> >& cost) { vector<vector<int>> dist(n, vector<int>(n, 100000));//邻接矩阵 for(vector<int>& it : cost) { //将边加入,处理重边 dist[it[0] - 1][it[1] - 1] = min(dist[it[0] - 1][it[1] - 1], it[2]); dist[it[1] - 1][it[0] - 1] = min(dist[it[1] - 1][it[0] - 1], it[2]); } int ans = 0, need = n; vector<int> vis(n);//判断是否访问过 priority_queue<pair<int,int>, vector<pair<int, int>>, cmp> q; q.push({0, 0});//加入0节点 while(need) { int c = q.top().first; int p = q.top().second; q.pop(); //若节点未加入,加入并更新 if(vis[p] == 0) { vis[p] = 1; ans += c; --need; for(int i = 0; i < n; ++i) { if(!vis[i] && dist[p][i] < 100000) { q.push({dist[p][i], i}); } } } } return ans; } };
复杂度分析:
时间复杂度:,需要选取n个点,每个点进行对其他n-1个点进行判断和加入,总的时间复杂度为
空间复杂度:,使用了大小为的邻接矩阵,队列和vis数组大小都为