Luogu P3177 樹上染色 [ 藍 ] [ 樹形 dp ] [ 貢獻思維 ]

KS_Fszha發表於2024-07-26

一道很好的樹形 dp !!!!!

樹上染色

錯誤思路

定義 \(dp[u][i]\) 表示以 \(u\) 為根的子樹中,把 \(i\) 個點染成黑色的最大收益。

但這樣寫,就在轉移的時候必須列舉每一個點,複雜度過大,而且還不好寫,是十分錯誤的寫法。

正確思路

一般看到有關樹上“路徑”的題,就要把路徑拆成一個個獨立的單邊,對每個單邊獨立計算貢獻。

我們嘗試對某一條邊 \((u,v)\) 進行考慮( \(u\) 為父親,\(v\) 為兒子):
設兒子下面有 \(j\) 個黑點,整棵樹有 \(k\) 個黑點,樹上總共 \(n\) 個點 ,以 \(x\) 為根的子樹的點數為 \(sz[x]\)
那麼經過這條邊的路徑總數為:兒子下面和父親上面的黑點的路徑數 \((j*(k-j))\) 以及兒子下面和父親上面的白點的路徑數 \(((sz[v]-j)*(n-sz[v]-(k-j)))\) 的和。
然後乘上這條邊的長度 \(w_i\)

\[(j*(k-j)+(sz[v]-j)*(n-sz[v]-(k-j)))*w_i \]

就是這條邊的貢獻。

於是 dp 狀態就出來了:
定義 \(dp[u][i]\) 表示 以 \(u\) 為根的子樹中,選 \(i\) 個點作為黑點,對答案整體的最大貢獻
對於轉移,我們只需要考慮 \(u\)\(v\) 中的那條邊,然後像樹形揹包一樣轉移就好了:

\[dp[u][i]=max(dp[u][i],dp[u][i-j]+dp[v][j]+(j*(k-j)+(sz[v]-j)*(n-sz[v]-(k-j)))*w_i) \]

細節處理

上界的處理(for(int i=min(k,sz[u]);i>=0;i--)for(int j=0;j<=min(i,sz[v]);j++))不必多說,這題的坑點在於下界:

考慮一條鏈的資料,對於一個點 \(u\) ,其子樹的黑色點個數一定是 以 \(u\) 為根的樹的黑色點個數 或者 比以 \(u\) 為根的樹的黑色點個數少 \(1\)

所以如果我們不控制迴圈的下界,那麼在鏈的 hack 下複雜度將直逼 \(O(n^3)\)

這是因為除去該子樹,自己父節點下的其他子樹的總結點數比 \(i-j\) 小,造成無效轉移。

所以解一個不等式:

\[i-j \le sz[u]-sz[v] \]

解得

\[j \ge i-sz[u]+sz[v] \]

這就是 \(j\) 的下界。

時間複雜度 \(O(n^2)\)

樹形揹包檢驗自己複雜度正確與否,只需要構造一條鏈的資料,看看會不會被卡成立方的就好了。

程式碼

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,k,sz[2005];
struct edge{
	int to;
	ll w;
};
vector<edge>s[2005];
ll dp[2005][2005];
void dfs(int u,int fa)
{
	sz[u]=1;
	for(auto tmp:s[u])
	{
		int v=tmp.to;
		ll w=tmp.w;
		if(v==fa)continue;
		dfs(v,u);
		sz[u]+=sz[v];
		for(int i=min(k,sz[u]);i>=0;i--)
		{
			for(int j=max(0,i-sz[u]+sz[v]);j<=min(i,sz[v]);j++)
			{
				dp[u][i]=max(dp[u][i],dp[v][j]+dp[u][i-j]+(1ll*j*(k-j)+1ll*(sz[v]-j)*(n-k-(sz[v]-j)))*w);
			}
		}
	}
}
int main()
{
	cin>>n>>k;
	for(int i=1;i<=n-1;i++)
	{
		int u,v;
		ll w;
		cin>>u>>v>>w;
		s[u].push_back({v,w});
		s[v].push_back({u,w});
	}
	dfs(1,0);
	cout<<dp[1][k];
	return 0;
}

相關文章