介绍
树形dp就是在树上进行dp,常用于 “树上选一组点/边,满足某些条件,且使得某些权值和最大” 的问题。
一般来说,DP的形式为:
设 为: 以 为根的子树当中,选了 个元素得到的最大值。
这样,状态转移就有:
代码如下:
copy
1 2 3 4 5
| for (int j = m; j >= 1; j--) { for (int k = 1; k <= j-1; k++) { dp[cur][j] = max(dp[cur][j], dp[cur][j-k] + dp[nei][k]); } }
|
需要注意的点:
- 类似于 背包,在枚举
j
的值时,要从大到小,防止一个child被重复选择。
- 对于依赖关系(例如,child需要依赖parent),可以利用DP过程中,调整状态转移的 上下限 来达到!
时间复杂度:,其中 为节点数, 为第二维的大小。
优化
子树 size 优化
对于一个子树,可能它第二维的上限并没有这么高,我们需要尽量避免枚举一些无用的范围。
我们可以优化一下:
对于每一个root节点 cur
,我们记录一下 int sz[cur]
,代表 以cur
为根的子树的节点数量, 这样枚举的时候我们就可以优化成这样:
copy
1 2 3 4 5
| for (int j = min(m, sz[cur]); j >= 1; j--) { for (int k = 1; k <= min(j-1, sz[nei]); k++) { dp[cur][j] = max(dp[cur][j], dp[cur][j-k] + dp[nei][k]); } }
|
例题可见 例1
复杂度:
DFS序 + 背包 优化
有的时候,第二维表示的不一定是 选择 个元素,而可能是背包的一个容量,此时上面的优化的效果就不显著了。
我们可以利用 DFS序 进行优化。
令 为 的DFS序编号,则对于当前节点 ,我们有两种选择:
-
选择当前节点:
-
不选当前节点:
• 第一维度是选到了dfs序中的前 项,第二维度就是背包的容量。
解释:
如果选择当前节点,说明可以继续往子树里传递,所以传递到 。
如果不选择当前节点,由于依赖关系,就必须跳过子树,所以传递到 。
最终的答案就是 。
复杂度:
例题可见 例2
例题
题意
有 门课程,每门课程有 或 门前置课程,需要上了前置课程才能上这门课。每门课 有 学分。
现要选 门课,使得学分总和最大。
题解
设 为: 以 为根的子树当中,选了 个课程得到的最大值
根据前置课程的关系建图(会发现这是一棵树),因为有前置课程,所以必须选了root才能选别的,故:
在处理某一个节点i的时候, 代表的是: 以它为root的 “已探索” 子树中的最大值,所以在探索各个子树过程中有:
实现细节
- 我们利用 来处理前置课程,是非常高效的做法!
- 状态转移的时候,要 倒序枚举 , 也就是 , 因为此时 代表的是已探索的部分,不能包括 (因为 正在被探索)。为了防止同一个 被考虑多次,要倒序枚举!
- 给定的图可能是一个森林,所以创建一个超级root ,并且将 ++ (因为 肯定要包含进去),最终答案就是
算法优化
注意,在dp状态转移的时候,我们可能用的是如下loop:
copy
1 2 3 4 5
| for (int j = m; j >= 1; j--) { for (int k = 1; k <= j-1; k++) { dp[cur][j] = max(dp[cur][j], dp[cur][j-k] + dp[nei][k]); } }
|
每个节点 cur
都这样loop一次,总复杂度是 ,看起来不可接受。
我们可以优化一下:
对于每一个root节点 cur
,我们记录一下 int sz[cur]
,代表 以cur
为根的子树的节点数量, 这样枚举的时候我们就可以优化成这样:
copy
1 2 3 4 5
| for (int j = min(m, sz[cur]); j >= 1; j--) { for (int k = 1; k <= min(j-1, sz[nei]); k++) { dp[cur][j] = max(dp[cur][j], dp[cur][j-k] + dp[nei][k]); } }
|
时间复杂度:
证明:我们考虑每一个 nei
被用来转移 的次数,会发现它只会在计算它的 parent 的dp值时才会被拿来统计,又因为每一个节点只有1个parent,所以每个节点对应的子树都只会被统计一次。
所以时间复杂度就是
更严谨的数学证明可以参见 https://www.luogu.com.cn/blog/Chenxiao-Yan/solution-p4322
luogu-P2014-AC代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #include <bits/stdc++.h> using namespace std; const int maxn = 305; const int maxm = 305; int n,m; int s[maxn]; int dp[maxn][maxn]; int sz[maxn]; struct Edge { int to, nxt; } edges[maxm]; int head[maxn], ecnt = 1; void add(int u, int v) { Edge e = {v, head[u]}; edges[ecnt] = e; head[u] = ecnt++; } void init() { scanf("%d%d",&n,&m); fill(head, head+n+2, -1); for (int i = 1; i <= n; i++) { int sc,k; scanf("%d%d",&k,&sc); s[i] = sc; add(k, i); } } void dfs(int cur, int par) { if (sz[cur]) return; dp[cur][1] = s[cur]; sz[cur] = 1; for (int e = head[cur]; ~e; e = edges[e].nxt) { int nei = edges[e].to; if (par == nei) continue; dfs(nei, cur); sz[cur] += sz[nei]; for (int j = min(m, sz[cur]); j >= 1; j--) { for (int k = 1; k <= min(j-1, sz[nei]); k++) { dp[cur][j] = max(dp[cur][j], dp[cur][j-k] + dp[nei][k]); } } } } int main() { init(); m++; dfs(0, -1); printf("%d\n", dp[0][m]); }
|
如果每门课的前置课程不止1门,就不再是一棵树了,这样的话似乎可以用状压dp来解,leetcode某次比赛中出现过。
题意
给定 个软件,每个软件 要占用 的空间,价值为 ,并且每个软件 会依赖最多一个软件 。
如果要安装软件 ,必须要安装所有的直接/间接依赖软件。
电脑的空间为 ,求最大价值。
其中,
题解
注意到,本题可能有 循环依赖。
对于循环依赖,直接用 SCC 缩点,剩下的就是一个标准的有依赖背包(本质上,树形DP)问题了:
如果 依赖 ,那么将 所在的 SCC 连一条有向边,指向 所在的 SCC 即可。
另外需要注意,本题可能是一个森林。所以缩点后,对于所有入度为 的点 ,需要用 连一条有向边指向 。
最后,从 开始 DP 即可。
DP 的核心代码:
copy
1 2 3 4 5 6 7 8 9 10 11
| void dfs(int u) { if (wei[u] <= m) dp[u][wei[u]] = val[u]; for (int to : adj2[u]) { dfs(to); for (int i = m; i >= wei[u]; i--) { for (int j = wei[to]; j <= i - wei[u]; j++) { dp[u][i] = max(dp[u][i], dp[u][i-j] + dp[to][j]); } } } }
|
注意几点:
dp[u][wei[u]] = val[u];
放在最前面。
- DP的过程中,类似于 背包, 是从大到小的,并且下限是
wei[u]
。
- DP的过程中, 的下限是
wei[to]
,上限是 i - wei[u]
(因为 i-j
的有效值必须 wei[u]
)。
• 另外,别忘记 tarjan 过程中,更新 low[]
要判断是否在栈内。
树形DP(无优化版本)代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| #include <bits/stdc++.h> using namespace std; int n, m, w[105], v[105], d[105]; int dp[103][503]; vector<int> adj1[103]; set<int> adj2[103]; int dfn[103], low[103], id = 0, st[103], tail = -1; bool in[103]; int from[103], scc = 0; void tarjan(int u) { in[u] = 1; st[++tail] = u; dfn[u] = low[u] = ++id; for (int to : adj1[u]) { if (!dfn[to]) { tarjan(to); low[u] = min(low[u], low[to]); } else if (in[to]) { low[u] = min(low[u], dfn[to]); } } if (low[u] == dfn[u]) { from[u] = ++scc; while (tail >= 0 && st[tail] != u) { int cur = st[tail--]; in[cur] = 0; from[cur] = scc; } tail--; in[u] = 0; } } void init() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> w[i]; for (int i = 1; i <= n; i++) cin >> v[i]; for (int i = 1; i <= n; i++) { cin >> d[i]; if (d[i] == 0) continue; adj1[d[i]].push_back(i); } for (int i = 1; i <= n; i++) { if (!dfn[i]) tarjan(i); } } int wei[103], val[103], ind[103]; void rebuild() { for (int i = 1; i <= n; i++) { int fu = from[i]; wei[fu] += w[i]; val[fu] += v[i]; int fv = from[d[i]]; if (fu == fv || fv == 0) continue; adj2[fv].insert(fu); ind[fu]++; } for (int i = 1; i <= scc; i++) { if (!ind[i]) adj2[0].insert(i); } } void dfs(int u) { if (wei[u] <= m) dp[u][wei[u]] = val[u]; for (int to : adj2[u]) { dfs(to); for (int i = m; i >= wei[u]; i--) { for (int j = wei[to]; j <= i - wei[u]; j++) { dp[u][i] = max(dp[u][i], dp[u][i-j] + dp[to][j]); } } } } int main() { init(); rebuild(); dfs(0); int ans = 0; for (int i = 0; i <= m; i++) ans = max(ans, dp[0][i]); cout << ans << endl; }
|
DFS序 优化
我们可以利用 DFS序 进行优化,让时间复杂度从 降到 。
令 为 的DFS序编号,则对于当前节点 ,我们有两种选择:
-
选择当前节点:
-
不选当前节点:
解释:
如果选择当前节点,说明可以继续往子树里传递,所以传递到 。
如果不选择当前节点,由于依赖关系,就必须跳过子树,所以传递到 。
最终的答案就是 。
另外需要注意,由于依赖问题,我们需要 记录每一个节点 的所有ancestor的权值和,在枚举的时候,将下限设置为这个权值和。(在代码中,pre[u]
代表 所有ancestor的权值和。)
优化代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| int mp[103], idcnt = -1, sz[103]; int pre[103]; void dfs(int u) { mp[++idcnt] = u; sz[u] = 1; for (int to : adj2[u]) { pre[to] = pre[u] + wei[u]; dfs(to); sz[u] += sz[to]; } } void solve() { for (int i = 1; i <= scc; i++) { int u = mp[i]; for (int j = pre[u]; j <= m; j++) { dp[i+sz[u]][j] = max(dp[i+sz[u]][j], dp[i][j]); if (j + wei[u] <= m) dp[i+1][j+wei[u]] = max(dp[i+1][j+wei[u]], dp[i][j] + val[u]); } } } int main() { init(); rebuild(); dfs(0); solve(); int ans = 0; for (int j = 0; j <= m; j++) ans = max(ans, dp[scc+1][j]); cout << ans << endl; }
|
题意
给一棵 个节点的树,要求给所有节点标上正整数权值,使得相邻节点不能权值相同,求最小总权值。
其中,。
题解
第一眼看过去是个树染色问题,要么权值为 ,要么为 。
很遗憾这个做法是错的,考虑一下这个数据,两个菊花接在一起:

这里很明显应该给 分别标上 的权值,其余全部标 。
所以我们暴力枚举一下每个节点上可能标什么权值就好了,直觉上告诉我们这个上限不会太大。
然后利用树形DP来求最小值即可。
代码中取了上限 ,所以复杂度就是
代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <bits/stdc++.h> using namespace std; const int maxn = 10005; const int M = 55; struct Edge { int to, nxt; } edges[maxn<<1]; int head[maxn], ecnt = 1, n, m; int dp[maxn][M+3]; void addEdge(int u, int v) { Edge e = {v, head[u]}; head[u] = ecnt; edges[ecnt++] = e; } void dfs(int u, int p) { for (int e = head[u]; e; e = edges[e].nxt) { int to = edges[e].to; if (to == p) continue; dfs(to, u); for (int j = 1; j <= M; j++) { int r = 1e9; for (int k = 1; k <= M; k++) { if (j == k) continue; r = min(r, dp[to][k]); } dp[u][j] += r; } } for (int j = 1; j <= M; j++) dp[u][j] += j; } int main() { fastio; cin >> n; for (int i = 1; i <= n-1; i++) { int u,v; cin >> u >> v; addEdge(u,v); addEdge(v,u); } dfs(1, 0); int ans = 1e9; for (int j = 1; j <= M; j++) ans = min(dp[1][j], ans); cout << ans << endl; }
|
题意
给定一棵 个节点的树,树的边上有权值。
给定一个整数 ,要求从树上选择 个点,将其染成黑色,剩余的染成白色。
染色后,获得的价值为 黑点两两之间的距离之和 + 白点两两之间的距离之和。
求最大价值?
其中,。
题解
数据范围看起来就非常树形DP。
设 dp[u][i]
为在 的子树内,染 个黑点的总价值。
现在考虑一下怎么转移?
如果我们直接进行转移,就需要维护节点的深度之和,然而这很明显不现实,所以我们可以 考虑每条边 的贡献。
假设考虑的子树 中,染了 个黑点,那么一条边 的贡献刚好就是:
其中 j * (k-j)
是这条边两端的黑点数量的乘积,而 (sz[to] - j) * (n - k - sz[to] + j)
就是这条边两端的白点数量的乘积。
然后直接转移就可以了(注意第二维度的转移顺序是从 开始,否则会导致 被更新两次),注意一下使用 sz[]
进行时间复杂度优化到 ,否则是 。
代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <bits/stdc++.h> using namespace std; const int maxn = 2003; ll dp[maxn][maxn]; ll sz[maxn]; int n,k; struct Edge { int to, nxt; ll w; } edges[maxn<<1]; int ecnt = 1, head[maxn]; void addEdge(int u, int v, ll w) { Edge e = {v, head[u], w}; head[u] = ecnt; edges[ecnt++] = e; } void dfs1(int u, int p) { sz[u] = 1; for (int e = head[u]; e; e = edges[e].nxt) { int to = edges[e].to; if (to == p) continue; dfs1(to, u); sz[u] += sz[to]; } } void dfs(int u, int p) { dp[u][0] = dp[u][1] = 0; for (int e = head[u]; e; e = edges[e].nxt) { int to = edges[e].to; if (to == p) continue; dfs(to, u); ll w = edges[e].w; for (ll i = (ll)min((ll)k, sz[u]); i >= 0; i--) { for (ll j = 0; j <= min(i,sz[to]); j++) { ll cnt = j * (k - j) + (sz[to] - j) * (n - k - sz[to] + j); dp[u][i] = (ll)max(dp[u][i], dp[u][i-j] + dp[to][j] + cnt * w); } } } } int main() { cin >> n >> k; for (int i = 1; i <= n-1; i++) { int u,v; ll w; cin >> u >> v >> w; addEdge(u,v,w); addEdge(v,u,w); } for (int i = 1; i <= n; i++) for (int j = 2; j <= k; j++) dp[i][j] = -1e18; dfs1(1, 0); dfs(1, 0); cout << dp[1][k] << endl; }
|
注意事项
- 本题枚举 时,注意要加上 作为上限来优化。
- 注意初始化 为负无穷,并且设 。
- 注意第二维度的转移顺序(第一维倒序这个是必须的)。
例5 [1到n经过特定点的最短路]
题意
给定 个节点的一棵树,树上有 个点被标记了,求从节点 走到节点 ,中途需要经过这些标记点的最短路长度,所有边权值为 1。
其中,。
题解
• 这是一道群里传的题目,没有代码。
首先将 到 的路径拉成一条链,链上所有的点一定会被经过,不用管。
接下来就是挂在链上的若干子树,对于每个子树,思考如下问题:
从一棵树的根出发,经过树内的所有标记点,再回到这棵树的根,需要的最短路长度?
对于这个问题,我们考虑对于每个被标记的点 ,从根(假设为 )到 的路径一定会被经过,被经过的这些边被打上标记,代表这些边一定会被经过。
打完标记后,我们知道在最优解中,每条边一定会被经过 次,所以答案就是标记边 。
如何打标记?
只要一次dfs即可,每次dfs时判断这个子树里面有没有需要标记的点,如果有,这条边就打上标记。
题意
给定一棵树,每个节点有权值 。
定义一组节点为:两个直接相连的节点 ,这组节点的权值定义为 。
求选出若干组节点的方法,使得每组节点用到的点各不相同,并且满足选出节点组数最多。
在节点组数量最多的情况下,求出这些节点的权值和最大值。
其中,。
题解
这个题需要维护最多节点组数,还要维护权值和最大值。所以 dp
数组维护两个值。
定义 为: 的parent没有选它, 则是选了它的。
dp 值则是一个pair:<int, double>
,代表子树中最大的节点组数,以及对应的最大权值和。
在转移的时候,注意到 不能选任何 转移。而 只能选一个 转移(或者也可以不选)。
转移的过程中,优先更新第一个值(节点组数),如果第一个值相等,再更新第二个值(权值和最大值)。
代码
copy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| #include <bits/stdc++.h> using namespace std; const int maxn = 2e3+5; pair<int,double> dp[maxn][2]; vector<string> adj[maxn]; map<string, int> mp; double sp[maxn]; struct Edge { int to, nxt; } edges[maxn<<1]; int head[maxn], ecnt = 1; void addEdge(int u, int v) { Edge e = {v, head[u]}; edges[ecnt] = e; head[u] = ecnt++; } int rt; void dfs(int u, int p) { int cnt = 0, b = 0; double sum = 0; for (int e = head[u]; e; e = edges[e].nxt) { int v = edges[e].to; if (v == p) continue; dfs(v, u); cnt += dp[v][0].first; sum += dp[v][0].second; if (b == 0) b = v; else if (dp[v][1].first - dp[v][0].first > dp[b][1].first - dp[b][0].first) { b = v; } else if (dp[v][1].first - dp[v][0].first == dp[b][1].first - dp[b][0].first && min(sp[u], sp[v]) + dp[v][1].second - dp[v][0].second > min(sp[u], sp[b]) + dp[b][1].second - dp[b][0].second) { b = v; } } if (!b) return; cnt -= dp[b][0].first; cnt += dp[b][1].first; sum -= dp[b][0].second; sum += min(sp[u], sp[b]) + dp[b][1].second; dp[u][0].first = cnt + 1; dp[u][0].second = sum; cnt = 0, sum = 0; for (int e = head[u]; e; e = edges[e].nxt) { int v = edges[e].to; if (v == p) continue; cnt += dp[v][0].first; sum += dp[v][0].second; } dp[u][1].first = cnt; dp[u][1].second = sum; if (cnt > dp[u][0].first || (cnt == dp[u][0].first && sum > dp[u][0].second)) { dp[u][0].first = cnt; dp[u][0].second = sum; } } int main() { fastio; int n; cin >> n; int id = 0; for (int i = 1; i <= n; i++) { string s; cin >> s; if (!mp.count(s) && s != "CEO") mp[s] = ++id; double w; cin >> w; string t; cin >> t; if (!mp.count(t) && t != "CEO") mp[t] = ++id; int u = mp[s], v = mp[t]; if (t == "CEO") { rt = u; } else { addEdge(u,v); addEdge(v,u); } sp[u] = w; } dfs(rt, -1); cout << dp[rt][0].first << " " << dp[rt][0].second / dp[rt][0].first << "\n"; }
|