本文部分题目出自《树的动态规划与构造》一文
/*问题可以分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。要使整个活动的总体效果达到最优的问题,称为多阶段决策问题。动态规划就是解决多阶段决策最优化问题的一种思想方法。
因为树可以描述比较复杂的关系,这对选手分析问题的能力有较高的要求,在寻找最优子结构、组织状态时往往需要创造性思维,而且树型动态规划对数学要求不高,不涉及单调性优化等方面,所以竞赛中往往将它作为侧重考察选手分析思考能力的题型出现。
大多数动规都是在一维二维这种规则的背景下的,可以解决的问题比较局限,而树作为一种特殊的图,可以描述比较复杂的关系,再加上树的递归定义,是一种非常合适动规的框架,树型动态规划就成为动规中很特殊的一种类型。*/
本文通过对几道题目分析进行一个总结。
【例1】 加分二叉树
给定一个中序遍历为1,2,3,…,n的二叉树
每个结点有一个权值
定义二叉树的加分规则为:
左子树的加分× 右子树的加分+根的分数
若某个树缺少左子树或右子树,规定缺少的子树加分为1。
构造符合条件的二叉树
该树加分最大
输出其前序遍历序列
分析:这道题划分在区间dp中也一样可以处理。只是代码写法有些许差别。 我们首先先解决加分最大的问题,设f[i][j]表示中序遍历为i到j的二叉树的最大加分。 则显然有f[i][j]=max{f[i][k-1]*f[k+1][j]+val[k]}. val[k]表示k节点的权值大小。 时间复杂度O(N³); 对于记录前序遍历,只要将每次f[i][j]中计算出来的最优的k值存入root[i][j]中,前序遍历即可。 代码比较简单不再赘述。
【例2】二叉苹果树
有一棵苹果树,如果树枝有分叉,一定是分 2 叉(就是说没有只有 1 个儿子的结点)。 这棵树共有 N(1<=N<=100) 个结点(叶子点或者树枝分叉点),编号为 1-N, 树根编号一定是 1。
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。 给定需要保留的树枝数量P,求出最多能留住多少苹果。
分析:由于权值在边上,边与点又具有对应关系。为了方便处理可以将权值转移到点上,状态便可以表示。
f[i][j]表示以节点i作为子树保留j个节点的最大值(注意因为根节点没有权值所以要保留p+1个点)
通过转移得到方程:f[i][j]=max{f[i_left][k]+f[i_right][j-1-k]}
边界f[i][0]=0;f[i][1]=val[i];
输出f[1][p+1]
【例3】选课
给定N门课程,每门课程有一个学分
要从N门课程中选择M门课程,使得学分总和最大
其中选择课程必须满足以下条件:
每门课程最多只有一门直接先修课
要选择某门课程,必须先选修它的先修课
M,N<=500
分析:本题考察的主要是树中多叉树转二叉树的解法技巧。 由于每门课程最多只有一门先修课,所以给定的图要么是一棵树或者森林。但是对于两个不相关的点,我们可以构造一个虚拟的点,将每门先修课连在这个点上,这样就构造出了模型:在一棵具有n个节点的树上寻找m个点使得权值和最大。
构造方程:设前i个选取j门课程的学分最大为g[i][j];
想想是否还有其他做法? 假如这是一棵二叉树,那么我们对问题的求解就会简单得多。所以不妨将多叉树转化成二叉树。左孩子选的时候一定要选取根节点,而右孩子(兄弟)则与之无关。
【例4】 没有上司的舞会(加强版)
这道例题主要是要阐述通过两种办法来进行遍历以免爆栈。题目可以自行百度,时间O(N)空间O(N),其中N<=100000.
方法一:通过广搜拓展节点,然后再反向刷新(从儿子节点向父亲节点刷新)
void solve(){ queue
q; q.push(root); int now,p=0; while (!q.empty()){ now=q.front();q.pop();s[++p]=now; for (int i=head[now];i;i=nex[i]) q.push(to[i]);} for (int i=p;i>=1;--i){ now=s[i]; for(int j=head[now];j;j=nxt[j]) f[now][0]+=max(f[to[j]][0],f[to[j]][1]); for(int j=head[now];j;j=nxt[j]) f[now][1]+=f[to[j]][0]; f[now][1]+=val[now]; } } 方法二:深搜的非递归实现方法如下:
扩展每个节点,将当前儿子节点加入栈并处理,直到其所有儿子节点都已经被处理,就更新其父亲节点并将其弹出栈。void DP(int a){ S.push(a); memcpy(head2,head,sizeof(head)); while (!S.empty()) { int a=S.top(); used[a]=true; if (head2[a]){ //还有儿子没走 int tal=f[head2[a]].go; if (!used[tal]) S.push(tal); //儿子未被访问过 head2[a]=f[head2[a]].next; continue; } //儿子节点已处理完 ans[a][1]+=val[a]; ans[fa[a]][0]=max(ans[fa[a]][0],max(ans[a][0],ans[a][1])); //更新父节点 ans[fa[a]][1]=max(ans[fa[a]][1],ans[a][0]); S.pop();//弹出该点 } }
【例5】tyvj P1520 树的直径 点击打开链接分析:对于求树的直径有两种方法:
方法一:两次深搜:任找一点A为源点,深搜遍历得到最远点B,这个最远点B必定在直径中(感性想想,以A点为源点找到的最长路后面一段必定属于树的直径的一部分);再以这个最远点B为源点深搜遍历求一个最长路,这个最长路即为树的直径。 (贪心思想不给出具体证明)
方法二:DP:显然最长路的两个端点必然是叶子或者根节点。设f(i)表示到i最远的叶子,g(i)表示到i次远的叶子,则有f(i)=max{f(j)}+1;
g(i)=second{f(j)}+1;
其中j必须是i的儿子,计算顺序是自底向上。最终答案为 max{f(i)+g(i)}+1 。具体代码详见博文。
树直径拓展:见codeforces 338B 或者数据生成器一题。
求两次树直径也可参照bzoj1912
【例6】求树的重心 链接戳我
分析:重心有几个挺美妙的性质,在今年北大的夏令营出了一题求树上各个点到某个点距离总和最短的点,并输出。事实上直接找到树的重心即可。由于重心可能有一个或两个所以找两次的重心。
void getroot(int u,int father)//求树的重心 { int i,v; f[u]=0;son[u]=1; for(i=head[u];i!=-1;i=e[i].next) { v=e[i].ed; if(v==father)continue; getroot(v,u); son[u]+=son[v]; f[u]=max(f[u],son[v]); } f[u]=max(f[u],size-son[u]); if(f[u]