2417. 指挥网络(朱刘算法,最小树形图)

活动 - AcWing

在漫长的骂战过后,利特肯王国和克努斯海洋王国之间爆发了一场武装战争。

克努斯海洋王国部队的猛烈进攻使得利特肯王国的指挥网络彻底瘫痪

临时指挥网络的建立刻不容缓。

利特肯命令史努比负责该项目。

利特肯王国共有 N 个指挥部,位于平面中的 N 个节点上(编号 1∼N)。

其中利特肯所在的指挥总部位于节点 1。

通过对战时情况的详尽研究,史努比认为,当前最关键的一点在于建立一个单向通信网络,使得利特肯的命令能够成功传达至平面中的每个节点处。

如果希望利特肯的命令能够直接从节点 A 传递到另一个节点 B,则必须沿着连接两个节点的直线段构建一条单向传输电线。

因为战争还未停止,所以并不是所有节点对之间都能建立电线。(甚至能够建立从节点 A 传递消息至节点 B 的电线,也不一定能够建立从节点 B 传递消息至节点 A 的电线)

史努比希望这项工程所需耗费的电线长度尽可能短,以便施工可以尽快完成。

输入格式

输入包含若干测试数据。

每组数据第一行包含两个整数 N,M,表示节点总数以及可在其间建立电线的节点对数。

接下来 N 行,其中第 i 行包含两个整数 xi,yi,表示节点 i 的位置坐标为 (xi,yi)。

接下来 M 行,每行包含两个整数 a,b,表示可以建立一条单向电线使得命令可以从节点 a 传递至节点 b。

处理至文件末尾。

输出格式

对于每个测试数据,输出结果占一行。

如果临时网络可以成功构建,则输出所需耗费电线的最小可能长度,保留两位小数。

如果不能成功构建,则输出 poor snoopy

数据范围

1≤N≤100,
1≤M≤104,
0≤xi,yi≤105,
1≤a,b≤N,
每个输入最多包含 10 组测试。

输入样例:
4 6
0 6
4 6
0 0
7 20
1 2
1 3
2 3
3 4
3 1
3 2
4 3
0 0
1 0
0 1
1 2
1 3
4 1
2 3
输出样例:
31.19
poor snoopy

解析: 

树形图的定义:

以某一个点为根的有向树,被称为 树形图

一个有向图,满足无环且每个点的入度为 1 (除了根节点),被称为 树形图

最小树形图: 对于所有树形图中,找到一个总权值和最小的树形图,被称为 最小树形图

最小树形图问题本质上其实就是有向图上的最小生成树问题。Prim 算法和 Kruskal 算法可以解决无向图上的最小生成树问题。朱刘算法可以解决有向图上的最小生成树问题。

朱刘算法

朱刘算法是一个迭代算法,每一次迭代:
1. 除了根节点,对于每个点,找一下这个点的入边中权值最小的边
2. 判断选出的边中是否存在环
(1) 无环,算法结束
(2) 有环,进入步骤 3

3. 将所有环缩点,得到新图 G′,对于每条边:
(1) 环内部的边,删去
(2) 边的终点 u 在环内,该边的权值变成原权值减去 u 在环内的边的权值,即 w−w环

(3) 其他边,不变

算法结束后,之前每一次迭代选出的所有边的总权值之和就是答案。

证明朱刘算法

如果第一次选出的边中不存在环,就意味着当前选出的边满足两个条件,无环且每个点都有一个入边,说明我们选出的是一个树形图,由于每个点选出的边都是所有入边里面权值最小的边,所以一定不可能存在其他方案使得我们选择的边权和更小。

如果有环,那么为什么算法还是对的呢?

假设原图是 G,而缩完点且更新完边权之后的图是 G′。

我们考虑 G 中任意一个环,由于最终图中一定不能存在环,所以这个环一定存在两个性质,第一点是至少需要去掉一条边,第二点是必然存在一个最优解只去一条边。

假设我们现在任意去掉一条边 a,那么必然会选一条新边 b 连向 a 的终点,此时如果我们在任意去掉另外一条边 c,那么必然会再选一条新边 d 连向 c 的终点。此时由于 c 一定小于等于 d,并且由于去掉了 a,选上 c 并不能让图中存在环且权值会变小,因此我们一定可以把 d 换回 c。按照这个原理,任意给我们一个最优解,如果最优解中去掉的边数大于 1,那么我们必然可以从新加的边去掉,换回环上的边,这样它仍然满足是一个树形图,但是总边权和不会变大。由此得出对于任意一个环,必然存在一个最优解只去一条边。

有了以上两个性质,我们可以进行证明。

假设图 G 中所有环里面只去掉环上一条边的树形图的集合放在左边,将 G′ 里面所有树形图的集合放在右边。

对于左边集合中的图的任何一个环,我们只去掉一条边,然后连上一条新边,由于环已经被我们缩点,那么新边就会连向缩点后的新点,对于新点而言,入边就是唯一的,所以去掉一条边后图中无环且每个点的入度为 1,所以去掉一条边后会构成一个树形图,说明左边集合的任意一个图,我们都可以转化成右边集合的一个树形图。

反过来,对于右边集合中任意一个树形图,我们找到一个不是根节点的缩点后的点,那么这个点必然存在一个入边,且这个入边必然是原图里的某一条边,且它一定连向缩点后这个点内部的某一个点,我们将这个点对应的内部的边去掉,将这条原图中的边加上。这样可以发现,任给我们一个右边集合的树形图,我们都可以转化成左边集合的一个满足两个性质的树形图。

因此两个集合是相互对应的。

然后看一下数量关系,可以发现左边集合加上了一条环外边 w,去掉了一条环内边 w′,因此整个操作等于是加上了 w−w′,而右边集合中我们定义每条边就是 w−w′,所以两个集合在数量关系上也是完全一样的。

综上所述,我们想求左边集合的最小树形图只需要求右边集合的最小树形图就行了,因此每次图中有环进行的处理是正确的。

每次迭代最多去掉一个点,最多迭代 n 次,每次迭代内部是时间复杂度是 O(m),因此整个算法时间复杂度是 O(nm)

作者:小小_88
链接:https://www.acwing.com/file_system/file/content/whole/index/content/6745786/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<ctime>
#include<algorithm>
#include<utility>
#include<stack>
#include<queue>
#include<vector>
#include<set>
#include<math.h>
#include<map>
#include<sstream>
#include<deque>
#include<unordered_map>
#include<unordered_set>
#include<bitset>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
const int N = 1e2+10, M = 1e4+10;
const double INF=1e8;
int n, m;
double dist[N][N],bdist[N][N];
PDD p[N];
int g[N][N],pre[N];
int dfn[N], low[N], ts;
int stk[N], top;
int id[N], cnt;
bool in_stk[N],st[N];

void dfs(int u) {
	st[u] = 1;
	for (int i = 1; i <= n; i++) {
		if (g[u][i] && !st[i])dfs(i);
	}
}

bool check() {
	memset(st, 0, sizeof st);
	dfs(1);
	for (int i = 1; i <= n; i++) {
		if (!st[i])return 1;
	}
	return 0;
}

double get_dist(int a, int b) {
	double x = p[a].first - p[b].first;
	double y = p[a].second - p[b].second;
	return sqrt(x * x + y * y);
}

void tarjan(int u) {
	dfn[u] = low[u] = ++ts;
	stk[++top] = u, in_stk[u] = 1;
	int j = pre[u];
	if (!dfn[j]) {
		tarjan(j);
		low[u] = min(low[j], low[u]);
	}
	else if (in_stk[j])low[u] = min(low[u], dfn[j]);
	if (dfn[u] == low[u]) {
		int y;
		cnt++;
		do {
			y = stk[top--];
			id[y] = cnt;
			in_stk[y] = 0;
		} while (y != u);
	}
}

double work() {
	double ans = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (g[i][j]) dist[i][j] = get_dist(i, j);
			else dist[i][j] = 1e8;
		}
	}
	while (1) {
		for (int i = 1; i <= n; i++) {
			pre[i] = i;
			for (int j = 1; j <= n; j++) {
				if (dist[pre[i]][i]>dist[j][i]) {
					pre[i] = j;
				}
			}
		}
		memset(dfn, 0, sizeof dfn);
		cnt = ts = 0;
		for (int i = 1; i <= n; i++) {
			if (!dfn[i])tarjan(i);
		}
		if (cnt == n) {
			for (int i = 2; i <= n; i++)ans += dist[pre[i]][i];
			return ans;
		}
		for (int i = 2; i <= n; i++) {
			if (id[pre[i]] == id[i])ans += dist[pre[i]][i];
		}

		for (int i = 1; i <= cnt; i++) {
			for (int j = 1; j <= cnt; j++) {
				bdist[i][j] = 1e8;
			}
		}
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				if (dist[i][j] < INF && id[i] != id[j]) {
					int a = id[i], b = id[j];
					if (id[pre[j]] == id[j])bdist[a][b] = min(bdist[a][b], dist[i][j] - dist[pre[j]][j]);
					else bdist[a][b] = min(bdist[a][b], dist[i][j]);
				}
		n = cnt;
		memcpy(dist, bdist, sizeof dist);
	}
	return ans;
}

int main() {
	while (cin >> n >> m) {
		memset(g, 0, sizeof g);
		for (int i = 1; i <= n; i++) {
			scanf("%lf%lf", &p[i].first, &p[i].second);
		}
		for (int i = 1,a,b; i <= m; i++) {
			scanf("%d%d", &a, &b);
			if(a!=b&&b!=1)g[a][b] = 1;
		}
		if (check())cout << "poor snoopy" << endl;
		else {
			printf("%.2lf\n", work());
		}
	}
	return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值