树的特征
1.N个点 只有N-1条边的无向图
2.无向图里 任意两点有且只有一条路
3.一个点只有一个前驱 但可以有多个后继
4.无向图没有环
树形DP
由于树有着天然的递归结构 父子结构 而且它作为一种特殊的图 可以描述许多复杂的信息 因此在树就成了一种很适合DP的框架
问题:给你一棵树 要求用最少的代价(最大的收益)完成给定的操作
树形DP 一般来说都是从叶子从而推出根 当然 从根推叶子的情况也有 不过很少(本蒟蒻还没有做到过~)
一般实现方式: DFS(包括记忆化搜索),递推等
例题
1.二叉苹果树
传送门
二叉树 很爽的一种dp结构 由于二叉树父亲节点只用管它的左右儿子 状态转移变得较为轻松
在遇到多叉树时 我们时常会考虑把多叉树转化为二叉树来做
而这道题直接是二叉树
首先考虑给DP数组下定义 一般来说树形DP的DP数组的第一维都是当前节点的编号
这道题光一维肯定是不够的 那么加维 发现dp[i][j]表示当前节点为i 保留j个节点的最大苹果数量比较ok
那么方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.lson][k]+dp[i.rson][j-k-1]+apple[i])
很明显 该问题具有很明显的最优子结构性质 也具备无后效性(每一步只与儿子有关系 而与爸爸之类的没有关系 )
另外 还可以在dfs时运用记忆化 可以大大提高速度
再提一句 由于题目中给的权值在边上 让人特别难受 于是 我们把权值转化到儿子上会方便操作
//f[i][j] 当前在i点 保留j个节点
//f[i][j]=max(f[i][j],f[tree[i].l][k]+f[tree[i].r][j-k-1]+tree[i].v);
#include
using namespace std;
const int N=150;
int n,q,dp[N][N];
struct node
{
int lson;
int rson;
int val;
}tree[N*20];
int dfs(int now,int point)
{
if(now==0||point==0)
{
return 0;
}
if(tree[now].lson==0&&tree[now].rson==0)
{
return tree[now].val;
}
if(dp[now][point]>0) return dp[now][point];//记忆化
for(int k=0;k>n>>q;
for(int i=1;i<=n-1;i++)
{
int fa,son,v;
cin>>fa>>son>>v;
tree[son].val=v;
if(tree[fa].lson==0) tree[fa].lson=son;
else tree[fa].rson=son;
}
cout<
2.选课
传送门
这道题就是采用刚才提到过的 把多叉树转化为二叉树来做
关于如何把多叉树转化为二叉树 有个口诀 叫做左儿子不变 右儿子兄♂弟
详细的不多说 可以去参考一下相关资料
等转化为二叉树了过后 让我们来琢磨一下
左儿子:原根节点的孩子
右儿子:原根节点的兄♂弟
也就是说 不能直接套用第一题的方程 但是可以对dp数组进行相同的定义
对于一个根节点 都可以 选 或者 不选
当给左儿子分配资源时 根节点必须选 而与右儿子无关
因此 方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.rson][j],dp[i.lson][k]+dp[i.rson][j-k-1]+val[i]) (0<=k 3.树的直径 传送门 这是解析...然而我觉得bfs或者dfs就够了 何苦dp 4.战略游戏 传送门 假设dp[i]表示以i为根的子树上需要安放的最少士兵 希望能从i的儿子推出i的情况 然而无法做到 考虑加维 由于每个节点可以选 或者不选 如果选了的话 那他的儿子可选可不选 如果没选的话 那他的儿子就必须选 因此dp[i][0]表示选了节点i所需要安防的最少士兵 dp[i][1]表示不选 方程显而易见 dp[i][0]=sigma min(,dp[i.son][0],dp[i.son][1]) dp[i][1]=sigma min(dp[i][1],dp[i.son][0]) 5.皇宫看守 网址实在没找到....我提交的地方是学校题库 (题面) 对于每个点 都有三种情况 1.自己放 2.父亲放(被父亲看到) 3.儿子放(被儿子看到) 这意味着什么呢? 对于一个i if 自己放了 也就是说儿子一定被父亲看到 也可以安排警卫 也可以被它的儿子看见 else 如果父亲放了 也就是说儿子可以安♂排 也可以被它的儿子看见 如果儿子放了 它的儿子必定有一个安排了的 否则被它的儿子看见 具体可以进行一些 特♂判 其实这道题很像上一道题的升级版 点到为止 不多说了( 6.消息传递 传送门 由于根是不一定的 所以需要遍历所有点 作为根 设dp[i]是以i为根的子树传遍它所有子树需要的最少时间 dp[i]取决于花费时间最多的那颗子树(当然还要加上每次一秒的传递时间) 不过也不是一定的 万一话费时间最多的和次多的只差了一秒之类的情况也会出现 所以需要遍历所有的儿子~ 方程:dp[i]=max{dp[i.son]+i.son.number(传递时间)} 7.有线电视网 点击打开链接 树上背包。 这道对于本蒟蒻来说比较难 背包的总容量相当于该点为根节点的子树中所有的用户数量。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。 总结: 通常来说 把一棵树转化为二叉树 然后整个问题的最优只涉及到左右儿子的最优 然后考虑根节点随之的变化 这样化简了问题 也很容易推出状态转移方程 当然 也不是所有问题都要这样 我们应该仔细推敲每个结点的状态 以及相应状态与父子结点的联系等 就是如何从子节点的最优值推出父节点的最优值//dp[i][j]: i的所有兄弟和i的所有儿子 和i自己 学j门课的最大学分总和。
//dp[i][j]=max(dp[rson][j],dp[lson][k]+dp[rson][j-k-1]+val[i])
#include
//dp[i][0] 选i dp[i][1] 不选i 的所需最小个数
//如果选了i 意味着可以选或者不选他的儿子
//如果没选 意味着必须选所有的儿子
//dp[i][1]=sigma(dp[i.son][0])
//dp[i][0]=sigma(min(dp[i.son][0],dp[i.son][1]))
#include
其实只是懒)#include
转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。
//dp[i][j] 当前节点为i选j个用户 所能得到的最大收益
//dp[i][1]=pay[i];(叶子节点)
//dp[i][某个儿子的编号]=max{dp[i][某个儿子的编号-k(需要枚举)]+dp[vis][k]-val} (j>k>=1,)
#include