初识树形dp

树形dp

什么是树形dp?

传统dp的递推过程都是用线性循环,而树形dp的递推过程是采用了树的遍历方式(dfs,bfs)

可以应用在树形结构求解最优性问题上

没有上司的舞会

来自https://www.luogu.com.cn/problem/P1352

题目描述

某大学有 \(n\) 个职员,编号为 \(1\ldots n\)

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 \(r_i\),但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

输入格式

输入的第一行是一个整数 \(n\)

\(2\) 到第 \((n + 1)\) 行,每行一个整数,第 \((i+1)\) 行的整数表示 \(i\) 号职员的快乐指数 \(r_i\)

\((n + 2)\) 到第 \(2n\) 行,每行输入一对整数 \(l, k\),代表 \(k\)\(l\) 的直接上司。

输出格式

输出一行一个整数代表最大的快乐指数。

样例 #1

样例输入 #1

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

样例输出 #1

5

提示

数据规模与约定

对于 \(100\%\) 的数据,保证 \(1\leq n \leq 6 \times 10^3\)\(-128 \leq r_i\leq 127\)\(1 \leq l, k \leq n\)​,且给出的关系一定是一棵树。

思路

题目很明显描述了一个树形结构,又是求解最优性问题,所以考虑树形dp

定义状态:f(i,0):i号上司不来参加舞会时的欢乐指数f(i,1):i号上司来参加舞会时的欢乐指数

状态转移:\(f(i,0) = \sum max(f(x,1), f(x,0))\) \(x\)\(i\)号上司的直接下属。

\(f(i,1) = \sum f(x, 0) + v(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

题目描述

有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)

这棵树共有 \(N\) 个结点(叶子点或者树枝分叉点),编号为 \(1 \sim N\),树根编号一定是 \(1\)

我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 \(4\) 个树枝的树:

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。

给定需要保留的树枝数量,求出最多能留住多少苹果。

输入格式

第一行 \(2\) 个整数 \(N\)\(Q\),分别表示表示树的结点数,和要保留的树枝数量。

接下来 \(N-1\) 行,每行 \(3\) 个整数,描述一根树枝的信息:前 \(2\) 个数是它连接的结点的编号,第 \(3\) 个数是这根树枝上苹果的数量。

输出格式

一个数,最多能留住的苹果的数量。

样例 #1

样例输入 #1

5 2
1 3 1
1 4 10
2 3 20
3 5 20

样例输出 #1

21

提示

\(1 \leqslant Q < N \leqslant 100\),每根树枝上的苹果 \(\leqslant 3 \times 10^4\)

思路:

状态定义:f(i, j):以i为根的节点,保留j个树枝能留住的苹果的最大数量。

状态转移:

只保留一棵树:当只保留此时遍历到的子树时,\(f(i, j) = f(to, j - 1) + e(i, to)\)

两颗树都保留时:\(f(i, j) = f(left, k) + f(right, j - 2 - k) + e(i, left) + e(i, right)\)​。

一个树枝都不保留:因为苹果都在树枝上,一个树枝都不保留就一个苹果都没有,所以对于任何根t, \(f(t, 0) = 0\)。由此可知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

题目描述

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 \(N\) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 \(M\) 门课程学习,问他能获得的最大学分是多少?

输入格式

第一行有两个整数 \(N\) , \(M\) 用空格隔开。( \(1 \leq N \leq 300\) , \(1 \leq M \leq 300\) )

接下来的 \(N\) 行,第 \(I+1\) 行包含两个整数 $k_i $和 \(s_i\), \(k_i\) 表示第I门课的直接先修课,\(s_i\) 表示第I门课的学分。若 \(k_i=0\) 表示没有直接先修课(\(1 \leq {k_i} \leq N\) , \(1 \leq {s_i} \leq 20\))。

输出格式

只有一行,选 \(M\) 门课程的最大得分。

样例 #1

样例输入 #1

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

样例输出 #1

13

思路

题目所给的是一个森林结构,但是为了做题方便,可以新增一门0学分的课程编号也为0。凡事没有前修课程的课都会直接连接到0号节点下,这样我们就得到了一个树的结构

状态定义:\(f(i, j)\)\(i\)为根,选了\(j\)门课程的最大学分,其中根是必选的

根据容斥原理,\(f(i, j)\)只由两部分组成,一部分是在\(i\)\(x\)号子树里选了\(k\)门课程,一部分是在其他子树里选了\(j-k\)​门课程

状态转移:\(f(i, j) = max(f(i,j), f(i,j-k) + f(x,k))\),\(x\)\(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;

//状态定义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问题

状态定义:

\(size[i]\):以\(i\)为根的子树中有多少个节点

\(f[i]\):i 到以 i 为根的子树中的其他节点的距离之和

\(v[i]\):当 i 的父亲作他的子树时的贡献

状态转移:

\(size[i] = \sum_j size[j] + 1\),其中 j 为 i 的直接孩子

\(f[i] = \sum_j f[j] + size[i] - 1\),由于要用到孩子的信息,所以求的时候是自底向上求

\(v[i] = v[j] + n - size[i] + f[j] - f[i] - size[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

https://www.luogu.com.cn/problem/P10974

注意事项

下面的代码在洛谷上会有三个点过不去,要使用链式前项星建图才能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;
}
posted @ 2024-12-24 18:01  califeee  阅读(52)  评论(0)    收藏  举报