一、概念
顾名思义,树型动态规划就是在“树”的数据结构上的动态规划,平时作的动态规划都是线性的或者是建立在图上的,线性的动态规划有二种方向既向前和向后,相应的线性的动态规划有二种方法既顺推与逆推,而树型动态规划是建立在树上的,所以也相应的有二个方向:
1、叶->根:在回溯的时候从叶子节点往上更新信息
2、根 - >叶:往往是在从叶往根dfs一遍之后(相当于预处理),再重新往下获取最后的答案。
不管是 从叶->根 还是 从 根 - >叶,两者都是根据需要采用,没有好坏高低之分。
二、优点
树本身至少就有“子结构”性质(树和子树);也本身就具有递归性。所以在树上DP其实是其所当然的事,相比线性动态规划来讲,转移方程更直观,更易理解。
三、难点
1、和线性动态规划相比,树形DP往往是要利用递归的,所以对递归不熟悉的同学,是一道小小的障碍,说是小小的,因为要去理解也不难。
2、细节多,较为复杂的树形DP,从子树,从父亲,从兄弟……已经一些小的要处理的地方,脑子不清晰的时候做起来颇为恶心
3、状态表示和转移方程,也是真正难的地方。做到后面,树形DP的老套路都也就那么多,难的还是怎么能想出转移方程,状压DP、概率DP这些特别的DP应该说做到最后都是这样!
前置知识:边表
//一、边表(链式前向星) struct edge { int e;//e为一条边的终点 int next;//next为同一父节点的下一条边 //例如,3和2是1的子节点且3在2前面 //那么3.next=2 }ed[maxn]; int first[maxn]; //first[n]是父节点为n的第一条边 //加边: void add_edge(int s,int e)//添加一条从s到e的边 { en++;//新加入的边 ed[en].next=first[s]; //表示:现在新加的这一条边,当做从s开始的第一条边 //类似一个倒序输入,越晚加的边序号越小 first[s]=en; //由上述分析可知:那么父节点为s的第一条边就是en了 ed[en].e=e; //这一条边的终点就是e } //遍历: for(int p=first[s];p!=0;p=ed[p].next) //p从s的第一条子边开始循环,每次变成相同父节点(s)的下一条边 //直到为0时结束循环
最简单的树形DP:给定一棵N个点的树,用树形DP的方法求出该棵树的节点数目
设置状态:f[i]为以i为根的子树,此时表示以i为根的子树有多少节点 void dfs(int now,int fa) { //fa为now的父节点 f[now]=1; for(int p=first[now];p!=0;p=ed[p].next) if(ed[p].next!=fa) { //如果这条边不是指向它父亲的话 dfs(ed[p].e,now); //就dfs,把now当作父节点 f[now]+=f[ed[p].e]; //此时ed[p].e是now的一个子节点 } }