首页 > 其他分享 >[学习笔记]换根 DP 的常用处理方式

[学习笔记]换根 DP 的常用处理方式

时间:2024-02-15 22:45:45浏览次数:26  
标签:sz 回滚 笔记 节点 DP 移除 换根 dp

[学习笔记]换根 DP 的常用处理方式

换根 DP,又称作二次扫描法,通常用于“对每个根求出树形 DP 的答案”。以每个点作为根节点进行一次树形 DP 的代价通常无法承受,因此我们会使用两次 DFS:

  • 第一次 DFS 指定一个点为根节点,运行一次常规的树形 DP。
  • 第二遍 DFS 进行换根 DP,得到将根转移到每个节点的答案。

换根 DP 常用的例题是一些具有特别良好性质的问题,例如求所有节点的深度总和。当根节点从 \(u\) 转移到 \(v\) 时,所有在 \(v\) 子树中的节点(以 \(0\) 为根/以 \(u\) 为根)深度减一,而其余节点深度加一。在第一遍 DFS 中处理出子树大小,在第二遍 DFS 中即可直接用 \(ans_v = ans_u + n - sz_v - sz_v = ans_u + n - 2 sz_v\) 更新答案,甚至没有用到第一遍树形 DP 的“子树节点深度和”信息。

但对于一般的问题,我们并不总能如此简单地更新答案。例如求最深节点的深度,不另外使用数据结构,无法快速地完成子树深度加减、全局深度最大值这两个操作。

算法步骤

一般地,在第二次 DFS 搜索到 \(u\) 时,首先更新以 \(u\) 为根的答案;之后枚举 \(u\) 的子节点 \(v\),要将根从 \(u\) 转移到 \(v\),我们分为以下几步:

  1. 将 \(v\) 的贡献从 \(dp_u\) 移除;
  2. 把 \(u\) 作为 \(v\) 的子节点,将贡献加入到 \(dp_v\)。
  3. 以 \(v\) 为新的根节点继续进行第二次扫描。
  4. 回滚 \(dp_u\) 的状态。

注意第三步结束后,我们需要把 \(dp_u\) 的状态恢复到第一步之前,因为还要对其它的儿子重复这一个过程。而在 \(v\) 的(以 \(0\) 为根/以 \(u\) 为根)子树上的节点均已完成所有计算,它们的状态并不重要。说到回滚,就自然有多种方式。最简单地,若 Copy 的代价可以承担(例如要维护状态所占空间为 \(O(1)\) 时),我们可以每次为 \(dp_u\) 创建一个新的副本,在其上执行前两步操作。

让我们回到所有节点深度总和的问题,有 \(dp_u = 1 +\sum_{v \in sons(u)} (sz_v + dp_v), sz_u = 1 +\sum_{v \in sons(u)} sz_v\)。换根时我们依次执行上面的步骤,并使用“创建副本”的方法而避免回滚。

  1. 创建新的副本,在其中去掉 \(v\) 的贡献:
    • \(dp_u' = dp_u - sz_v - dp_v, sz_u' = sz_u - sz_v = n - sz_v\)。
  2. 将 \(u\) 作为 \(v\) 的子节点,将贡献加入到 \(dp_v\):
    • \(dp_v \larr dp_v + sz_u' + dp_u' = dp_u + n - 2sz_v, sz_v \larr sz_v + sz_u' = n\)。

虽然推导步骤不同,我们得到了相同的式子。

但复制的代价并不总是能够承担。例如有的时候我们被迫(或为了方便)需要使用一个数据结构(如 STL set/ multiset)维护所有子树的信息以进行 DP 转移,此时就需要采用另外的回滚方法。例如:

  • 可以记录所有操作,并逐个进行逆操作。它可以是各种层面上的逆操作。
    • 如果所有宏观上的操作都是可逆的,可以倒序执行一遍所有操作的逆操作。例如 multisetinsertextract;以及更高层面的,题目中具体使用的操作。
      • 甚至更暴力地,对每一次内存写入,记录位置以及写入前的值;回滚时逆序撤销。
    • 尽管 \(dp_v\) 已经不再需要用到,但出于正确回滚 \(dp_u\) 的需要,\(dp_v\) 很可能会被一并回滚。
  • 甚至可以采用可持久化等方式。

例如 CF1796E 题,为了方便使用 multiset 维护最小值、次小值。建立宏观上互为逆操作的的添加/删除贡献的函数 adddel,以恰当的顺序和参数调用,以确保贡献的加入/移除、状态的回滚都能够正确完成。参考 这份题解 的相关代码:

void add(int u, int val) {
    if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
    f[u].insert(val);
    if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}

void del(int u, int val) {
    if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
    f[u].erase(f[u].find(val));
    if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}

void dfs2(int u, int fa) {
    ans = max(ans, min(getlen(u), se.empty() ? INF : *se.begin()));
    for (auto v : G[u]) {
        if (v == fa) continue;
        del(u, getlen(v));
        add(v, getlen(u));
        dfs2(v, u);
        del(v, getlen(u));
        add(u, getlen(v));
    }
}

有关贡献的移除

尽管我们可能需要回滚第二步“把 \(u\) 的贡献添加到 \(dp_v\)”,它在效果上等价于移除 \(u\) 对 \(dp_v\) 的贡献,一般情况下贡献的移除不像回滚这么简单。这是因为我们等价于要计算如下问题:

  • 对 \(u\) 的每个儿子 \(v\),计算 \(fa_u\)(若存在)以及 \(u\) 除 \(v\) 外的所有儿子的贡献的聚合。

对加减乘除等有结合律且存在逆元的聚合操作,移除贡献特别容易。但一般情形下,移除一个特定节点的贡献比回滚最后一次操作要难得多。

回到最深节点深度问题,有 \(dp_u = \max\limits_{v \in sons(u)} \{1 +dp_v\}\)。第一眼看上去 \(\max\) 操作是不可逆的,因为它始终只保存当前最大的贡献,会丢失信息。但注意到所谓的“移除”永远只会涉及到一个节点,这是个很重要的性质:我们只要在更新 \(dp_u\) 的同时记录次大值 \(sdp_u\),按照 \(dp_v\) 是否是最大值选择 \(dp_u'\)。按照上面的步骤,算法如下:

  • 若 \(dp_u = dp_v + 1\),\(dp_u' = sdp_u\);否则 \(dp_u' = dp_u\)。
  • 根据 \(dp_u' + 1\) 更新 \(dp_v\) 与 \(sdp_v\)。

同样我们使用复制副本以避免回滚。类似的,我们可以 \(O(1)\) 处理最小值、次小值等贡献的移除。

对更特殊的聚合操作,如 \(\gcd\)、\({\rm lcm}\)、对非质数的模乘法,这个性质也帮不上大忙。所幸我们有熟悉的小技巧:前后缀分解(我们总认为聚合操作是满足交换律的。若不满足,也不代表一定不能使用这一技巧),在状态的大小、聚合的均为 \(O(1)\) 时,总能保证换根 DP 的总时间复杂度为 \(O(n)\)。

标签:sz,回滚,笔记,节点,DP,移除,换根,dp
From: https://www.cnblogs.com/cpchenpi/p/-/Rerooting

相关文章

  • DP 做题记录
    复杂DP各种巨大DP。CF123CBrackets题意:括号数组是一个只有“(”或“)”两类字符的二维数组。括号数组中的合法路径只能从任意位置开始,向右或向下移动。如果一个n×m括号数组中从(1,1)到(n,m)的所有路径经过的字符构成的字符串均为可以完全匹配的括号序列,则这个括号......
  • DP 优化
    DP优化一些典型或者不典型的DP优化实例。CF354DTransferringPyramid题意:有一个\(n\)层的金字塔,现在能进行两种操作,一是给某个点染色,代价是3,或者画一个底边是金字塔底边的子三角形,给每个点,代价是点数+2,要求用最小代价覆盖所有要染色的\(m\)个点。思路:定义\(h_i\)表......
  • Python笔记09——Set(集合)
    九、集合9.1基础集合(set)是一个无序的不重复元素序列,可进行交、集、差等常见的集合操作。与序列的区别:无序,每次输出顺序随机;元素不重复;创建格式:parame={value01,value02,...}或者set(value)(创建空集合只能用set())创建集合示例set1={1,2,3,4}#直接使用......
  • gnuradio笔记[3]-播放音频并观测幅度谱
    摘要使用GNURadio观测音频文件的幅度谱并播放音频,将数据保存到wav文件.关键信息GNURadioCompanion:3.10.8.0(Python3.10.13)实现音频文件参数名称参数值音频组成信标信号+音乐文件信标波形正弦波信标频率800Hz信标幅度(Vpp)1000mV信标持续......
  • 【阅读笔记】边缘损耗率评价指标《A New Hardware-Efficient Algorithm and Reconfigu
    论文《ANewHardware-EfficientAlgorithmandReconfigurableArchitectureforImageContrastEnhancement》提到对对比度增强的图像进行客观评价,引用论文《ImageEnhancementforBacklight-ScaledTFT-LCDDisplays》中的边缘损耗率指标(Theedgelossrate)。原文:Contrast......
  • Delete a node from bst【2月15日学习笔记】
    点击查看代码//Deleteanodefrombst#include<iostream>structnode{intdata;node*left;node*right;};node*getnewnode(intx){node*temp=newnode;temp->data=x;temp->left=temp->right=NULL;returntem......
  • 容斥学习笔记
    容斥基本公式\(|\bigcup\limits_{i=1}^na_i|=\sum\limits_{T\subseteq[1,n]}-1^{|T|+1}|\bigcap\limits_{i\inT}a_i|\)min-max容斥\(\max_{i\inS}a_i=\sum\limits_{T\subseteqS}(-1)^{|T|+1}\min_{i\inT}a_i\)\(\min_{i\inS}a_i=\sum\limits_{T\su......
  • 【无评级杂题】DP/贪心
    题目这题后来看了看网上的思路,发现贪心就能做,亏我还写了个O(2*N)的DP...浪费时间了属于是my-code#include<iostream>#include<cstring>#include<string>#include<cstdio>#include<cstdlib>#include<algorithm>#include<stack>#include<queue>......
  • 动态规划基础笔记
    背包问题 01背包  一般的动态规划要先考虑好状态,这个状态是一个集合,要能分成几个子集,然后从这些子集(小问题),推到这一整个集合(大问题),且求解过程是一样的,就可以可以转换成大问题分解成小问题一个一个求解,最后合并先要知道状态表示什么再要知道dp的属性,应该跟所求有关,只会......
  • 思源笔记—代码片段
    Vue引用主题样式.protyle-wysiwyg[data-node-id].bq,.b3-typographyblockquote{border-left:0.25emsolid#5ebc1d!important;border-radius:0em0.31em0.31em0.em;background-color:rgb(10624190/5%)!important;margin-top:8px!important......