树形DP
什么是树形DP?
顾名思义,就是在树上DP。
怎么树形DP?
通常情况下,就是就是从根节点开始\(DFS\),并进行\(DP\)。
状态转移一般是从子节点向上转移,具体怎么转移看题目。
所以总的来说就是把平常的动态规划在树上转移而已。
我知道我上面讲的就是废话qwq
一道模板题
题解:
首先肯定是要定义状态:\(f_{x, 0 / 1}\)代表\(x\)这个节点选或者不选时,以\(x\)为根的子树能够获得最大的快乐值。
由于\(x\)节点选或者不选只会影响它的子节点\(y\),一旦\(x\)选了,那么它的所有子节点一定不能选;反之如果\(x\)不选,因为子节点互相之间是不会影响的,所以它的子节点选不选是无所谓的,我们可以选择较大值,即\(max(f_{y, 0}, f_{y, 1})\)。
那么便得到状态转移方程:
\(f_{x, 1} = \sum f_{y, 0}\),
\(f_{x, 0} = \sum max(f_{y, 0}, f_{y, 1})\)。
刚才说到了树形\(DP\)需要\(DFS\),我们需要在状态转移之前先\(DFS\),因为最开始我们只知道叶子节点的值,所以需要\(DFS\)到最深处,即叶子节点,才往上转移。
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 6010;
int n, h[N], f[N][2], head[N], ver[N], next[N], vis[N], tot, ans, root;
void add (int x, int y) {
ver[++ tot] = y;
next[tot] = head[x];
head[x] = tot;
}
void dfs1 (int x) {
for (int i = head[x]; i; i = next[i])
vis[ver[i]] = 1;
}
void dfs2 (int x) {
f[x][1] = h[x]; //每个节点被选择的初始值
for (int i = head[x]; i; i = next[i]) {
int y = ver[i];
dfs2(y); //先DFS
f[x][0] += max(f[y][0], f[y][1]);
f[x][1] += f[y][0];
}
}
int main () {
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%d", &h[i]);
for (int i = 1; i <= n; i ++) {
int x, y;
scanf("%d%d", &x, &y);
if (x != 0 && y != 0)
add(y, x);
}
/*
题目并没有给我们根节点,需要我们自己找。
方法很简单,把所有节点的子节点都打上标记,最后没有被标记过的即是根节点。
*/
for (int i = 1; i <= n; i ++)
dfs1(i);
for (int i = 1; i <= n; i ++)
if (!vis[i]) {
root = i;
break;
}
dfs2(root);
ans = max(f[root][0], f[root][1]);
printf("%d", ans);
return 0;
}
试题
以上两题题都是树形分组背包\(DP\),思路都是差不多的。
题解
二叉苹果树
状态应该是很好定义的,\(f_{x, i}\)表示以\(x\)为根的子树中,选择\(i\)条树枝可以获得的最多苹果。
接下来便是如何转移了,我们把每个子节点看成一组,那把每组选择的树枝数量加起来正好等于\(i\)即可转移。
但是枚举每个子节点选择的树枝数量实在是太暴力了,我们可以用分组背包来解决。
由于每组内的物品只能选择一次,所以代码中倒着循环(类似\(01\)背包)。
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 110;
int n, q, f[N][N], size[N], head[2 * N], ver[2 * N], next[2 * N], edge[2 * N], tot;
void add (int x, int y, int z) {
ver[++ tot] = y;
edge[tot] = z;
next[tot] = head[x];
head[x] = tot;
}
void dfs (int x, int fa) {
for (int i = head[x]; i; i = next[i]) {
int y = ver[i];
if (y == fa) continue;
dfs(y, x);
size[x] += size[y] + 1;
/*
下面三行是树上分组背包的核心代码
j表示当前选择了j条树枝
k表示这个y子节点选择了k条树枝
于是得到转移 f[x][j - k - 1] + f[y][k] + edge[i],即x少选k条树枝,补上y选的k条树枝
*/
for (int j = size[x]; j > 0; j --)
for (int k = min(j - 1, size[y]); k >= 0; k --)
f[x][j] = max(f[x][j], f[x][j - k - 1] + f[y][k] + edge[i]);
}
}
int main () {
scanf("%d%d", &n, &q);
for (int i = 1; i < n; i ++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
add(y, x, z);
}
dfs(1, 0);
printf("%d", f[1][q]);
return 0;
}
选课
这道题有很多细节。
先定义状态,\(f_{x, i}\)表示以\(x\)为根节点的子树中选了\(i\)节课获得的最大学分。
然后是建图,假设\(y\)的直接先修课是\(x\),那么建一条由\(x\)到\(y\)的有向边,即\(x\)是\(y\)的父亲。
因为这样建图后,\(x\)的子树被选的前提要求是\(x\)必须被选,同时,这样建图保证每个节点的父亲节点最多只有一个,符合树的性质。
但是你可能会发现,这棵树似乎不止有一个根(即没有直接选修课的节点),这要怎么解决?
我们新增一个\(0\)节点,连向所有没有直接选修课的节点,这么就完美解决了没有唯一根的问题了。
现在我们新增了一个根节点,根节点又是必须要被选的,这意味着我们\(m\)次选课机会将有一次被用来选了这个并不存在的根节点,我们是不是得换个方法了qwq
别急,我们直接让\(m + 1\)不就完事了吗(╹▽╹)!
然后直接套上树上分组背包\(DP\)就可以了。
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 310;
int n, m, s[N], a[N], head[N], ver[N], nex[N], tot, f[N][N];
inline void add (int x, int y) {
ver[++ tot] = y;
nex[tot] = head[x];
head[x] = tot;
}
void dfs (int x) {
f[x][1] = a[x];
for (int i = head[x]; i; i = nex[i]) {
int y = ver[i];
dfs(y);
for (int j = m; j >= 2; j --)
for (int k = 1; k <= m; k ++)
if (j > k)
f[x][j] = max(f[x][j], f[x][j - k] + f[y][k]);
}
}
int main () {
scanf("%d%d", &n, &m);
m ++;
for (int i = 1; i <= n; i ++) {
scanf("%d%d", &s[i], &a[i]);
add(s[i], i);
}
dfs(0);
printf("%d", f[0][m]);
return 0;
}
骑士
这道题的状态实际上和没有上司的舞会是一样的,我直接写状态转移方程:
\(f_{x, 1} = \sum f_{y, 0}\),
\(f_{x, 0} = \sum max(f_{y, 0}, f_{y, 1})\)。
然后直接复制一遍没有上司的舞会的代码就完了吗?
仔细看题,你会发现这张图中会出现环(\(n\)个点,\(n\)条边),这是一棵基环树。
那岂不是用不了树形\(DP\)了嘛!?qwq
但是,我们可以把环上的某条边切掉,这不就又变成树了嘛~
然后我们就可以愉快的在这棵树上跑树形\(DP\)了。
还需注意一点,假设\(x, y\)是被切掉的边的两端,显然\(x, y\)是不能同时选中的,于是我们跑两遍树形\(DP\),一次让\(x\)不选,一次让\(y\)不选,最后求两者的较大值即可。
还有,此题有个坑点,数据给的图不一定是个联通图,也就是可能有多棵基环树,所以对于每棵基环树我们都要跑树形\(DP\),最后答案就是每一棵基环树的答案和。
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1000010;
int n, head[N], ver[N], nex[N], cut[N], tot, vis[N], root, fa[N];
long long a[N], f[N][2], ans, tmp1, tmp2;
inline void add (int x, int y) {
ver[++ tot] = y;
nex[tot] = head[x];
head[x] = tot;
}
void dfs (int x) {
vis[x] = 1;
f[x][1] = a[x];
f[x][0] = 0;
for (int i = head[x]; i; i = nex[i]) {
int y = ver[i];
if (y == root) //对于根节点,我们不选
continue;
dfs(y);
f[x][0] += max(f[y][0], f[y][1]);
f[x][1] += f[y][0];
}
}
int main () {
scanf("%d", &n);
for (int i = 1; i <= n; i ++) {
int x;
scanf("%lld%d", &a[i], &x);
add(x, i);
fa[i] = x;
}
for (int i = 1; i <= n; i ++) { //由于数据给出的不一定是一个基环树,所以每个节点都跑一遍
if (vis[i]) continue;
root = i;
while (!vis[fa[root]]) {
root = fa[root];
vis[root] = 1;
}
//上面是找环上的某条边的操作
dfs(root);
tmp1 = f[root][0];
root = fa[root];
dfs(root);
tmp2 = f[root][0];
ans += max(tmp1, tmp2);
}
printf("%lld", ans);
return 0;
}

浙公网安备 33010602011771号