[JOISC 2016 Day3 T3]「電報」基环树 + 拓扑判环

文章目录

  • 题目描述
  • 样例输入输出
  • 题解
  • 参考代码

题目描述

友情链接:https://loj.ac/problem/2737
题目描述
题目译自 JOISC 2016 Day3 T3「 電報 」
给出 n 个点,每个点的出度均为 1,给出这 n 个点初始指向的点Ai ,和改变这个点指向的目标所需要的价值 Ci
求让所有点强连通的最小花费。

输入格式
第一行输入一个数 n 表示点的个数。
之后的 n 行每行两个数 Ai,Ci 表示第 i 个点指向第 Ai 个点,更改该点指向的点花费为 Ci

输出格式
共一行,为让所有点强连通的最小花费。

样例输入输出

Sample Input 1
4
2 2
1 4
1 3
3 1
Sample Output 1
4

Sample Input 2
4
2 2
1 6
1 3
3 1
Sample Output 2
5

Sample Input 3
4
2 2
1 3
4 2
3 3
Sample Input 3
4

Sample Input 4
3
2 1
3 1
1 1
Sample Output 4
0

题解

(讲的有点混乱,理解一下。。。)
看了看题目所求,又看了看输入格式,然后发现,想要形成一个强连通,那不是要形成一个环吗?!!
既然如此,那么我们就可以开始进一步思考了,
对于任意一个图而言,我们都可以将它拆分成几个不同的部分,这些部分的结构只能是环(单独的一个点也算作是环)或者是树,因此,这个图是一个基环树
[JOISC 2016 Day3 T3]「電報」基环树 + 拓扑判环_第1张图片
那么, 为了能让一堆基环树连成一个环,首先就是要断边,那么断哪些边捏??
接下来,我们进行分类讨论:

  • 对于任意一个环上的点,我们需要让它只存在于本环,不存在于其它树(可以想到,它只会存在于一个环)
  • 对于任意一棵树上的点,我们需要让它只存在于本树,与环不再有任何瓜葛。。。

知道了该怎么断边的断边的效果,就该思考怎么断掉这些边了。
(盯着上面那副图。。。)

  • 对于第一种情况,我们以节点2为例:我们需要断掉 5到2的边 以及 6到2的边 ,使节点2只存在于环中,节点5和节点6成为一条独立的链(一个点也是链)
    简单点说,就是需要断掉环上点与树上点相连的边
  • 对于第二种情况,我们以节点9为例:为了使它只存在于一条链当中,我们需要断掉它与节点1相连的边,成为独立的一个点(这图画得不太好,理解就好);如果它与其它非环上的点还有边相连,那么也需断掉多余的边,使每个节点有且仅有一条入边(题目保证了它只会有一条出边)
    一言以毕之,树上点只能保存一条入边,多余的边都要删掉

接下来,我们继续思考它们的贡献
(还是上面那幅图。。。还是相似的分类讨论)

  • 对于节点2而言,我们需要将除了3到2的边和2到1的边留下,其它的都需要街道其它节点,因此,我们需要加上修改这两条边的贡献;
    简而言之,除了与本环上点相连的边,都需要破掉,即累加这些边的贡献
  • 对于节点9而言,它需要破掉指向节点1的边(在判断节点1时会被破掉,但判断节点9时不会有影响)
    总的来说,就是要破掉多余的边,使之只剩一条入边,一条出边(或是单独一个点)

破完边之后,又出现了一个新问题:
这些破掉的边,最后会指向哪个节点?会不会就是原本指向的节点?
完全有可能!
因此,我们需要思考,“复活”一些边,使之成为一个环。
根据最优策略,我们需要复活消耗最高的那些边,才能使的答案花费最小。
至此,代码就可以出炉了

参考代码

#include 
#include 
#include 
#include 
using namespace std;
#define INf 0x7f7f7f7f

const int N = 1e5;
int n, nxt[N + 5], cost_[N + 5], in[N + 5], isCircle[N + 5], f[N + 5], cnt;
//isCircle[]:-1--该节点不在环上;0--该节点在环上但尚未确定在哪个环;1--已经确定了该节点位于哪个环上
long long ans;
bool flag;
vector < int > G[N + 5], circle[N + 5];
queue < int > q;

void init () {
	scanf ("%d", &n);
	for (int i = 1; i <= n; ++ i) {
		scanf ("%d %d", &nxt[i], &cost_[i]);
		G[nxt[i]].push_back( i );
		//这里建的是反图,因为我们可以发现:
		//所有的删边操作删掉的都是对于单个节点来说的入边(不善言辞,敬请谅解。。。)
		++ in[nxt[i]];
	}
}

//拓扑排序找环
void topSort () {
	for (int i = 1; i <= n; ++ i)
		if (! in[i]) {
			q.push( i );
			isCircle[i] = -1;
			flag = true;
		}
	while (! q.empty()) {
		int u = q.front();
		q.pop();
		-- in[nxt[u]];
		if (! in[nxt[u]]) {
			q.push( nxt[u] );
			isCircle[nxt[u]] = -1;
		}
	}
} 

//删边
void cut () {
	for (int i = 1; i <= n; ++ i) {
		if (isCircle[i] == -1) {
			int maxx = 0;
			for (int j = 0; j < G[i].size(); ++ j) {
				int v = G[i][j];
				maxx = max (maxx, cost_[v]);
				ans += cost_[v];
			}
			ans -= maxx;
		}
		else {
			for (int j = 0; j < G[i].size(); ++ j) {
				int v = G[i][j];
				if (isCircle[v] == -1) {
					ans += cost_[v];
					f[i] = max (f[i], cost_[v]);
				}
			}
			if (isCircle[i])
				continue;
			++ cnt; 
			while (! isCircle[i]) {
				circle[cnt].push_back( i );
				isCircle[i] = 1;
				i = nxt[i];
			}
		}
	}
}

//计算最终的答案
void sovle () {
	for (int i = 1; i <= cnt; ++ i) {
		int minn = INf;
		for (int j = 0; j < circle[i].size(); ++ j) {
			int v = circle[i][j];
			minn = min ( minn, cost_[v] - f[nxt[v]] );
		} 
		if (minn >= 0)
			ans += minn;
		else 
			for (int j = 0; j < circle[i].size(); ++ j) {
				int v = circle[i][j];
				if (cost_[v] - f[nxt[v]] < 0)
					ans += cost_[v] - f[nxt[v]];
			}
	}
}

int main () {
	init ();
	topSort ();
	cut ();
	//这张图不全是环或者不只有一个环
	if (flag || cnt > 1)
		sovle ();	
	printf ("%lld\n", ans);
	return 0;
} 

你可能感兴趣的:([JOISC 2016 Day3 T3]「電報」基环树 + 拓扑判环)