[CF1515F] Phoenix and Earthquake(图论推导,构造)

题面

在紧张又忙碌地准备联合省选时,发生了大地震,把原本要参赛的 n n n 个城市之间的全部 m m m 条道路震垮了,使得原本互相都能到达的这 n n n 个城市无法交通了。现在,需要紧急恢复 n − 1 n-1 n1 条原来的道路,使得任意两个城市可以互相到达。

好在每个城市分别存有 a i a_i ai 吨沥青。修复每条道路需要 x x x 吨沥青,如果两个城市 i i i j j j 之间有一条损坏的道路,且两个城市的沥青总量不小于 x x x 吨,那么就可以消耗这两个城市的 x x x 吨沥青来修路。修好了路之后,装载沥青的卡车就可以在路上跑,在这一部分连通的城市中任意地来往运送沥青了。

给你道路的信息,以及一开始每个城市分别有的沥青吨数 a i a_i ai ,问能否让 n n n 个城市连通,如果能,就输出任意一种修路的方案(按时间顺序输出恢复的道路序号)

题解

首先判断无解:如果 ∑ a i < ( n − 1 ) x \sum a_i <(n-1)x ai<(n1)x ,那么明显不可能连通了,因为总沥青不足。


接着,我们要证明其余的情况一定有解。

首先,如果某个时刻 ∑ a i ′ ≥ ( n ′ − 1 ) ∗ x \sum a'_i\geq (n'-1)*x ai(n1)x ,即此时每个连通块缩成一个点,也符合上面的条件,那么一定有某两个城市之间是可以修路的。

  • 证明:用反证法,假设所有路都不能修,即 ∀ ( i , j ) ∈ E ′ , a i ′ + a j ′ < x \forall (i,j)\in E',a'_i+a'_j<x (i,j)E,ai+aj<x
    那么我们取 ∀ S ⊆ E ′ \forall S\sube E' SE 满足 ∣ S ∣ = n ′ − 1 |S|=n'-1 S=n1,都有 ∀ ( i , j ) ∈ S ′ , a i ′ + a j ′ < x \forall (i,j)\in S',a'_i+a'_j<x (i,j)S,ai+aj<x (这很废话),
    然后由于不等号同向,可以把这 n − 1 n-1 n1 个不等式左右两边都加起来,
    得到: ∑ ( i , j ) ∈ S ′ a i ′ + a j ′ < ( n ′ − 1 ) x \sum_{(i,j)\in S'}a'_i+a'_j<(n'-1)x (i,j)Sai+aj<(n1)x
    令第 i i i 个点的度数为 d i ′ d'_i di ,则 ∑ a i ′ ⋅ d i ′ < ( n ′ − 1 ) x \sum a'_i\cdot d'_i<(n'-1)x aidi<(n1)x
    由于图连通,所以 ∀ d i ≥ 1 \forall d_i\geq1 di1,再加上 a i a_i ai 肯定非负,因此
    ∑ a i ′ ≤ ∑ a i ′ ⋅ d i ′ < ( n ′ − 1 ) x \sum a'_i\leq\sum a'_i\cdot d'_i<(n'-1)x aiaidi<(n1)x这和 ∑ a i ′ ≥ ( n ′ − 1 ) ∗ x \sum a'_i\geq (n'-1)*x ai(n1)x 是矛盾的,假设不成立。因此一定有某两个城市之间可以修路,证毕。

然后,修完一个路过后,相当于 ∑ a i ′ \sum a'_i ai 变成了 ∑ a i ′ − x \sum a'_i-x aix ( n ′ − 1 ) ∗ x (n'-1)*x (n1)x 变成了 ( n ′ − 1 ) ∗ x − x (n'-1)*x-x (n1)xx,不等式两边同减 x x x ,仍然满足上面的关系。所以可以一直修到最后把树修完。


有解了,也就是说 YES/NO 的问题解决了,我们可以想想怎么构造了。

利用上面证明的性质,我们发现对于 ∀ S ⊆ E , ∣ S ∣ = n − 1 \forall S\sube E,|S|=n-1 SE,S=n1,树 T = ( V , S ) T=(V,S) T=(V,S) 都可以是最终的结果,那么我们就先随便取一棵生成树。

然后,构造方法就千奇百怪了,大都随便过,在性能上却有差异,这里笔者给一个比较好的方法。

有这么一种好写好调不用太多判断的构造方法:

用并查集维护缩点。先随便定个根,开始 dfs ,回溯的时候如果可以,就把自己和父节点合并(修路),回溯到根节点时,下面剩余的后代彼此之间肯定不能修边了(不然回溯之前一定会合并),由上面的性质可以得出,此时只有根节点可以往下合并,那么就从根节点开始像吸面条一样把后代全部合并过来就行了,可以按照深度从小到大和根合并,也可以用其他方法都行,笔者是回溯的时候用栈存了没有修的边,最后从栈顶依次拿出来。

CODE

#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 300005
#define ENDL putchar('\n')
#define LL long long
#define DB double
#define lowbit(x) ((-x) & (x))
#define eps 1e-9
LL read() {
	LL f = 1,x = 0;char s = getchar();
	while(s < '0' || s > '9') {if(s=='-')f = -f;s = getchar();}
	while(s >= '0' && s <= '9') {x=x*10+(s-'0');s = getchar();}
	return f * x;
}
int n,m,i,j,s,o,k;
int a[MAXN],fe[MAXN];
int b[MAXN],rk[MAXN],X;
bool cmp(int x,int y) {return a[x] > a[y];}
bool f[MAXN];
struct it{
	int v,id;it(){v=id=0;}
	it(int V,int I){v=V;id=I;}
};
vector<it> g[MAXN];
int d[MAXN];
priority_queue<int,vector<int>,greater<int> > q;
int as[MAXN],cn;
int fa[MAXN];
LL sum[MAXN];
int findf(int x) {return x==fa[x] ? x:(fa[x]=findf(fa[x]));}
void UNION(int a,int b) {fa[findf(a)]=findf(b);}
bool unionSet(int a,int b) {
	int u = findf(a),v = findf(b);
	if(d[u] < d[v]) swap(u,v);
	if(sum[u]+sum[v] < X) return 0;
	fa[u] = v; sum[v] += sum[u] - X;
	return 1;
}
stack<int> ad;
void dfs(int x,int ff,int fe) {
	d[x] = d[ff] + 1;
	for(int i = 0;i < (int)g[x].size();i ++) {
		int y = g[x][i].v,id = g[x][i].id;
		if(y != ff) {
			dfs(y,x,id);
		}
	}
	if(ff == x) return ;
	else if(unionSet(x,ff)) as[++ cn] = fe;
	else ad.push(fe);
	return ;
}
int main() {
	n = read();m = read();X = read();
	LL sma = 0;
	for(int i = 1;i <= n;i ++) {
		a[i] = read();b[i] = i; fa[i] = i;
		sma += a[i]; sum[i] = a[i];
	}
	sort(b + 1,b + 1 + n,cmp);
	for(int i = 1;i <= n;i ++) rk[b[i]] = i;
	if(sma < (n-1) *1ll* X) {
		printf("NO\n");
		return 0;
	}
	for(int i = 1;i <= m;i ++) {
		s = read();o = read();
		if(findf(s) != findf(o)) {
			UNION(s,o);
			g[s].push_back(it(o,i));
			g[o].push_back(it(s,i));
		}
	}
	for(int i = 1;i <= n;i ++) fa[i] = i,sum[i] = a[i];
	dfs(1,1,0);
	printf("YES\n");
	while(!ad.empty()) as[++ cn] = ad.top(),ad.pop(); 
	for(int i = 1;i < n;i ++) {
		printf("%d\n",as[i]);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值