我虽然做了好几道树形背包的题,但是一直不是十分理解,对于每一道题,总是看题解就明白,然后换一道题自己写不出来。临近NOIP,gg让我们强化一下背包以及树形背包,我也恰有此打算,于是又开始从头学习了树形背包。
看了好多博客以及论文之后,对树形背包确实有了一个全新的认识,尤其是这篇博客以及徐持恒的论文《浅谈积累背包问题》,对我有很大的帮助。两者都提到了泛化物品(当然这个名词最初是在背包九讲里面提到的)这个概念,我觉得这是对树形背包O(n * v2)做法的一种不同理解,不过我认为引入这个名词的主要目的还是对O(nv)的做法做出了解释。遗憾的是,我虽然用O(nv)的做法成功写出了一道题,然而却仍旧不是很懂。所以这篇博文主要是讲解O(n * v2)的做法,也算是整理自己的学习笔记吧。
如果哪一天我把O(nv)的做法看懂了的话,可能还会来更这篇博客。
上文已经提到,对于O(n * v2)的做法有两种不同的理解,那么我在这里就分别阐述一下。
都以这道题为例
一、用分组背包来理解
首先题中给的依赖关系是一个森林,那么可以建立一个虚拟节点0,作为森林的根,形成一棵树。
令dp[u][j]表示以 u 为根的子树中,选 j 门课(体积)能得到的最大学分。那么 u 一定要选(初始化dp[u][1] = val[u]),而对于子树内其他点的选取情况,可以把每一种选取方案看成一个物品,又因为每一种方案都是互斥的,每一组只能选一个,那么就是一个分组背包了。这里的组数,是 u 的儿子个数 p = |son(u)|,对于一个vi ∈son(u),他其实代表了j - 1个物品(因为还要选u),拿其中一个为例,dp[vi][k](0 <= k < j)这种选取方案才代表一个物品。
现在考虑转移方程。按照分组背包的写法,我们应该先加一维,dp[u][k][j]表示以x为根的子树,选到第k组,选了 j 门课得到的最大学分。于是有dp[u][k][j] = max(dp[u][k - 1][j], dp[u][k - 1][j - h] + dp[v][sz][h])。注意,dp[v][sz][h]代表一个物品,sz是v的所有组数,因为要保证最优,所以一定从v的所有组数选完的状态转移到u。
然后再模仿分组背包省去第二维,把 j 倒着枚举。
核心代码:
1 void dfs(int now) 2 { 3 for(int i = head[now]; i; i = e[i].nxt) 4 { 5 dfs(e[i].to); 6 for(int j = m + 1; j; --j) 7 for(int k = 0; k < j; ++k) //这一维正着倒着都行,有很多书上是倒着的 8 dp[now][j] = max(dp[now][j], dp[now][j - k] + dp[e[i].to][k]); 9 } 10 }
对于每一个节点只会进行一次O(v2)的分组背包,所以复杂度O(n * v2)。
二、用泛化物品来理解
首先得解释一下啥叫泛化物品:一个价值随体积改变而改变的物品,而且对于一个体积 i,有对应的v[i]。
这个其实人人都见过,只不过没有听说这个名词而已。比如求解01背包就是泛化一个物品的过程,得到的dp[i]就是一个泛化物品。
还有这么回事,泛化物品的和 :有两个泛化物品G1[i], G2[i],要将这两个物品合并。做法就是对于每一个体积 i ,枚举分配给这两个物品的体积 j ,G[i] = max{G1[j], G2[i - j]}。复杂度O(v2)。
现在用泛化物品的概念看看树形背包。dp[u][j]表示的是u所在的泛化物品,则从子树向上递归的时候,其实就是不断地将u所在的泛化物品和他的子树vi的泛化物品合并。合并一次的复杂度O(v2),一共n各节点,每合并一次减少一个,所以总复杂度还是O(n * v2)。
代码和上面完全相同,因为这本来就是对树形背包的两种理解,而不是两种写法。
1 void dfs(int now) 2 { 3 for(int i = head[now]; i; i = e[i].nxt) 4 { 5 dfs(e[i].to); 6 for(int j = m + 1; j; --j) 7 //倒着枚举,因为左边的dp[now][j]代表新的物品,右边的dp[now][j]是原来的物品 8 for(int k = 0; k < j; ++k) //枚举分配体积 9 dp[now][j] = max(dp[now][j], dp[now][j - k] + dp[e[i].to][k]); 10 } 11 }
树形背包O(n * v2)的做法到此也基本讲完了,但这其实都是基础,深入的话还是得靠自己刷题去“悟”。还有一点就是如果哪位大佬会O(nv)的做法,能不能给我讲讲……