题目链接:点击查看
题目大意:给出 n 个点组成的树,每个点都有一个颜色,非黑即白,现在问对于每个点而言,选出一个连通块,使得白色点的个数与黑色点的个数做差最大
题目分析:记录一下div3的第一次ak,实际上应该是伪ak,感谢zx学长最后抬了我一手F题,dp真的是天克我,不过通过这个题真的学到了不少
首先这个题,题意给出的subtree一定别想当然以为是子树,我一开始就是因为这里读错题然后就被卡懵了,这个题中的subtree其实是子连通块的意思,知道了子连通块之后,我们就可以两次树形dp来做了,因为我们要使得 cnt_white - cnt_black 尽可能大,那么每个节点如果颜色为白色,其贡献就是 1 ,否则就是 -1 ,这样问题就转换为了两次 dp 求出最大子连通块的权值和,一次是自底向上,一次自顶向下,自底向上的话我们可以直接维护dp[ i ],表示以点 i 为根的子树中的最大连通块,注意这个子树是有方向的,这也就意味着我们需要自顶向下再来一次,这一次我们主要是为了处理自底向上过程中无法包含的 fa - u 这条树链所代表的子树,第二次dfs的时候我们就可以直接维护答案了,这一次我们从上向下走的时候,需要额外维护一个sum变量,用于储存 fa - u 这条链所代表的子树的贡献,显然 sum 最小为 0,代表的意义就是不选 fa - u 这条链,此时的答案也就是 ans[ u ] = dp[ u ] + sum 了,现在的问题转换为了如何向下传递 sum ,其实无非只有两种情况:
- ans[ u ]大于 0,可以向子树传递贡献
- ans[ u ]小于等于 0,无法向子树传递贡献
为什么要讨论ans[ u ]呢,因为ans[ u ]此时已经完成了自底向上+自顶向下的更新了,也就是说ans[ u ]代表的就是包含 u 在内的子连通块的最大权值了,所以ans[ u ]的正负,就决定着能否对相邻的子节点做出贡献了,如果ans[ u ]为正的话,那么减去 dp[ v ] 就是 fa - u 这条链代表的子树的权值了
最后代码实现起来还是比较好写的了
代码:
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
const int inf=0x3f3f3f3f;
const int N=2e5+100;
int a[N],dp[N],ans[N];
vector<int>node[N];
void dfs1(int u,int fa)//自底向上
{
dp[u]=a[u];
for(auto v:node[u])
{
if(v==fa)
continue;
dfs1(v,u);
dp[u]+=max(dp[v],0);
}
}
void dfs2(int u,int fa,int sum)//自顶向下
{
ans[u]=dp[u]+sum;
for(auto v:node[u])
{
if(v==fa)
continue;
dfs2(v,u,max(0,ans[u]-max(0,dp[v])));
}
}
int main()
{
#ifndef ONLINE_JUDGE
// freopen("input.txt","r",stdin);
// freopen("output.txt","w",stdout);
#endif
// ios::sync_with_stdio(false);
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",a+i);
if(a[i]==0)
a[i]=-1;
}
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
node[u].push_back(v);
node[v].push_back(u);
}
dfs1(1,-1);
dfs2(1,-1,0);
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
return 0;
}