返回顶部

最小生成树

一些定义:

  • 生成树:无向图\(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\)表示图的边数。适合稠密图(边相对较多)。

问题一

P3366 【模板】最小生成树

题目描述:给出一个无向有权图,求其最小生成树,如果不存在,则输出\(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;
}

问题二

1584. 连接所有点的最小费用

题目描述:给你一个\(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;
}

暂时写这么多,难度更高的后面看情况再补充吧。

posted @ 2021-12-28 10:10  cherish-lgb  阅读(40)  评论(0编辑  收藏  举报