树形 dp 介绍

概念

树形 dp,顾名思义,就是在树上做 dp,将 dp 的思想建立在树状结构之上。

常见的树形 dp 有两种转移方向:

  1. 从叶节点向根节点转移,这种也是树形 dp 中较为常见的一种。通常是在 dfs 回溯后时更新当前节点的答案。

  2. 从根节点向叶节点转移,通常是在从叶往根dfs一遍之后,再重新往下获取最后的答案。

特点

  1. 是在树上做的。

  2. 主要是在对一棵树做 dfs 时进行转移。

  3. 转移方程非常直观,但是细节较多。

套路

这个分类偏主观。

  1. 选择节点类:

    • 无限制类:一般是点权(边权)和最大(最小)的子树的点权(边权)和。

      转移方程:\(dp_u = dp_u+max/min(dp_v,0)\)(叶 \(\to\) 根,即 \(v \to u\),\(dp_u\) 为以 \(u\) 为根节点的子树选出的最大/最小值)
    • 有限制类:一般是子节点选了父节点就不能选。

      转移方程:初始化 \(dp_{u,1}=a_u\),\(dp_{u,1}=dp_{u,1}+dp_{v,0},dp_{u,0}=dp_{u,0}+max(dp_{v,0},dp_{v,1})\)(\(a_u\) 为 \(u\) 的点权,叶 \(\to\) 根,即 \(v \to u\),\(dp_{u,1}\) 为选择了该节点的最大值,\(dp_{u,0}\) 代表没选该节点的最大值)
  2. 树上背包类

    常见的是一件物品只有选择了它的父亲才能选择该物品。

上文提到的转移方程将会在对应的例题中推导。

习题

习题1 P1122 最大子树和

题意

对于一个树,求出其点权和最大的子树的点权和。

思路

无限制的选节点套路。

对于一个节点 \(u\),如果其子节点 \(v\) 的子树产生了负贡献,那么不选更优,反之选更优。

#include<bits/stdc++.h>
using namespace std;
const int maxn=16000+10;
vector<int> G[maxn];
int dp[maxn],a[maxn],n,m,ans=-2147483647;
void dfs(int x,int fa){
dp[x]=a[x];
for(int i=0;i<G[x].size();i++){
int nxt=G[x][i];
if(nxt!=fa){
dfs(nxt,x);
dp[x]+=max(dp[nxt],0);
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,-1);
for(int i=1;i<=n;i++){
ans=max(ans,dp[i]);
}
cout<<ans;
return 0;
}

习题2 P1352 没有上司的舞会

题意

给出一棵树,每个点都有点权,现在从中选出一些节点,满足任意两点不为父子关系(即选了子节点就不能选父节点),求点权和最大值。

思路

树的最大独立集板子/带限制的选节点套路。

令 \(dp_{u,0}\) 为选 \(u\) 节点,以 \(u\) 为根的子树选出的最大值,\(dp_{u,1}\) 为不选 \(u\) 节点,以 \(u\) 为根的子树选出的最大值。

如果选了这个节点,那么这个节点的儿子节点全部不能选,即 \(dp_{u,1}=dp_{u,1}+dp_{v,0}\),初始化为该点点权。

反之儿子节点可以选也可以不选,即 \(dp_{u,0}=dp_{u,0}+max(dp_{v,0},dp_{v,1})\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=6000+10;
int n,r[maxn],root,dp[maxn][2],haveson[maxn];
vector<int> G[maxn];
void dfs(int u,int fa){
dp[u][0]=0;
dp[u][1]=r[u];
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa) continue;
dfs(v,u);
//转移方程
dp[u][0]+=max(dp[v][0],dp[v][1]);//不选这个节点
dp[u][1]+=dp[v][0];//选这个节点
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>r[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;haveson[v]=1;
G[v].push_back(u);
G[u].push_back(v);
}
for(int i=1;i<=n;i++){
if(!haveson[i]) root=i;
}
dfs(root,-1);
cout<<max(dp[root][1],dp[root][0]);
return 0;
}

习题3 P2016 战略游戏

题意

给出一棵树,选出最少的点,使得树中每条边都至少有一个端点被选。(树的最小点覆盖)

思路

令 \(dp_{u,0}\) 为不选 \(u\) 时以 \(u\) 为根的子树的最少数量,\(dp_{u,1}\) 为选 \(u\) 时以 \(u\) 为根的子树的最少数量。

如果当前节点不选,那么这个节点的所有子节点全部都要选。所以有 \(dp_{u,0}=dp_{u,0}+dp_{v,1}\)。

反之我们可以取选与不选之间最少的那个,即 \(dp_{u,1}=dp_{u,1}+min(dp_{v,0},dp_{v,1})\)。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1500+10;
int n,dp[maxn][2];
vector<int> G[maxn];
void dfs(int u,int fa){
dp[u][1]=1;
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v!=fa){
dfs(v,u);
dp[u][1]+=min(dp[v][0],dp[v][1]);
dp[u][0]+=dp[v][1];
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
int k,sum;
cin>>k>>sum;
for(int j=1;j<=sum;j++){
int u;
cin>>u;
G[k+1].push_back(u+1);
G[u+1].push_back(k+1);
}
}
dfs(1,1);
cout<<min(dp[1][0],dp[1][1]);
return 0;
}

习题4 POJ3659 Cell Phone Network

题意

给出一棵树,如果选择了这个点,这个点和与其相邻的点都会被覆盖,求最少选多少个点能使所有点都被覆盖。(树的最小支配集)

思路

我们令 \(dp_{u,0}\) 为选择点 \(u\),以 \(u\) 为根的子树的最小支配集,\(dp_{u,1}\) 为不选 \(u\),且 \(u\) 被儿子覆盖时的以 \(u\) 为根的最小支配集,\(dp_{u,2}\) 为不选 \(u\),且 \(u\) 被父亲覆盖时的最小支配集。

当我们选择 \(u\) 时,因为 \(u\) 的所有儿子都会被 \(u\) 覆盖,所以可以取最小值,即 \(dp_{u,0}=dp_{u,0}+min(dp_{v,0},dp_{v,1},dp_{v,2})\),初始化为 \(1\)。

当我们不选 \(u\) 时:

  • 若 \(u\) 没有被覆盖,即将要被父节点覆盖,此时可以选儿子节点支配,也可以选当前节点支配:\(dp_{u,2}=dp_{u,2}+min(dp_{v,0},dp_{v,1})\)
  • 若 \(u\) 被覆盖,此时枚举 \(u\) 的所有子节点,选出其中之一使得代价最小:\(dp_{u,1}=min(dp_{u,1},dp_{u_k,0}+dp_{u,2}-\sum\limits^{n}\limits_{k=1}dp_{v_k,1},dp_{v_k,0})\)。

不想贴代码了。个人更偏向于贪心做法。

习题5 树的直径

题意

给出一棵树,求出其直径长度。

思路1

引入一个定理:对一棵树作 dfs,所到达的边一定为直径的一端。(证明)

那么我们对这棵树做一遍 dfs,找到一端,从这个点开始,再做一次 dfs 找到最远的那个点,这两个点之间的路径即为直径。

void dfs(int u,int fa,int dep){
if(maxdep<dep){
maxdep=dep;
pos=u;
}
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v!=fa){
dfs(v,u,dep+1);
}
}
}

好处:简单易懂

坏处:跟 Dij 一样,遇到负权边就寄

思路2

令 \(dp1_u\) 为以 \(u\) 为根的子树,\(u\) 在这棵子树内所能延伸的最长路径,\(dp2_u\) 为最长路径无公共边的次长路径。此时答案为 \(\max\limits_{i=1}\limits^{n}dp1_i+dp2_i\)。

void dfs(int u,int fa) {
d1[u]=d2[u]=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa) continue;
dfs(v,u);
int t=d1[v]+1;
if(t>d1[u]){
d2[u]=d1[u];
d1[u]=t;
}
else if(t>d2[u]){
d2[u] = t;
}
}
d=max(d,d1[u]+d2[u]);
}

好处:能处理负权边

坏处:第一次学不知道咋实现(就像我)。

习题6 P2015 二叉苹果树

题意 by wsy

给定一棵有 \(n\) 个节点苹果树,不难发现有 \(n - 1\) 条边,第 \(i\) 边连接 \(x_i\) 和 \(y_i\),边上有 \(z_i\) 个苹果。

现在要剪掉一些边,只保留 \(q\) 条边,问最终最多能保留多少苹果。

这棵树以 \(1\) 号节点为根,当一条边被剪掉时,这条边上深度较高的那个点的所有苹果都无法保留

思路 bu xhr

  • 明显的树形 DP,令 \(dp_{i, j}\) 表示 \(i\) 的子树保留 \(j\) 根树干能得到的最多的苹果数量。

  • 令 \(ls_i,rs_i\) 为结点 \(i\) 的左右儿子,\(lc_i,rc_i\) 为 \(i\) 到左右儿子的树干的苹果数量。则 \(dp_{i,j} = \max\{dp_{ls_i, j - 1} + lc,dp_{rs_i,j-1} + rc, dp_{ls_i,k - 1} + lc + dp_{rs,j - k - 1} + rc(1 \le k \le j - 1)\}\)。

  • 答案为 \(dp_{i,q}\)。

  • 时间复杂度:\(O(nq^2)\)。

#include <iostream>
#include <vector> using namespace std; struct Edge {
int x, y;
}; int n, q, x, y, z, fa[110], h[110], dp[110][110];
bool f[110][110];
vector<Edge> v[110]; void dfs_ (int x) {
h[x]++;
for (auto [u, v] : v[x]) {
if (u != fa[x]) {
fa[u] = x, h[u] = h[x], dfs_(u);
}
}
} int dfs (int x, int y) {
if (!f) {
return 0;
}
if (f[x][y]) {
return dp[x][y];
}
f[x][y] = 1;
int l = 0, r = 0, sl = 0, sr = 0;
for (auto [u, v] : v[x]) {
if (u == fa[x]) {
continue;
}
if (!l) {
l = u, sl = v;
}
r = u, sr = v;
}
int sum = 0, num;
for (int j = 0; j <= y; j++) {
num = 0;
if (j) {
num = dfs(l, j - 1) + sl;
}
if (j < y) {
num += dfs(r, y - j - 1) + sr;
}
sum = max(sum, num);
}
return dp[x][y] = sum;
} int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> q;
for (int i = 1; i < n; i++) {
cin >> x >> y >> z;
v[x].push_back({y, z}), v[y].push_back({x, z});
}
dfs_(1);
cout << dfs(1, q);
return 0;
}

学习笔记——树形dp的更多相关文章

  1. [学习笔记]树形dp

    最近几天学了一下树形\(dp\) 其实早就学过了 来提高一下打开树形\(dp\)的姿势. 1.没有上司的晚会 我的人生第一道树形\(dp\),其实就是两种情况: \(dp[i][1]\)表示第i个人来 ...

  2. [学习笔记] 数位DP的dfs写法

    跟着洛谷日报走,算法习题全都有! 嗯,没错,这次我也是看了洛谷日报的第84期才学会这种算法的,也感谢Mathison大佬,素不相识,却写了一长篇文章来帮助我学习这个算法. 算法思路: 感觉dfs版的数 ...

  3. [学习笔记]区间dp

    区间 \(dp\) 1.[HAOI2008]玩具取名 \(f[l][r][W/I/N/G]\) 表示区间 \([l,r]\) 中能否压缩成 \(W/I/N/G\) \(Code\ Below:\) # ...

  4. [学习笔记]插头dp

    基于连通性的状压dp 巧妙之处:插头已经可以表示内部所有状态了. 就是讨论麻烦一些. 简介 转移方法:逐格转移,分类讨论 记录状态方法:最小表示法(每次要重新编号,对于一类没用“回路路径”之类的题,可 ...

  5. 【学习笔记】dp基础

    知识储备:dp入门. 好了,完成了dp入门,我们可以做一些稍微不是那么裸的题了. dp基础,主要是做题,只有练习才能彻底掌握. 洛谷P1417 烹调方案 分析:由于时间的先后会对结果有影响,所以c[i ...

  6. 【学习笔记】dp入门

    知识点 动态规划(简称dp),可以说是各种程序设计中遇到的第一个坎吧,这篇博文是我对dp的一点点理解,希望可以帮助更多人dp入门.   先看看这段话 动态规划(dynamic programming) ...

  7. 笔记-树形dp

    this is not a 正经的note you may not understand Problem 1:二叉树,有权,要选它父亲才能选它,$n\leq200,m\leq500$ I: $dp_{ ...

  8. [学习笔记]动态dp

    其实就过了模板. 感觉就是带修改的dp [模板]动态dp 给定一棵n个点的树,点带点权. 有m次操作,每次操作给定x,y表示修改点x的权值为y. 你需要在每次操作之后求出这棵树的最大权独立集的权值大小 ...

  9. [学习笔记]整体DP

    问题: 有一些问题,通常见于二维的DP,另一维记录当前x的信息,但是这一维过大无法开下,O(nm)也无法通过. 但是如果发现,对于x,在第二维的一些区间内,取值都是相同的,并且这样的区间是有限个,就可 ...

  10. [BZOJ4011][HNOI2015] 落忆枫音(学习笔记) - 拓扑+DP

    其实就是贴一下防止自己忘了,毕竟看了题解才做出来 Orz PoPoQQQ 原文链接 Description 背景太长了 给定一个DAG,和一对点(x, y), 在DAG中由x到y连一条有向边,求生成树 ...

随机推荐

  1. v-imgerror作用:当图片链接无效的时候,显示默认图片内容

    // 回顾自定义指令 // 作用: 自定义一些对DOM的操作快捷指令 // 前提: 指令就是用来操作DOM (v-if/v-show/v-for....) // 语法: Vue.directive(指 ...

  2. python学习记录(三)-数据类型

    字符串格式化 var = 'abcde' # 切片 print(var[2],var[-1]) # c e print(var[1:3:1],var[-2:-5:-1],var[::-1]) # bc ...

  3. Word 找不到 Endnote选项

    Word 2010 找不到 Endnote选项汇总(不是Office有效加载项)因为基本百度上的问题我全都遇到了-说明:在我们使用Word的过程中,常常发现没有Endnote选项.然后去找百度方法:1 ...

  4. MySql.Data 链接MySql数据库 查询语句中带有中文的奇怪问题

    首先Nuget管理器安装MySql.Data 1.ado.net 直接链接 public static void Test() { MySqlConnection myconn = null; MyS ...

  5. vue下拉选择select option el-cascader删除重选值的问题

    select当下拉值多的时候 以及input cascader级联选择一个值后  后面我不想要了 vue  提供了一个关键字  可以帮你全部清空 这个关键字就是:clearable

  6. 初探redis缓存击穿、穿透、雪崩问题

    现分析Redis缓存使用过程失效的一些问题,在有缓存的情况下,查询数据的顺序是先查询缓存,如果查询到数据则直接返回数据,如果没有查询到数据,则到数据库中查询,数据库中有数据的话,将查询出的数据写到缓存 ...

  7. 基于TDesign风格的Blazor企业级UI组件库

    作为一名Web开发人员,开发前端少不了使用JavaScript,而Blazor就是微软推出的基于.net平台交互式客户 Web UI 框架,可以使用C#替代JavaScript,减少我们的技术栈.降低 ...

  8. 从零开始学习Java系列之Java运行机制与跨平台特性

    全文大约[4000]字,不说废话,只讲可以让你学到技术.明白原理的纯干货!并带有丰富的案例及配图,让你更好地理解和运用文中的技术概念,给你带来具有足够启迪的思考-- 在上一篇文章中,壹哥给大家介绍了J ...

  9. Netty 线程模型(Reactor 线程模型)

    更多内容,前往个人博客 当说到 Netty 线程模型的时候,一般首先会想到经典的 Reactor 线程模型,尽管不同的 NIO 框架对于 Reactor 模式的实现存在差异,但本质上还是遵循了 Rea ...

  10. 解决ueditor表格拖拽没反应的问题

    背景 ueditor作为百度推出的富文本编辑框,以功能强大著称. 笔者最近用这个编辑框做了一个自定义打印格式的功能.允许用户在富文本编辑框中设定打印格式,再实际打印时,根据关键字替换数据库中信息,然后 ...