树形 dp
P2585 [ZJOI2006] 三色二叉树
- 设 \(dp_{i, 0 / 1 / 2}\) 表示以 \(i\) 为根的子树,且点 \(i\) 颜色为 \(0 / 1 / 2\) 的最多绿色节点数。
- 答案为 \(\max \{ dp_{root, 0}, dp_{root, 1}, dp_{root, 2} \}\)
- 状态转移方程:
- 设点 \(x\) 的左右儿子分别为 \(ls_x, rs_x\)。
\[\begin{cases} dp_{x, 0} = dp_{x, 0} + \max \{ dp_{ls_x, 1} + dp_{rs_x, 2}, dp_{ls_x, 2} + dp_{rs_x, 1} \} \\ dp_{x, 1} = dp_{x, 1} + \max \{ dp_{ls_x, 2} + dp_{rs_x, 0}, dp_{ls_x, 0} + dp_{rs_x, 2} \} \\ dp_{x, 2} = dp_{x, 2} + \max \{ dp_{ls_x, 0} + dp_{rs_x, 1}, dp_{ls_x, 1} + dp_{rs_x, 0} \} \\ \end{cases}
\]
- 初始状态:\(dp_{x, 0} = 1, dp_{x, 1} = dp_{x, 2} = 0\)。
P2016 战略游戏
- 设 \(dp_{i, 0 / 1}\) 表示以 \(i\) 为根的子树覆盖所有边最少放置的士兵数。
- 答案为 \(\min \{ dp_{root, 0}, dp_{root, 1} \}\)
- 状态转移方程:
\[\begin{cases} dp_{x, 0} = dp_{x, 0} + dp_{u, 1} \\ dp_{x, 1} = dp_{x, 1} + \min \{ dp_{u, 0}, dp_u, 1 \} \end{cases}
\]
- 初始状态:\(dp_{x, 1} = 1, dp_{x, 0} = 0\)。
- 转移顺序:自下而上转移。
代码:
#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
const int N = 1505;
int n, k, u, v, dp[N][2];
vector<int> g[N];
inline void dfs(int x, int last) {
for(auto u : g[x])
if(u != last) {
dfs(u, x);
dp[x][0] += dp[u][1];
dp[x][1] += min(dp[u][0], dp[u][1]);
}
}
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for(int i = 1 ; i < n ; ++ i) {
cin >> u >> k;
++ u;
for(int j = 1 ; j <= k ; ++ j) {
cin >> v;
++ v;
g[u].pb(v), g[v].pb(u);
}
}
for(int i = 1 ; i <= n ; ++ i)
dp[i][1] = 1;
dfs(1, -1);
cout << min(dp[1][0], dp[1][1]);
return 0;
}
P1272 重建道路
P3478 POI2008 STA-Station
考虑随便选点为根进行答案的求取。接下来考虑换根对答案的贡献(这一部分一定要画图 & 细心!有时候以不同点为根的变量的定义是不同的!),对答案求 \(\max\) 即可,显然是线性做法。
CF1187E Tree Painting
同上。
CF461B Appleman and Tree
- 设 \(dp_{i, 0 / 1}\) 为以 \(i\) 为根的子树内有 \(0 / 1\) 个黑节点的方案数。
- 答案:\(dp_{1, 1}\)。
- 转移:\(u \to x\)。
- 删除边:
\[dp_{x, 0} = dp_{x, 0} \times dp_{u, 1}
\]
\[dp_{x, 1} = dp_{x, 1} \times dp_{u, 1}
\]
- 保留边:
\[dp_{x, 0} = dp_{x, 0} \times dp_{u, 0}
\]
\[dp_{x, 1} = dp_{x, 1} \times dp_{u, 0} + dp_{x, 0} \times dp_{u, 1}
\]
- 初始状态:
\[dp_{u, col_u} \to 1
\]
代码:
#include <bits/stdc++.h>
#define int long long
#define pb emplace_back
using namespace std;
const int N = 1e5 + 5;
const int mod = 1e9 + 7;
int n, u, dp[N][2];
bool col[N];
vector<int> g[N];
inline void dfs(int x, int last) {
dp[x][col[x]] = 1;
for(auto u : g[x])
if(u != last) {
dfs(u, x);
dp[x][1] = (dp[x][1] * (dp[u][0] + dp[u][1]) % mod + dp[x][0] * dp[u][1] % mod) % mod;
dp[x][0] = dp[x][0] * (dp[u][0] + dp[u][1]) % mod;
}
return ;
}
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for(int i = 1 ; i < n ; ++ i) {
cin >> u;
++ u;
g[u].pb(i + 1), g[i + 1].pb(u);
}
for(int i = 1 ; i <= n ; ++ i)
cin >> col[i];
dfs(1, -1);
cout << dp[1][1];
return 0;
}
P12382 [蓝桥杯 2023 省 Python B] 树上选点
- 优先考虑深度的限制。
- 设 \(dp_i\) 表示从上到下必须选点 \(i\) 的最大点权和。
- 答案:\(\max \{ dp_i \}\)。
- 转移:
设点 \(i\) 的深度为 \(d\),那么:
\[dp_i = val_i + \max_{dp_u \in [1, d - 1] \land u \not = fa_x} dp_u
\]
- 维护前 \(d - 1\) 层的最大和次大 dp 值即可。
代码:
#include <bits/stdc++.h>
#define int long long
#define pb emplace_back
using namespace std;
const int N = 2e5 + 5;
int n, u, a[N], dp[N], fa[N], dep[N], rid1, rid2, rmax1, rmax2;
vector<int> g[N], vec[N];
inline void dfs(int x, int last) {
for(auto u : g[x])
if(u != last) {
fa[u] = x;
dep[u] = dep[x] + 1;
dfs(u, x);
}
vec[dep[x]].pb(x);
}
inline void DP() {
for(int d = 1 ; d <= n ; ++ d) {
int id1 = rid1, id2 = rid2, max1 = rmax1, max2 = rmax2;
for(auto i : vec[d]) {
if(rid1 == fa[i]) dp[i] = a[i] + rmax2;
else dp[i] = a[i] + rmax1;
if(dp[i] > max1) {
id2 = id1, id1 = i;
max2 = max1, max1 = dp[i];
}
else if(dp[i] > max2) id2 = i, max2 = dp[i];
}
rid1 = id1, rid2 = id2, rmax1 = max1, rmax2 = max2;
}
return ;
}
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for(int i = 1 ; i < n ; ++ i) {
cin >> u;
g[u].pb(i + 1), g[i + 1].pb(u);
}
for(int i = 1 ; i <= n ; ++ i)
cin >> a[i];
dep[1] = 1;
dfs(1, -1);
DP();
cout << rmax1;
return 0;
}
P12238 [蓝桥杯 2023 国 Java A] 单词分类
题意:
给定 \(n\) 个字符串,选取恰好 \(k\) 个字符串作为前缀,使得 \(n\) 个字符串都恰好有 \(1\) 个前缀是这 \(k\) 个字符串,求选取的方案数。
分析:
- 先将 \(n\) 个字符串插入 Trie,并标记终止节点。
- 对于任意字符串 s,它的前缀字符串的终止节点一定在 s 的终止节点到根的路径上。
- 选取 \(k\) 个字符串等价于选 Trie 树上选 \(k\) 个终止节点。
- 对于选取后 \(n\) 个字符串的每个终止节点到根的路径上恰好有 \(1\) 个选取的点。
- 问题转化为给定一棵树,有 \(n\) 个染色节点,要求在 \(n\) 个染色节点中标记 \(k\) 个使得 \(n\) 个染色节点向上到根的路径上恰好有 \(1\) 个点被标记。
- 设 \(dp_{i, j}\) 表示以 \(i\) 为根的子树选 \(j\) 个点标记的方案数。
- 答案:\(dp_{0, k}\)。
- 转移 \(u \to x\):
- 如果 \(x\) 本身是染色节点且被标记:\(dp_{x, 1} = 1\)。
- \(dp_{x, j} = (dp_{x, j} + dp_{x, j - p} \times dp_{u, p})\)。
- 初始状态:\(dp_{i, 0} = 1\)。
代码:
#include <bits/stdc++.h>
#define int long long
#define pb emplace_back
using namespace std;
const int N = 205;
const int S = N * 10;
const int SIGMA = 4;
const int mod = 1e9 + 7;
int n, k, sz[S], dp[S][S];
string s;
vector<int> g[S];
namespace Trie {
int tot, cnt[S * SIGMA], t[S][SIGMA];
inline int to(char c) {
if(c == 'l') return 1;
if(c == 'q') return 2;
return 3;
}
inline void insert(string s) {
int p = 0, l = s.size();
for(int i = 0 ; i < l ; ++ i) {
int c = to(s[i]);
if(! t[p][c]) t[p][c] = ++ tot;
p = t[p][c];
if(i == l - 1) ++ cnt[p];
}
return ;
}
inline void dfs(int x) {
sz[x] = dp[x][0] = 1;
for(int c = 1 ; c <= 3 ; ++ c) {
if(! t[x][c]) continue;
int u = t[x][c];
dfs(u);
for(int i = min(sz[x], k) ; ~ i ; -- i) {
dp[x][i] = 0;
for(int j = 1 ; j <= min(sz[u], i) ; ++ j)
dp[x][i] = (dp[x][i] + dp[x][i - j] * dp[u][j] % mod) % mod;
}
sz[x] += sz[u];
}
dp[x][1] = (dp[x][1] + 1) % mod;
if(cnt[x]) {
for(int i = 0 ; i <= k ; ++ i)
dp[x][i] = 0;
dp[x][1] = 1;
}
return ;
}
}
using namespace Trie;
signed main() {
ios_base :: sync_with_stdio(NULL);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> k;
for(int i = 1 ; i <= n ; ++ i)
cin >> s, insert(s);
dfs(0);
cout << dp[0][k];
return 0;
}