AtCoder Regular Contest 098 F - Donation(贪心+树形DP+并查集)

题解

  这题真的太妙了…需要好几个结论才能做出这个题

结论11:给一个点捐赠之后就不会再经过这个点

  正确性显然,如果捐赠后还经过这个点,那么实际上可以推迟捐赠,等到最后一次经过这个点的时候再捐赠会更优。
  有了这个结论我们可以得到一个走法。如果把一个点xx从图GG中删去,并把图分成了G1...GkG_1...G_k这几个子图,那么肯定是把这些子图走剩下一个,然后回到xx给它捐钱,最后走剩下的那个子图,并且再也不从那个子图里出来了。

结论22max(AiBi,0)max(A_i-B_i,0)的值越大的点尽量先到达的走法是最优的

  结论22的正确性需要仔细思考。
  实际上我们可以看成到达一个点前要先给他AiA_i块钱,然后到达之后它会还给你max(AiBi,0)max(A_i-B_i,0)块钱。显然最后剩下的钱是一定的,那么倒着考虑一下,相当于把返还的钱吐出去,然后把AiA_i元拿回来,这个显然要按吐出去的钱升序来进行操作,那么正着考虑就是按降序操作了。

  有了这两个结论之后就可以DP了,构造一棵根为最大max(AiBi,0)max(A_i-B_i,0)的树,然后从叶子到根满足max(AiBi,0)max(A_i-B_i,0)递增。
  设adiad_i表示除了子树内BiB_i的和之外要多准备多少钱,然后从叶子往上DP,并查集维护一下连通性就完了。

代码

#include<iostream> 
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<cmath> 
#include<algorithm>
#define ll long long 
using namespace std;
const int maxn=500010, inf=1e9;
struct edge{int too, pre;}e[maxn];
int n, m, x, y, tot;
int fa[maxn], a[maxn], b[maxn], last[maxn], pos[maxn];
ll ad[maxn], sum[maxn];
bool v[maxn];

template<typename T>
inline void read(T &k)
{
	int f=1; k=0; char c=getchar();
	while(c<'0' || c>'9') c=='-'&&(f=-1), c=getchar();
	while(c<='9' && c>='0') k=k*10+c-'0', c=getchar();
	k*=f;
}

inline void add(int x, int y){e[++tot]=(edge){y, last[x]}; last[x]=tot;}

inline bool cmp(int x, int y){return a[x]<a[y];}

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

int main()
{
	read(n); read(m);
	for(int i=1;i<=n;i++) read(a[i]), read(b[i]), a[i]=max(a[i]-b[i], 0);
	for(int i=1;i<=m;i++) read(x), read(y), add(x, y), add(y, x);
	for(int i=1;i<=n;i++) pos[i]=i, fa[i]=i, ad[i]=a[i], sum[i]=b[i];
	sort(pos+1, pos+1+n, cmp);
	for(int i=1;i<=n;i++)
	{
		int x=pos[i]; v[x]=1;
		for(int j=last[x], too;j;j=e[j].pre)
		if(v[too=e[j].too])
		{
			int fx=gf(x), fy=gf(too);
			if(fx==fy) continue;
			sum[fx]+=sum[fy]; fa[fy]=fx;
			ad[fx]=min(ad[fx], ad[fy]+max(0ll, a[x]-ad[fy]-sum[fy]));
		}
	}
	printf("%lld\n", sum[pos[n]]+ad[pos[n]]);
}