最近做了幾道樹上背包的題目,很多題目的數據范圍都很小,但實際上樹上背包有多種方式可以優化到 \(O(nm)\) (\(n\) 為節點數,\(m\) 為體積的值域),比如先序遍歷優化(何森《先序遍歷用於優化樹形背包問題》),求泛化物品的並(徐持衡《淺談幾類背包題》)……經過一番學習,覺得還是上下界優化理解起來最簡單,也比較好寫,適用范圍廣,唯一比其它做法復雜的地方就是復雜度分析。
例題講解
這里以一道經典的樹上背包作為例題:【數據加強版】選課
直接把我出的數據加強版放上來了..反正題面里有原題鏈接QAQ
注:本文中用 \(a_i\) 代指題面中的 \(s_i\) 。
\(O(nm^2)\) 做法
用 \(f_{u,i}\) 表示以 \(u\) 為根的子樹中選 \(i\) 門課的最大得分,那么 \(f_{u,i}=\min\limits_{\forall fa[v_j]=u,\sum k_j=i-1}(\sum f[v_j][k_j])+a_u\),而這個轉移可以通過背包實現,依次合並每棵子樹,每次合並時枚舉 \(i\) 和 \(k_j\) ,\(f_{u,i}=\max(f_{u,i},f_{u,i-k_j}+f_{v_j,k_j})\) 。
需要倒序枚舉 \(i\) 防止狀態在轉移前被覆蓋。否則的話dp數組要多一維。
由於可能是森林,所有沒有直接先修課的節點,父親視為節點 \(0\),實際上就要選 \(m+1\) 個節點。
參考代碼:
void dfs(int u)
{
f[u][1]=a[u];
int i,j,k,v;
for (i=head[u];i;i=nxt[i])
{
v=to[i];
dfs(v);
for (j=m+1;j>=1;--j)
{
for (k=1;k<j;++k)
{
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
}
}
}
上下界優化
注意背包轉移的這部分:
for (j=m+1;j>=1;--j)
{
for (k=1;k<j;++k)
{
f[u][j]=max(f[u][j],f[u][k]+f[v][j-k]);
}
}
實際上,這里面有很多狀態都是沒有意義的:
-
轉移時已經合並了大小之和為 \(s\) 的一些子樹,那么 \(f_{u,i}(i>s)\) 實際上是沒有意義的。
-
\(f_{v,i}(i>siz[v])\) 也是沒有意義的。
-
\(f_{u,i}(i>m)\) 是沒有作用的。
所以,可以對 \(j\) 和 \(k\) 的枚舉范圍進行優化:
void dfs(int u)
{
siz[u]=1;
f[u][1]=a[u];
int i,j,k,v;
for (i=head[u];i;i=nxt[i])
{
v=to[i];
dfs(v);
for (j=min(m+1,siz[u]+siz[v]);j>=1;--j)
{
for (k=max(1,j-siz[u]);k<=siz[v]&&k<j;++k)
{
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
}
siz[u]+=siz[v];
}
}
復雜度分析
可以參考這篇博客。
形象的解釋
每個點對都只會在 \(lca\) 處合並一次,所以總的復雜度是 \(O(n^2)\) 的。
這個解釋很簡潔,需要自己意會一下..
嚴格?證明
令 \(T_u\) 為處理子樹 \(u\) 的總用時,那么:
\(\begin{aligned}T_u&=\left(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\right)+t_u\\\\t_u&=1+(1+siz[v_1])\times siz[v_1]+(1+siz[v_1]+siz[v_2])\times siz[v_2]+\cdots+siz[u]\times siz[v_k]\\&=1+\sum\limits_{\forall fa[v_i]=u}siz[v_i]\times(siz[u]+1)\\&=siz[u]^2\end{aligned}\)
對於葉子節點 \(u\) ,\(T(u)=1\) ,是 \(O(siz[u]^2)\) 的。
對於兒子都是葉子節點的節點 \(u\),由於平方和小於和平方,\(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\) 也是 \(O(siz[u]^2)\) 的。
可以這樣遞歸地說明,對於任意節點 \(u\) ,\(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\) 都是 \(O(siz[u]^2)\) 的。
又因為 \(t(u)\) 是 \(O(siz[u]^2)\) 的,\(T(u)\) 就是 \(O(siz[u]^2)\) 的。
所以解決整個問題就是 \(O(n^2)\) 的。
嚴格!證明
枚舉過程中還要對 \(m\) 取 min ,所以應該是這樣的:
\(\begin{aligned}t_u&=1+\min(m,1+siz[v_1])\times \min(m,siz[v_1])+\min(m,1+siz[v_1]+siz[v_2])\times \min(m,siz[v_2])+\cdots+\min(m,siz[u])\times \min(m,siz[v_k])\\&\le m\times siz[u]\end{aligned}\)
所以,\(t(u)\) 是 \(O(\min(siz[u],m)\times siz[u])\) 的。
對於 \(siz[u]\le m\),\(T(u)\) 是 \(O(siz[u]^2)\) 的。
對於 \(siz[u]>m\),\(\sum\limits_{\forall fa[v_i]=u,siz[v_i]\le m}T_{v_i}\) 是 \(O\left(\left(\sum\limits_{\forall fa[v_i]=u,siz[v_i]\le m}siz[v_i]\right)^2\right)\) 的;\(\sum\limits_{\forall fa[v_i]=u,siz[v_i]>m}T_{v_i}\) 是 \(O\left(m\times\sum\limits_{\forall fa[v_i]=u,siz[v_i]>m}siz[v_i]\right)\) 的;所以,\(T(u)\) 是 \(O(m\times siz[u])\) 的。
所以,解決整個問題是 \(O(nm)\) 的。
其它例題
dl代碼
我出的那兩道數據加強版略有些毒瘤..(\(n\times m\le 10^8\))
大約需要這樣寫:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
void dfs(int u);
void add(int u,int v);
const int N=100010;
int head[N],nxt[N],to[N],cnt;
int n,m,a[N],f[100000010],siz[N];
int main()
{
int i,k;
scanf("%d%d",&n,&m);
for (i=1;i<=n;++i)
{
scanf("%d%d",&k,a+i);
add(k,i);
}
dfs(0);
printf("%d",f[m+1]);
return 0;
}
void add(int u,int v)
{
nxt[++cnt]=head[u];
head[u]=cnt;
to[cnt]=v;
}
void dfs(int u)
{
siz[u]=1;
f[u*(m+2)+1]=a[u];
int i,j,k,v;
for (i=head[u];i;i=nxt[i])
{
v=to[i];
dfs(v);
for (j=min(m+1,siz[u]+siz[v]);j>=1;--j)
{
for (k=max(1,j-siz[u]);k<=siz[v]&&k<j;++k)
{
f[u*(m+2)+j]=max(f[u*(m+2)+j],f[u*(m+2)+j-k]+f[v*(m+2)+k]);
}
}
siz[u]+=siz[v];
}
}
關於另一種 \(O(nm)\) 做法
一開始我在洛谷發了篇選課的題解,然后沒過...
那篇題解用的是求泛化物品的並(徐持衡《淺談幾類背包題》)
雖然說洛谷好像還沒有上下界優化的題解..但最近好幾篇題解沒過審,都不太想在洛谷發題解了...