树形dp摸瞎历程
树形\(dp\)摸瞎历程
前言:
- 什么是树形\(dp\)?
简而言之,树形dp,就是在树形结构上的动态规划,由于树形结构具有一定的特点,可以描述比较复杂的关系,再加上树的递归定义,是一种非常合适动规的框架,属于动规中很特殊的一种类型。
-
如何实现树形\(dp\)?
树形dp的状态表示中,第一位通常是节点编号(代表以该节点为根的子树),大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点x,我们先递归x的所有子节点,并在其子节点上dp,在回溯时,从子节点向节点x进行状态转移。
-
树形\(dp\)的分类
- 选择节点类:给定一棵树,每个节点都有一定的值,让你在树上选择任意数量的点,但要满足一定的父子关系(比如,\(fa\)和\(son\)要共存,或者不能共存),求最大或最小的贡献。
常用dp套路:
定义状态:f[i][0/1]表示在以i号节点为根的子树中,i号节点是(1)否(1)选择所能造成的最大或最小贡献.
- 分组背包类:给定一棵树,节点或者树边有一定的值,让你在树上选择有限数量的点或边,求最大或最小贡献。
定义状态:f[i][j]表示在以i号节点为根的子树中,选择j个节点或边所得的最大值或最小值
- 以上两种是比较常见的,但树形dp远远不止上述两类,后文会提到一些其他类型的。
-
递归实现树形dp的方法
- 对于无根树,即题目强制要求无向边,可用如下模板递归:
inline void dfs(int x,int fa)//fa是x的父亲节点 { for(int i=head[x];i;i=Next[i])//邻接表常规便利 { int y=ver[i]; if(y==fa) continue; dfs(y,x); /*dp具体内容*/ } }
-
对于有根树,即从题面中我们很容易可以看出是一张有向图(如“没有上司的舞会”),对于这种情况我们可以加两次边,把它当做无根树用上述模板来处理,或者在原有向图中找出一个根,再以此递归。
inline void dfs(int x)
{
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
dfs(y);
//dp
}
}//找根可以用其入度为零这一特点来找,这里不再介绍。
例题选讲:
注:后文代码仅给出核心部分,涉及到的基础函数将在这里给出
邻接表加边函数:
inline void add(int x,int y,int z)
{
ver[++tot]=y;
Next[tot]=head[x];
head[x]=tot;
edge[tot]=z;
}
快读函数:
inline int read()
{
int num=0,w=1;char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') w=-1;ch=getchar();}
while(ch<='9' && ch>='0')
num=(num<<1)+(num<<3)+ch-'0',ch=getchar();
return num*w;
}
宏定义:
#define Mi return
#define manchi 0
后文除第一个例题外,都只叙述无向图的解法。
选择节点类:
没有上司的舞会
很容易看出这是个选择节点类的树形dp,因为题目对于选择职员的数量并没有限制。
那么我们依据套路定义dp状态:
定义状态:
f[i][1]表示在以i号节点为根的子树中,i号职员来参加舞会所得的最大快乐指数
f[i][0]则表示i号职员不来参加所得的最大快乐指数
接下来我们明确父子关系:如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了,即父亲与儿子不能共存。
然后以此进行状态转移:
状态转移:
f[i][1]+=max(f[son][0])//由于i去了,所以i的儿子就不能去了,即f[son][0]
f[i][0]+=max(f[son][1],f[son][0])//i没有去,那么i的儿子可去可不去
考虑边界:
f[i][1]=happy[i]//初始状态去参加舞会的快乐指数肯定等于自身的快乐指数
然后套上dfs就能A掉了,\(QwQ\).
Code(有向图):
const int N = 6000+5;
int n,happy[N],root;
int head[N],ver[N<<1],tot,Next[N<<1];
int f[N][3];bool vis[N];//vis数组用于找根
//f[i][0]以i为根,i不去;f[i][1]以i为根,i去
//f[i][0]+=max(f[j][0],f[j][1]),j是i的儿子
//f[i][1]+=max(f[j][0])
inline void slove(int x)
{
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
slove(y);
//dp
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
f[i][1]=read();//直接读入的时候初始化
for(int i=1;i<=n-1;i++)
{
int x=read(),y=read();
vis[x]=1;add(y,x);
}int a=read(),b=read();
//找根
for(int i=1;i<=n;i++)
if(!vis[i]){root=i;break;}
slove(root);
cout<<max(f[root][1],f[root][0]);//输出答案
return 0;
}
Code(无向图):
inline 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);
f[x][0]+=max(f[y][0],f[y][1]);
f[x][1]+=f[y][0];
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
f[i][1]=read();
for(int i=1;i<=n-1;i++)
{
int x=read(),y=read();
add(y,x);add(x,y);//注意加两次边,转为无向图
}int a=read(),b=read();
dfs(1,0);//任选一个点作为根进行dp,这里选择1作为根.
cout<<max(f[1][1],f[1][0]);
return 0;
}
最大子树和
由于对于选择的花数量没有限制,这显然是一道节点选择类的树形dp,再确定父子关系:父亲不选择,儿子就无法选择(这点显然),那就直接走套路就行了
const int N = 16000+5;
/*
定义状态:
f[i][1]表示在以i号节点为根的子树中,选择第i号花所得最大美丽指数
f[i][0]则表示不选择i号花的最大美丽指数
状态转移:
f[i][1]+=max(f[son][0],f[son][1])
f[i][0]=0
边界:
f[i][1]=a[i]
*/
int n,f[N][2],root,a[N],ans=-0x7fffffff;
int ver[N<<1],head[N<<1],Next[N<<1],tot;
inline 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);
f[x][0]=0;
f[x][1]+=max(f[y][1],f[y][0]);
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++) f[i][1]=read();
for(int i=1;i<=n-1;i++)
{
int x=read(),y=read();
add(y,x),add(x,y);
}
dfs(1,0);
for(int i=1;i<=n;i++)
ans=max(ans,f[i][1]);
printf("%d",ans);
Mi manchi;
}
战略游戏
显然是一道节点选择类的树形dp,确定父子关系:父亲和儿子中有一个存在即可(只要存在一个,那么父亲与儿子之间的这条边就可以被覆盖)。
都是套路\(QwQ\)
const int N = 1500+5;
/*
定义状态:
f[i][0]表示以i为根的子树,i结点不放士兵的最小值
f[i][1] 放士兵的最小值
状态转移:
f[i][0]+=f[son][1]//父亲不选择的话,儿子就一定要选择才符合题意
f[i][1]+=min(f[son][1],f[son][0]);//父亲选择了的话,那么儿子选不选都可以.
边界:
f[i][1]=1
ans=min(f[i][1],f[i][0])
*/
int n,f[N][2];
int tot,head[N],ver[N],Next[N];
inline void dfs(int x,int fa)
{
f[x][1]=1;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa) continue;
dfs(y,x);
f[x][1]+=min(f[y][1],f[y][0]);
f[x][0]+=f[y][1];
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),k=read();
for(int j=1;j<=k;j++)
{
int y=read();
add(x,y);add(y,x);
}
}
dfs(1,-1);//注意是从节点0开始编号的,所以我们选的root的fa不能为0
printf("%d",min(f[1][0],f[1][1]));
Mi manchi;
}
上面都是挺水的题目,接下来看一道有点思维难度的题目。
保安站岗
我们发现这题和战略游戏那题很像,但是战略游戏关注的是边,而这题关注的是点有没有被覆盖,所以我们就要重新确定父子关系,并重新定义状态。
父子关系:
自己(i),父亲(fa),儿子(son)中必须要存在一个
定义状态:
f[i][0]表示i被自己覆盖 的最小花费
f[i][1]表示i被儿子覆盖 的最小花费
f[i][2]表示i被父亲覆盖 的最小花费
状态转移:
1.f[i][0]+=min(f[son][1],f[son][2],f[son][0])
2.f[i][1]=f[x][0]+sigma(min (f[son][0],f[son][1]) )
3.f[i][2]+=min(f[son][0],f[son][1])
我们分析一下上面状态转移的过程:
1.此时情况是节点i处放置警察,那么它的儿子可以选择被自己看守(f[son][0]),也可以选择被儿子看守(f[son][1]),当然也可以选择被父亲看守(f[son][2])
转移2有点小难度,我们先分析转移3
3.此时的情况是节点i被父亲覆盖(即i节点没有放置警察),那i的儿子(son)就只能选择被自己看守(f[son][0]),或者被它的儿子的看守(f[son][1])
现在讨论转移2:
此时的情况是节点\(i\)处不放置警察,节点\(i\)由它的儿子看守,那么我们显然要在\(i\)的众多儿子中,选择一个儿子(对应上面转移方程中的\(x\))放置警察,这样当前节点\(i\)才会被看守到,然后对于剩余的儿子,我们进行和转移方程3一样的操作就可以了,因为此时节点\(i\)没有放置警察,那\(i\)的儿子就只能选择被自己看守,或者被它的儿子的看守。
那么我们只需要找到对于节点\(i\)来说,一个最优的儿子\(x\)就行了,可以考虑枚举所有儿子,但是也有数学方法来优化,以下内容参考
对于x来说,有\(f[i][1]=f[x][0]+\Sigma_{j\subset son(i),j!=x}{min(f[j][0],f[j][1])}\)
若x不是最优的,则存在y满足\(f[x][0]+\Sigma_{j\subset son(i),j!=x}{min(f[j][0],f[j][1])}<f[y][0]+\Sigma_{j\subset son(i),j!=y}{min(f[j][0],f[j][1])}\)
合并同类项,整理得\(f[x][0]-min(f[x][0],f[x][1])>f[y][0]-min(f[y][0],f[y][1])\)
所以对于最优的x,只需要满足\(f[x][0]-min(f[x][0],f[x][1])\)是所有儿子中最小的就可以了。
我们不妨设最优的\(x\)一开始为0,然后对\(f[0][0]\)赋一个极大值来转移
const int N = 1500+5;
int n,f[N<<1][3];
int tot,head[N],ver[N<<1],Next[N<<1];
inline void dfs(int x,int fa)
{
int special_son=0;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa) continue;
dfs(y,x);
f[x][0]+=min(f[y][0],min(f[y][1],f[y][2]));
f[x][2]+=min(f[y][0],f[y][1]);
//找最优的x
if((f[special_son][0]-min(f[special_son][0],f[special_son][1])) > (f[y][0]-min(f[y][0],f[y][1])))
special_son=y;
}
//找到x之后,我们还需要求出simga(min (f[j][0],f[j][1]) )
f[x][1]=f[special_son][0];
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa || y==special_son) continue;
f[x][1]+=min(f[y][0],f[y][1]);
}
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
int x=read(),val=read(),k=read();
f[x][0]=val;//初始化
for(int j=1;j<=k;j++)
{
int y=read();
add(x,y);add(y,x);
}
}
f[0][0]=0x3f3f3f3f;dfs(1,-1);//赋初值
printf("%d",min(f[1][0],f[1][1]));
Mi manchi;
}
通过上一道例题我们可以发现,节点选择类的树形dp,其状态的第二维,就不只局限于是否选择当前这个节点,而是针对题意拓展为当前这个点的状态是什么,就如上一道例题,状态的第二维表示的是节点\(i\)的具体被看守情况。我们再来看一道例题。
三色二叉树
这道题目相对简单,可以说是节点选择类dp的进阶练手题,此时我们如果还用\(f[i][0/1]\)来表示i节点是否染成绿色的话,显然是无法解决问题的,所以我们要考虑拓展第二维,这拓展是很好想的。由于最大值和最小值得求法本质是类似的,故下面只叙述最大值的做法
定义状态:
f[i][0/1/2]表示在以i为根的子树中,染色绿色的最多个数
且i的颜色为 0/1/2 (绿/红/蓝)
接下来我们确定父子关系,然后依据具体情况转移就行了。
明确父子关系:父亲和儿子状态不能相同,儿子和儿子之间状态也不能相同
状态转移:
1.i为绿色,那么左右儿子只能为红或蓝
f[i][0]=max(f[lson][1]+f[rson][2],f[lson][2]+f[ron][1])+1
2.i为红/蓝
f[i][1]=max(f[lson][0]+f[rson][2],f[lson][2]+f[rson][0]);
f[i][2]=max(f[lson][1]+f[rson][0],f[lson][0]+f[rson][1]);
初始f[i][0]=1,f[i][1/2]=0;
ans=max(f[1][0/1/2])
本题对于树的结构是有严格限制的,即只能有一个儿子或者两个儿子,那么我们就可以用数组来模拟这棵树,而非用领接表加边建树。
using namespace std;
const int N = 500000+5;
int f[N][3],g[N][3];
int tot,tree[N][2];
//数组模拟这棵树,tree[i][0/1]表示以i为根的树中,左/右儿子的编号
char TREE[N];
inline void build(int root)//递归建树
{
tot++;
if(TREE[root]=='0') return;
else if(TREE[root]=='1') {tree[root][0]=tot+1;build(tot+1);return;}
else if(TREE[root]=='2') {tree[root][0]=tot+1;build(tot+1);tree[root][1]=tot+1;build(tot+1);return;}
}
inline void dfs(int fa)
{
f[fa][0]=g[fa][0]=1;
if(tree[fa][0] && tree[fa][1])//左右儿子都有
{
int l=tree[fa][0],r=tree[fa][1];
dfs(l);dfs(r);
f[fa][0]=max(f[l][1]+f[r][2],f[l][2]+f[r][1])+1;
f[fa][1]=max(f[l][0]+f[r][2],f[l][2]+f[r][0]);
f[fa][2]=max(f[l][1]+f[r][0],f[l][0]+f[r][1]);
g[fa][0]=min(g[l][1]+g[r][2],g[l][2]+g[r][1])+1;
g[fa][1]=min(g[l][0]+g[r][2],g[l][2]+g[r][0]);
g[fa][2]=min(g[l][1]+g[r][0],g[l][0]+g[r][1]);
}
else if(tree[fa][0] && !tree[fa][1])
{
int son=tree[fa][0];
dfs(son);
f[fa][0]=max(f[son][1],f[son][2])+1;
f[fa][1]=max(f[son][0],f[son][2]);
f[fa][2]=max(f[son][0],f[son][1]);
g[fa][0]=min(g[son][1],g[son][2])+1;
g[fa][1]=min(g[son][0],g[son][2]);
g[fa][2]=min(g[son][0],g[son][1]);
}
}
int main()
{
scanf("%s",TREE+1);build(1);
dfs(1);
printf("%d ",max(f[1][0],max(f[1][1],f[1][2])));
printf("%d",min(g[1][0],min(g[1][1],g[1][2])));
Mi manchi;
}
叶子的染色
不是很人性的题意翻译:给定一棵树,要求在树中选择节点染成黑色或白色,使得从根节点到叶子结点的简单路径上至少包含一个有色节点,同时给出限制\(c[i]\),要求在从根节点到\(i\)号叶子节点的简单路径中,最后一个有色节点的颜色为\(c[i]\),求需要选择着色的最少节点数。
本质上仍然是节点选择类的树形dp,其状态与上一道例题很相似,这里先给出整体思路。
/*
定义状态:
f[i][0/1/2]表示在以i为根的子树中,i的颜色为(黑色/白色/不染色)所需染色的最小值
确定父子关系:
保证根结点到每个叶子的简单路径上都至少包含一个有色结点
状态转移:
贪心地去想,保证简单路径上都包含一个有色节点,显然是将这个有色节点放得越高越好,
即让经过这个有色节点的简单路径数增多.
考虑c[i]这个限制条件,我们只要固定第i个节点的颜色就可以了
1.如果i节点染成黑色,那我们的子节点就不用染成黑色。
由于是最少的着色节点,所以我们父亲的值显然为sigma(son),所以用f[i][]+=
f[i][0]+=min(f[son][0]-1,f[son][1],f[son][2])
2.如果i节点染成白色
f[i][1]+=min(f[son][0],f[son][1]-1,f[son][2])
3.如果当前节点不染色
不染色的实质是因为无法确定当前节点要染黑色还是白色,还要看fa的情况
f[i][2]+=min(f[son][0/1/2])
边界:f[i][0]=f[i][1]=f[i][2]=1; f[i][2]的实质是染了一个不确定的颜色,所以其初始值也为1
f[i][!(c[i])]=INF;即i号节点绝对不能染成与c[i]相反的颜色,这点很显然
答案:ans=min(f[i][0/1/2])
*/
贪心策略:
若某结点的儿子结点中需要染黑色的比白色多,就将此节点标记为要染黑色,把要染白色的儿子染成白色;
例如节点\(i\)有三个儿子,其中两个儿子需要染成黑色才符合条件,另一个则需要染成白色,那么我们显然应该把节点\(i\)染成黑色,这样它的两个儿子就不需要染色了,只需要将另一个儿子染成白色即可。
儿子节点中需要染成白色的比黑色的多,同理。
那还有一种情况就是要染成黑色和白色的个数一样多,那么我们这时就考虑先不染色,看看\(i\)的兄弟的情况再具体染色,例如节点\(i,j\)是兄弟,其父亲为\(fa\),\(i\)节点暂时没有染色(可以染成任意颜色),\(j\)节点需要染成黑色,那么我们显然应该把\(i\)节点看成黑色,此时\(fa\)就需要染成黑色,\(i,j\)就不用染色,这样显然是最优的。
以上策略参考(建议阅读):
const int N = 100500;
const int INF = 0x3f3f3f3f;
int n,m,tot,c[N],f[N][3];
int ver[N<<1],head[N],Next[N<<1];
inline void dfs(int x,int fa)
{
if(x<=m) return ;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(y==fa) continue;
dfs(y,x);
f[x][0]+=min(f[y][0]-1,min(f[y][2],f[y][1]));
f[x][1]+=min(f[y][0],min(f[y][2],f[y][1]-1));
f[x][2]+=min(f[y][0],min(f[y][1],f[y][2]));
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++) c[i]=read();
for(int x,y,i=1;i<n;i++)
{
x=read(),y=read();
add(x,y);add(y,x);
}
for(int i=1;i<=n;i++)
{
f[i][0]=f[i][1]=f[i][2]=1;
if(i<=m) f[i][!(c[i])]=INF;
}
dfs(m+1,-1);//以一个不是叶子节点的根 dfs
printf("%d",min(f[m+1][1],min(f[m+1][0],f[m+1][2])));
Mi manchi;
}