【知识点复习】最小生成树

前言

没有前言

我的 \(flag\) 终究还是倒了555.

参考链接:数据结构--最小生成树详解 以及 《算法进阶指南》

介绍

最小生成树,其实就是给定 \(n\) 个点 \(m\) 条边,从 \(m\) 条边中选出 \(n - 1\) 条边使得边权和最小。

定理:任意一棵最小生成树一定包含无向图中权值最小的边。

可以用反证法证明,具体过程参见《算法进阶指南》

而利用最小生成树的 \(MST\) 性质就可以生成最小生成树,下面介绍两种常用的最小生成树算法。

Kruskal 算法

核心思想:维护无向图的最小生成森林。

思路:先按边权排序,利用并查集判断连接一条边的两个端点是否联通,如果联通就跳下一边;不连通就作为树边连上,直到将无向图变为树。

代码

struct edge {
	int x, y, val;
};
bool cmp(node a, node b) {
	return a.val < b.val;
}
int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void Keuskal() {
	sort(edge + 1, edge + 1 + m, cmp);
	for(int i = 1; i <= n; i ++) fa[i] = i;
	for(int i = 1; i <= m; i ++) {
		int tx = find(edge[i].x), ty = find(edge[i].y);
		if(tx != ty) {
			fa[tx] = ty;
			sum += edge[i].val;
		}
	}
}

Prim 算法

核心思想:维护最小生成树的一部分。

思路:维护两个点集——已确定属于最小生成树 \(and\) 未确定的。每次找出端点属于两个集合且边权最小的边连接,再把该条边原属于未确定点集的端点放入已确定的中。直到未确定点集为空。

代码


int map[N][N], d[M];
bool vis[N];
void prim() {
	memset(d, 0x3f, sizeof(d));
	memset(vis, 0, sizeof(vis));
	d[1] = 0;
	for(int i = 1; i < n; i ++) {
		int x = 0;
		for(int j = 1; j <= n; j ++) {
			if(!vis[j] && (!x || d[j] < d[x])) x = j;
		}
		vis[x] = 1;
		for(int y = 1; y <= n; y ++) {
			if(!vis[y]) d[y] = min(d[y], a[x][y]);
		}
	}
}
int main() {
	prim();
	for(int i = 2; i <= n; i ++) ans[i] += d[i];
	return 0;
}

通过代码也不难发现, \(Prim\) 算法更适用于稠密图,尤其是完全图

应用

走廊泼水节

走廊泼水节

给定一棵 N 个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。

求增加的边的权值总和最小是多少。

注意: 树中的所有边权均为整数,且新加的所有边权也必须为整数。

分析 \(Kruskal\) 算法中最小生成树的形成过程,不难得到,当一条边被确定为树边时,其实就是两个并查集的联通,而这两条边之所以可以连通,就是因为不存在连接两个集合且边权更小的边了。

因此,用数组 \(siz[]\) 维护每个并查集的点数,在两个集联通所贡献的答案数就是,\((e[i].val + 1) * (siz[u] * siz[v] - 1)\)

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
const int N = 6e3 + 5;
int t, n, fa[N], siz[N];
ll ans;
struct node {
	int x, y, val;
}e[N];
bool cmp(node a, node b) {
	return a.val < b.val;
}
int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void connect(int x, int y) {
	x = find(x), y = find(y);
	fa[y] = x;
	siz[x] += siz[y];
}

int main() {
	int u, v;
	scanf("%d", &t);
	while(t --) {
		ans = 0;
		scanf("%d", &n);
		for(int i = 1; i <= n; i ++) fa[i] = i, siz[i] = 1;
		for(int i = 1; i < n; i ++) scanf("%d %d %d", &e[i].x, &e[i].y, &e[i].val);
		sort(e + 1, e + n, cmp);
		for(int i = 1; i < n; i ++) {
			u = find(e[i].x), v = find(e[i].y);
			if(u != v) {
				ans += (ll)(e[i].val + 1) * (siz[u] * siz[v] - 1);
				connect(u, v);
			}
		}
		printf("%lld\n", ans);
	}
	
	return 0;
}

野餐规划

题目传送门

一群小丑演员,以其出色的柔术表演,可以无限量的钻进同一辆汽车中,而闻名世界。

现在他们想要去公园玩耍,但是他们的经费非常紧缺。

他们将乘车前往公园,为了减少花费,他们决定选择一种合理的乘车方式,可以使得他们去往公园需要的所有汽车行驶的总公里数最少。

为此,他们愿意通过很多人挤在同一辆车的方式,来减少汽车行驶的总花销。

由此,他们可以很多人驾车到某一个兄弟的家里,然后所有人都钻进一辆车里,再继续前进。

公园的停车场能停放的车的数量有限,而且因为公园有入场费,所以一旦一辆车子进入到公园内,就必须停在那里,不能再去接其他人。

现在请你想出一种方法,可以使得他们全都到达公园的情况下,所有汽车行驶的总路程最少。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<map>
#include<cmath>
#include<iostream>
using namespace std;
const int N = 2e6 + 5;
int n, tot, cnt, rt, S, res, ans = 1e9, dp[N], fa[N];

struct node {
    int x, y, dis;
}e[N];
bool cmp(node a, node b) {
    return a.dis < b.dis;
}

map<string, int> mp;

string s, t;

int find(int x) {
    return fa[x] == x ? x : find(fa[x]);
}

void dfs(int x) {
    if(dp[rt] > S) return;
    int tag = 1;
    for(int i = 1; i < tot; i ++) {
        if(find(i) != find(i + 1)) tag = 0;
    }
    if(tag) {
        ans = min(ans, res);
        return;
    }
    if(x == n + 1) return;
    int u =  find(e[x].x), v = find(e[x].y);
    if(u == v) {
        dfs(x + 1);
        return;
    }
    dp[e[x].y] ++, dp[e[x].x] ++;
    fa[u] = v;
    res += e[x].dis;

    dfs(x + 1);

    dp[e[x].y] --, dp[e[x].x] --;
    fa[u] = u;
    res -= e[x].dis;
    if(e[x].x == rt || e[x].y == rt) dfs(x + 1);
}

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++) {
        cin >> s >> t >> e[i].dis;
        if(!mp.count(s)) mp[s] = ++tot;
        if(!mp.count(t)) mp[t] = ++tot;
        e[i].x = mp[s], e[i].y = mp[t];
    }
    rt = mp["Park"];
    for(int i = 1; i <= tot; i ++) fa[i] = i;
    sort(e + 1, e + 1 + n, cmp);
    scanf("%d", &S);
    dfs(1);
    printf("Total miles driven: %d", ans);
    return 0;
}


沙漠之王

题目传送门

大卫大帝刚刚建立了一个沙漠帝国,为了赢得他的人民的尊重,他决定在全国各地建立渠道,为每个村庄提供水源。

与首都相连的村庄将得到水资源的浇灌。

他希望构建的渠道可以实现单位长度的平均成本降至最低。

换句话说,渠道的总成本和总长度的比值能够达到最小。

他只希望建立必要的渠道,为所有的村庄提供水资源,这意味着每个村庄都有且仅有一条路径连接至首都。

他的工程师对所有村庄的地理位置和高度都做了调查,发现所有渠道必须直接在两个村庄之间水平建造。

由于任意两个村庄的高度均不同,所以每个渠道都需要安装一个垂直的升降机,从而使得水能够上升或下降。

建设渠道的成本只跟升降机的高度有关,换句话说只和渠道连接的两个村庄的高度差有关。

需注意,所有村庄(包括首都)的高度都不同,不同渠道之间不能共享升降机。

先二分答案,然后每次新建一课最小生成树检验。

因为图比较稠密,所以用 \(Prim\) 比较好。

点击查看代码
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 1005;
const double eps = 1e-6, inf = 1e18; 
int n, x[N], y[N], h[N];
double dis[N];
bool tag[N];

int pow(int x) {
	return x * x;
}

double get_dis(int a, int b) {
	return sqrt(pow(x[a] - x[b]) + pow(y[a] - y[b]));
}

bool check(double mid) {
	fill(dis, dis + 1 + n, inf);
	memset(tag, 0, sizeof(tag));
	dis[1] = 0;
	double ans = 0;
	for(int i = 1; i <= n; i ++) {
		int t = -1;
		for(int j = 1; j <= n; j ++) {
			if(!tag[j] && (t == -1 || dis[j] < dis[t])) t = j;
		}
		tag[t] = 1, ans += dis[t];
		for(int j = 1; j <= n; j ++) {
			double tmp = abs(h[t] - h[j]) - mid * get_dis(t, j) + eps;
			if(!tag[j] && dis[j] > tmp) dis[j] = tmp - eps;
		}
	}
	return ans >= 0.0;
}

int main() {
	while(~scanf("%d", &n) && n) {
		for(int i = 1; i <= n; i ++) {
			scanf("%d %d %d", &x[i], &y[i], &h[i]);
		} 
		double l = 0, r = 10000001.0;
		while((r - l) > eps) {
			double mid = (l + r) / 2;
			if(check(mid)) l = mid;
			else r = mid;
		} 
		printf("%.3f\n", l);
	}
	return 0;
}

黑暗城堡

题目传送门

在顺利攻破 Lord lsp 的防线之后,lqr 一行人来到了 Lord lsp 的城堡下方。

Lord lsp 黑化之后虽然拥有了强大的超能力,能够用意念力制造建筑物,但是智商水平却没怎么增加。

现在 lqr 已经搞清楚黑暗城堡有 N 个房间,M 条可以制造的双向通道,以及每条通道的长度。

lqr 深知 Lord lsp 的想法,为了避免每次都要琢磨两个房间之间的最短路径,Lord lsp 一定会把城堡修建成树形的。

但是,为了尽量提高自己的移动效率,Lord lsp 一定会使得城堡满足下面的条件:

设 D[i] 为如果所有的通道都被修建,第 i 号房间与第 1 号房间的最短路径长度;而 S[i] 为实际修建的树形城堡中第 i 号房间与第 1 号房间的路径长度;要求对于所有整数 i,有 S[i]=D[i] 成立。

为了打败 Lord lsp,lqr 想知道有多少种不同的城堡修建方案。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
#define int long long
const int N = 1005, M = 2e6;
const int mod = (1ll << 31) - 1;
bool vis[N];
int n, m, cnt[N], dis[N], ans = 1ll;
int tot, head[N], to[M], nex[M], w[M];
void add(int x, int y, int z) {
	to[++tot] = y, nex[tot] = head[x], head[x] = tot, w[tot] = z;
	to[++tot] = x, nex[tot] = head[y], head[y] = tot, w[tot] = z;
}
void djs() {
	queue<int> q;
	memset(dis, 0x3f, sizeof(dis));
	dis[1] = 0, q.push(1), vis[1] = 1;
	int now, ver;
	while(!q.empty()) {
		now = q.front(), q.pop(), vis[now] = 0;
		for(int i = head[now]; i; i = nex[i]) {
			ver = to[i];
			if(dis[ver] > dis[now] + w[i]) {
				dis[ver] = dis[now] + w[i];
				if(!vis[ver]) q.push(ver), vis[ver] = 1;
			}
		}
	}
}



signed main() {
	scanf("%lld %lld", &n, &m);
	int u, v, z;
	for(int i = 1; i <= m; i ++) {
		scanf("%lld %lld %lld", &u, &v, &z);
		add(u, v, z);
	}
	djs();
	for(int i = 1; i <= n; i ++) {
		for(int j = head[i]; j; j = nex[j]) {
			v = to[j];
			if(dis[v] == dis[i] + w[j]) cnt[v] ++;
		}
	}
	for(int i = 1; i <= n; i ++) {
		if(cnt[i]) ans = ans * cnt[i] % mod;
	}
	printf("%lld", ans);
	return 0;
}
posted @ 2022-07-10 17:28  Spring-Araki  阅读(51)  评论(0编辑  收藏  举报