題目鏈接:點擊查看
題目大意:給出 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;
}