多叉树的树形背包常见建模方法

来源:互联网 发布:电脑像素绘画软件 编辑:程序博客网 时间:2024/05/20 13:16

一.多叉树变二叉树。


这个技巧其实也有两种具体的方法:树的孩子兄弟表示法与dfs序法。

1.树的孩子兄弟表示法。

   大家在学习树形结构时一定接触了一个多叉树变二叉树的方法,就是把每个点与它的第一个儿子连边,然后将它的儿子依次连接起来。可以结合下面的图片理解这句话。


总结成口诀就是:第一个儿子是左子树,其他儿子是左子树的右子树(似乎并不押韵,手动滑稽)

2.dfs序法

dfs序就是对一个树进行一个dfs,然后对于每个点记录一个时间戳dfn数组,这个时间戳就是这个节点的dfs序,然后我们再记录一个size数组,表示以每个节点为根的子树中节点的数量。

假设根节点是u,那么可以容易的推出

第一个儿子的dfs序dfn[first_son]就是dfn[u]+1

第二个儿子的dfs序dfn[second_son]就是dfn[u]+size[first_son]+1

其余同理。

那么u的下一个兄弟的dfs序dfn[next_brother]就是dfn[u]+size[u]+1


这两个方法大多用于树形依赖形背包(即使用一个节点必须要使用它所有的祖先),

主要解决点权问题。


主要作用就是可以使决策转移的过程变成O(1)的了。


最常见的模型就是:有n个物品,有一个m大小的包,每个物品有wi物重与vi物品价值,物品之间存在只有装了物品a,才能装物品b的n-1条关系(就是一个树)。问能取得的最大价值。


简要分析:显然是一个多叉树,考虑转换。

1.孩子兄弟表示法:对于一个节点i,设dp[i][j]表示在以i为根的子树中,用大小为j的包能取得的最大价值,那么dp[i][j]=max(dp[left[i]][j-w[i]]+v[i],dp[right[i]][j])

注意,这里的left[i]是i在原树中的第一个儿子,right[i]是i在原树中的下一个兄弟

这个方程是非常好理解的。效率就是O(nm)的。

2.dfs序法:对于一个dfs序为i的节点u,同样设dp[i][j]表示在以u为根的子树中,用大小为j的包能取得的最大价值,那么dp[i][j]+v[i]->dp[i+1][j-w[i]]

dp[i][j]->dp[i+size[i]+1][j]

注意,这里的转移并不是常见的dp[i][j]=max()....(用dp[i][j]的前驱状态去计算dp[i][j]),而是用dp[i][j]去更新它的后继状态。这种方法被称为刷表法。

两种方法都是非常巧妙的。但作用也是有限的,只能解决依赖性背包中的点权问题。


二.分组的树形背包。


这类问题也是有一个常见模型的,具体可参考洛谷P1272重建道路。

下面针对这道题来分析,能够解决多叉树的,分组的树形背包。

此时,我们的儿子与父亲之间并不存在依赖型关系,那么我们设dp[k][i][j]表示以i为根的子树,在前k个儿子中,分离出一个大小为j的子树(必须包含i),所需要最少的操作次数。

那么我们每计算到第k+1个新的儿子v时(full_son[v]表示v的儿子个数),

dp[k+1][i][j]=min(dp[k][i][j-t]+dp[full_son[v]][v][t]);

由于一个树形关系,我们需要在一个dfs上进行dp,即先dfs(v),然后更新dp[k+1][i][j]。

这个k的一维显然可以用滚动数组优化掉。

那么就是

j=m->1

t=1->j

dp[i][j]=min(dp[i][j-t]+dp[v][t]);

同时,dp一律要注意初始化,即刚开始时所有的dp[i][1]=du[i](du[i]表示与i连边的节点数,又称i的入度(树是无向边哟!))

给出参考代码,方便理解:

#include<cstdio>#include<algorithm>#include<cstring>using namespace std;const int INF=0x3f3f3f3f;const int N=201;struct Edge{int to,next;}e[N*2];int du[N],a[N],dp[N][N];int n,k,res=INF,EdgeCnt=0;void addedge(int u,int v){int p=++EdgeCnt;e[p].to=v;e[p].next=a[u];a[u]=p;}void dfs(int u,int fa){dp[u][1]=du[u];for (int p=a[u];p;p=e[p].next){int v=e[p].to;if (v!=fa){dfs(v,u);for (int j=k;j>=1;j--)for (int k=1;k<=j;k++)dp[u][j]=min(dp[u][j],dp[u][j-k]+dp[v][k]-2);}}res=min(res,dp[u][k]);}int main(){scanf("%d%d",&n,&k);memset(dp,0x3f,sizeof(dp));for (int i=1;i<n;i++){int u,v;scanf("%d%d",&u,&v);addedge(u,v);addedge(v,u);du[u]++;du[v]++;}dfs(1,0);printf("%d",res);return 0;}


同样,这个方法也是有缺陷的,就是无法解决点权问题。大多数运用于边权问题。点权当然也可以,但是效率较低。


最后总结一句:树形背包都与dfs离不开关系,所以我们可以在dfs上dp可以写的更简单,也可以在dfs预处理后再总刷表法dp。


这三种方法都各有长处,各有短处,实际运用时还是要注意题目本身的。

原创粉丝点击