树形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\)就行了,可以考虑枚举所有儿子,但是也有数学方法来优化,以下内容参考

题解 P2458保安站岗

对于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;
}

posted @ 2019-10-30 21:56  zbwer  阅读(152)  评论(0编辑  收藏  举报