特点
- 问题可以划分为若干个子问题,从最小的子问题开始向上递推,最终解决总问题
例题
1、树形 dp(基础版)
题目描述
n 个结点的无权无向图,结点存权重,有 n-1 条边,边相连结点不能同时选择,求最大权重和
代码逻辑
- 边数比结点数少 1,构成树,无向图可以用任意结点作根结点
- 当下结点是否被选择这一信息必须保留,这是一个二维 dp 问题
- dp
i0表示以 i 为根且 i 不选的最优结果,dpi1表示以 i 为根且 i 选的最优结果,树划分为若干个子树
- 对每一个结点取它的最优解用来更新它父节点的结果,最终得到根结点的最优结果
核心代码
1 2 3 4 5 6 7 8 9 10 11
| void dfs(int cur,int fath) { for(int ver : edge[cur]) { if(ver==fath) continue; dfs(ver,cur); dp[cur][0] += max(dp[ver][0],dp[ver][1]); dp[cur][1] += dp[ver][0]; } }
|
2、树形 dp(拓展版)
题目描述
n 个结点的无权有向图,结点存代价,边相连结点可以互相监视,求全部结点都被监视的最小代价
代码逻辑
- 有向,所以要先找到总根,然后划分为子树,解决子问题再递推到总问题
- 二维 dp 问题,以 i 为根的子问题:如果 i 被选择,那么子结点选不选都可以让这棵树全部结点都被监视,如果 i 不被选择且要求这棵树不借助外来监视就可以被完全监视,那么子结点至少选一个,如果 i 不被选择且必须借助外来监视,那么子结点可以不选
- 0 表示选择根结点,1 表示不选择根节点但是整棵树被监视,2 表示不选择根结点,除了根节点之外整棵树被监视
- 根结点不选而且整棵树必须不借助外来监视就被完全监视,子树只能是 0 或者 1 状态,取最小值加到当下树即可,但是子树至少有一个是 0 状态,才能保证当下树是 0 状态,为了去掉子树的最小值都在 1 状态取的情况,求出每一个子树从最小值对应的状态到 0 状态的最小偏移量,当下树的结果加上这个偏移量
- 最后的答案从 dp
root,0和 dproot,1中取一个最小值,不能取 dp~root,2
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void dfs(int cur) { for(int ver : edge[cur]) { dfs(ver); dp[cur][0] += min(dp[ver][0],dp[ver][1],dp[ver][2]); dp[cur][1] += min(dp[ver][0],dp[ver][1]); dt = min(dt,dp[ver][0] - min(dp[ver][0],dp[ver][1]) ); dp[cur][2] += dp[ver][1]; } dp[cur][1] += dt; }
|
3、区间 dp
问题描述
n 块石头重量分别是 wi,合并两堆石头的代价是两堆石头的总重量和,只能合并相邻两堆石堆,求所有石头被合并为一堆石头最小代价
代码逻辑
- 由于只能合并相邻两堆石堆,把问题划分为区间最值,dp
ij表示 i 到 j 之间的石头被合成一堆的最小代价,i 到 j 之间的石头被分作两堆的方式很多,子问题划分的时候只有一件事是确定的,那就是子问题的石堆的长度小于总问题,比如 1-5 合并成 1 堆,可以先把 1-3 合并为 1 堆,4-5 合并成 1 堆,然后再把这两堆合并,也可以 1-2 合并,3-5 合并,然后这两堆合并,但是无论无何,总问题 1 堆长 5,子问题 1 堆的长度必然小于 5,根据堆的长度做递推。长度确定后,遍历起点 i,终点 j 就固定为 i+len-1,此时考虑 dpij,它可以由很多种不同的方式转移过来,选一个结果最优的即可。
核心代码_预处理
1 2 3 4 5 6 7 8 9 10
| memset(dp,inf,sizeof(dp)) for(int i=1;i<=n;i++) dp[i][i] = 0;
for(int i=1;i<=n;i++) { cin>>w[i]; pre[i] = pre[i-1] + w[i]; }
|
核心代码_递推
1 2 3 4 5 6 7 8 9 10
| for(int len=2;len<=n;len++) { for(int i=1;i<=n-len+1;i++) { j = i + len - 1; for(int k=i;k<j;k++) dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+pre[j]-pre[i-1] );
} }
|