树形DP
一、简单树形DP
树形 \(DP\),即在树上进行 \(DP\),一般以递归进行,以题为例:
模板题 没有上司的舞会。
\(\Rightarrow\) 分析:对于一个结点 \(x\),其一个儿子结点为 \(y\),分两种情况:
(1)如果 \(x\) 不去,那么他的儿子结点可去可不去,即:
$ f_{x,0}=\sum max(f_{y,0},f_{y,1}) $
(2)如果 \(x\) 去,那么他的儿子结点只能不去,即:
$ f_{x,1}=\sum f_{y,0} $
\(\Rightarrow\) 代码实现:
(1)输入的时候输入的值相当于最开始的 \(f_{i,1}\)。
(2)利用 \(dfs\) 递归记录每个结点的值即可。
(3)\(dfs\) 要从根节点开始,此时我们只要根据输入来判断,对于每一个 \(l_i\) 肯定不是根节点,所以最后看哪一个点没有在 \(l_i\) 中出现过即是根节点。
长城之子,归于长城。
#include <iostream>
#include <cstdio>
#define N 6003
using namespace std;
int n,f[N][2],jilu[N],vis[N];
struct edge{
int next,to;
}edge[N];
int head[N],cnt;
void add(int from,int to){
edge[++cnt].next = head[from];
edge[cnt].to = to;
head[from] = cnt;
}
void Input(){
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&f[i][1]);
for(int i = 1,l,k;i < n;i ++){
scanf("%d%d",&l,&k);
add(k,l);jilu[l] = 1;
}
}
void dfs(int x){
vis[x] = 1;
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(vis[y]) continue;
dfs(y);
f[x][1] += f[y][0];
f[x][0] += max(f[y][1],f[y][0]);
}
}
void work(){
for(int i = 1;i <= n;i ++){
if(!jilu[i]){
dfs(i);
printf("%d\n",max(f[i][0],f[i][1]));
return ;
}
}
}
int main(){
Input();
work();
return 0;
}
三色二叉树
\(\Rightarrow\) 分析:先考虑如何状态转移,有三种情况:
令 \(x\) 为当前根节点,分别令 \(fmax_{x,0},fmax_{x,1},fmax_{x,2}\) 表示如果结点 \(x\) 为绿色 \(or\) 红色 \(or\) 蓝色,那么其对应子树可以得到被染成绿色结点的最大值,\(fmin\) 为最小值。
根据题目输入性质,结点 \(x\) 的左儿子可定紧挨着这个结点,所以转移左儿子时直接使用 \(x + 1\) 就可以了。
那么如果当前结点为绿色,则:
$ fmax_{x,0} = max(fmax_{x + 1,1},fmax_{x + 1,2}) + 1 $
$ fmin_{x,0} = min(fmin_{x + 1,1},fmin_{x + 1,2}) + 1 $
如果是红色/蓝色,则:
$ fmax_{x,1} = max(fmax_{x + 1,0},fmax_{x + 1,2}) $
$ fmin_{x,1} = min(fmin_{x + 1,0},fmin_{x + 1,2}) $
$ fmax_{x,2} = max(fmax_{x + 1,0},fmax_{x + 1,1}) $
$ fmin_{x,2} = min(fmin_{x + 1,0},fmin_{x + 1,1}) $
那么右儿子如何处理呢?此时我们令 \(t\) 为右儿子所在位置。
那么如果当前结点为绿色,则:
$ fmax_{x,0} = max(fmax_{x + 1,1} + fmax_{t,2},fmax_{x + 1,2} + fmax_{t,1}) + 1 $
$ fmin_{x,0} = min(fmin_{x + 1,1} + fmin_{t,2},fmin_{x + 1,2} + fmin_{t,1}) + 1 $
如果是红色/蓝色,则:
$ fmax_{x,1} = max(fmax_{x + 1,0} + fmax_{t,2},fmax_{x + 1,2} + fmax_{t,0}) $
$ fmin_{x,1} = min(fmin_{x + 1,0} + fmin_{t,2},fmin_{x + 1,2} + fmin_{t,0}) $
$ fmax_{x,2} = max(fmax_{x + 1,0} + fmax_{t,1},fmax_{x + 1,1} + fmax_{t,0}) $
$ fmin_{x,2} = min(fmin_{x + 1,0} + fmin_{t,1},fmin_{x + 1,1} + fmin_{t,0}) $
\(\Rightarrow\) 代码实现:
现在只有一个问题,如何知道 \(t\) 是多少呢?
首先我们知道,左儿子肯定挨着该儿子的父亲节点,右儿子肯定挨着左儿子,所以我们可以遍历时随时记录遍历到的最右边的点的下标 \(cnt\),那么 \(t\) 其实就相当于是 \(cnt + 1\),这个可以自己想一下,也可以模拟一下样例。这样可以节约时间复杂度。
本猫守望的长城,屹立不倒。
#include <iostream>
#include <cstdio>
#define N 500005
using namespace std;
string s;
int cnt = 0,f_max[N][3],f_min[N][3];
void dfs(int x){
if(s[x] == '0'){
f_max[x][0] = f_min[x][0] = 1;
return ;
}
dfs(++cnt);
if(s[x] == '1'){
f_max[x][0] = max(f_max[x + 1][1],f_max[x + 1][2]) + 1;
f_max[x][1] = max(f_max[x + 1][0],f_max[x + 1][2]);
f_max[x][2] = max(f_max[x + 1][0],f_max[x + 1][1]);
f_min[x][0] = min(f_min[x + 1][1],f_min[x + 1][2]) + 1;
f_min[x][1] = min(f_min[x + 1][0],f_min[x + 1][2]);
f_min[x][2] = min(f_min[x + 1][0],f_min[x + 1][1]);
}
else{
int t = ++ cnt;
dfs(t);
f_max[x][0] = max(f_max[x + 1][1] + f_max[t][2],f_max[x + 1][2] + f_max[t][1]) + 1;
f_max[x][1] = max(f_max[x + 1][0] + f_max[t][2],f_max[x + 1][2] + f_max[t][0]);
f_max[x][2] = max(f_max[x + 1][1] + f_max[t][0],f_max[x + 1][0] + f_max[t][1]);
f_min[x][0] = min(f_min[x + 1][1] + f_min[t][2],f_min[x + 1][2] + f_min[t][1]) + 1;
f_min[x][1] = min(f_min[x + 1][0] + f_min[t][2],f_min[x + 1][2] + f_min[t][0]);
f_min[x][2] = min(f_min[x + 1][1] + f_min[t][0],f_min[x + 1][0] + f_min[t][1]);
}
}
int main(){
cin >> s;
dfs(cnt);
printf("%d %d\n",max(f_max[0][0],max(f_max[0][1],f_max[0][2])),min(f_min[0][0],min(f_min[0][1],f_min[0][2])));
return 0;
}
二、树上背包
其实也就是树形 \(DP\) 和背包的结合啦。
还是以题为例:
选课
\(\Rightarrow\) 分析:每一门功课都最多有一门先修课,所以可以看成一个树形结构。
考虑如何实现状态转移,令 \(f_{x,i,j}\) 表示对于结点 \(x\),已经遍历了其前 \(i\) 个叶子结点,共学习 \(j\) 门功课可以获得的最大学分。
从三维的角度分析,我们令每个结点的叶子结点所有的编号依次递增,那么:
\(f_{x,i,j} = max(f_{x,i - 1,j},f_{x,i - 1,k} + f_{y,siz_y,j - k})\)
其中 \(y\) 表示 \(x\) 的第 \(i\) 个叶子结点,\(siz_y\) 表示 \(y\) 的子树的大小。
如果卡空间怎么办?——考虑可否去掉一维。
刚刚的问题其实就想到于对于一个结点的贡献进行 \(01\) 背包来计算,所以类似 \(01\) 背包一样,可以去掉第二维,即:
\(f_{x,j} = max(f_{x,j},f_{x,k} + f_{y,j - k})\)
\(\Rightarrow\) 代码实现:
(1)输入的时候相当于输入最开始时候的 \(f_{i,1}\)。
(2)可以根据题意,把 \(0\) 作为根节点,但是处理的时候要注意:一共要取 \(m + 1\) 门功课,因为 \(0\) 必须的选。
(3)注意循环的顺序以及循环的端点。
即便三分钟热度,也足以穿透次元壁。
#include <iostream>
#include <cstdio>
#define N 305
using namespace std;
int n,m,f[N][N],siz[N],vis[N];
struct edge{
int next,to;
}edge[N];
int head[N],cnt;
void add(int from,int to){
edge[++cnt].next = head[from];
edge[cnt].to = to;
head[from] = cnt;
}
void Input(){
scanf("%d%d",&n,&m);
for(int i = 1,k;i <= n;i ++) {
scanf("%d%d",&k,&f[i][1]);
add(k,i);
}
}
void dfs(int x){
vis[x] = 1;
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(vis[y]) continue;
dfs(y);
for(int j = m + 1;j >= 1;j --){
for(int k = 1;k <= j;k ++){
f[x][j] = max(f[x][j],f[x][k] + f[y][j - k]);
}
}
}
}
void work(){
dfs(0);
printf("%d",f[0][m + 1]);
}
int main(){
Input();
work();
return 0;
}
二叉苹果树
\(\Rightarrow\) 分析:与上题基本类似,只不过有一个地方不太一样:上题相当于选结点,但是本题相当于选路径,所以在状态转移上有小的差别。
仍然,令 \(f_{x,i,j}\) 表示对于结点 \(x\),已经遍历了其前 \(i\) 个叶子结点,共选 \(j\) 个树枝可以获得的最多的苹果。
从三维的角度分析,我们令每个结点的叶子结点所有的编号依次递增,那么:
\(f_{x,i,j} = max(f_{x,i - 1,j},f_{x,i - 1,k} + f_{y,siz_y,j - k - 1} + w_{x,y})\)
还是可以像 \(01\) 背包一样,去掉第二维:
\(f_{x,j} = max(f_{x,j},f_{x,k} + f_{y,j - k - 1} + w_{x,y})\)
\(\Rightarrow\) 代码实现:注意循环顺序已经循环的端点即可。
崩塌的不止防御塔,还有你最后的倔强。
#include <iostream>
#include <cstdio>
#define N 105
using namespace std;
int n,Q,vis[N],f[N][N];
struct edge{
int next,to,dis;
}edge[N << 1];
int head[N],cnt;
void add(int from,int to,int dis){
edge[++cnt].next = head[from];
edge[cnt].to = to;
edge[cnt].dis = dis;
head[from] = cnt;
}
void Input(){
scanf("%d%d",&n,&Q);
for(int i = 1,u,v,w;i < n;i ++){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
}
void dfs(int x){
vis[x] = 1;
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(vis[y]) continue;
dfs(y);
for(int j = Q;j >= 1;j --){
for(int k = 0;k < j;k ++){
f[x][j] = max(f[x][j],f[x][k] + f[y][j - k - 1] + edge[i].dis);
}
}
}
}
void work(){
dfs(1);
printf("%d",f[1][Q]);
}
int main(){
Input();
work();
return 0;
}
换根DP
换根 \(DP\),就是根结点不确定的时候,有些属性可能会随着根结点的变化而变化(比如深度),这个时候就要引入换根 \(DP\) 来求解。还是以题为例:
模板题:STA-Station
\(\Rightarrow\) 分析:如果单纯暴力的话,就是将每一个结点作为根结点进行答案求解,最后记录答案,时间复杂度 \(O(n^2)\),肯定不行,考虑优化。
我们肯定是想知道两个结点分别作为根结点之间有什么关系。我们令 \(x\) 作为根结点的时候深度和为 \(f_x\),其一个儿子结点为 \(y\),深度和为 \(f_y\),子树大小为 \(siz_y\),那么如果根结点由 \(x\) 转变成 \(y\) 时,所有 \(y\) 的子树的结点的深度全部加 \(1\),其他结点的深度都减 \(1\),由此,状态转移就很容易出来了:
$ f_y = f_x + n - 2 \times siz_y $
\(\Rightarrow\) 代码实现:
可以先把 \(1\) 作为根结点预处理出来 \(siz_i\) 和 \(f_1\),然后进行状态转移即可。
注意在极端情况下 \(f\) 数组会爆 \(int\),注意开 \(long\) \(long\)。
一硫二硝三木炭,解构你轰轰烈烈的人生。
#include <iostream>
#include <cstdio>
#define N 1000006
using namespace std;
int n,res;
long long ans,dep[N],siz[N],f[N];
struct edge{
int next,to;
}edge[N << 1];
int head[N << 1],cnt;
void add(int from,int to){
edge[++cnt].next = head[from];
edge[cnt].to = to;
head[from] = cnt;
}
void dfs1(int x,int fa,int deep){
siz[x] = 1,dep[x] = deep,f[1] += dep[x];
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(y == fa) continue;
dfs1(y,x,deep + 1);
siz[x] += siz[y];
}
}
void Input(){
scanf("%d",&n);
for(int i = 1,u,v;i < n;i ++){
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
dfs1(1,0,1);
}
void dfs2(int x,int fa){
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(y == fa) continue;
f[y] = f[x] + n - 2 * siz[y];
dfs2(y,x);
}
}
void work(){
dfs2(1,0);
for(int i = 1;i <= n;i ++) if(f[i] > ans) ans = f[i],res = i;
printf("%d",res);
}
int main(){
Input();
work();
return 0;
}
Great Cow Gathering G
\(\Rightarrow\) 分析:此题和上题基本类似,只不过有了路径长度以及结点的权值。大体的思路不变,仍然令 \(x\) 作为根结点的时候不方便和为 \(f_x\),其一个儿子结点为 \(y\),不方便和为 \(f_y\),只不过这个时候 \(siz_y\) 代表这个子树下有多少头牛。那么当根节点由 \(x\) 转为 \(y\) 时,所有 \(y\) 的子树的结点的不方便值减去了 \(w_{x,y}\),其他的都加上了 \(w_{x,y}\),那么状态转移方程又出来了(注:\(res\) 为牛头的总数):
$ f_y = f_x + w_{x,y} \times (res - siz_y \times 2)$
\(\Rightarrow\) 代码实现:仍然预处理出 \(siz_i\) 和 \(f_1\),以及注意开 \(long\) \(long\)。
让本猫示范下,动口又动手的输出。
#include <iostream>
#include <cstdio>
#define N 100005
#define int long long
using namespace std;
int n,siz[N],c[N],dis[N],ans,f[N],res;
struct Edge{
int next,to,dis;
}edge[N << 1];
int head[N],cnt;
void add(int from,int to,int dis){
edge[++cnt] = (Edge){head[from],to,dis};
head[from] = cnt;
}
void dfs1(int x,int fa){
siz[x] = c[x];f[1] += dis[x] * c[x];
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(y == fa) continue;
dis[y] = dis[x] + edge[i].dis;
dfs1(y,x);
siz[x] += siz[y];
}
}
void Input(){
scanf("%lld",&n);
for(int i = 1;i <= n;i ++) scanf("%lld",&c[i]),res += c[i];
for(int i = 1,u,v,w;i < n;i ++){
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
dfs1(1,0);
}
void dfs2(int x,int fa){
for(int i = head[x];i;i = edge[i].next){
int y = edge[i].to;
if(y == fa) continue;
f[y] = f[x] + edge[i].dis * (res - 2 * siz[y]);
ans = min(ans,f[y]);
dfs2(y,x);
}
}
void work(){
ans = f[1];
dfs2(1,0);
printf("%lld",ans);
}
signed main(){
Input();
work();
return 0;
}