一.多叉树变二叉树。
这个技巧其实也有两种具体的方法:树的孩子兄弟表示法与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
#include
#include
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
同样,这个方法也是有缺陷的,就是无法解决点权问题。大多数运用于边权问题。点权当然也可以,但是效率较低。
最后总结一句:树形背包都与dfs离不开关系,所以我们可以在dfs上dp可以写的更简单,也可以在dfs预处理后再总刷表法dp。
这三种方法都各有长处,各有短处,实际运用时还是要注意题目本身的。