树形 dp

P2585 [ZJOI2006] 三色二叉树

  1. \(dp_{i, 0 / 1 / 2}\) 表示以 \(i\) 为根的子树,且点 \(i\) 颜色为 \(0 / 1 / 2\) 的最多绿色节点数。
  2. 答案为 \(\max \{ dp_{root, 0}, dp_{root, 1}, dp_{root, 2} \}\)
  3. 状态转移方程:
  • 设点 \(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} \]

  1. 初始状态:\(dp_{x, 0} = 1, dp_{x, 1} = dp_{x, 2} = 0\)

P2016 战略游戏

  1. \(dp_{i, 0 / 1}\) 表示以 \(i\) 为根的子树覆盖所有边最少放置的士兵数。
  2. 答案为 \(\min \{ dp_{root, 0}, dp_{root, 1} \}\)
  3. 状态转移方程:

\[\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} \]

  1. 初始状态:\(dp_{x, 1} = 1, dp_{x, 0} = 0\)
  2. 转移顺序:自下而上转移。

代码:

#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

  1. \(dp_{i, 0 / 1}\) 为以 \(i\) 为根的子树内有 \(0 / 1\) 个黑节点的方案数。
  2. 答案:\(dp_{1, 1}\)
  3. 转移:\(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} \]

  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] 树上选点

  1. 优先考虑深度的限制。
  2. \(dp_i\) 表示从上到下必须选点 \(i\) 的最大点权和。
  3. 答案:\(\max \{ dp_i \}\)
  4. 转移:

设点 \(i\) 的深度为 \(d\),那么:

\[dp_i = val_i + \max_{dp_u \in [1, d - 1] \land u \not = fa_x} dp_u \]

  1. 维护前 \(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\) 个字符串,求选取的方案数。

分析:

  1. 先将 \(n\) 个字符串插入 Trie,并标记终止节点。
  2. 对于任意字符串 s,它的前缀字符串的终止节点一定在 s 的终止节点到根的路径上。
  3. 选取 \(k\) 个字符串等价于选 Trie 树上选 \(k\) 个终止节点。
  4. 对于选取后 \(n\) 个字符串的每个终止节点到根的路径上恰好有 \(1\) 个选取的点。
  5. 问题转化为给定一棵树,有 \(n\) 个染色节点,要求在 \(n\) 个染色节点中标记 \(k\) 个使得 \(n\) 个染色节点向上到根的路径上恰好有 \(1\) 个点被标记。
  6. \(dp_{i, j}\) 表示以 \(i\) 为根的子树选 \(j\) 个点标记的方案数。
  7. 答案:\(dp_{0, k}\)
  8. 转移 \(u \to x\)
  • 如果 \(x\) 本身是染色节点且被标记:\(dp_{x, 1} = 1\)
  • \(dp_{x, j} = (dp_{x, j} + dp_{x, j - p} \times dp_{u, p})\)
  1. 初始状态:\(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;
}
posted @ 2025-08-02 22:00  endswitch  阅读(5)  评论(0)    收藏  举报