树形DP学习笔记

Part 0 树形DP介绍

       树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。对于每个节点 X,先递归在它的每个子节点上进行 DP,在回溯时,从子节点向节点 X 进行状态转移。

Part 1 树的最大独立集

       树的独立集是指从树上选取一些点组成一个集合,使得集合内的点两两之间没有边相连。其中节点数量最多的集合就称为树的最大独立集。

例题 P1352 没有上司的舞会

       这道题是一道和树的最大独立集原理相同的经典题目。熟读完题目之后,我们可以作如下操作,令:

        · dp[u][0] :u 属于独立集(选u),以 u 为根的子树所能得到的最大的权值之和

        · dp[u][1] :u 不属于独立集(不选u),以 u 为根的子树所能得到的最大的权值之和

       根据题目和树的最大独立集的定义我们可以知道,当节点 u 被选中后,那么 u 的子节点 v 就不能选择,所以有关于 dp[u][0] 的转移方程如下:

         dp[u][0] = \sum_{v=son(u)}^{} dp[v][1] + a[u]

       而当节点 u 没有被选中时,并不简单地代表 u 的子节点 v 一定要选,因为有可能会出现 u 的父亲和 v 的儿子被选中的情况。因此子节点 v 选不选无所谓,最重要的是看 v 能否对题目的要求有所贡献,所以有关于 dp[u][1] 的转移方程如下:

        dp[u][1] = \sum_{v=son(u)}^{} max(dp[v][0],dp[v][1])

        以下给出关键代码:

void dfs(int u)
{
	dp[u][0]=a[u];//选u节点
    dp[u][1]=0;//不选u节点
    for(int i=0;i<son[u].size();i++)
    {
        int v=son[u][i];
        dfs(v);
        //因为u被选,所以子节点v不能选
        dp[u][0]+=dp[v][1];
        //如果u不选,就看子节点v哪一种情况下贡献最大 
        dp[u][1]+=max(dp[v][0],dp[v][1]);
    }
}

Part 2 树的最小点覆盖

       树的点覆盖集是指从树上选取一些点组成一个集合,使得树上的所有边都能够在该集合内找到至少一个点并与其相连。其中节点数量最少的集合就称为树的最小点覆盖。这里可能有点绕口,通俗地讲,点覆盖就是指某一节点 u 能够覆盖与 u 相连的所有边。

例题 POJ 1463 Strategic game

        这道题是一道和树的最小点覆盖有关的经典题目,它的转移方程和 Part 1 非常类似。我们令:

        · dp[u][0] :u 属于点覆盖集(选u),以 u 为根的子树中所连接的边都被覆盖的情况下,点覆盖集中所包含的最少的节点个数。

        · dp[u][1] :u 不属于点覆盖集(不选u),以 u 为根的子树中所连接的边都被覆盖的情况下,点覆盖集中所包含的最少的节点个数。

       根据题目和树的最小点覆盖的定义我们可以知道,当节点 u 被选中后,与 u 相连的边都被覆盖了,那么 u 的子节点 v 选不选就无所谓了,同样也是看 v 能否对题目的要求有所贡献,所以有关于 dp[u][0] 的转移方程如下:

        dp[u][0] = \sum_{v=son(u)}^{} min(dp[v][0],dp[v][1]) + 1

        而节点 u 要是没被选,那么它的子节点 v 肯定得选,不然 E(u,v) 无法被覆盖,所以有关于 dp[u][1] 的转移方程如下:

        dp[u][1] = \sum_{v=son(u)}^{} dp[v][0]

        以下给出关键代码:

void dfs(int u)
{
	dp[u][0]=1;//选u节点
    dp[u][1]=0;//不选u节点
    for(int i=0;i<son[u].size();i++)
    {
        int v=son[u][i];
        dfs(v);
        //因为u被选,就看子节点v哪一种情况下贡献最大
        dp[u][0]+=max(dp[v][0],dp[v][1]);
        //如果u不选,所以子节点v都得选
        dp[u][1]+=dp[v][0];
    }
}

Part 3 树的最小支配集

       树的支配集是指从树上选取一些点组成一个集合,使得树上其他未被选中的点都有边与集合内至少一个点相连。其中节点数量最少的集合就称为树的最小支配集。这里可能有点绕口,通俗地讲,支配就是指某一节点 u 能够支配与 u 相连的所有节点。

例题 P2899 [USACO08JAN]Cell Phone Network G

       这道题是一道和树的最小支配集有关的经典题目,但是它的转移方程会稍微复杂一点。我们令:

        · dp[u][0] :u 属于点支配集(选u),以 u 为根的子树中所有节点都被支配的情况下,点支配集中所包含的最少的节点个数。

        · dp[u][1] :u 不属于点支配集(不选u),u 被至少一个子节点 v 所支配(至少一个子节点是属于支配集的),以 u 为根的子树中所有节点都被支配的情况下,点支配集中所包含的最少的节点个数。

        · dp[u][2] :u 不属于点支配集(不选u),u 的子节点 v 也都不属于支配集(不选v),以 u 为根的子树中所有节点都被支配的情况下,点支配集中所包含的最少的节点个数。

       先从简单的入手,很容易知道,如果节点 u 被选中后,与 u 相连的子节点都被支配了,那么 u 的子节点 v 选不选就无所谓了,同样也是看 v 能否对题目的要求有所贡献,所以有关于 dp[u][0] 的转移方程如下:

        dp[u][0] = \sum_{v=son(u)}^{} min(dp[v][0],dp[v][1],dp[v][2]) + 1

        另一种情况,如果 u 和子节点 v 都不选的话,那么就必须得选 u 的父亲和 v 的儿子,而我们是以 u 为根的子树来表示 dp[u][2] 的,所以实际上 dp[u][2] 只与 dp[v][1] 有关,所以在求 dp[u][2] 时,我们必须保证 u 的所有子节点 v 都不是叶子节点(u 是叶子节点当且仅当 dp[u][1]=INF,INF表示正无穷),转移方程如下:

        dp[u][2]\neq INF,\forall v \in son(u) ,dp[v][1] \neq INF\rightarrow dp[u][2] = \sum_{v=son(u)}^{} dp[v][1]

        最后,对于 dp[u][1] 的情况会稍微复杂一点,由上述我们对 dp[u][1] 的表示可以知道,我们必须保证至少有一个 u 的子节点 v 被选中,而对于每一个子节点 v,它既可以自己支配自己,也可以由儿子节点来支配,所以我们不断枚举 dp[v][0] 和 dp[v][1],并定义一个变量 inc,初始化为正无穷,用来找最小差值,当 dp[v][1] < dp[v][0] 时,不断用 dp[v][0]-dp[v][1] 去更新 inc 的值,如果枚举完所有子节点,发现没有一个子节点选择自己支配自己的话,我们就得加上 inc,必须得有一个子节点来支配 u 。转移方程如下:

        dp[v][0]> dp[v][1]\rightarrow inc = min(inc,dp[v][0]-dp[v][1])

        dp[u][1] = \sum_{v=son(u)}^{} min(dp[v][0],dp[v][1]) + inc

        以下给出关键代码:

void dfs(int u,int fa)
{
	dp[u][0]=1;//选u
	dp[u][2]=0;//不选u,选u的孙子节点
	int sum=0,st=0,inc=INF;//初始化
	for(int i=0;i<son[u].size();i++)
	{
		int v=son[u][i];
		if(v==fa) continue;
		dfs(v,u);
        //第一种情况,选u,看子节点哪种情况下对答案的贡献最大
		dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
		if(dp[v][0]<=dp[v][1])
		{
            //子节点v被自己支配
			sum+=dp[v][0];
			st=1;
		}
		else{
            //子节点v被自己的儿子支配
			sum+=dp[v][1];
			inc=min(inc,dp[v][0]-dp[v][1]);
		}
        //保证u的所有子节点都不是叶子节点,dp[u][2]才有解
		if(dp[v][1]!=INF&&dp[u][2]!=INF) dp[u][2]+=dp[v][1];
		else dp[u][2]=INF;
	}
	if(inc==INF&&!st) dp[u][1]=INF;//当u为叶子节点时
	else{
		dp[u][1]=sum;
        //如果子节点都选择被自己的儿子支配,必须让其中一个子节点来支配u
		if(!st) dp[u][1]+=inc;
	}
}

Part 4 树的直径

        树上任意两节点之间最长的简单路径即为树的直径。树的直径可以跑两遍 DFS 来求,也可以用树形 DP 来求,而且树形 DP 可以在存在负权边的情况下求解出树的直径。

例题 SP1437 PT07Z - Longest path in a tree

        这是一道有关于树的直径的板子题。我们令:

        · d1[u] :在以 u 为根的子树里,从 u 出发所能延伸的最远距离

        · d2[u] :在以 u 为根的子树里,从 u 出发所能延伸的次远距离

        那么树的直径就是所有 d1+d2 的最大值。

        以下给出关键代码;

void dfs(int u,int fa)
{
	d1[u]=d2[u]=0;
	for(int i=0;i<son[u].size();i++)
	{
		int v=son[u][i];
		if(v==fa) continue;
		dfs(v,u);
		int t=d1[v]+1;
		if(t>d1[u])
		{
            //如果当前距离比u的最远延伸距离大
            //那就都给它更新掉
			d2[u]=d1[u];
			d1[u]=t;
		}
        //如果只比u的次远延伸距离大,那就只更新次远距离
		else if(t>d2[u]) d2[u]=t;
	}
	ans=max(ans,d1[u]+d2[u]);
}

Part 5 树上背包

        树上的背包问题,简单来说就是背包问题与树形 DP 的结合。它有一个明显的特点,就是节点之间有依赖性,即你要选择节点 u,那么你必须先选择 u 的父节点。

例题 P2014 [CTSC1997] 选课

        题目中,每一门课最多只有一门选修课与树上每个节点最多只有一个父节点类似。由此我们可以将各门课组成一个森林,然后再添加一个0号节点,使所有课程都有先修课,把森林转换成一棵有 n+1 个节点的树。我们令:

        · dp[u][x] :在以 u 为根的子树里,选 x 门课能获得的最高学分

        每当我们修完 u 这门课后(还剩x-1门课要修),对于 u 的每个子节点 v,我们可以在以 v 为根的子树里选修若干门课,使得所有课程加起来为 x-1。令 p 为 u 子节点的个数,转移方程如下:

        dp[u][x]=max ( \sum_{i=1}^{p} dp[v][ci]) + score[u],\sum_{i=1}^{p}ci=x-1

        这个转移和分组背包相类似,有 p 组物品,每组物品都有 x-1 个,其中第 i 组第 j 个物品体积为 j,价值为 dp[vi][j],从每组中选出不超过1个的物品(对于每个子节点 v 只有一个状态可以转移到 u),使得物品体积不超过 x-1 的前提下,价值总和最大。我们可以在合并子树时枚举 ci 和 x。

        以下给出关键代码:

void dfs(int u)
{
	dp[u][1]=a[u];
	for(int i=0;i<son[u].size();i++)//组
	{
		int v=son[u][i];
		dfs(v);
		for(int j=m+1;j>=1;j--)//算上0号节点,倒序枚举组内每个物品
			for(int k=1;k<j;k++)
                //给v分配k个,u保留j-k个
                //而dp[u][j-k]则是之前就计算出来的
				dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
	}
}

Part 5.1 上下界优化

        上述代码的时间复杂度是 O(nm^2),如果要是遇上一些比较毒瘤的数据,准T飞了,这里介绍一种简单好懂的优化——上下界优化,它能够将时间复杂度降低至 O(nm)。

例题 U53204 【数据加强版】选课

        我们仔细观察上述代码里背包转移这部分,其中有许多没必要的遍历。以下我们令数组 siz[i]  表示当前合并到节点 i 时,子树的大小。当我们在第一层循环以 u 为根的子树所能选取的课程数时,因为我们是从最左边的子树一路合并到最右边的子树,所以那些大于已经合并好的节点数 siz[u] 与正准备合并的子树的节点数 siz[v] 之和的枚举都没意义;而对于第二层循环枚举以子节点 v 为根的子树所能选取的课程数时,顶天只能选 siz[v] 门,多了也没用。

        以下给出关键代码:

void dfs(int u)
{
	siz[u]=1;
	dp[u*(m+2)+1]=a[u];//把二维压成一维来写,不然按照题目的数据会爆空间
	for(int i=0;i<son[u].size();i++)
	{
		int v=son[u][i];
		dfs(v);
        //优化枚举j时的上下界
		for(int j=min(m+1,siz[u]+siz[v]);j>=1;j--)
            //优化枚举k时的上下界
			for(int k=max(1,j-siz[u]);k<=siz[v]&&k<j;k++)
                //dp[u*(m+2)+j-k]上一个i循环时就求出来了
				dp[u*(m+2)+j]=max(dp[u*(m+2)+j],dp[u*(m+2)+j-k]+dp[v*(m+2)+k]);
		siz[u]+=siz[v];
	}
}

Part 5.2 其他优化

        将树上背包优化成 O(nm) 的方法还有许多种,例如DFS序优化、前序遍历优化等等,上下界优化也不是一瓶万金油,对于不同情形的题目,优化方式不一样,这儿先占个坑,等我学明白了再来qwq。

Part 6 二次扫描与换根法

        大部分树形DP问题都是有根的,当然也有一些题目是不定根的,这类题目的特点是:给定一个树形结构,需要以每个节点为根进行一系列统计。这类题目一般利用二次扫描与换根法解决。

例题 P3478 [POI2008] STA-Station

        这是一道二次扫描与换根法的板子题,题目让我们求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。算法具体执行操作如下:

        1、第一次扫描,任选一个节点作为根,执行一次树形DP,对于每个节点 u,统计 u 的深度和以 u 为根的子树大小。

        2、第二次扫描,还是从第一次扫描时选择的根节点出发,跑一遍 DFS,在每次递归前进行自顶向下的推导,计算“换根”后的解。

        3、怎么“换根”呢?如下图所示,原本是以1号节点为根,现在换根换成以2号节点为根,我们容易发现以2号节点为根的子树的深度都+1了,而图中蓝色线条框起来的节点的深度都-1了。令数组 f[i] 为以节点 i 为根的时候的答案,所以推导公式为:f[v]=f[u]+n-2*siz[v]。

 

         以下给出关键代码:

oid dfs1(int u,int fa)
{
	siz[u]=1;
	dep[u]=dep[fa]+1;
	for(int i=0;i<son[u].size();i++)
	{
		int v=son[u][i];
		if(v==fa) continue;
		dfs1(v,u);
		siz[u]+=siz[v];
	}
}
void dfs2(int u,int fa)
{
	for(int i=0;i<son[u].size();i++)
	{
		int v=son[u][i];
		if(v==fa) continue;
		f[v]=f[u]+n-2*siz[v];
		dfs2(v,u);
	}
}
/*
输入略去
*/
    dfs1(1,0);//第一次扫描
    for(int i=1;i<=n;i++) f[1]+=dep[i];
    dfs2(1,0);//第二次扫描

后记

        以上只是博主在学习树形DP的过程中总结出来的一些简单模型,建议还是多刷点树形DP的相关题目,才能真正掌握住这个知识点。文章里仍有许多不足之处,请读者指出,帮助作者改进学习。

参考文献

树形dp学习笔记

树上背包的上下界优化

子树合并背包类型的dp的复杂度证明

《算法竞赛进阶指南》李煜东

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值