初识树形dp
树形dp
什么是树形dp?
传统dp的递推过程都是用线性循环,而树形dp的递推过程是采用了树的遍历方式(dfs,bfs)
可以应用在树形结构求解最优性问题上
没有上司的舞会
来自https://www.luogu.com.cn/problem/P1352
题目描述
某大学有
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数
第
第
输出格式
输出一行一个整数代表最大的快乐指数。
样例 #1
样例输入 #1
7 1 1 1 1 1 1 1 1 3 2 3 6 4 7 4 4 5 3 5
样例输出 #1
5
提示
数据规模与约定
对于
思路
题目很明显描述了一个树形结构,又是求解最优性问题,所以考虑树形dp
定义状态:
f(i,0):i号上司不来参加舞会时的欢乐指数
,f(i,1):i号上司来参加舞会时的欢乐指数
状态转移:
为 号上司的直接下属。
递推:采用树的遍历方式
评述
比较简单的题,但是很有参考价值。
代码
#include <bits/stdc++.h> typedef std::pair<int, int> pii; #define INF 0x3f3f3f3f #define MOD 998244353 using i64 = long long; const int N = 1e5+5; void solve(){ int n; std::cin >> n; std::vector<int> v(n+1), d(n+1); for (int i = 1; i <= n; i++) std::cin >> v[i]; std::vector g(n + 1, std::vector<int>()); for (int i = 1; i <= n - 1; i++){ int a, b; std::cin >> a >> b; g[b].emplace_back(a); d[a] = 1; } int st = -1; for (int i = 1; i <= n; i++) if (d[i] == 0) st = i; //状态定义f(i, j)表示第i个员工来不来参加舞会,j=1时来,j=0是不来 std::vector f(n+1, std::vector<int>(2)); auto dfs = [&](auto self, int x) -> void{ f[x][1] = v[x]; for (auto to : g[x]){ self(self, to); f[x][0] += std::max(f[to][0], f[to][1]); f[x][1] += f[to][0]; } }; dfs(dfs, st); std::cout << std::max(f[st][0], f[st][1]) << '\n'; } signed main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2); int t = 1, i; for (i = 0; i < t; i++){ solve(); } return 0; }
二叉苹果树
题目来源https://www.luogu.com.cn/problem/P2015
题目描述
有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有
2 5 \ / 3 4 \ / 1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第一行
接下来
输出格式
一个数,最多能留住的苹果的数量。
样例 #1
样例输入 #1
5 2 1 3 1 1 4 10 2 3 20 3 5 20
样例输出 #1
21
提示
思路:
状态定义:f(i, j):以i为根的节点,保留j个树枝能留住的苹果的最大数量。
状态转移:
只保留一棵树:当只保留此时遍历到的子树时,
。 两颗树都保留时:
。 一个树枝都不保留:因为苹果都在树枝上,一个树枝都不保留就一个苹果都没有,所以对于任何根
t
,。由此可知dp函数的初始值。
评述
自己做出来了,题目的思考过程真的很有趣。
代码:
#include <bits/stdc++.h> typedef std::pair<int, int> pii; #define INF 0x3f3f3f3f #define MOD 998244353 using i64 = long long; const int N = 1e5+5; void solve(){ int n, q; std::cin >> n >> q; std::vector e(n+1, std::vector<int>(n+1)); // 存树枝上苹果的数量 std::vector g(n+1, std::vector<int>()); std::vector f(n+1, std::vector<int>(n+1)); for (int i = 0; i < n - 1; i++){ int a, b, c; std::cin >> a >> b >> c; g[a].push_back(b); g[b].push_back(a); e[a][b] = c; e[b][a] = c; } auto dfs = [&](auto self, int x, int father) -> void { for (auto to : g[x]){ if (to == father) continue; self(self, to, x); for (int i = 1; i <= q; i++){ f[x][i] = std::max(f[x][i], f[to][i-1] + e[x][to]); } } //由于两个孩子节点都保留时的情况下要同时用到两个孩子节点的信息,所以要等上面把每个孩子的信息都求出来后才能进行 if (g[x].size() != 1){ int left = g[x][0], right = g[x][1], tmp = g[x][2]; if (left == father){ std::swap(left, tmp); } if (right == father){ std::swap(right, tmp); } for (int i = 1; i <= q; i++){ for (int k = 0; k <= i && i - k - 2 >= 0; k++){ f[x][i] = std::max(f[x][i], f[left][k] + f[right][i-k-2] + e[x][left] + e[x][right]); } } } }; dfs(dfs, 1, -1); std::cout << f[1][q] << '\n'; } signed main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2); int t = 1, i; for (i = 0; i < t; i++){ solve(); } return 0; }
P2014 [CTSC1997] 选课
来自https://www.luogu.com.cn/problem/P2014
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有
输入格式
第一行有两个整数
接下来的
输出格式
只有一行,选
样例 #1
样例输入 #1
7 4 2 2 0 1 0 4 2 1 7 1 7 6 2 2
样例输出 #1
13
思路
题目所给的是一个森林结构,但是为了做题方便,可以新增一门0学分的课程编号也为0。凡事没有前修课程的课都会直接连接到0号节点下,这样我们就得到了一个树的结构
状态定义:
以 为根,选了 门课程的最大学分,其中根是必选的 根据容斥原理,
只由两部分组成,一部分是在 的 号子树里选了 门课程,一部分是在其他子树里选了 门课程 状态转移:
, 为 的直接子节点 递推:采用树的遍历方式
代码
#include <bits/stdc++.h> typedef std::pair<int, int> pii; #define INF 0x3f3f3f3f #define MOD 998244353 using i64 = long long; const int N = 1e5+5; //状态定义f(i,j):以i为根,选了j门课的最大学分,其中i是必选的 void solve(){ int n, m; std::cin >> n >> m; std::vector g(n+1, std::vector<int>()); std::vector f(n+1, std::vector<int>(m+2)); for (int i = 1; i <= n; i++){ int a, b; std::cin >> a >> b; g[a].emplace_back(i); f[i][1] = b; } auto dfs = [&](auto self, int x) -> void { for (auto to : g[x]){ self(self, to); for (int j = m + 1; j >= 1 ; j--){ //背包容量为j但是有一个位置是留给根节点的,所以k取最大值也只能为j-1 for (int k = 0; j - k >= 1; k++){ f[x][j] = std::max(f[x][j], f[x][j-k] + f[to][k]); } } } }; dfs(dfs, 0); // 因为引入了0这个根节点,根据状态定义,0又是必选的,所以实际的背包容量要+1 std::cout << f[0][m+1] << '\n'; } signed main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2); int t = 1, i; for (i = 0; i < t; i++){ solve(); } return 0; }
换根DP
引用 OI Wiki
树形 DP 中的换根 DP 问题又被称为二次扫描与换根法,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
第一次扫描时,任选一个点为根,在“有根树”上执行一次树形DP,也就是自底向上的状态转移
第二次扫描时,从刚才选出的根节点出发,对整棵树执行一次深度优先遍历(dfs),在每次递归前进行自顶向下的推导,计算出换根后的解
这类题目的特点是,给定一个树形结构,需要以每一个节点为根进行一系列统计
例题
834. 树中距离之和
思路
经典换根DP问题
状态定义:
:以 为根的子树中有多少个节点
:i 到以 i 为根的子树中的其他节点的距离之和
:当 i 的父亲作他的子树时的贡献 状态转移:
,其中 j 为 i 的直接孩子
,由于要用到孩子的信息,所以求的时候是自底向上求
,这里的 j 是 i 的父亲,由于要用到父亲的信息,所以求的时候自顶向下求
代码
class Solution { public: vector<int> sumOfDistancesInTree(int n, vector<vector<int>>& edges) { std::vector g(n, std::vector<int>()); for (auto f : edges){ int a = f[0], b = f[1]; g[a].push_back(b); g[b].push_back(a); } std::vector<int> size(n), f(n), v(n); auto up = [&](auto self, int cur, int fa) ->void{ size[cur] = 1; for (auto to : g[cur]){ if (to == fa) continue; self(self, to, cur); size[cur] += size[to]; f[cur] += f[to]; } f[cur] += size[cur] - 1; }; auto down = [&](auto self, int cur, int fa) ->void{ for (auto to : g[cur]){ if (to == fa) continue; v[to] = v[cur] + n - size[to] + f[cur] - f[to] - size[to]; self(self, to, cur); } }; up(up, 0, -1); down(down, 0, -1); std::vector<int> ans(n); for (int i = 0; i < n; i++){ ans[i] = f[i] + v[i]; } return ans; } };
P10974 Accumulation Degree
注意事项
下面的代码在洛谷上会有三个点过不去,要使用链式前项星建图才能AC
代码
#include <bits/stdc++.h> typedef std::pair<int, int> pii; #define INF 0x3f3f3f3f #define MOD 998244353 using i64 = long long; const int N = 2e5+5; i64 D[N], f[N]; void solve(){ int n; std::cin >> n; std::vector g(n + 1, std::vector<int>()); std::vector v(n+1, std::vector<i64>(n+1)); std::vector<int> deg(n+1, 0); for (int i = 1; i <= n - 1; i++){ int a, b, c; std::cin >> a >> b >> c; g[a].push_back(b); g[b].push_back(a); deg[a] += 1; deg[b] += 1; v[a][b] = c; v[b][a] = c; } // std::vector<i64> D(n+1), f(n+1, 0); memset(D, 0, sizeof(D)); memset(f, 0, sizeof(f)); auto up = [&](auto self, int cur, int fa) -> void { for (auto to : g[cur]){ if (to == fa) continue; self(self, to, cur); if (deg[to] == 1){ D[cur] += v[cur][to]; }else{ D[cur] += std::min(D[to], v[cur][to]); } } }; auto down = [&](auto self, int cur, int fa) -> void{ for (auto to : g[cur]){ if (to == fa) continue; if (deg[cur] == 1) f[to] = D[to] + v[to][cur]; else if (deg[to] == 1) { f[to] = D[to] + std::min(f[cur] - v[cur][to], v[cur][to]); } else f[to] = D[to] + std::min(f[cur] - std::min(D[to], v[to][cur]), v[to][cur]); self(self, to, cur); } }; up(up, 1, -1); f[1] = D[1]; down(down, 1, -1); i64 ans = 0; for (int i = 1; i <= n; i++) ans = std::max(ans, f[i]); std::cout << ans << '\n'; } signed main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout<<std::setiosflags(std::ios::fixed)<<std::setprecision(2); int t = 1, i; std::cin >> t; for (i = 0; i < t; i++){ solve(); } return 0; }
本文作者:califeee
本文链接:https://www.cnblogs.com/califeee/p/18628436
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步