树形dp基础
分类
选点
- 最大独立集:在树上选取一些点,使得点之间两两之间没有边相连,求能选取的最多点数或最大点权。
- 最小点覆盖:选取最少的点,使得树中的每条边都至少有一个端点被选中。可通过最大独立集的关系转化求解,因为树上的最大独立集与最小点覆盖互补。
选线
............
树上背包
............
树上选点问题
- 状态定义:根据问题的性质和求解目标,合理定义状态。一般为dp[i][0/1]表示选和不选,如最大独立集
- 状态转移:依据树的结构,通过子节点答案推导父节点答案,再根据题目要求一选和不选分类
- 遍历顺序:一般dfs遍历树,先计算子节点的状态,在根据子节点的状态向上更新,这样可以确保在计算每个节点的状态时,所需的子问题已求出。
P1122 最大子树和
状态
f[i]表示以i为根,点权和最大的一棵子树或者只有根
转移
遍历i的每一个子节点j,可以选或者不选,如果选答案就是f[i]+f[j],不选就是f[i],取较大值
答案,初始化
以1为根节点,答案就是f[1]
f[i]的初始值是a[i],因为以i为根必须选i,不然答案最后没有意义,题目也没有说可以全部截掉
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
vector<int>G[16010];
int n,a[16010],ans=-1e9;
int f[16010];
void dfs(int u,int fa){
f[u]=a[u];
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa)continue;
dfs(v,u);
f[u]=max(f[u],f[u]+f[v]);
}
ans=max(ans,f[u]);
return;
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
cout<<ans<<'\n';
return 0;
}
P1352 没有上司的舞会
状态
f[i][0/1]表示选取i和不选取i
转移
选i f[i][1]
初始化肯定是a[1],因为这个点已经选了,遍历子节点,对于每个子节点来说,因为自己已经选了,所以每个子节点只能加上不选的情况,即f[j][0],当然子节点肯能带来负贡献,所以要将f[j][0]+f[i][1]和f[i][1]取较大值。
不选i f[i][0]
初始化f[i][0]为0,因为没有选。
遍历子节点,因为自己没有选,所以子节点可以选也可以不选,也可以直接不选整个当前子树,将这几种情况全部取max
f[i][0]=max(f[i][0],f[i][0]+max(f[j][0],f[j][1]))
答案
因为不知道根节点选还是不选,所以直接去max
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
vector<int>G[16010];
int n,a[16010],ans=-1e9;
int f[16010][2];
void dfs(int u,int fa){
f[u][1]=a[u];
f[u][0]=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa)continue;
dfs(v,u);
f[u][0]=max(f[u][0],f[u][0]+max(f[v][0],f[v][1]));
f[u][1]=max(f[u][1],f[u][1]+f[v][0]);
}
return;
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,0);
cout<<max(f[1][0],f[1][1])<<'\n';
return 0;
}
战略游戏
状态
f[i][0/1]表示i这可子树满足条件时选i和不选i的最小放置数量。
转移
选i f[i][1]
因为选i,所以初始值f[i][1]为1,因为这个节点已经选了。因为i选了,所以子节点j可以选也可以不选,取最小值,然后把每棵子树的贡献加起来,就是i这一整个的贡献
不选i f[i][0]
没有选,所以f[i][0]初始为0,但是因为i没有选,所以子节点一定要选,加上f[j][1]
答案
不确定选子节点更优还是不选更优,所以在f[1][0]和f[1][1]只见取最小值
#include<bits/stdc++.h>
#define ll long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
int n,a[1000010];
int f[1000010][2];
vector<int >G[1000010];
void dfs(int u,int fa){
f[u][1]=1;
f[u][0]=0;
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa)continue;
dfs(v,u);
f[u][1]+=min(f[v][0],f[v][1]);
f[u][0]+=f[v][1];
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++){
int u,v,k;
cin>>u>>k;
u++;
while(k--){
cin>>v;
v++;
G[u].push_back(v);
G[v].push_back(u);
}
}
dfs(1,0);
cout<<min(f[1][0],f[1][1]);
return 0;
}
三色二叉树
建树
递归处理,用一个标记表示现在正在处理的节点编号,返回当前节点编号,如果是2就左右节点都递归,是1就只递归左子树,是0不管
状态
因为最大最小其实是一样的,所以用两个dp数组分别表示就好了。
f[i][0/1/2]分别表示i涂三种颜色的最优值
转移
对于f[i][0]它已经涂了这个颜色,所以它的贡献是1,对于f[i][1]和f[i][2]它们的初始值为0
转移f[i][0],它自己已经涂了0,所以子节点就只能涂1和2,要么左子树涂1,右子树涂2,要么左子树涂2,右子树涂1,两者之间取更优值
f[i][1]和f[i][2]的转移是一样的
答案和初始化
因为不知道最终答案是使根节点涂什么颜色更优,所以取最优值
求最大的初始为0,求最小值初始化为极大值
但是最小值的f[0][0/1/2]要初始化为0,因为左右子树没有赋值时是0,是极大值的话会影响最终答案
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
int n,tot;
string s;
int l[500010],r[500010];
int f1[500010][3];
int f2[500010][3];
int init(){
int u=++tot;
if(s[u]=='2'){
l[u]=init();
r[u]=init();
}
else if(s[u]=='1'){
l[u]=init();
}
return u;
}
void dfs(int u){
if(u==0)return;
f1[u][0]=f2[u][0]=1;
f1[u][1]=f2[u][1]=f1[u][2]=f2[u][2]=0;
int ll=l[u],rr=r[u];
dfs(ll),dfs(rr);
f1[u][0]+=max(f1[ll][1]+f1[rr][2],f1[ll][2]+f1[rr][1]);
f2[u][0]+=min(f2[ll][1]+f2[rr][2],f2[ll][2]+f2[rr][1]);
f1[u][1]+=max(f1[ll][0]+f1[rr][2],f1[ll][2]+f1[rr][0]);
f2[u][1]+=min(f2[ll][0]+f2[rr][2],f2[ll][2]+f2[rr][0]);
f1[u][2]+=max(f1[ll][0]+f1[rr][1],f1[ll][1]+f1[rr][0]);
f2[u][2]+=min(f2[ll][0]+f2[rr][1],f2[ll][1]+f2[rr][0]);
return;
}
signed main(){
ios::sync_with_stdio(false);
cin>>s;
n=s.size();
s=' '+s;
init();
// for(int i=1;i<=n;i++){
// cout<<i<<'\n';
// for(int j=0;j<G[i].size();j++)cout<<G[i][j]<<' ';
// cout<<'\n';
// }
memset(f2,63,sizeof f2);
f2[0][0]=f2[0][1]=f2[0][2]=0;
dfs(1);
cout<<max(f1[1][0],max(f1[1][1],f1[1][2]))<<' '<<min(f2[1][0],min(f2[1][1],f2[1][2]));
return 0;
}
Barn Painting G
状态
f[i][1/2/3]表示i涂三种颜色的方案数
转移
与上一题相同
当前节点涂1时,那么遍历子节点,只能涂2和3,方案数用乘法,f[i][1]*=(f[j][2]+f[j][3]),涂2,3一样
初始化时,如果这个节点题目已经给它着色,那么已着色的方案数颜色是1,其他的初始化为0,若没有着色说明可以任意涂色,所以全部初始化为1。
答案
如果根节点没有涂色,最终答案是根节点涂三种颜色的方案数之和,因为可以任意如色,不然根节点涂的什么颜色,最终答案就是根节点涂那种颜色的方案数
#include<bits/stdc++.h>
#define int long long
#define fore(i,a,b) for( int i=(a); i<=(b); ++i)
#define repe(i,a,b) for( int i=(a); i>=(b); --i)
using namespace std;
int n,k;
int f[100010][4];
int a[100010];
vector<int>G[100010];
const int mod=1e9+7;
void dfs(int u,int fa){
f[u][a[u]]=1;
if(a[u]==0){
f[u][1]=f[u][2]=f[u][3]=1;
}
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(v==fa)continue;
dfs(v, u);
f[u][1]*=(f[v][2]+f[v][3]);
f[u][2]*=(f[v][1]+f[v][3]);
f[u][3]*=(f[v][1]+f[v][2]);
f[u][1]%=mod;
f[u][2]%=mod;
f[u][3]%=mod;
}
}
signed main(){
ios::sync_with_stdio(false);
cin>>n>>k;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
for(int i=1;i<=k;i++){
int id,cl;
cin>>id>>cl;
a[id]=cl;
}
dfs(1,0);
// for(int i=1;i<=n;i++)cout<<f[i][0]<<' '<<f[i][1]<<' '<<f[i][2]<<' '<<f[i][3]<<'\n';
int ans=0;
ans=f[1][a[1]];
if(a[1]==0){
ans=f[1][1]+f[1][2]+f[1][3];
}
cout<<ans%mod<<'\n';
return 0;
}

浙公网安备 33010602011771号