1.引言
第一篇blog,就挑点简单的东西写吧。
有一个这样的问题。有n个城市,先要在城市之间修路,城市u与城市v之间修路的成本是 au,v,求让所有城市建通所需要的最短成本。这就是经典的最小生成树(MST)问题。
2.MST基础
前置概念:
生成树:包含所有顶点,无环,联通的子图;
最小生成树:边权和最小的生成树;
MST本质就是给你一个图,让你求最小生成树,这种问题我们通常用贪心的策略解决。
下面介绍两种求最小生成树的方法。
1.Kruskal算法
贪心选边,将所有边按边权从小到大排序,每次考虑能不能将边权最小的边加入树。若加入这条边后不形成环,就允许加入树。直到选出n-1条边。具体动画演示可以看这里。那下面直接给出代码
ini
struct Edge {
int u, v, w;
bool operator<(const Edge& other) const {
return w < other.w;
}
};
vector<Edge> edges; // 存所有边
vector<int> parent, sz;
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}//找祖先
bool unite(int a, int b) {
a = find(a), b = find(b);
if (a == b) return false;//在同一个连通块,再加边则成环了
if (sz[a] < sz[b]) swap(a, b);
parent[b] = a;
sz[a] += sz[b];
return true;
}
ll kruskal(int n) {
sort(edges.begin(), edges.end());
parent.resize(n+1);
sz.assign(n+1, 1);
for(int i=0;i<parent.size();i++){
parent[i]=i;//父节点指向自己
}
ll res = 0;
int cnt = 0;//已经加入的边的个数
for (auto [u, v, w] : edges) {
if (unite(u, v)) {
res += w;
if (++cnt == n-1) break;
}
}
return res;
}
设边数m,时间复杂度O(mlogm).n为点数,m为边数,后续分析一样。 Kruskal算法是从边出发的,但是存在稠密图(即边很多的图,尤其是完全图),m极大,对Kruskal很不友好,那么就需要从点出发了。
2.Prim算法
只关注于一个联通块,选择其连出去的边权最小的边,将边和新点加入,用优先队列维护候选边。动态演示上面视频有,直接给出代码
arduino
vector<pair<int,int>> adj[N]; // adj[u] = {v, w} 邻接表,u连到v权值w
bool vis[N];
ll prim(int n, int start = 1) {
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
//pq按第一个数(w,权值)顺序排序
pq.push({0, start});
ll res = 0;
int cnt = 0;
while (!pq.empty()) {
auto [w, u] = pq.top(); pq.pop();
if (vis[u]) continue;
vis[u] = true;
res += w;
if (++cnt == n) break;
for (auto [v, nw] : adj[u]) {
if (!vis[v]) pq.push({nw, v});
}//目标连通块的所有未选连出边都在pq里
}
return res;
}
时间复杂度O(mlogn).
各位注意方法选择,稠密图用Prim会更优
3.Borůvka 算法
具体步骤如下:
初始时,每个点都是一个连通分量
重复一下步骤,直至仅剩一个连通分量:
对于每一个连通分量,找到它连向其他边的最小边权
将这些最小边权全部加到MST中
如图:
下面给出代码
struct
int u, v, w;
};
int find(int x, vector<int>& parent) {
return parent[x] == x ? x : parent[x] = find(parent[x], parent);
}
bool unite(int a, int b, vector<int>& parent, vector<int>& sz) {
a = find(a, parent), b = find(b, parent);
if (a == b) return false;
if (sz[a] < sz[b]) swap(a, b);
parent[b] = a;
sz[a] += sz[b];
return true;
}
ll boruvka(int n, vector<Edge>& edges) {
vector<int> parent(n + 1), sz(n + 1, 1);
iota(parent.begin(), parent.end(), 0);
ll res = 0;
int components = n;
while (components > 1) {
// minEdge[i] 存分量 i 的最小出边 {u, v, w}
vector<Edge> minEdge(n + 1, {0, 0, INT_MAX});
for (auto& e : edges) {
int u = find(e.u, parent);
int v = find(e.v, parent);
if (u == v) continue; // 同分量,跳过
if (e.w < minEdge[u].w) minEdge[u] = e; // 更新 u 的最小出边
if (e.w < minEdge[v].w) minEdge[v] = e; // 更新 v 的最小出边
}
bool merged = false;
for (int i = 1; i <= n; i++) {
if (minEdge[i].w == INT_MAX) continue; // 这个分量没有出边了
int u = minEdge[i].u, v = minEdge[i].v, w = minEdge[i].w;
if (unite(u, v, parent, sz)) {
res += w;
components--;
merged = true;
}
}
if (!merged) break; // 没有可以合并的了(图不连通)
}
return components == 1 ? res : -1; // -1 表示图不连通
}
用于可以快速找出最小边的题目,不是很常用。 3.例题 \[\](造桥与砍树 - Problem - QOJ.ac) Prim和Borůvka都可以做,快去试试吧