树形DP 换根DP 树形背包

posted on 2023-08-04 14:05:43 | under 笔记 | source

前言

夏令营某天,zsq:大家今晚复习树形 dp,明早测试。

我:啊?什么东西(马上搜索树形 dp)

部分内容借鉴了网络资源,以及曹文、李建等人所著的 《信息学奥赛一本通 提高篇》。

介绍

  • 概念

    顾名思义,就是在树上做 \(\operatorname{dp}\)

    显然,树中的父、子节点关系满足子问题关系。与线性 \(\operatorname{dp}\) 的顺推逆推类似,树形 \(\operatorname{dp}\) 也有两种转移方向。

    1. 子节点转移至父节点:将子节点的信息转移至父节点,如树形背包问题。

      在遍历子节点后,整理信息至父节点。

    2. 父节点转移至子节点:将父节点转移至子节点,应用于换根 \(\operatorname{dp}\) 中。

      先得出父节点的信息,再转移至子节点,往往需要考虑信息重复的问题。

  • 时空复杂度

    一定有第一维 \([u]\) 代表树上每个节点。

    第二维 \([k]\) 视情况而定,通常表示容积、最长链点数等,变化多端。

    除此之外,对于一些复杂问题往往会有多组 \(\operatorname{dp}\) 数组,以及树上常用数据(深度、子树大小等)。

    由于每个点只会被父节点访问一次、作为其它点的父节点一次,因此时间复杂度是 \(O(n)\) 的。如有第二维,则时间复杂度为 \(O(nk)\)

例题分析

在此之前,定义:\(v∈u\),当且仅当 \(v\)\(u\) 子节点。

  • 朴素树形 \(\operatorname{dp}\)

    最常见的树形 \(\operatorname{dp}\) ,即由子节点传至父节点。

  • 例题 P2016 战略游戏

    大意:给出一棵树,选择一些特殊点,每个特殊点可以覆盖相连之边。求覆盖所有边的最小特殊点数?

    思路:需要在树型结构上求最值,而且与每个点的取舍有关,考虑树形 \(\operatorname{dp}\)

    定义 \(dp[u][0/1]\) 代表:以 \(u\) 为根的子树中,\(u\) 是否为特殊点时( \(0\) 不放,\(1\) 放 ),覆盖子树所有边的最小花费。

    然后分类讨论:

    1. \(u\) 不为特殊点,则它的所有子节点必为特殊点。

    2. \(u\) 为特殊点,则它的子节点可为可不为特殊点,取花费最小的方案即可。

    整理得:

    \(dp[u][0]=\sum\limits_{v_i∈u}dp[v_i][1]\)

    \(dp[u][1]=\sum\limits_{v_i∈u}min(dp[v_i][0],dp[v_i][1])\)

    主要代码如下:

    void dfs(int u,int fa){
        dp[u][1]=1; //初始值一定要注意
        for(int i=head[u]; i;i=e[i].nxt){
            int v=e[i].v;
            if(v^fa){
                dfs(v,u);
            }
        }
        for(int i=head[u]; i;i=e[i].nxt){
            int v=e[i].v;
            if(v^fa){
                dp[u][1]+=min(dp[v][0],dp[v][1]);
                dp[u][0]+=dp[v][1];
            }
        }
    }
    
  • 树形背包

    树形 \(\operatorname{dp}\) 与背包问题的结合,即每次转移均做一次背包。

    也可视为依次合并子树,并更新合并后的值,也就是说 \(siz[u]\) 的值是动态变化的。

    仿照 \(\operatorname{01}\) 背包倒序枚举的写法,可忽略掉 \(\operatorname{dp}\) 数组关于时间轴的一维。

    转移方程如下:

    这样写要用滚动数组 \(a\)

    \(dp_{u,i+j}=\max\limits_{v∈u,i≤min(k,siz[u]),j≤min(k,siz[v]),i+j\le k}a_{u,i}+dp_{v,j}\)

    这样写要倒序枚举,时间有时会挂:

    \(dp_{u,i}=\max\limits_{v∈u,i≤min(k,siz[u]),j≤min(i,siz[v])}dp_{u,i-j}+dp_{v,j}\)

    没啦~

    时间复杂度的分析

    看似是 \(O(n^3)\) 的复杂度,但真是如此吗?

    首先,任意两点只会在其 \(\operatorname{LCA}\) 处合并,合并后信息传递至 \(\operatorname{LCA}\),故不再被合并,因此所有点合并次数为 \(n\)

    所以时间复杂度是 \(O(nk^2)\)?事实上是 \(O(nk)\)

    对每次合并进行讨论,不妨设两棵子树的大小分别为 \(x\)\(y\),显然合并两背包的时间复杂度为 \(O(xy)\)

    然后对 \(x\)\(y\)\(k\) 的大小关系分类讨论。

    1. \(x=k\)\(y=k\):合并复杂度为 \(O(k^2)\)。此时最坏情况下将有 \(\frac nk\) 棵 大小为 \(k\) 的子树,因此合并次数不超过 \(\frac nk\)。总复杂度为 \(O(k^2*\frac nk)=O(nk)\)

    2. \(x<k\)\(y<k\),合并后 \(x+y<k\):考虑贡献法,合并操作可视为 \(x\) 中的每个点产生 \(O(y)\) 的贡献,由于子树大小始终 \(<k\),因此每个点的贡献至多为 \(O(k)\),总复杂度便为 \(O(nk)\)

    3. \(x<k\)\(y<k\),合并后 \(x+y≥k\):每次合并时间复杂度为 \(O(k^2)\),结合 \(\operatorname{1}\) 的分析,最坏情况下这样的合并次数不超过 \(\frac nk\),因此总复杂度为 \(O(k^2*\frac nk)=O(nk)\)

    所以想写对时间的话,一定要卡好每次循环的上下界。

  • 例题 P4516 潜入行动

    普通树形背包题。

    定义 \(f_{u,i,0/1,0/1}\) 表示以 \(u\) 为根的子树中,安装了 \(i\) 个装置,点 \(u\) 是否安装装置、是否被覆盖,且除根外的节点均被覆盖时的方案数。

    状态转移时需注意如下要点,别写着写着就搞错了:

    • 对于子节点而言:子节点状态已经确定,也就是根节点的状态不能与其产生矛盾(例如:根节点安装装置的状态,不能由 子节点不被覆盖的状态 转移而来)。

    • 对于根节点而言:“是否安装装置”这一状态已经固定,但“是否被覆盖”这一状态是可以变的(例如:现在根节点被覆盖的方案数,可由 原先根节点未被覆盖的方案数 \(×\) 子节点安装装置的方案数 得来)。

    还有,这道题要用滚动数组进行正向枚举,常规的逆向枚举会 \(\rm {TLE}\)。这让我们了解到正逆两种枚举方式的不同(逆向枚举可能会枚举到一些无效状态,避免这一点也是可以过的)。

    要注意的就这几点,可自行推导后翻阅题解比对。

  • 换根 \(\operatorname{dp}\)

    在该题型中,需求出以不同节点为根时所要求的值。

    解决此类题型的套路是:先由叶到根求出以 \(u\) 为根的子树的某种值 \(g_u\),再由根到叶求出 \(u\) 作为整棵树的某种值 \(f_u\),一般通过容斥原理求 \(f_u\)(其实就是去掉重复值)。

    预处理后再做一次 \(\operatorname{dp}\),此时处理的是父节点转移至子节点。

    换根 \(\operatorname{dp}\) 思维量较大,掌握根之间的转换是解题的重要步骤。

  • 例题 P9437 一棵树

    大意:看题目,很清晰。

    思路:考虑换根 \(dp\)

    以下所提的 \(a[i]\) 为原题的定义。

    值得一提的是 \(e(u,u)\) 也算合法路径。

    定义以下数组:

    • \(g[u]\):原图中,以 \(u\) 为根的子树中的所有点到达 \(u\) 的路径值之和。

    • \(f[u]\):原图中,非以 \(u\) 为根的子树中的所有点到达 \(u\) 的路径值之和。

    • \(d[i]\):预处理数组,由于 \(a[i]\) 不只一位。表示 \(10\)\(a[i]\) 数位大小次方。

    • \(siz[u]\):预处理数组,原图中 \(u\) 的子树大小。

    到达 \(u\) 的路径值 \(P\),可理解为一个以 \(u\) 结尾的数(题意)。

    之所以这样定义,是为了简化转移操作。 如果存在 \(e(u,v)\),则路径值 \(P2=d[v]*P+a[v]\)

    转移方程的推导

    建议画图推导。

    • \(siz[u]=1+\sum\limits_{v_i∈u}siz[v]\),这是显然的。

    • \(g[u]\) 将在第一次遍历中得到,理解为在 \(g[v]\) 后面再接一个 \(a[u]\)

      \(g[u]=\sum\limits_{v_i∈u}g[v_i]*d[u]+siz[v_i]*a[u]\)

    • \(f[u]\) 将在第二次遍历中得到,也是较难理解的部分。

    \(fa\)\(u\) 的父节点,遍历 \(u\) 时已经得到 \(f[fa]\)

    为了方便处理,我们先得出除 $u$ 子树外的所有点到 $fa$ 的路径值之和 $S$,再由 $S$ 得到 $f[u]$。
    
    如图所示,$S$ 又可分为 $A$(蓝)、$B$(红) 两部分,主意 $fa$ 同时处于 $A$、$B$ 中:
    
    ![](https://cdn.luogu.com.cn/upload/image_hosting/nurn6h62.png?x-oss-process=image/resize,m_lfit,h_170,w_225)
    
    显然,$A=f[fa]$。
    
    而 $B$ 的含义是:$fa$ 子树中,除 $u$ 子树外的所有点至 $fa$ 的路径值之和,考虑大减小:
    * 红色部分:$g[fa]$
    * 黄色部分:$g[u]*d[fa]+siz[u]*a[fa]$,此处不是指黄点至 $u$ 的路径和,而是指黄点至 $fa$ 的路径和,因此要多算上 $a[fa]$。
    
    于是: 
    
    $B=g[fa]-(g[u]*d[fa]+siz[u]*a[fa])$
    
    $S=A-B==f[fa]-(g[fa]-(g[u]*d[fa]+siz[u]*a[fa]))$
    
    注意 $e(fa,u)$ 会被经过 $n-siz[u]$ 次,即树上除 $u$ 子树外的所有点。
    
    所以:
    
    $f[u]=d[u]*S+(n-siz[u])*a[u]$
    
    大功告成!
    

    代码的所有重要部分已被推导出。至于具体实现,太过复杂故不展示。

posted @ 2026-01-15 08:12  Zwi  阅读(4)  评论(0)    收藏  举报