学习笔记——树形dp
树形 dp 介绍
概念
树形 dp,顾名思义,就是在树上做 dp,将 dp 的思想建立在树状结构之上。
常见的树形 dp 有两种转移方向:
从叶节点向根节点转移,这种也是树形 dp 中较为常见的一种。通常是在 dfs 回溯后时更新当前节点的答案。
从根节点向叶节点转移,通常是在从叶往根dfs一遍之后,再重新往下获取最后的答案。
特点
是在树上做的。
主要是在对一棵树做 dfs 时进行转移。
转移方程非常直观,但是细节较多。
套路
这个分类偏主观。
选择节点类:
- 无限制类:一般是点权(边权)和最大(最小)的子树的点权(边权)和。
转移方程:\(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}\) 代表没选该节点的最大值)
- 无限制类:一般是点权(边权)和最大(最小)的子树的点权(边权)和。
树上背包类
常见的是一件物品只有选择了它的父亲才能选择该物品。
上文提到的转移方程将会在对应的例题中推导。
习题
习题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的更多相关文章
- [学习笔记]树形dp
最近几天学了一下树形\(dp\) 其实早就学过了 来提高一下打开树形\(dp\)的姿势. 1.没有上司的晚会 我的人生第一道树形\(dp\),其实就是两种情况: \(dp[i][1]\)表示第i个人来 ...
- [学习笔记] 数位DP的dfs写法
跟着洛谷日报走,算法习题全都有! 嗯,没错,这次我也是看了洛谷日报的第84期才学会这种算法的,也感谢Mathison大佬,素不相识,却写了一长篇文章来帮助我学习这个算法. 算法思路: 感觉dfs版的数 ...
- [学习笔记]区间dp
区间 \(dp\) 1.[HAOI2008]玩具取名 \(f[l][r][W/I/N/G]\) 表示区间 \([l,r]\) 中能否压缩成 \(W/I/N/G\) \(Code\ Below:\) # ...
- [学习笔记]插头dp
基于连通性的状压dp 巧妙之处:插头已经可以表示内部所有状态了. 就是讨论麻烦一些. 简介 转移方法:逐格转移,分类讨论 记录状态方法:最小表示法(每次要重新编号,对于一类没用“回路路径”之类的题,可 ...
- 【学习笔记】dp基础
知识储备:dp入门. 好了,完成了dp入门,我们可以做一些稍微不是那么裸的题了. dp基础,主要是做题,只有练习才能彻底掌握. 洛谷P1417 烹调方案 分析:由于时间的先后会对结果有影响,所以c[i ...
- 【学习笔记】dp入门
知识点 动态规划(简称dp),可以说是各种程序设计中遇到的第一个坎吧,这篇博文是我对dp的一点点理解,希望可以帮助更多人dp入门. 先看看这段话 动态规划(dynamic programming) ...
- 笔记-树形dp
this is not a 正经的note you may not understand Problem 1:二叉树,有权,要选它父亲才能选它,$n\leq200,m\leq500$ I: $dp_{ ...
- [学习笔记]动态dp
其实就过了模板. 感觉就是带修改的dp [模板]动态dp 给定一棵n个点的树,点带点权. 有m次操作,每次操作给定x,y表示修改点x的权值为y. 你需要在每次操作之后求出这棵树的最大权独立集的权值大小 ...
- [学习笔记]整体DP
问题: 有一些问题,通常见于二维的DP,另一维记录当前x的信息,但是这一维过大无法开下,O(nm)也无法通过. 但是如果发现,对于x,在第二维的一些区间内,取值都是相同的,并且这样的区间是有限个,就可 ...
- [BZOJ4011][HNOI2015] 落忆枫音(学习笔记) - 拓扑+DP
其实就是贴一下防止自己忘了,毕竟看了题解才做出来 Orz PoPoQQQ 原文链接 Description 背景太长了 给定一个DAG,和一对点(x, y), 在DAG中由x到y连一条有向边,求生成树 ...
随机推荐
- Qt之如何创建并显示一个柱状图
创建一个简单的柱状图 第一步:创建一个QBarSet对象:QBarSet类代表条形图中的一组条形. QBarSet *set0 = new QBarSet("Jane"); QBa ...
- echarts 玫瑰图+中间文字
option = { tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, title: { text: ...
- css卡片样式
.view-1-user-card { margin: 20px 5% 10px 5%; height: 124px; width: 90%; background: linear-gradient( ...
- ASP.NET在Repeater中使用Button控件报错
普通Button在这里会报错,小编找了一天也没有解决这个问题, 这里可以换做LinkButton或者ImageButton替换普通的Button
- beamforming源码标记
p:各阵元的声压信号矩阵 R:接收数据的自协方差矩阵 Pcbf:交叉谱矩阵
- 腾讯云等Linux环境下Redis安装配置
1.下载redis解压安装命令教程 https://www.cnblogs.com/hunanzp/p/12304622.html 2.配置远程连接 修改bind 127.0.0.0 为 bind ...
- SpringSecurity登录验证,多个用户表
在开始之前我想感叹一句,技术久了不回头看看真的会忘记的,这次公司让我重新开发一个程序,项目架构为单体多模块开发,其中有个需求就是需要不同用户表进行登录,且不同表的用户名.手机号都可以重复. 这样的需求 ...
- LaTeX in 24 Hours - 3. Formatting Texts I
文章目录 本章内容:文本格式 I 3.1 Sectional Units 3.2 Labeling and Referring Numbered Items 3.3 Texts Alignment 3 ...
- 在创建maven项目时提示找不到插件 'org.springframework.boot:spring-boot-maven-plugin:'
因为是版本号缺失,不过我idea自建项目没这个问题,但是从springboot官网上创建下载就出现了这个问题. 找到文件夹的打开pom.xml文件 然后找到下图位置添加版本号,我的是2.6.1 添加完 ...
- ElasticSearch 实现分词全文检索 - term、terms查询
数据准备 ElasticSearch 实现分词全文检索 - 测试数据准备 ElasticSearch的各种查询 不会对查询关键字进行分词 term 查询 term的查询是代表完全匹配,搜索之前不会对你 ...