一道很好的樹形 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\) 。
就是這條邊的貢獻。
於是 dp 狀態就出來了:
定義 \(dp[u][i]\) 表示 以 \(u\) 為根的子樹中,選 \(i\) 個點作為黑點,對答案整體的最大貢獻。
對於轉移,我們只需要考慮 \(u\) 與 \(v\) 中的那條邊,然後像樹形揹包一樣轉移就好了:
細節處理
上界的處理(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\) 小,造成無效轉移。
所以解一個不等式:
解得
這就是 \(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;
}