Content :DP (Interval, Tree)
Date :2025.7.24
概览
例题
题目大意
题目大意
给定一颗树,要求在树上选取恰好 k \large k k 个节点 (不得重复),每个选取的节点可以覆盖它的邻居,但是不能覆盖自己本身 。要求选取的这 k \large k k 个节点覆盖所有的 n \large n n 个节点。求方案数。
数据范围 n ≤ 10 5 , k ≤ 100 \large n \le 10^5, k \le 100 n ≤ 1 0 5 , k ≤ 100 。
思路
我们考虑树形动态规划。定义状态 f u , i , 0 / 1 , 0 / 1 \large f_{u,i,0/1,0/1} f u , i , 0/1 , 0/1 表示以 u \large u u 为根的子树内,选取了 i \large i i 个节点,其中点 u \large u u 放了/不放,被/不被覆盖的方案数。初始状态为:
f u , 0 , 0 , 0 = 1 f u , 1 , 1 , 0 = 1 \large
\begin{aligned}
f_{u,0,0,0} = 1 \\
f_{u,1,1,0} = 1 \\
\end{aligned}
f u , 0 , 0 , 0 = 1 f u , 1 , 1 , 0 = 1
可以发现如果只看前两维的话,是一个典型的树形背包问题。接下来我们考虑加上后两维后如何转移 (本质还是树形背包)。
对于 f u , i , 0 , 0 \large f_{u,i,0,0} f u , i , 0 , 0 ,即点 u \large u u 既不选择,也 不覆盖,所以 u u u 的子节点 v v v 一定不能选择,而 v v v 必须 覆盖,所以其转移如下:
f u , i , 0 , 0 = ∑ v ∈ son ( u ) f u , i − j , 0 , 0 × f v , j , 0 , 1 \large
f_{u,i,0,0} = \sum_{v \in \operatorname{son}(u)} f_{u,i - j,0,0} \times f_{v,j,0,1}
f u , i , 0 , 0 = v ∈ son ( u ) ∑ f u , i − j , 0 , 0 × f v , j , 0 , 1
对于 f u , i , 0 , 1 \large f_{u,i,0,1} f u , i , 0 , 1 ,即点 u \large u u 不选择,但是被覆盖。因为状态为点 u u u 被覆盖,所以既可以是 v v v 覆盖的,也可能是 u u u 的其他子节点覆盖的,而因为 u u u 不被选择,所以 v v v 一定是已经被覆盖了的,所以其转移如下:
f u , i , 0 , 1 = ∑ v ∈ son ( u ) f u , i − j , 0 , 1 × ( f v , j , 0 , 1 + f v , j , 1 , 1 ) + ∑ v ∈ son ( u ) f u , i − j , 0 , 0 × f v , j , 1 , 1 \large
f_{u,i,0,1} = \sum_{v \in \operatorname{son}(u)} f_{u,i-j,0,1} \times (f_{v,j,0,1} + f_{v,j,1,1}) + \sum_{v \in \operatorname{son}(u)} f_{u,i-j,0,0} \times f_{v,j,1,1}
f u , i , 0 , 1 = v ∈ son ( u ) ∑ f u , i − j , 0 , 1 × ( f v , j , 0 , 1 + f v , j , 1 , 1 ) + v ∈ son ( u ) ∑ f u , i − j , 0 , 0 × f v , j , 1 , 1
对于 f u , i , 1 , 0 \large f_{u,i,1,0} f u , i , 1 , 0 ,即点 u \large u u 选择了,但还没有被覆盖。因为不被覆盖,所以点 v \large v v 一定不能被选择,而点 u \large u u 已经被选择了,所以点 v \large v v 覆不覆盖无所谓。转移如下:
f u , i , 1 , 0 = ∑ v ∈ son ( u ) f u , i − j , 1 , 0 × ( f v , j , 0 , 1 + f v , j , 0 , 0 ) \large
f_{u,i,1,0} = \sum_{v \in \operatorname{son}(u)} f_{u,i-j,1,0} \times (f_{v,j,0,1} + f_{v,j,0,0})
f u , i , 1 , 0 = v ∈ son ( u ) ∑ f u , i − j , 1 , 0 × ( f v , j , 0 , 1 + f v , j , 0 , 0 )
对于 f u , i , 1 , 1 \large f_{u,i,1,1} f u , i , 1 , 1 ,即点 u \large u u 选择了,也被覆盖了的情况。由 1 , 3 1,3 1 , 3 两种情况类似的推导就可以得到。转移为:
f u , i , 1 , 1 = ∑ v ∈ son ( u ) f u , i − j , 1 , 1 × ( f v , j , 0 , 0 + f v , j , 0 , 1 + f v , j , 1 , 0 + f v , j , 1 , 1 ) + ∑ v ∈ son ( u ) f u , i − j , 1 , 0 × ( f v , j , 1 , 0 + f v , j , 1 , 1 ) \large
f_{u,i,1,1} = \sum_{v \in \operatorname{son}(u)} f_{u,i-j,1,1} \times (f_{v,j,0,0} + f_{v,j,0,1} + f_{v,j,1,0} + f_{v,j,1,1}) + \sum_{v \in \operatorname{son}(u)} f_{u,i-j,1,0} \times (f_{v,j,1,0} + f_{v,j,1,1})
f u , i , 1 , 1 = v ∈ son ( u ) ∑ f u , i − j , 1 , 1 × ( f v , j , 0 , 0 + f v , j , 0 , 1 + f v , j , 1 , 0 + f v , j , 1 , 1 ) + v ∈ son ( u ) ∑ f u , i − j , 1 , 0 × ( f v , j , 1 , 0 + f v , j , 1 , 1 )
最后,注意 取模 和 long long !!!
Code
Code
#include <bits/stdc++.h> namespace IO { template <typename name> name read () { name x = 0 , f = 1 ; char ch = getchar (); while (!isdigit (ch)) { if (ch == '-' ) f = -1 ; ch = getchar (); } while (isdigit (ch)) { x = (x << 1 ) + (x << 3 ) + (ch ^ 48 ); ch = getchar (); } return x * f; } template <typename name> void _write(name x) { if (x > 9 ) _write(x / 10 ); putchar (x % 10 + '0' ); } template <typename name> void write (name x) { if (x < 0 ) putchar ('-' ), x = -x; _write(x); } } constexpr int N = 1e5 + 5 , K = 105 , MOD = 1e9 + 7 ;int head[N], next[N << 1 ], to[N << 1 ], cnt = 0 ;int dp[N][K][2 ][2 ], size[N], tmp[K][2 ][2 ];int n, k, u, v;inline void AddEdge (const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++; } inline void Dfs (int u, int father) { dp[u][0 ][0 ][0 ] = dp[u][1 ][1 ][0 ] = 1 ; size[u] = 1 ; for (int i = head[u]; ~i; i = next[i]) { const int v = to[i]; if (v == father) continue ; Dfs (v, u); size[u] += size[v]; for (int t = 0 ; t <= std::min (k, size[u]); t++) { tmp[t][0 ][0 ] = dp[u][t][0 ][0 ]; tmp[t][1 ][0 ] = dp[u][t][1 ][0 ]; tmp[t][0 ][1 ] = dp[u][t][0 ][1 ]; tmp[t][1 ][1 ] = dp[u][t][1 ][1 ]; dp[u][t][0 ][0 ] = dp[u][t][1 ][0 ] = dp[u][t][0 ][1 ] = dp[u][t][1 ][1 ] = 0 ; } for (int t = 0 ; t <= std::min (size[u], k); t++) { for (int j = 0 ; j <= std::min (size[v], t); j++) { if (t - j > size[u] - size[v]) continue ; (dp[u][t][0 ][0 ] += (long long ) tmp[t - j][0 ][0 ] * dp[v][j][0 ][1 ] % MOD) %= MOD; (dp[u][t][0 ][1 ] += ((long long ) tmp[t - j][0 ][1 ] * ((dp[v][j][0 ][1 ] + dp[v][j][1 ][1 ]) % MOD) % MOD + (long long ) tmp[t - j][0 ][0 ] * dp[v][j][1 ][1 ] % MOD) % MOD) %= MOD; (dp[u][t][1 ][0 ] += (long long ) tmp[t - j][1 ][0 ] * ((dp[v][j][0 ][1 ] + dp[v][j][0 ][0 ]) % MOD) % MOD) %= MOD; (dp[u][t][1 ][1 ] += ((long long ) (tmp[t - j][1 ][1 ] * (((long long ) dp[v][j][0 ][1 ] + dp[v][j][0 ][0 ] + dp[v][j][1 ][0 ] + dp[v][j][1 ][1 ]) % MOD) % MOD) + (long long ) tmp[t - j][1 ][0 ] * ((dp[v][j][1 ][1 ] + dp[v][j][1 ][0 ]) % MOD) % MOD) % MOD) %= MOD; } } } } int main () { using namespace IO; n = read <int >(); k = read <int >(); for (int i = 1 ; i <= n; i++) head[i] = -1 ; for (int i = 1 ; i < n; i++) { u = read <int >(), v = read <int >(); AddEdge (u, v); AddEdge (v, u); } Dfs (1 , 1 ); write ((1ll * dp[1 ][k][0 ][1 ] + dp[1 ][k][1 ][1 ]) % MOD); return 0 ; }
题目大意
题目大意
给定一颗有 n n n 个节点的树,表示员工的架构,现在有一个舞会需要举办,你要选取一些人参加,第 i \large i i 个人参加可以为舞会带来 r i \large r_i r i 的快乐值,但是如果第 i \large i i 个人的直接上司 (i \large i i 的父亲节点) 参加了,那么 i \large i i 就不会参加,你要最大化选择的人的快乐值之和。
数据范围:n ≤ 6 × 10 3 \large n \le 6 \times 10^3 n ≤ 6 × 1 0 3 。
思路
定义状态 f u , 0 / 1 \large f_{u,0/1} f u , 0/1 表示节点 u \large u u 参加/不参加能为舞会带来的最大价值。
转移如下:
f u , 0 = ∑ v ∈ son ( u ) max ( f v , 0 , f v , 1 ) f u , 1 = ∑ v ∈ son ( u ) f v , 0 + r i \large
\begin{aligned}
f_{u,0} &= \sum_{v \in \operatorname{son}(u)} \max(f_{v,0}, f_{v,1}) \\
f_{u,1} &= \sum_{v \in \operatorname{son}(u)} f_{v,0} + r_i
\end{aligned}
f u , 0 f u , 1 = v ∈ son ( u ) ∑ max ( f v , 0 , f v , 1 ) = v ∈ son ( u ) ∑ f v , 0 + r i
注意我的树存的是双向边,和边有关的数组要开 2 \large 2 2 倍 。
Code
Code
#include <bits/stdc++.h> using std::cin;using std::cout;constexpr int MAXN = 6e4 + 5 ;int head[MAXN], to[MAXN], next[MAXN], cnt = 0 ;int n, r[MAXN], u, v;int dp[MAXN][2 ];void add_edge (const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++; } void dfs (const int u, const int father) { dp[u][1 ] = r[u]; for (int i = head[u]; ~i; i = next[i]) { const int v = to[i]; if (v == father) continue ; dfs (v, u); dp[u][0 ] += std::max (dp[v][0 ], dp[v][1 ]); dp[u][1 ] += dp[v][0 ]; } } int main () { std::ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); std::memset (head, -1 , sizeof (head)); cin >> n; for (int i = 1 ; i <= n; i++) { cin >> r[i]; } for (int i = 1 ; i < n; i++) { cin >> u >> v; add_edge (u, v); add_edge (v, u); } dfs (1 , 1 ); cout << std::max (dp[1 ][0 ], dp[1 ][1 ]) << '\n' ; return 0 ; }
洛谷-P3478 消防局的 STA-Station
题目大意
P3478 [POI 2008] STA-Station 题目大意
给定一个 n \large n n 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。
一个结点的深度之定义为该节点到根的简单路径上边的数量。
数据范围:n ≤ 10 6 \large n \le 10^6 n ≤ 1 0 6 .
思路
换根 DP。设 f u \large f_u f u 表示以 u \large u u 为根节点的时候所有节点的深度之和是多少。换根一般都有两个 d f s \large dfs df s ,我们在第一遍 d f s \large dfs df s 的时候记录节点的深度 d e p \large dep d e p ,子树大小 s i z e \large size s i ze ,然后 f u \large f_u f u 的状态为:f u = d e p u \large f_u = dep_u f u = d e p u 。
在第二遍 d f s dfs df s 时,我们自上而下更新 f v f_v f v 的值,转移为:
f v = f u − s i z e v + n − s i z e v \large
f_v = f_u - size_v + n - size_v
f v = f u − s i z e v + n − s i z e v
即子树 v v v 内所有点的深度减一,子树外的所有深度加一。
注意 long long !!!
Code
Code
#include <bits/stdc++.h> using std::cin;using std::cout;constexpr int N = 1e6 + 5 ;int head[N], next[N << 1 ], to[N << 1 ], cnt = 0 ;int n, u, v, dp[N], dep[N], size[N];void AddEdge (const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++; } void Dfs1 (const int u, const int father) { dp[u] += dep[u]; dep[u] = dep[father] + 1 ; size[N] = 1 ; for (register int i = head[u]; ~i; i = next[i]) { register const int v = to[i]; if (v == father) continue ; Dfs1 (v, u); size[u] += size[v]; } } void Dfs2 (const int u, const int father) { for (register int i = head[u]; ~i; i = next[i]) { register const int v = to[i]; if (v == father) continue ; dp[v] = dp[u] - size[v] + n - size[v]; Dfs2 (v, u); } } int main () { std::ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); std::memset (head, -1 , sizeof (head)); cin >> n; for (int i = 1 ; i < n; i++) { cin >> u >> v; AddEdge (u, v); AddEdge (v, u); } Dfs1 (1 , 1 ); Dfs2 (1 , 1 ); int ans = 1 ; for (int i = 2 ; i <= n; i++) { if (dp[i] > dp[ans]) ans = i; } cout << ans << '\n' ; return 0 ; }