[CF1515F] Phoenix and Earthquake(图论推导,构造)
题面
在紧张又忙碌地准备联合省选时,发生了大地震,把原本要参赛的 n n n 个城市之间的全部 m m m 条道路震垮了,使得原本互相都能到达的这 n n n 个城市无法交通了。现在,需要紧急恢复 n − 1 n-1 n−1 条原来的道路,使得任意两个城市可以互相到达。
好在每个城市分别存有 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<(n−1)x ,那么明显不可能连通了,因为总沥青不足。
接着,我们要证明其余的情况一定有解。
首先,如果某个时刻 ∑ a i ′ ≥ ( n ′ − 1 ) ∗ x \sum a'_i\geq (n'-1)*x ∑ai′≥(n′−1)∗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' ∀S⊆E′ 满足 ∣ S ∣ = n ′ − 1 |S|=n'-1 ∣S∣=n′−1,都有 ∀ ( 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 n−1 个不等式左右两边都加起来,
得到: ∑ ( i , j ) ∈ S ′ a i ′ + a j ′ < ( n ′ − 1 ) x \sum_{(i,j)\in S'}a'_i+a'_j<(n'-1)x ∑(i,j)∈S′ai′+aj′<(n′−1)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 ∑ai′⋅di′<(n′−1)x,
由于图连通,所以 ∀ d i ≥ 1 \forall d_i\geq1 ∀di≥1,再加上 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 ∑ai′≤∑ai′⋅di′<(n′−1)x这和 ∑ a i ′ ≥ ( n ′ − 1 ) ∗ x \sum a'_i\geq (n'-1)*x ∑ai′≥(n′−1)∗x 是矛盾的,假设不成立。因此一定有某两个城市之间可以修路,证毕。
然后,修完一个路过后,相当于 ∑ a i ′ \sum a'_i ∑ai′ 变成了 ∑ a i ′ − x \sum a'_i-x ∑ai′−x, ( n ′ − 1 ) ∗ x (n'-1)*x (n′−1)∗x 变成了 ( n ′ − 1 ) ∗ x − x (n'-1)*x-x (n′−1)∗x−x,不等式两边同减 x x x ,仍然满足上面的关系。所以可以一直修到最后把树修完。
有解了,也就是说 YES/NO
的问题解决了,我们可以想想怎么构造了。
利用上面证明的性质,我们发现对于 ∀ S ⊆ E , ∣ S ∣ = n − 1 \forall S\sube E,|S|=n-1 ∀S⊆E,∣S∣=n−1,树 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;
}