最小生成树
最小生成树
-
最小生成树(英语: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;
}