最小生成树
最小生成树
-
最小生成树(英语:Minimum spanning tree,简称MST)是指在无向带权图中选择一些边,在保证连通性的情况下,边的总权值最小
-
最小生成树不唯一
-
如果无向带权图有 n 个点,最小生成树一定有 n-1 条边
P3366 【模板】最小生成树
- Kruskal 算法
- 把所有的边,根据权值从小到大排序,从权值小的开始考虑
- 如果连接当前的边不会形成环,就把当前边加入 MST,否则跳过
- 考察所有的边后就能得到 MST
- 其实就是每次找不会导致出现环的权值最小的边,依次加入到 MST
- 时间复杂度:O(n+m) + O(m*logm)
#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; // 并查集中存放的是点的编号 vector<int> father; void build(int n) { father.resize(n + 1); // 顶点下标从 1 开始 for (int i = 1; i <= n; ++i) father[i] = i; } int find(int i) { if (i != father[i]) father[i] = find(father[i]); return father[i]; } bool isSameSet(int a, int b) { return find(a) == find(b); } void un1on(int a, int b) { int fa = find(a); int fb = find(b); if (fa == fb) return; father[fa] = fb; } int main() { int n, m; cin >> n >> m; vector<vector<int>> edges(m); for (int i = 0; i < m; ++i) { edges[i].resize(3); cin >> edges[i][0] >> edges[i][1] >> edges[i][2]; } // 按照边的权重排序 sort(begin(edges), end(edges), [](vector<int> &a, vector<int> &b) { return a[2] < b[2]; }); // 构建并查集 build(n); // mst 的权重 int weight = 0; // 已经加入 mst 的边数 int count = 0; // 凑够 n - 1 条边就可以退出 for (int i = 0; i < m && count < n - 1; ++i) { // 两个点在一个集合里,说明已经他俩之间已经有路径了,如果再把当前边加入 mst 就会出现环,所以跳过 if (isSameSet(edges[i][0], edges[i][1])) continue; // 把两个顶点加入 mst un1on(edges[i][0], edges[i][1]); // 累加 mst 权重 weight += edges[i][2]; count++; } if (count == n - 1) { cout << weight; } else { cout << "orz"; } }
- Prime 算法
- 解锁的点的集合叫 set,解锁的边的集合叫 heap(小顶堆),初始状态都为空。
- 从任意点开始,开始点加入 set,开始点的所有边加入到 heap
- 从 heap 中弹出权值最小的边 e,查看边 e 去往的点 x
- 如果 x 已经在 set 中,舍弃边 e,重复步骤 3
- 如果 x 不在 set 中,边 e 属于 MST,把 x 加入 set,重复步骤 3
- 当 heap 为空,MST 就得到了。
- 其实就是两个集合,一个由组成 MST 的点构成的集合 A,一个为剩下元素构成的集合 B。每次从集合 B 中找到 集合 A 距离最短的点,加入到集合 A中。距离是指集合 B 中的一个点,到集合 A 中的某个点有边直接相连,且这个边的权值最小。
- 时间复杂度:O(n+m) + O(m*logm)
#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; // 标记顶点是否已经加入 MST 的集合中 vector<bool> visited; // 小顶堆,下标从 0 开始,pair<点的下标, 边的权值> vector<pair<int, int>> heap; int lenOfHeap; // 顶点下标从 1 开始 void build(int n, int m) { visited.resize(n + 1, false); // 堆的最大容量就是边数的两倍,tmd,找了半天错才发现不是 m heap.resize(m * 2); // 堆的初始容量为 0 lenOfHeap = 0; } // curIndex 为要调整位置的元素的下标 void adjustHeap(int curIndex) { auto temp = heap[curIndex]; int leftChild = 2 * curIndex + 1; while (leftChild <= (lenOfHeap - 1)) { if (leftChild < (lenOfHeap - 1) && heap[leftChild].second > heap[leftChild + 1].second) leftChild++; if (heap[leftChild].second >= temp.second) break; heap[curIndex] = heap[leftChild]; curIndex = leftChild; leftChild = curIndex * 2 + 1; } heap[curIndex] = temp; } void pushToHeap(pair<int, int> p) { heap[lenOfHeap] = p; int curIndex = lenOfHeap; int parentIndex = (curIndex - 1) / 2; lenOfHeap++; while (parentIndex >= 0) { if (heap[parentIndex].second <= p.second) break; heap[curIndex] = heap[parentIndex]; curIndex = parentIndex; if (curIndex == 0) break; parentIndex = (curIndex - 1) / 2; } heap[curIndex] = p; } pair<int, int> getHeapTop() { auto res = heap[0]; heap[0] = heap[lenOfHeap - 1]; lenOfHeap--; adjustHeap(0); return res; } int main() { int n, m; cin >> n >> m; vector<vector<int>> edges(m); for (int i = 0; i < m; ++i) { edges[i].resize(3); cin >> edges[i][0] >> edges[i][1] >> edges[i][2]; } // 建堆和 visited 数组 build(n, m); // 邻接表存放带权值的无向图 vector<vector<pair<int, int>>> graph(n + 1); for (const auto &item: edges) { graph[item[0]].emplace_back(make_pair(item[1], item[2])); graph[item[1]].emplace_back(make_pair(item[0], item[2])); } // 任意一点作为初始点加入集合 visited[1] = true; // 把初始点的相关边都入堆 for (const auto &item: graph[1]) // 边的另一个点的下标和边的权值入堆 pushToHeap(make_pair(item.first, item.second)); // MST 的权值 int weight = 0; // 已经加入 MST 的边数 int count = 0; // 每次选出一个与 MST 集合距离最短的顶点加入集合 while (lenOfHeap != 0) { // 堆顶就是到 MST 集合距离最短的 auto top = getHeapTop(); // 已经在 MST 集合中,就跳过 if (visited[top.first] == true) continue; // 否则把这个点加入集合 visited[top.first] = true; // 累加 MST 权值 weight += top.second; count++; // 再把与 top.first 相关的边入堆 for (const auto &item: graph[top.first]) pushToHeap(make_pair(item.first, item.second)); } if (count == n - 1) cout << weight; else cout << "orz"; }
#include <iostream> #include <vector> #include <queue> #include <unordered_set> #include <algorithm> using namespace std; struct cmp { bool operator()(pair<int, int> &p1, pair<int, int> &p2) { return p1.second > p2.second; } }; int main() { int n, m; cin >> n >> m; vector<vector<int>> edges(m); for (int i = 0; i < m; ++i) { edges[i].resize(3); cin >> edges[i][0] >> edges[i][1] >> edges[i][2]; } // pair<点的下标, 边的权值> priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> heap; // 标记顶点是否已经加入 MST 的集合中 unordered_set<int> st; // 邻接表存放带权值的无向图 vector<vector<pair<int, int>>> graph(n + 1); for (const auto &item: edges) { graph[item[0]].emplace_back(make_pair(item[1], item[2])); graph[item[1]].emplace_back(make_pair(item[0], item[2])); } // 任意一点作为初始点加入集合 st.emplace(1); // 把初始点的相关边都入堆 for (const auto &item: graph[1]) // 边的另一个点的下标和边的权值入堆 heap.push(make_pair(item.first, item.second)); // MST 的权值 int weight = 0; // 已经加入 MST 的边数 int count = 0; // 每次选出一个与 MST 集合距离最短的顶点加入集合 while (!heap.empty()) { // 堆顶就是到 MST 集合距离最短的 auto top = heap.top(); heap.pop(); // 已经在 MST 集合中,就跳过 if (st.find(top.first) != st.end()) continue; // 否则把这个点加入集合 st.emplace(top.first); // 累加 MST 权值 weight += top.second; count++; // 再把与 top.first 相关的边入堆 for (const auto &item: graph[top.first]) heap.push(make_pair(item.first, item.second)); } if (count == n - 1) cout << weight; else cout << "orz"; }
-
Prime 算法优化
-
小顶堆里放(节点,到节点的花费也就是边的权值),根据到达节点的花费来组织小顶堆
-
小顶堆弹出(u 节点,到达 u 节点的花费 y),y 累加到总的权值上,然后考察 u 出发的每一条边
假设,u 出发的边,去往 v 节点,权值 w
A. 如果 v 已经弹出过,也就是已经加入到 MST 集合里了(MST 集合存放构成 MST 的节点),忽略该边
B. 如果 v 还没进入过堆,就入堆,加入记录(v,w)
C. 如果 v 在堆里,且记录为(v,x):
a. 若 w < x,则记录改为(v,w),然后调整该记录在堆中的位置
b. 若 w >= x,忽略该边
-
重复步骤 2,直到小顶堆清空
-
-
其中该记录的操作需要给堆加一个反向索引表,通过节点 v,找到(v,x)在堆中的位置才能将其改成(v, w)
-
这样堆的大小就跟节点的数量相关而不是和边的数量相关
-
优化后为时间复杂度为 O(n+m) + O((m+n)*logn),非常适合节点少边多的情况
// todo 链式前向星 + 优化 Prime
1697. 检查边长度限制的路径是否存在
#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; class Solution { public: vector<int> father; void build(int n) { father.resize(n); for (int i = 0; i < n; ++i) father[i] = i; } int find(int i) { if (i != father[i]) father[i] = find(father[i]); return father[i]; } bool isSameSet(int a, int b) { return find(a) == find(b); } void un1on(int a, int b) { int fa = find(a); int fb = find(b); if (fa == fb) return; father[fa] = fb; } // 顶点下标从 0 开始 vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>> &edgeList, vector<vector<int>> &queries) { // 将边按照权值排序 sort(begin(edgeList), end(edgeList), [](vector<int> &a, vector<int> &b) { return a[2] < b[2]; }); int len = queries.size(); vector<vector<int>> questions(len, vector<int>(4)); for (int i = 0; i < len; ++i) { questions[i][0] = queries[i][0]; questions[i][1] = queries[i][1]; questions[i][2] = queries[i][2]; // 多加一条记录初始问题的位置 questions[i][3] = i; } // 也按照边长度限制进行排序 sort(begin(questions), end(questions), [](vector<int> &a, vector<int> &b) { return a[2] < b[2]; }); // 构建并查集 build(n); vector<bool> res(len); for (int i = 0, edgeIndex = 0; i < len; ++i) { // 问题已按照边长度限制的大小排序,每次把小于这次边限制的所有边生成 MST,查看要求的两个点是否都在 MST 中 // 也就是每次把点合并到 MST 集合上,然后判断要求的两个点是否属于同一个结合 while (edgeIndex < edgeList.size() && edgeList[edgeIndex][2] < questions[i][2]) { un1on(edgeList[edgeIndex][0], edgeList[edgeIndex][1]); edgeIndex++; } res[questions[i][3]] = isSameSet(questions[i][0], questions[i][1]); } return res; } };
P2330 [SCOI2005] 繁忙的都市
- 最小瓶颈树:使图连通,且最大边尽量小
- 最小生成树一定是最小瓶颈树
#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; vector<int> father; void build(int n) { father.resize(n + 1); for (int i = 1; i <= n; ++i) father[i] = i; } int find(int i) { if (i != father[i]) father[i] = find(father[i]); return father[i]; } bool isSameSet(int a, int b) { return find(a) == find(b); } void un1on(int a, int b) { int fa = find(a); int fb = find(b); if (fa == fb) return; father[fa] = fb; } int main() { int n, m; cin >> n >> m; vector<vector<int>> edges(m, vector<int>(3)); for (int i = 0; i < m; ++i) cin >> edges[i][0] >> edges[i][1] >> edges[i][2]; // 按照边的权值排序 sort(begin(edges), end(edges), [](vector<int> &a, vector<int> &b) { return a[2] < b[2]; }); build(n); int count = 0; int res; for (int i = 0; i < m && (count < n - 1); ++i) { if (isSameSet(edges[i][0], edges[i][1])) continue; un1on(edges[i][0], edges[i][1]); count++; if (count == n - 1) res = edges[i][2]; } cout << n - 1 << " " << res; }
本文作者:n1ce2cv
本文链接:https://www.cnblogs.com/sprinining/p/18413494
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2021-09-14 行为型设计模式