最小生成树
一些定义:
- 生成树:无向图\(G\)的一个无环连通子集\(T\)称为图\(G\)的生成树
- 对于带权图,生成树的成本等于树中所有边的权重之和
- 最小生成树:具有最小权重的生成树称为最小成本生成树,简称最小生成树(\(MST\))
最小生成树并不唯一
如何求最小生成树:
一个贪心策略设计如下:在每个时刻,该方法生长最小生成树的一条边,并在整个策略的实施过程中,管理一个遵守下述循环不变式的边的集合\(A\):在每遍循环之前,\(A\)是某棵最小生成树的一个子集。
处理策略:每一步,我们选择一条边\((u,v)\)加入集合\(A\),使得\(A\)不违反循环不变式,即\(A\cup \{(u,v)\}\)后还是某棵最小生成树的子集。
这样的边使得我们可以“安全地”将之加入到集合\(A\)而不会破坏\(A\)的循环不变式,因此称之为集合\(A\)的“安全边”。
说明:算法第\(3\)行找一条安全边,这条安全边必然是存在的。因为在执行算法第\(3\)行时,循环不变式告诉了我们存在一棵生成树,满足 \(A \subseteq T\)。在进入\(while\)循环时,\(A\)是\(T\)的真子集,因此必然存在一条边\((u , v) \in T\),使得\((u , v) \notin A\),并且\((u , v)\)对于集合\(A\)是安全的。
关于怎么找安全边以及相关证明不在此赘述,简单来讲就是找一条权值最小的边\((u , v)\),将\((u , v)\)放入\(A\)不会使\(A\)产生环路。
两种基于贪心的求最小生成树的算法:
Kruskal算法
集合\(A\)始终是一个森林,开始时,其结点集就是\(G\)的 结点集,并且\(A\)是所有单节点树构成的森林。之后每次加入到集合\(A\)中的安全边是\(G\)中连接\(A\)的两个不同分量的权重最小的边。时间复杂度:\(O(mlogm)\),\(m\)表示图的边的数量,复杂度的上限在于对边按照权值从小到大排序,判断一条边是否已经在集合\(A\)中需要使用并查集优化。适合稀疏图(边相对较少)。
Prim算法
集合\(A\)始终是一棵树,每次加入到\(A\)中的安全边是连接\(A\)和\(A\)之外某个结点的边中权重最小的边。时间复杂度:\(O(n^2 + m)\),\(n\)表示图的顶点数,\(m\)表示图的边数。适合稠密图(边相对较多)。
问题一
题目描述:给出一个无向有权图,求其最小生成树,如果不存在,则输出\(orz\)。
思路:最小生成树模板题,将使用上述两种算法对其实现。
\(Prim\)算法参考代码:
#include<bits/stdc++.h>
const int INF = 0x3f3f3f3f;
using namespace std;
int n, m, u, v, w;
const int N = 5005;
int graph[N][N];
int dis[N];
bool vis[N];
//如果图不连通,返回INF
int Prim() {
memset(dis, INF, sizeof(dis));//初始化距离为无穷
int res = 0;
for (int i = 0; i < n; ++i) {//n次迭代
int t = -1;
//找当前未更新且dis最小的顶点
for (int j = 1; j <= n; ++j)
if (!vis[j] && (t == -1 || dis[t] > dis[j])) t = j;
//如果不是第一次迭代 且当前未更新的点的dis为INF 贼说明图不连通
if (i && dis[t] == INF) return INF;
if (i) res += dis[t]; //不是第一次迭代 就将这个值加入到答案中
//使用刚刚找到的点更新距离
for (int j = 1; j <= n; ++j) dis[j] = min(dis[j], graph[t][j]);
vis[t] = true;
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
memset(graph, INF, sizeof(graph));//初始化图
//读入图
while (m--) {
cin >> u >> v >> w;
graph[u][v] = graph[v][u] = min(graph[u][v], w);
}
int t = Prim();
if (t == INF) cout << "orz" << endl;
else cout << t << endl;
return 0;
}
\(Kruskal\)算法参考代码:
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
const int N = 5e3 + 5, M = 2e5 + 5;
int p[N];
struct Edge {
int u, v, w;
Edge(int _u = 0 , int _v = 0 , int _w = 0):u(_u) , v(_v) , w(_w){}
bool operator < (const Edge& a)const {
return w < a.w;
}
};
Edge edges[M << 1];//开二倍 因为是无向图
int find(int x) {//并查集
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int n, m, u, v, w;
int main() {
std::ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
int tot = 0;
for (int i = 1; i <= n; ++i) p[i] = i;//并查集初始化
for (int i = 1; i <= m; ++i) {
cin >> u >> v >> w;
edges[++tot] = { u , v , w };
edges[++tot] = { v , u , w };
}
std::sort(edges + 1, edges + 1 + tot);
int cnt = 0, res = 0;//cnt表示当前A中已经有多少条边了
for (int i = 1; i <= tot; ++i) {
auto [u, v, w] = edges[i];
u = find(u); v = find(v);
if (u == v) continue;
p[u] = v;
res += w;
++cnt;
if (cnt == n - 1) break;
}
if (cnt < n - 1) cout << "orz" << '\n';
else cout << res << '\n';
return 0;
}
问题二
题目描述:给你一个\(points\) 数组,表示 \(2D\) 平面上的一些点,其中 \(points[i] = [x_i, y_i]\) 。连接点 \([x_i, y_i]\)和点\([x_j, y_j]\) 的费用为它们之间的 曼哈顿距离 :\(|x_i - x_j| + |y_i - y_j|\) ,其中 \(|val|\) 表示 \(val\) 的绝对值。请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
数据范围:$ 1\leq n \leq 1000$
思路:比较明显的求最小生成树的问题,这个图有\(n^2\)条边数量是为\(1e6\),使用上述两种算法都可通过此题,当然\(Prim\)会快一点。
参考代码:
class Solution {
public:
struct Node{
int u , v , w;
bool operator < (const Node& a)const{
return w < a.w;
}
};
int p[1005];
Node a[2000005];
int find(int x){
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
int minCostConnectPoints(vector<vector<int>>& points) {
int n = points.size(),cnt = 0;
for(int i = 0 ; i < n - 1 ; ++i)
for(int j = i + 1 ; j < n ; ++j){
int dx = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
a[cnt].u = i + 1;a[cnt].v = j + 1; a[cnt++].w = dx;
a[cnt].u = j + 1;a[cnt].v = i + 1; a[cnt++].w = dx;
}
for(int i = 1 ; i <= n ; ++i) p[i] = i;
sort(a , a + cnt);
int res = 0, u = 0 , v = 0 , w = 0;
for(int i = 0 ; i < cnt ; ++i){
u = find(a[i].u);v = find(a[i].v);w= a[i].w;
if(u != v){
p[u] = v;
res += w;
}
}
return res;
}
};
问题三
题目描述:与上题类似,具体请看题目。
思路:这题的数据范围为:\(1\leq n \leq 2000\),如果使用\(Kruskal\)算法,那么会有\(2n^2 = 8e6\)条边,\(O(mlogm)\)的复杂度会TLE。故只能使用\(Prim\)算法,注意这题初始时\(dis_i = c_i\)。并需要维护一个结点的父亲结点的信息用于输出。
时间复杂度:\(O(n^2 + n^2)\) 后一个\(n^2\)是边数
参考代码:
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
using ll = long long;
const int N = 2e3 + 5;
ll dis[N];
int px[N],py[N], n;
int c[N] , k[N], p[N];
bool vis[N];
std::vector<int> resc;
std::vector<std::pair<int , int>> resl;
ll cal(int i , int j){
int dx = px[i] - px[j];
int dy = py[i] - py[j];
return 1ll * (abs(dx) + abs(dy)) * (k[i] + k[j]);
}
ll Prim(){
ll res = 0;
dis[0] = 0;
vis[0] = true;
for(int i = 1 ; i <= n ; ++i) dis[i] = c[i];
for(int i = 0 ; i < n ; ++i){
int t = -1;
for(int j = 1 ; j <= n ; ++j)
if(!vis[j] && (t == -1 || dis[t] > dis[j])) t = j;
vis[t] = true;
res += dis[t];
if(!p[t]) resc.push_back(t);
else resl.push_back({p[t] , t});
for(int j = 1 ; j <= n ; ++j){
if(!vis[j] && dis[j] > cal(t , j)){
dis[j] = cal(t , j);
p[j] = t;
}
}
}
return res;
}
int main(){
std::ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
for(int i = 1 ; i <= n ; ++i) cin >> px[i] >> py[i];
for(int i = 1 ; i <= n ; ++i) cin >> c[i];
for(int i = 1 ; i <= n ; ++i) cin >> k[i];
ll res = Prim();
cout << res << '\n';
cout << resc.size() <<'\n';
for(auto& re : resc) cout << re << ' ';
cout <<'\n';
cout << resl.size() <<'\n';
for(auto& pi : resl) cout << pi.first << " " << pi.second << '\n';
return 0;
}
暂时写这么多,难度更高的后面看情况再补充吧。
作者:cherish.
出处:https://home.cnblogs.com/u/cherish-/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。