樹形背包總結


概念

樹形背包,就是說,在樹上選一個包含根的連通塊,或背包存在依賴關系(選父才能選子),或者需要知道每個點的子樹中選了多少……
通常,我們有兩種方法:

一、基於dfs合並:

我們設\(dp(i,j)\)表示在i的子節點中選j個的狀態。
在轉移時,先dfs子節點,然后依次把子節點合並,每次合並2個。
即枚舉\(a,b\),用\(dp(i,a)\)\(dp(son,b)\)組合為\(f(a+b)\),每次合並后,把f賦給dp。

下面分析時間復雜度:

1、物品大小為1,沒有限制:

(偽)代碼:

void Tree_Dp(int p)
{
    size[p]=1;
    each(x,son[p])
    {
        Tree_Dp(x);
        for(int i=0;i<=size[p];++i)
            for(int j=0;j<=size[x];++j)
                update dp[p][i+j];
        size[p]+=size[x];
    }
}

時間復雜度為\(O(n^2)\)
考慮那個二重循環,可以看做分別枚舉兩棵子樹的每個點。可以發現,點對\((u,v)\),只會在\(Tree_Dp(lca(u,v))\)處被考慮到,所以復雜度是\(O(n^2)\)

2、有物品大小:

這個復雜度可以卡到\(O(wn^2)\),w是物品大小。

代碼:

void dfs(int x)
{
	sum[x]=c[x];
	dp[x][c[x]]=v[x];
	if(!he[x])
		return;
	for(int i=he[x];i!=0;i=e[i].ne)
	{
		int t=e[i].to;
                dfs(t);sum[x]+=sum[t];
		for(int s1=min(sum[x],lim);s1>=c[x];s1--)//注意倒序
		{
			for(int s2=min(sum[t],min(s1-c[x],lim));s2>=c[t];s2--)//注意倒序
				dp[x][s1]=max(dp[x][s1],dp[x][s1-s2]+dp[t][s2]);
		}
	}
}

3、物品大小為1,有k的限制。

(偽)代碼:

void dfs(int u, int fu) {
    int si = 0;
    for (int i = fr[u]; i != -1; i = ne[i]) {
        if (v[i] != fu) 
            dfs(v[i], u);
    }
    for (int i = fr[u]; i != -1; i = ne[i]) {
        if (v[i] == fu) continue;
        int rt = sz[v[i]];
        for (int a = 0; a <= min(si, k); a++) {
            for (int b = 0; b <= min(rt, k - a); b++) {
                   //轉移dp
            }
        }
        si += rt;
    sz[u] = si + 1;
}

這個算法,最初覺得是\(O(nk^2)\)的,實際上是\(O(nk)\)的。

復雜度證明:

  1. 根據正常樹形背包的復雜度\(O(n^2)\),小於等於k的最多產生\(n/k*k^2\)的復雜度。
  2. 大於k與大於k的合並一次,被合並的就增加k,最多n/k次,最多產生\(n/k*k^2\)的復雜度。
  3. 大於k的與小於等於k的合並時,每個小於等於k的最多被合並一次,所以是\(n*s_1+n*s_2+...+n*s_m\),也是\(nk\)

還有一種理解:

把樹按照dfs序變為序列。
然后,在子樹中枚舉取x個,可以理解為取dfs序的前(后)x個。
而合並時,認為一棵子樹取后x個,另一棵取前y個。\((x+y\leq k)\)。這可以合並為長x+y的區間。
這其實就是長度不大於k的子串,最多有nk個。

但是,因為有取0個的情況,所以實際做題時,大約有2的常數。但那個常數就忽略了可以。

二、dfs序上dp:

按照dfs序考慮:
我們設\(dp(i,j)\)表示考慮到第i個,剩余容量為j的狀態:
有兩種轉移:
1、不選i,那么i的子樹都不能選,轉移到\(dp(i+Size_i,j)\)
2、選i,那么按照dfs序考慮下一個,轉移到\(dp(i+1,j-w)+v\)
正確性顯然。
但是,有些樹上的信息無法知道。

代碼

for(int i=1;i<=m;i++)
{
	for(int j=0;j<=n;j++)
	{
		if(j>=cost[i].v)
			dp[i][j]=max(dp[f[i]][j],dp[i-1][j-cost[i].v]+cost[i].w);
		else
			dp[i][j]=dp[i-size(i)][j];
	}
}
printf("%d\n",dp[m][n]);

時間復雜度分析

這個比較顯然,n個點,m的容量限制(沒有則m=n),狀態有\(nm\)個,轉移代價為\(O(1)\),復雜度為\(O(nm)\)
而且,這種方法更好寫,且常數更小。
在物品有多個等特殊問題時,也方便優化。

例題1

題目描述

媽媽昨天對他說:“你的房間需要購買哪些物品,怎么布置,你說了算,只要不超過 N 元錢就行”。今天一早,金明就開始做預算了,他把想買的物品分為兩類:主件不附件,附件是從屬於某個件的,下表就是一些主件不附件的例子:
如果要買歸類為附件的物品,必須先買該附件所屬的件。每個主件可以有很多個附件。附件可能有從屬於自己的附件。金明想買的東西很多,肯定會超過媽媽限定的 N 元。於是,他把每件物品規定了一個重要度,分為 5 等:用整數 1−5 表示,第 5 等最重要。他還從因特網上查到了每件物品的價格(都是在10 元以內)。他希望在不超過 N 元(可以等於 N 元)的前提下,使每件物品的價格不重要度的乘積的總和最大。 設第 j 件物品的價格為 v[j] ,重要度為 w[j] ,共選中了 k 件物品,編號依次為 j1,j2,…,jk,則所求的總和為: v[j1]×w[j1]+v[j2]×w[j2]+…+v[jk]×w[jk] 請你幫助金明設計一個滿足要求的購物單。

輸入格式
第 1 行,為兩個正整數,用一個空格隔開:
N,m (其中 N(<8000) 表示總錢數, m(<8000) 為希望購買物品的個數。) 從第 2 行到第 m+1 行,第 j 行給出了編號為 j−1的物品的基本數據,每行有 3 個非負整數:v,p,q(其中 v 表示該物品的價格(v<10),p表示該物品的重要度(1−5),q 表示該物品是主件還是附件。如果 q=0,表示該物品為主件,如果 q>0 ,表示該物品為附件, q 是所屬件的編號(q< j-1))
輸出格式
一個正整數,為不超過總錢數的物品的價格與重要度乘積的總和的最大值。

這個,是顯然的樹型背包。
如果用方法1,則由於物品大小的影響,可以卡到\(O(nm^2)\)
但是,方法二就是\(O(nm)\)的,很快,還好寫。

(差距很大)。

例題2

  • P4516 [JSOI2018]潛入行動
    題解
    這題,由於我要知道父子關系的信息(例如:父節點是否選擇等),所以方法二便不能使用。
    使用方法一,復雜度為\(O(nk)\)

例題3

  • P3780 [SDOI2017]蘋果樹
    題解
    這題有些像例1,只不過物品有多個,轉成dfs序后,單調隊列優化即可,復雜度\(O(nk)\)
    如果方法1至少要\(O(nk^2)\)
    此時,dfs序還可以知道某條鏈兩側的狀態。

總結下:

方法一:
可以知道每個點的確切情況(比如子樹中選了多少,也能記錄父親的信息)。可以與普通的樹型dp結合使用(因為畢竟是在樹上)。
但是,復雜度相對較高(因為合並背包很慢)。

方法二:
復雜度較低(因為不需要合並背包,相當於依次添加)。
但是,不能知道每個點的確切情況(因為是在序列上),有些題不能使用。
如果是連通塊的DP,只能算出包含根的連通塊。如果要求所有連通塊的信息,需要使用點分治,復雜度多一個log。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM