树上启发式合并

DSU on tree,我也不知道DSU是啥意思

这是一种看似特别玄学的优化

可以把树上部分问题由 \(O(n^2)\) 优化到 \(O(n \log n)\)。

例如 CodeForces 600E

又例如一道神奇的题:

适用情况

可以离线部分树上问题。

需要子树上的所有信息,但是信息无法快速合并的情况。

或者说可以使用树上莫队的题目一般都可以使用启发式合并?(至少OI-Wiki是这么说的)

树上启发式合并并不是很常用

合并思路

首先定义一点概念:

  • 重子节点:一个结点的子结点中度最大的结点(孩子个数最多的点)
  • 轻子节点:非重子节点的节点
  • 重边:重子结点(如果有)与其父结点相连的边
  • 轻边:非重边
  • 重链:相邻重边链接形成的链(这里好像用不到)

树上启发式合并的思路如下:

  • 处理 x 为根的情况

  • 递归处理 轻子节点 的答案,并舍弃其信息

  • 处理 重子节点 的答案,并保留其信息

  • 如果 x 为(其父亲的)重子节点,则加上所有 轻子节点 的信息。否则舍弃其信息

很明显,我们需要预处理出重子节点。同时可能需要用到 dfn 序来优化信息的加入

不妨我们以 vector 存图(非常方便)

void workSon(int x, int f) {
siz[x] = 1, fa[x] = f;
dfn[x] = ++cdfn, rdfn[cdfn] = x;
for (int y : G[x]) {
if (dfn[y]) continue;
workSon(y, x);
siz[x] += siz[y];
if (siz[son[x]] < siz[y]) son[x] = y;
}
edfn[x] = cdfn;
}

最终 son[x] 就是 x 的重子节点,同时我们处理出了 dfn 序以及以 x 为子树的 dfn 序范围:[dfn[x], edfn[x]] 注意为闭区间

那么伪代码大致如下:

// remain 用于获知需不需要保留数据,默认不保留
int currentAnswer;
void work(int x, bool remain = false) {
for (int y : G[x]) {
if (y != fa[x] && y != son[x]) work(y);
} if (son[x]) work(son[x], true); for (int y : G[x]) {
if (y == fa[x] || y == son[x]) continue;
addSubTreeInfoAndUpdateAnswer(y);
} answerOf[x] = currentAnswer;
if (!remain) {
clearAnswer();
for (int y : G[x]) {
if (y != fa[x]) removeSubTreeInfo(y);
}
}
}

每个部分的作用在函数名里面应该很清晰了。这里不再赘述。

复杂度证明

首先,根节点到树上任意节点的轻边数不超过 \(\log n\) 条。

只有当祖先节点为轻子节点时其信息会被删除。也就是加入 \(O(\log n)\) 次,删除 \(O(\log n)\) 次,故而每一个点的复杂度为 \(O(\log n)\),整体的复杂度为 \(O(n \log n)\)。

当然,考虑如果每一个节点加入信息或者删除信息的复杂度为 \(O(k)\),则整体复杂度为 \(O(n k \log n)\)。

非常玄学……但是就是能够优化

例题分析

就以开始为例子的两道题为例吧。

CodeForces 600E

这道题也就是树上数颜色的问题。

题目大意是:

对于每一个节点,做出贡献的颜色需要满足出现的次数是最多的之一。一个颜色的贡献即是这个颜色的编号。

最终输出每一个节点被贡献的结果

样例输入
4
1 2 3 4
1 2
2 3
2 4
样例输出
10 9 3 4

最主要也是最重要的就是颜色的统计。

加入点(颜色)的核心代码如下:

int colorBut[N];
long long maxExi = 0, cnt = 0;
void add(int c) {
if (++colorBut[c] == maxExi) {
cnt += c;
} else if (colorBut[c] > maxExi) {
maxExi = colorBut[c], cnt = c;
}
}

在合并部分也很清晰了

long long res[N];
void dsu(int x, int f, bool remain = false) {
for (int y : G[x]) {
if (y != f && y != son[x]) dsu(y, x);
} if (son[x]) dsu(son[x], x, true); // 记得把根节点的信息也加进去!
add(col[x]);
for (int y : G[x]) {
if (y == f || y == son[x]) continue;
// 添加信息
for (int i = dfn[y]; i <= edfn[y]; ++i) add(col[rdfn[i]]);
} res[x] = cnt;
if (!remain) {
maxExi = cnt = 0; // 重置答案
for (int i = dfn[x]; i <= edfn[x]; ++i) // 删除影响信息
colorBut[col[rdfn[i]]] = 0;
}
}

不正常国家

这道题稍微复杂一点。

考虑每对点对其 LCA 的贡献。或者换个思路,考虑每一个根节点能够被那些点贡献。

不难发现,有两种情况:

  • LCA 和其子节点之间的路径

  • LCA 的两个子节点之间的路径。这里要保证两个子节点在不同的子树里面

如果我们已经预处理出了树上异或前缀和 path。那么任意两个点对其 LCA 的贡献为 path[x] ^ path[y] ^ val[LCA]。我们不妨对于每一个 LCA,枚举所有 path[y] ^ val[LCA],同时在已知的 path[x] 中匹配最大的异或对。

最大异或对可以看此题:AcWing 143.最大异或对

利用了01Trie树和二进制贪心。

此处不展开。

同时,由于我们需要保证 xyu 的不同子树中,所以我们先查询完一颗子树再加入这棵子树的信息。

核心代码如下:

// 树上启发式合并
void work(int x, int f, bool remain = false) {
// 首先搞定所有非重子节点
for (int y : G[x]) {
if (y == f || y == son[x]) continue;
work(y, x);
} // 搞定重子节点,并保留数据
if (son[x]) work(son[x], x, true);
// path[fa[x]] 也就是 path[x] ^ val[x]
int ans = max(val[x], trie.pairMax(path[fa[x]]));
trie.insert(path[x]); // 加入其他节点,并搜索
for (int y : G[x]) {
if (y == f || y == son[x]) continue;
for (int j = dfn[y]; j <= edfn[y]; ++j) {
int pa = path[rdfn[j]] ^ val[x];
ans = max(ans, trie.pairMax(pa));
} for (int j = dfn[y]; j <= edfn[y]; ++j) {
trie.insert(path[rdfn[j]]);
}
} res[x] = ans;
if (!remain) trie.clear();
}

至于 01Trie 树代码如下:

const int LOG = 30; // 31位!下标为 [0, 30]

#define bit(x, i) ((x >> i) & 1)
class Trie01 {
private:
int ch[N << 4][2];
int usage;
public:
Trie01() : usage(1) {
} inline int newNode() {
++usage;
ch[usage][0] = ch[usage][1] = 0;
return usage;
} void insert(int x) {
int p = 1;
for (int k = LOG; k >= 0; --k) {
int s = bit(x, k);
if (!ch[p][s]) ch[p][s] = newNode();
p = ch[p][s];
}
} // 这是通过树的形状贪心寻找最大异或对
int pairMax(int x) {
int p = 1;
int r = 0;
for (int k = LOG; k >= 0; --k) {
int s = bit(x, k) ^ 1;
if (ch[p][s]) r = (r << 1) | 1, p = ch[p][s];
else if (ch[p][s ^ 1]) r <<= 1, p = ch[p][s ^ 1];
else p = 0, r = x; // 避免空树的情况
}
return r;
} void clear() {
usage = 1;
ch[1][0] = ch[1][1] = 0;
}
} trie;

那么这道 水题 也就这么水过去了。

忘了说,其复杂度为 \(O(n \log n L)\),其中 \(L\) 是位长,也就是代码中的 LOG = 30。所以复杂度也可以写为 \(O(n \log^2 n)\)


树上启发式合并的潜力不止于此,还望诸君发掘。

算法学习笔记(19): 树上启发式合并(DSU on tree)的更多相关文章

  1. 神奇的树上启发式合并 (dsu on tree)

    参考资料 https://www.cnblogs.com/zhoushuyu/p/9069164.html https://www.cnblogs.com/candy99/p/dsuontree.ht ...

  2. 树上启发式合并 (dsu on tree)

    这个故事告诉我们,在做一个辣鸡出题人的比赛之前,最好先看看他发明了什么新姿势= =居然直接出了道裸题 参考链接: http://codeforces.com/blog/entry/44351(原文) ...

  3. 【CF600E】Lomset gelral 题解(树上启发式合并)

    题目链接 题目大意:给出一颗含有$n$个结点的树,每个节点有一个颜色.求树中每个子树最多的颜色的编号和. ------------------------- 树上启发式合并(dsu on tree). ...

  4. 树上启发式合并(dsu on tree)学习笔记

    有丶难,学到自闭 参考的文章: zcysky:[学习笔记]dsu on tree Arpa:[Tutorial] Sack (dsu on tree) 先康一康模板题吧:CF 600E($Lomsat ...

  5. dsu on tree 树上启发式合并 学习笔记

    近几天跟着dreagonm大佬学习了\(dsu\ on\ tree\),来总结一下: \(dsu\ on\ tree\),也就是树上启发式合并,是用来处理一类离线的树上询问问题(比如子树内的颜色种数) ...

  6. 【学习笔记/题解】树上启发式合并/CF600E Lomsat gelral

    题目戳我 \(\text{Solution:}\) 树上启发式合并,是对普通暴力的一种优化. 考虑本题,最暴力的做法显然是暴力统计每一次的子树,为了避免其他子树影响,每次统计完子树都需要清空其信息. ...

  7. dsu on tree (树上启发式合并) 详解

    一直都没出过算法详解,昨天心血来潮想写一篇,于是 dsu on tree 它来了 1.前置技能 1.链式前向星(vector 建图) 2.dfs 建树 3.剖分轻重链,轻重儿子 重儿子 一个结点的所有 ...

  8. 【Luogu U41492】树上数颜色——树上启发式合并(dsu on tree)

    (这题在洛谷主站居然搜不到--还是在百度上偶然看到的) 题目描述 给一棵根为1的树,每次询问子树颜色种类数 输入输出格式 输入格式: 第一行一个整数n,表示树的结点数 接下来n-1行,每行一条边 接下 ...

  9. 树上启发式合并(dsu on tree)

    树上启发式合并属于暴力的优化,复杂度O(nlogn) 主要解决的问题特点在于: 1.对于树上的某些信息进行查询 2.一般问题的解决不包含对树的修改,所有答案可以离线解决 算法思路:这类问题的特点在于父 ...

  10. CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths 树上启发式合并(DSU ON TREE)

    题目描述 一棵根为\(1\) 的树,每条边上有一个字符(\(a-v\)共\(22\)种). 一条简单路径被称为\(Dokhtar-kosh\)当且仅当路径上的字符经过重新排序后可以变成一个回文串. 求 ...

随机推荐

  1. 法拉第未来任命新CFO!贾跃亭激动发声

    近段时间以来,贾跃亭旗下的的法拉第未来(Faraday Future,简称 FF)可谓是动作频频. 一天前,有媒体报道称,FF 任命 Zvi Glasman 为其首席财务官.其将负责公司财务.投资者关 ...

  2. mysql 的小问题

    首先按下win+R 执行 services.msc 进入服务,查找到MySQL,点击停止服务,然后在控制台cmd进入本地的MySQL文件夹,我的文件名是mysql-8.0.26-winx64,进入后执 ...

  3. MQTT服务器搭建——Liunx安装mosquitto,并设置用户密码

    一.安装 1.下载mosquitto安装包 地址:http://mosquitto.org/files/source/ 2.安装依赖包 yum install gcc gcc-c++ libstdc+ ...

  4. 虚拟机安装windows 7 32位 + sqlserver 2000

    安装包网盘地址:(https://pan.baidu.com/s/1ZoC-cTafBi8zZbCkvvmvNA?pwd=x1y2 提取码:x1y2 ) VMware 安装win7 32 位 http ...

  5. Centos7部署PXE+Kickstart 实现批量安装操作系统

    1.PXE环境概述 作为一名运维人员,在一些中小公司经常会遇到一些机械式的重复工作,比如:批量一次大批量的进行操作系统的安装等等.为了实现自动化运维,减少人员负担我们可以部署以下服务:Kickstar ...

  6. 12.8 linux学习第十五天

    今天老刘讲了第11章和第12章,感觉讲的速度很快,一气呵成,水都没怎么喝. 11.1 文件传输协议 一般来讲,人们将计算机联网的首要目的就是获取资料,而文件传输是一种非常重要的获取资料的方式.今天的互 ...

  7. CF1272 C Yet Another Broken Keyboard 题解+代码比对

    C. Yet Another Broken Keyboard time limit per test 2 seconds memory limit per test 256 megabytes inp ...

  8. Q:带宽检测 iperf工具

    一.下载 iperf的下载地址为:https://iperf.fr/iperf-download.php,选择相应的版本 linux安装 rpm -qa|grep -i rperf rpm -ivh ...

  9. 深入理解Java内存(图解)

    这篇文章是转自http://blog.csdn.net/shimiso/article/details/8595564博文. 本文分析基于jdk1.8 进入正题前首先要知道的是Java程序运行在JVM ...

  10. MySQL数据库sql_mode导致varchar字段超过长度被截断插入

    django数据库设置sql_mode MySQL的sql_mode解析与设置 mysql中sql_mode的修改 sql_mode:它定义了MySQL应该支持的sql语法,对数据的校验等等. 问题 ...