「Day 8—最小生成树之Kruskal & Prim」

最小生成树

定义

什么是最小生成树呢?
好问题,通俗来说就是给你一个图( \(n\) 个节点),让你选 \(n-1\) 条边使其保持连通,但是又要保证所选的边的和最小。

P3366 【模板】最小生成树

思路(Kruskal)

我们应该怎么实现呢,不妨将要选的边排个序,从小到大选,诶,好想法,这也是一种贪心的思路,那我们该如何保证选这个边有意义呢?万一选了个没必要加的边呢?且必须保证不能有环。
想一想,如果我们什么边都没加入,开始的(\(n\) 个节点就相当于 \(n\) 棵树,每次加边就是对于两棵树的一次合并)。那么怎么能完成这个操作呢?诶,前几天学的并查集不就行吗?

代码

#include<iostream>
#include<algorithm>
using namespace std;

const int MAXN = 800005;
const int MAXM = 4 * 1e7; 
int n,m;
struct node{
	int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
	e[++ tot] = {x,y,len};
}
inline int find(int x){
	return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
	return x.w < y.w;
}
inline int Krusal(){
	int cnt = 0;
	for(int i = 1;i <= n;i ++){
		f[i] = i;
	}
	sort(e + 1,e + tot + 1,cmp);
	int ans = 0;
	for(int i = 1;i <= tot;i ++){
		int x = find(e[i].u);
		int y = find(e[i].v);
		if(x != y){
            ans += e[i].w;
            f[x] = y;
            cnt ++;
        }
	}
	if(cnt != n - 1) return -1;
	return ans;
}

int main(){
	cin >> n >> m;
	for(int i = 1;i <= m;i ++){
		int x,y,z;
		cin >> x >> y >> z;
		add(x,y,z);
	}
	int sum = Krusal();
	if(sum == -1){
		cout << "orz\n";
	}
	else cout << sum << "\n";
	return 0;
}

思路(Prim)

\(Prim\) 算法与 \(Dijkstra\) 算法类似,我们维护一个当前选择的点的集合,表示我们当前的生成树中有这些点。
接下来,每次找到距离当前点集最近的一个顶点,将这个顶点加入到当前点集中,并且将这个顶点与点集相连的边加入到生成树中。

代码

//不带优化版的Prim
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1001;
const int maxm = 10101;
const int inf = 0x3f3f3f3f;
struct Edge {
    int to, next, len;
} edge[maxm * 2];
int h[maxn], tot = -1;
int n, m, s;
int d[maxn];
bool vis[maxn];
void addEdge(int x, int y, int len) {
    edge[++tot] = {y, h[x], len};
    h[x] = tot;
}
int prim() {
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        int mind = inf;
        int v = 0;
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && d[j] < mind) {
                mind = d[j];
                v = j;
            }
        }
        if (mind == inf) {
            break;
        }
        vis[v] = true;
        sum += d[v];
        for (int j = h[v]; j != -1; j = edge[j].next) {
            int to = edge[j].to;
            d[to] = min(d[to], edge[j].len);
        }
    }
    return sum;
}
int main() {
    cin >> n >> m >> s;
    memset(h, -1, sizeof(h));
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        addEdge(u, v, w);
        addEdge(v, u, w);
    }
    cout << prim() << endl;
    return 0;
}

习题

P1547 [USACO05MAR] Out of Hay S

思路

板子,就没啥说的,代码就不放了。

P1546 [USACO3.1] 最短网络 Agri-Net

思路

这个题吧,也是板子。。。输入的时候注意一下就好。

P1195 口袋的天空

思路

首先这个题的本质是求连同 \(K\) 个块的所用最小代价。
只需要改 \(cnt\) 的判断即可,原来是 \(1\) 个连通块,现在是 \(K\) 个,即 \(cnt = n - k\),然后跑一遍即可。

代码
#include<iostream>
#include<algorithm>
using namespace std;

const int MAXN = 800005;
const int MAXM = 4 * 1e7;
int n,m,k;
struct node{
	int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
	e[++ tot] = {x,y,len};
}
inline int find(int x){
	return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
	return x.w < y.w;
}
inline int Krusal(){
	int cnt = n - k;
	for(int i = 1;i <= n;i ++){
		f[i] = i;
	}
	sort(e + 1,e + tot + 1,cmp);
	int ans = 0;
	for(int i = 1;i <= tot;i ++){
		if(!cnt) break;
		int x = find(e[i].u);
		int y = find(e[i].v);
		if(x != y){
            f[x] = y;
            cnt --;
            ans += e[i].w;
        }
	}
	if(cnt) return -1;
	return ans;
}

int main(){
	cin >> n >> m >> k;
	for(int i = 1;i <= m;i ++){
		int u,v,w;
		cin >> u >> v >> w;
		add(u,v,w); 
	}
	int sum = Krusal();
	if(sum == -1){
		cout << "No Answer\n";
	}
	else cout << sum << "\n";
	return 0;
}

P1265 公路修建

思路

这个题一眼生成树,于是兴高采烈的交了一发 \(Kruskal\) 然后就挂了,问原因,答曰:\(TLE+MLE\).
那么这题怎么整呢,于是我们掏出了 \(Prim\) 算法,\(Prim\)稠密图中比 \(Kruskal\) 优,在稀疏图中比 \(Kruskal\) 劣。
那么就没有什么说的了,把坐标一存,用两点间距离公式(本质勾股定理)。

代码
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN = 5005;
const int MAXM = 2 * 1e5;
double dis(double x1,double y1,double x2,double y2){
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
double x[MAXM],y[MAXM];
int n, m, s;
double d[MAXN];
bool vis[MAXN];
double prim(){
    d[1] = 0.0;
    vis[1] = 1;
    double ans = 0;
    for(int i = 1; i <= n;i ++){
    	double mind = 1e9 * 1.0;
    	int v = 1;
    	for(int j = 1;j <= n;j ++){
    		if(!vis[j] && d[j] < mind){
    			mind = d[j];
    			v = j;
			}
		}
		vis[v] = 1;
		ans += d[v];
		for(int j = 1;j <= n;j ++){
			d[j] = min(d[j],dis(x[v],y[v],x[j],y[j]));
		}
	}
	return ans;
}
int main(){
    cin >> n;
    for (int i = 1; i <= n; i++){
        cin >> x[i] >> y[i];
        d[i] = 1e9 * 1.0;
    }
	printf("%.2lf\n",prim());
    return 0;
}

P1194 买礼物

思路

这个题还是很有思维难度的,我第一次还以为要用 \(dp\) 呢,但是看了眼标签,发现是最小生成树,于是我就开始研究怎么个生成树法,这个题的大意是买 \(B\) 件物品,怎么买便宜,有原价 \(A\) 和优惠价 \(K_i{,_j}\) 你可以进行选择。

翻译:你有 \(B\) 个节点,你要把他们连接起来保证边和最小,当然有部分的优惠为 \(0\) ,即没有优惠,或者优惠后比优惠前还贵的,需要注意。最后输出别忘了加上一个 \(A\)。问我为啥?答曰:第一个物品永远不会优惠。

代码
#include<iostream>
#include<algorithm>
using namespace std;

const int MAXN = 800005;
const int MAXM = 4 * 1e7; 
int A,B;
struct node{
	int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
	e[++ tot] = {x,y,min(len,A)};
}
inline int find(int x){
	return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
	return x.w < y.w;
}
int cnt = 0,id = 0;
inline int Krusal(){
	for(int i = 1;i <= B;i ++){
		f[i] = i;
	}
	sort(e + 1,e + tot + 1,cmp);
	int ans = 0;
	for(int i = 1;i <= tot;i ++){
		int x = find(e[i].u);
		int y = find(e[i].v);
		if(x != y){
            ans += e[i].w;
            f[x] = y;
            cnt ++;
        }
	}
	return ans;
}

int main(){
	cin >> A >> B;
	for(int i = 1;i <= B;i ++){
		for(int j = 1;j <= B;j ++){
			int w;
			cin >> w; 
			if(!w){
			    add(i,j,A);
			}
			else add(i,j,w);
		}
	}
	int sum = Krusal();
	cout << sum + A << "\n";
	return 0;
}

P1340 兽径管理

思路

首先最朴素的做法是每次加边都跑一遍,但是会炸,所以我们考虑这个时间的问题,如果都没有 \(n - 1\) 条边,那么肯定就不能联通,否则就跑 \(Kruskal\),但是不能瞎跑,跑的时候要判断在当前时间能走的边,不能走就不跑。

代码
#include<iostream>
#include<algorithm>
using namespace std;

const int MAXN = 800005;
const int MAXM = 4 * 1e7; 
int n,m;
struct node{
	int u,v,w,t;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len,int t){
	e[++ tot] = {x,y,len,t};
}
inline int find(int x){
	return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
	return x.w < y.w;
}
inline void Krusal(int tm){
	int ans = 0,cnt = 0;
	for(int i = 1;i <= m;i ++){
      //判断时间是否满足,不满足就不跑
		if(e[i].t <= tm){
			int x = find(e[i].u);
			int y = find(e[i].v);
			if(x != y){
	            ans += e[i].w;
	            f[x] = y;
	            cnt ++;
	        }
	        if(cnt == n - 1){
	        	cout << ans << "\n";
	        	return;
			}
		}
	}
	cout << "-1\n";
	return;
}

int main(){
	cin >> n >> m;
	for(int i = 1;i <= m;i ++){
		int u,v,w;
		cin >> u >> v >> w;
		add(u,v,w,i);
	}
	sort(e + 1,e + m + 1,cmp);
	for(int i = 1;i <= m;i ++){
		if(i < n - 1){
			cout << "-1\n";
			continue;
		}
		for(int j = 1;j <= n;j ++){
			 f[j] = j;
		}
		Krusal(i);
	}
	return 0;
}
posted @ 2024-08-12 22:34  To_Carpe_Diem  阅读(23)  评论(3编辑  收藏  举报