前言
树上启发式合并,用来解决子树查询问题,是一种离线的、“暴力的”算法。从某种角度而言,与莫队算法有些类似,即:按照我们指定的顺序对数据做一个遍历,在遍历过程中记录询问的答案。CF上有一个专门的blog,讲了很多,本文只涉及到有关轻重链剖分的部分。oi-wiki上有一个更短的文章。
简单而言,就是对每一个节点,分两步:
- 递归统计子节点的数据
- 先统计轻儿子(统计完的数据即刻清空)
- 最后统计重儿子(统计完的数据保留下来)
- 统计自己的数据(就是再统计一遍轻儿子的数据,加上本节点的数据)
在统计本节点的数据时,因为重儿子的数据已经保存,因此只需要再统计一遍轻儿子的数据,再加上本节点的数据,就有了本节点子树的所有数据,于是可以回答有关本节点子树的相关询问。
可以证明(并不会证明),这样重复统计以及清空操作,数量级是 O ( N log N ) O(N\log{N}) O(NlogN)的。
当然首先要会轻重链剖分,其实并不需要,因为这里其实并不需要实际剖出树链,只需要求出重儿子即可。因此实际上比剖分要简单的多。出于个人习惯,还是称之为剖分。
入门题
例1
为了简单起见,我们考虑一个不需要DSUonTree的问题,问子树的节点数量。这个显然 O ( N ) O(N) O(N)就能完成。我们考虑一下启发式合并。
#include <bits/stdc++.h>
using namespace std;
struct dsu_on_tree_t{
using vi = vector<int>;
int N;
vector<vi> G; // 树, 1-index
struct node_t{
// 树链剖分的结构
int size;
int hson; // 重儿子,这里是原树编号
int nid; // 在树链剖分中的新编号
int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old; // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Ans;
int Cnt; // 全局变量用于记录即时数据
void init(int n){
N = n;
G.assign(N + 1, {
});
Nodes.assign(N + 1, {
0, 0, 0, 0});
New2Old.assign(N + 1, 0);
TimeStamp = 0;
Ans.assign(N + 1, 0);
}
void mkDiEdge(int a, int b){
G[a].push_back(b);
}
void mkBiEdge(int a, int b){
mkDiEdge(a, b);
mkDiEdge(b, a);
}
void dfsHeavy(int u, int p){
// 递归重儿子
auto & n = Nodes[u];
n.size = 1;
New2Old[n.nid = ++TimeStamp] = u;
for(auto v : G[u]){
if(v == p) continue;
dfsHeavy(v, u);
n.size += Nodes[v].size;
if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;
}
n.mdes = TimeStamp;
return;
}
void dfs(int u, int p, bool keep){
// 递归
const auto & n = Nodes[u];
for(auto v : G[u]){
if(v == p or v == n.hson) continue;
dfs(v, u, false);
}
/// 最后递归重儿子
if(n.hson) dfs(n.hson, u, true);
/// 以下为统计u节点及其轻儿子
if(n.hson){
for(int i=n.nid;i<Nodes[n.hson].nid;++i) ++Cnt;
/// 刚好把重儿子忽略掉
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i) ++Cnt;
}else{
// 只有一个节点
++Cnt;
}
/// 此时可以回答问题
Ans[u] = Cnt;
/// 是否清空u子树对即时数据的影响
if(not keep){
for(int i=n.nid;i<=n.mdes;++i) --Cnt;
}
return;
}
}Tree;
int main(){
#ifndef ONLINE_JUDGE
freopen("z.txt", "r", stdin);
#endif
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin >> n;
Tree.init(n);
for(int a,i=2;i<=n;++i){
cin >> a;
Tree.mkDiEdge(a, i);
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(int i=1;i<=n;++i) cout << Tree.Ans[i] << "\n";
return 0;
}
首先关于树链剖分部分,加了一个数据域mdes,其作用主要是为了明确本子树的范围。这样方便后续将代码写成迭代实现。树链剖分的实现这里就略过了。
然后就是本文的核心实现函数dfs。
这一部分就是递归,并且把重儿子放在最后递归。
for(auto v : G[u]){
if(v == p or v == n.hson) continue;
dfs(v, u, false);
}
/// 最后递归重儿子
if(n.hson) dfs(n.hson, u, true);
接下来就是统计即时数据,即统计u子树中除了u的重儿子之外的节点对即时数据造成的影响。因为记录了mdes,这一段可以写成迭代。迭代实现对于初次实现本算法的程序员,在调试方面比较友好。当然,也可以写成递归实现。
/// 以下为统计u节点及其轻儿子
if(n.hson){
for(int i=n.nid;i<Nodes[n.hson].nid;++i) ++Cnt;
/// 刚好把重儿子忽略掉
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i) ++Cnt;
}else{
// 只有一个节点
++Cnt;
}
接下来是根据当前的即时数据,回答跟u有关的问题。这里的话比较简单。
/// 此时可以回答问题
Ans[u] = Cnt;
最后是根据参数,决定是否要清空u子树对即时数据的影响。这里写成迭代就非常自然了。当然,递归也是极好的。
/// 是否清空u子树对即时数据的影响
if(not keep){
for(int i=n.nid;i<=n.mdes;++i) --Cnt;
}
对于DSU-on-Tree而言,这个流程是固定的。只需要考虑每个节点对即时数据造成的影响,以及如何根据即时数据回答问题即可。当然这两个动作需要能够“很快的”完成。
例2
再来看标准的例子,每个节点有一个颜色,问子树的不同颜色的种类的数量。只需要有一个计数器记录各种颜色的数量,并且记录计数器中不同种类的数量即可,都可以在 O ( 1 ) O(1) O(1)完成。因此加上DSU的流程,在 O ( N log N ) O(N\log{N}) O(NlogN)可以完成。
#include <bits/stdc++.h>
using namespace std;
struct dsu_on_tree_t{
using vi = vector<int>;
int N;
vector<vi> G; // 树, 1-index
struct node_t{
// 树链剖分的结构
int size;
int hson; // 重儿子,这里是原树编号
int nid; // 在树链剖分中的新编号
int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old; // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int Root;
vector<int> Color;
vector<int> Flag;
int Cnt; // 全局变量用于记录即时数据
vector<int> Ans;
vector<vi> Questions;
void init(int n){
N = n;
G.assign(N + 1, {
});
Nodes.assign(N + 1, {
0, 0, 0, 0});
New2Old.assign(N + 1, 0);
TimeStamp = 0;
Color.assign(N + 1, 0);
Flag.assign(N + 1, Cnt = 0);
}
void mkDiEdge(int a, int b){
G[a].push_back(b);
}
void mkBiEdge(int a, int b){
mkDiEdge(a, b);
mkDiEdge(b, a);
}
void dfsHeavy(int u, int p){
// 递归重儿子
auto & n = Nodes[u];
n.size = 1;
New2Old[n.nid = ++TimeStamp] = u;
for(auto v : G[u]){
if(v == p) continue;
dfsHeavy(v, u);
n.size += Nodes[v].size;
if(Nodes[n.hson].size < Nodes[v].size) n.hson = v;
}
n.mdes = TimeStamp;
return;
}
void dfs(int u, int p, bool keep){
// 递归
const auto & n = Nodes[u];
for(auto v : G[u]){
if(v == p or v == n.hson) continue;
dfs(v, u, false);
}
/// 最后递归重儿子
if(n.hson) dfs(n.hson, u, true);
/// 以下为统计u节点及其轻儿子
if(n.hson){
for(int i=n.nid;i<Nodes[n.hson].nid;++i){
if(1 == ++Flag[Color[New2Old[i]]]){
++Cnt;
}
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
if(1 == ++Flag[Color[New2Old[i]]]){
++Cnt;
}
}
}else{
// 只有一个节点
if(1 == ++Flag[Color[u]]){
++Cnt;
}
}
/// 此时可以回答问题
for(auto i : Questions[u]){
Ans[i] = Cnt;
}
/// 是否清空u子树对即时数据的影响
if(not keep){
for(int i=n.nid;i<=n.mdes;++i) {
if(0 == --Flag[Color[New2Old[i]]]){
--Cnt;
}
}
}
return;
}
}Tree;
int main(){
#ifndef ONLINE_JUDGE
freopen("z.txt", "r", stdin);
#endif
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n; cin >> n;
Tree.init(n);
for(int a,b,i=2;i<=n;++i){
cin >> a >> b;
Tree.mkBiEdge(a, b);
}
for(int i=1;i<=n;++i) cin >>Tree.Color[i];
int q; cin >> q;
Tree.Questions.assign(n + 1, {
});
Tree.Ans.assign(q, {
});
for(int a,i=0;i<q;++i){
cin >> a;
Tree.Questions[a].push_back(i);
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(auto i : Tree.Ans) cout << i << "\n";
return 0;
}
NC23807的题目意思与本题是一模一样的,这里就不再赘述。
例3CF600E
这也是一个入门的例子,可以更好的认识清空操作的含义。给定树,每个节点有颜色。询问每一个子树的颜色的众数,即求出出现颜色最多的颜色编号;如果有多个颜色均出现最多次,则对编号求和。
本题显然也要弄一个计数器,然后每过一个节点,相应的颜色数量加加即可,对于众数或者众数求和都很简单。可能存在的一个问题在于如何清空?如果要考虑移除一个节点,对于答案的影响,那就很难办了。因为最值不支持快速的减法操作。但是注意到,本质上我们进行的是一个清空操作。考虑到递归顺序的安排,每次需要清空时,我们必然处在轻儿子处,且相应的重儿子必然在其之后。因此当本次清空完成时,实际上所有即时数据全部都清零了。之所以有些数据需要逐个节点、逐个节点的操作,是为了控制时间,只清除非零的数据。因为如果不管三七二十一,直接fill所有数据,显然是要超时的。
#include <bits/stdc++.h>
using namespace std;
struct dsu_on_tree_t{
using vi = vector<int>;
int N;
vector<vi> G; // 树, 1-index
struct node_t{
// 树链剖分的结构
int size;
int hson; // 重儿子,这里是原树编号
int nid; // 在树链剖分中的新编号
int mdes; // 本子树全部在[nid, mdes]之间, 这是剖分编号
};
vector<node_t> Nodes;
vi New2Old; // 剖分的编号为i,则原树节点编号为New2Old[i], 显然有Nodes[New2Old[i]].nid == i
int TimeStamp;
int R

最低0.47元/天 解锁文章
792

被折叠的 条评论
为什么被折叠?



