先序遍历用于优化树形分组背包问题

来源:互联网 发布:健身行业数据 编辑:程序博客网 时间:2024/06/05 07:47

转自 国家集训队 何森 论文

【问题描述】
什么是树形背包?简单的说就是在一棵有N个节点的树上,每个节点都相当于一件物品,每件物品都有一个价格和一个得分对于物品a,设其父节点为father[a],那么你必须购买了father[a]后才能购买a。现给你M元现金,要求求一个购买方案,使得物品的总得分最大。
我们见过很多此类题目:重修道路、技能树、选课……

【分析】
我们很容易想到一个树形动态规划:
cost[a]表示a节点所代表的物品的价格
score[a]表示a节点所代表的物品的得分
状态f[a][b]表示以节点a为根的子树,总共花费不超过b元的最多得分。
状态转移方程:
f[a][b]=Max{f[c1][b1]+f[c2][b2]+f[c3][b3]…f[ck][bk]}+score[a];
其中ci为a的子节点;∑bi=b-cost[a];
这样枚举的效率显然不高。
于是我们想到树形DP的常用优化方法:利用左儿子右兄弟表示法
这样f[a][b]表示以节点a为根的子树且及其兄弟子树总共花费不超过b元的最多得分。
f[a][b]=Max(Max{f[left[a]][b1]+f[right[a]][b-b1-cost[a]]+money[a]},f[right[a]][b]);
其中left[a],right[a]分别表示a的左、右子树。
这样我们得到了一个O(M2N)的动态规划。

然而,我们探索的脚步还未停下。
回想经典的01背包问题。
f[a][b]表示从第a个物品到第n个物品总共花费不超过b元所能得到的最大得分。显然我们要做的就是枚举一个物品买还是不买。
f[a][b]=Max(f[a+1][b-cost[a]]+money[a],f[a+1][b]);
复杂度是O(M*N)的。

为什么这个问题可以做到O(M*N)呢,树形背包可以优化到这个复杂度吗?
我们来探讨树形背包的复杂度是怎么来的。
我们从最原始的多叉树说起:
在状态转移的时候,我们需要枚举给每棵子树分配的现钱数,这导致了枚举量巨大,算法效率不高。
根据这一点我们把多叉树转化成了二叉树,目的就是要减少枚举量,显然我们做到了,在二叉树上我们只需要枚举给左子树分配多少。
关于树形背包先研究到这里,我们先看看经典的背包是怎么回事:
当我们在考虑一个物品a的时候,只用枚举是否要购买它,而且注意到不论我们是否要购买,剩下的钱都由后面所有的物品来分。
回到树上来。

我们发现,在一个分叉点上我们的现钱是需要分流的,即给每一棵子树一部分。
而且在子树中不会相互转移了。
本质的差异就在这里。
这时,我们就有了一个美妙的设想:将左子树和右子树连接起来,使得现钱在考虑完左子树后能再供右子树分配。这样,我们所想的实际上就是一个线形关系。
关于树的线形关系,我们立刻想到树的遍历序。
并且由于需要先处理父节点,从直觉上我们先选择前序遍历序。
这样,我们得到了一个线形关系。
注意树的前序遍历序并不能完全表示一棵树的结构。
我们需要记下每个节点的子孙节点总数childnum[]。
这时,我们看到了很美好的景像:
如我们所愿,树的结构成了 根—>左子树—>右子树 这样的线形结构。
假设我们将前序遍历序保存在list[]里面。
我们定义动态规划的状态:
f[a][b]表示从a到n总共花费b元所得的最大得分。

则我们有转移方程:
f[a][b]=Max{f[a+childnum[list[a]]][b],f[a+1][b-cost[list[a]]]+score[list[a]]};
即:枚举买不买a节点所代表的物品。如果买的话则转到线形表的下一个结点(a节点的后继),否则a节点所在的子树就不用讨论了,直接将钱转移到以a为根的子树的后继上去。
这样,我们就不必给子树分钱了,而是所有的子树共同分享剩下的钱。
根据前面的思考,我们先依次求出每棵树的先根遍历序,并保存在同一个序列list[]中。
为了利用上面的结论,我们还要求出以节点i为根的子树的节点总数count[i]。
现在我们来设计动态规划算法:

定义:
cost[i]表示第i个物品的价格
weight[i]表示第i个物品的权值
F[i][j]表示从第i个物品到第n个物品,最多花费j元,能得到的最大权值和。

状态转移:
对于一个节点,我们考虑是否购买它:
购买:那么我们继续考虑它后面的节点
不购买:那么我们跳过它的子孙节点

方程如下:
F[i][j]=Max{F[i+1][j-cost[list[i]]]+weight[list[i]],F[i+count[list[i]]][j]}
这个算法依然是O(NM)的,很完美!

原创粉丝点击