前言
树上启发式合并,用来解决子树查询问题,是一种离线的、“暴力的”算法。从某种角度而言,与莫队算法有些类似,即:按照我们指定的顺序对数据做一个遍历,在遍历过程中记录询问的答案。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 Root;
vector<int> Color;
vector<int> Flag;
vector<long long> Ans;
int MaxCnt;
long long NowAns;
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, MaxCnt = NowAns = 0);
Ans.assign(N, 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){
auto c = ++Flag[Color[New2Old[i]]];
if(c > MaxCnt) MaxCnt = c, NowAns = Color[New2Old[i]];
else if(c == MaxCnt) NowAns += Color[New2Old[i]];
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
auto c = ++Flag[Color[New2Old[i]]];
if(c > MaxCnt) MaxCnt = c, NowAns = Color[New2Old[i]];
else if(c == MaxCnt) NowAns += Color[New2Old[i]];
}
}else{ // 只有一个节点
Flag[NowAns = Color[u]] = MaxCnt = 1;
}
/// 此时可以回答问题
Ans[u - 1] = NowAns;
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 注意这里是清空,因此可以直接令全局变量为零
MaxCnt = NowAns = 0;
for(int i=n.nid;i<=n.mdes;++i) {
Flag[Color[New2Old[i]]] = 0;
/// 一般习惯写成减减,但实际上直接清零也可以
// --Flag[Color[New2Old[i]]];
}
}
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 i=1;i<=n;++i) cin >>Tree.Color[i];
for(int a,b,i=2;i<=n;++i){
cin >> a >> b;
Tree.mkBiEdge(a, b);
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(auto i : Tree.Ans) cout << i << " ";
cout << endl;
return 0;
}
这里统计答案的关键就在于清空的位置,一般习惯碰上计数器都是写减减,肯定不会有问题。但实际上直接把计数器清零也可以,因为这里其实不是清除单个节点对即时数据的影响,而是清空整个子树对即时数据的影响。又由于轻、重的递归顺序,因此清空轻儿子的效果实际上就是清空整个数据。因此这个操作才可以直接进行:MaxCnt = NowAns = 0;
。同样的计数器数组也可以直接清零。之所以不用fill(Flag.begin(),Flag.end(),0)
只是因为会超时。
CF570D
给定树,节点上有字符,有若干个询问(v, h),问子树v上深度为h的节点是否能够通过重排序构成回文串。注意节点深度是指整个树上的深度,因此是固定的。实际上就是问对应节点的字符数量,是否奇数个字母的数量少于等于1。由于只涉及到奇偶,可以使用一个26位二进制数表示数量,只需检查1的位数小于等于1即可。令cnt[d]表示表示深度为d的情况下的字符数量,使用DSU来维护cnt数组。然后对每个子树u,如果有关于它的问题,例如(u, h),则查询当时的cnt[h]、且记录下来即可。与前几题的区别,简而言之前几题使用了权值计数器,这里使用了深度计数器。
#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> Depth;
string S;
vector<int> Ans;
vector<vector<pair<int, int>>> Questions;
vector<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;
Depth.assign(N + 1, 0);
Cnt.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){ // 递归重儿子
Depth[u] = Depth[p] + 1;
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){
auto t = S[New2Old[i]] - 'a';
Cnt[Depth[New2Old[i]]] ^= (1 << t);
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
auto t = S[New2Old[i]] - 'a';
Cnt[Depth[New2Old[i]]] ^= (1 << t);
}
}else{ // 只有一个节点
Cnt[Depth[u]] = 1 << (S[u] - 'a');
}
/// 此时可以回答问题
for(const auto & pp : Questions[u]){
auto h = pp.first;
bool b = true;
int c = 0;
for(int i=0;i<26;++i){
if((1 << i) & Cnt[h])if(++c > 1){b = false; break;}
}
Ans[pp.second] = b ? 1 : 0;
}
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 清空
for(int i=n.nid;i<=n.mdes;++i) {
Cnt[Depth[New2Old[i]]] = 0;
}
}
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, q; cin >> n >> q;
Tree.init(n);
for(int a,i=2;i<=n;++i){
cin >> a;
Tree.mkDiEdge(a, i);
}
cin >> Tree.S;
Tree.S = " " + Tree.S;
Tree.Questions.assign(n + 1, {});
Tree.Ans.assign(q, 0);
for(int u,h,i=0;i<q;++i){
cin >> u >> h;
Tree.Questions[u].emplace_back(h, i);
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(auto i : Tree.Ans) cout << (i ? "Yes\n" : "No\n");
return 0;
}
CF246E是一个既有深度又有权值的题目,本质上就是询问子树u中深度为k的不同权值的数量。可以利用map
作为计数器,在增加一个 log \log log的情况下完成查询。
略复杂一点的题目
有了DSUonTree的基本流程,就可以把注意力集中在问题的转化与即时数据的快速维护上。只要能够在合理的时间复杂度内做到,就能解决整个问题。
CF208E
CF208E给定一个森林,定义父节点是子节点的一级祖先,类似可以定义k级祖先。如果两个节点u和v有一个共同的k级祖先,则它们互为k级兄弟。每次询问给定v和k,问v的k级兄弟有多少个。假设v的k级祖先是p,则等价于问p的k级子节点的数量。有了前面的关于深度相关计数器的经验,这个问题很容易解决。于是问题的关键变为了能否很快的知道任意节点的任意级祖先。这个显然是可以倍增解决的,与求 L C A LCA LCA类似。因此先做一个预处理,将问题转化一下,然后DSU即可。
#include <bits/stdc++.h>
using namespace std;
int const LOGSZ = 17;
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> Depth;
vector<vi> Parent;
vector<int> Log2;
vector<int> Ans;
vector<vector<pair<int, int>>> Questions;
vector<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;
Depth.assign(N + 1, 0);
Parent.assign(N + 1, vi(LOGSZ, 0));
Cnt.assign(N + 1, 0);
Log2.assign(N + 1, 0);
for(int i=1;i<=N;++i){
Log2[i] = Log2[i - 1] + ((1 << Log2[i - 1]) == i ? 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){ // 递归重儿子
Depth[u] = Depth[p] + 1;
auto & n = Nodes[u];
n.size = 1;
New2Old[n.nid = ++TimeStamp] = u;
Parent[u][0] = p;
for(int i=1;i<=Log2[Depth[u]];++i){
Parent[u][i] = Parent[Parent[u][i - 1]][i - 1];
}
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[Depth[New2Old[i]]];
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
++Cnt[Depth[New2Old[i]]];
}
}else{ // 只有一个节点
Cnt[Depth[u]] = 1;
}
/// 此时可以回答问题
for(const auto & pp : Questions[u]){
Ans[pp.second] = Cnt[pp.first + Depth[u]] - 1;
}
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 清空
for(int i=n.nid;i<=n.mdes;++i) {
Cnt[Depth[New2Old[i]]] = 0;
}
}
return;
}
}Tree;
vector<int> Root;
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;
Root.clear();
Root.reserve(n);
Tree.init(n);
for(int a,i=1;i<=n;++i){
cin >> a;
if(a) Tree.mkDiEdge(a, i);
else Root.push_back(i);
}
for(auto i : Root) Tree.dfsHeavy(i, 0);
int q; cin >> q;
Tree.Questions.assign(n + 1, {});
Tree.Ans.assign(q, 0);
for(int p,u,k,i=0;i<q;++i){
cin >> u >> k;
auto target = k;
for(int i=LOGSZ-1;i>=0;--i){
if((1 << i) & target){
u = Tree.Parent[u][i];
}
}
Tree.Questions[u].emplace_back(k, i);
}
for(auto i : Root) Tree.dfs(i, 0, false);
for(auto i : Tree.Ans) cout << (i) << " ";
cout << endl;
return 0;
}
NC235719
这个题目可以更好的展示DSUonTree的框架作用。每次回答询问[u,a,b],即u子树内深度在[a,b]区间内的所有节点的权值的最小值、最大值与和。这个明显是一个区间查询问题,再考虑到DSUonTree的统计过程,因此我们需要解决一个单点变动、区间查询的任务,因为要查询最值,所以使用线段树。如果不考虑线段树的部分,可以看到与DSUonTree有关的流程与之前的代码并无区别。
#include <bits/stdc++.h>
using namespace std;
using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;
struct SegTree{ // 线段树带延迟
using llt = long long;
int N;
using value_type = array<llt, 3>; // 最小值,最大值,和
vector<value_type> data; // 线段树
using lazy_type = pair<llt, int>;
vector<lazy_type> lazy; // 延迟标记
/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {
// assert(0);
return {min(ls[0], rs[0]), max(ls[1], rs[1]), ls[2] + rs[2]};
}
/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {
// assert(0);
if(delta.second == -1){ // 清零操作
data[t] = value_zero();
}else if(delta.second == 1){ // 增加一个权值, 必然是单点操作
data[t][0] = min(data[t][0], delta.first);
data[t][1] = max(data[t][1], delta.first);
data[t][2] += delta.first;
}
return;
}
/// 初始化,清零,不用动
void init(int n) {
data.assign((N = n) + 1 << 2, value_zero());
// lazy.assign(n + 1 << 2, lazy_zero());
}
/// 这个函数不用动
void modify(int a, int b, const lazy_type & delta){
_modify(1, 1, N, a, b, delta);
}
/// 这个函数不用动
value_type query(int a, int b){
return _query(1, 1, N, a, b);
}
/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {
if(a <= s and e <= b) {
return data[t];
}
// _pushDown(t, s, e);
int mid = (s + e) >> 1;
value_type ans = value_zero(); // 如果求最值,这里不能用zero
if(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));
if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));
return ans;
}
/// 几乎不用动
void _modify(int t, int s, int e, int a, int b, const lazy_type & delta) {
if(a <= s and e <= b) {
_dn_(t, s, e, delta);
return;
}
// _pushDown(t, s, e);
int mid = (s + e) >> 1;
if(a <= mid) _modify(lson(t), s, mid, a, b, delta);
if(mid < b) _modify(rson(t), mid + 1, e, a, b, delta);
_pushUp(t);
return;
}
/// 这个函数不用动
void _pushUp(int t) {
data[t] = _up_(data[lson(t)], data[rson(t)]);
}
/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {
static const lazy_type LAZY0 = {0, 0};
return LAZY0;
}
/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {
static const value_type VALUE0 = {INF, NINF, 0LL};
return VALUE0;
}
/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}
};
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> Depth;
SegTree St;
vector<long long> W;
vector<array<llt, 3>> Ans;
vector<vector<array<int, 3>>> 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;
Depth.assign(N + 1, 0);
W.assign(N + 1, 0);
St.init(N);
}
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){ // 递归重儿子
Depth[u] = Depth[p] + 1;
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){
St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});
}
}else{ // 只有一个节点
St.modify(Depth[u], Depth[u], {W[u], 1});
}
/// 此时可以回答问题
for(const auto & pp : Questions[u]){
Ans[pp[2]] = St.query(Depth[u] + pp[0], Depth[u] + pp[1]);
}
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 清空
for(int i=n.nid;i<=n.mdes;++i){
St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {0LL, -1});
}
}
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 i=1;i<=n;++i) cin >> Tree.W[i];
for(int a,b,i=1;i<n;++i){
cin >> a >> b;
Tree.mkBiEdge(a, b);
}
int q; cin >> q;
Tree.Questions.assign(n + 1, {});
Tree.Ans.assign(q, {});
for(int u,a,b,i=0;i<q;++i){
cin >> u >> a >> b;
Tree.Questions[u].push_back({a, b, i});
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(const auto & i : Tree.Ans) cout << i[0] << " " << i[1] << " " << i[2] << "\n";
return 0;
}
这道题还可以用来验证清空流程。上文说过对于需要清空的情况,实际上就是清空整个即时数据,之前之所以不这么做,只是因为超时。现在既然已经用上了线段树,可以再往里加点料,即支持延迟操作。这样的话直接清空整个线段树就没有压力了。
#include <bits/stdc++.h>
using namespace std;
using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;
struct SegTree{ // 线段树带延迟
using llt = long long;
int N;
using value_type = array<llt, 3>; // 最小值,最大值,和
vector<value_type> data; // 线段树
using lazy_type = pair<llt, int>;
vector<lazy_type> lazy; // 延迟标记
/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {
// assert(0);
return {min(ls[0], rs[0]), max(ls[1], rs[1]), ls[2] + rs[2]};
}
/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {
// assert(0);
if(delta.second == -1){ // 清零操作
data[t] = value_zero();
lazy[t] = delta;
}else if(delta.second == 1){ // 增加一个权值, 必然是单点操作
data[t][0] = min(data[t][0], delta.first);
data[t][1] = max(data[t][1], delta.first);
data[t][2] += delta.first;
}
return;
}
/// 初始化,清零,不用动
void init(int n) {
data.assign((N = n) + 1 << 2, value_zero());
lazy.assign(n + 1 << 2, lazy_zero());
}
/// 这个函数不用动
void modify(int a, int b, const lazy_type & delta){
_modify(1, 1, N, a, b, delta);
}
/// 这个函数不用动
value_type query(int a, int b){
return _query(1, 1, N, a, b);
}
/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {
if(a <= s and e <= b) {
return data[t];
}
_pushDown(t, s, e);
int mid = (s + e) >> 1;
value_type ans = value_zero(); // 如果求最值,这里不能用zero
if(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));
if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));
return ans;
}
/// 几乎不用动
void _modify(int t, int s, int e, int a, int b, const lazy_type & delta) {
if(a <= s and e <= b) {
_dn_(t, s, e, delta);
return;
}
_pushDown(t, s, e);
int mid = (s + e) >> 1;
if(a <= mid) _modify(lson(t), s, mid, a, b, delta);
if(mid < b) _modify(rson(t), mid + 1, e, a, b, delta);
_pushUp(t);
return;
}
/// 这个函数不用动
void _pushUp(int t) {
data[t] = _up_(data[lson(t)], data[rson(t)]);
}
/// 这个函数几乎不用动
void _pushDown(int t, int s, int e) {
if(lazy_zero() == lazy[t]) return;
auto & lz = lazy[t];
auto ls = lson(t), rs = rson(t);
int mid = (s + e) >> 1;
_dn_(ls, s, mid, lz);
_dn_(rs, mid + 1, e, lz);
lz = lazy_zero();
}
/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {
static const lazy_type LAZY0 = {0, 0};
return LAZY0;
}
/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {
static const value_type VALUE0 = {INF, NINF, 0LL};
return VALUE0;
}
/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}
};
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> Depth;
SegTree St;
vector<long long> W;
vector<array<llt, 3>> Ans;
vector<vector<array<int, 3>>> 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;
Depth.assign(N + 1, 0);
W.assign(N + 1, 0);
St.init(N);
}
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){ // 递归重儿子
Depth[u] = Depth[p] + 1;
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){
St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
St.modify(Depth[New2Old[i]], Depth[New2Old[i]], {W[New2Old[i]], 1});
}
}else{ // 只有一个节点
St.modify(Depth[u], Depth[u], {W[u], 1});
}
/// 此时可以回答问题
for(const auto & pp : Questions[u]){
Ans[pp[2]] = St.query(Depth[u] + pp[0], Depth[u] + pp[1]);
}
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 清空
St.modify(1, N, {0LL, -1});
}
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 i=1;i<=n;++i) cin >> Tree.W[i];
for(int a,b,i=1;i<n;++i){
cin >> a >> b;
Tree.mkBiEdge(a, b);
}
int q; cin >> q;
Tree.Questions.assign(n + 1, {});
Tree.Ans.assign(q, {});
for(int u,a,b,i=0;i<q;++i){
cin >> u >> a >> b;
Tree.Questions[u].push_back({a, b, i});
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(const auto & i : Tree.Ans) cout << i[0] << " " << i[1] << " " << i[2] << "\n";
return 0;
}
这份代码与上一份的区别,在DSUonTree的部分,就是这里:
if(not keep){
/// 清空
St.modify(1, N, {0LL, -1});
}
不再逐个逐个节点的清空,而是一次性的清空整个线段树(注意,其实无需找出本子树深度的区间进行清空,直接清空整个即可)。当然,相应的线段树部分需要支持延迟操作。
NC235715
给定一个树,每次询问(u, k),即u子树内权值不大于k的节点数量。由于权值取值范围在 [ 1 , 1 0 6 ] [1, 10^6] [1,106],因此可以直接使用树状数组进行计数。每增加一个节点u
,就将W[u]
加1。需要查询时,只需要查询 [ 1 , k ] [1,k] [1,k]的区间和即可。清空还是一个个清空,将对应位置减一即可。
#include <bits/stdc++.h>
using namespace std;
using llt = long long;
llt const INF = 0x1F2F3F4F5F6F7F8F;
llt const NINF = -INF;
struct FenwickTree{ // 树状数组
using value_type = long long int;
using vec_type = vector<value_type>;
int n;
vec_type c;
FenwickTree() = default;
static int lowbit(int x){return x & -x;}
void init(int nn){this->c.assign((this->n=nn) + 1, 0);}
void modify(int pos, value_type delta){
for(int i=pos;i<=this->n;i+=lowbit(i)) this->c[i] += delta;
}
value_type query(int pos)const{
value_type ans = 0;
for(int i=pos;i;i-=lowbit(i)) ans += this->c[i];
return ans;
}
value_type query(int s, int e)const{return this->query(e) - this->query(s - 1);}
};
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;
FenwickTree Bt;
vector<long long> W;
vector<int> Ans;
vector<vector<array<int, 2>>> 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;
W.assign(N + 1, 0);
Bt.init(1000001);
}
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){ // 递归重儿子
// Depth[u] = Depth[p] + 1;
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){
Bt.modify(W[New2Old[i]], 1);
}
for(int i=Nodes[n.hson].mdes+1;i<=n.mdes;++i){
Bt.modify(W[New2Old[i]], 1);
}
}else{ // 只有一个节点
Bt.modify(W[u], 1);
}
/// 此时可以回答问题
for(const auto & pp : Questions[u]){
Ans[pp[1]] = Bt.query(pp[0]);
}
/// 是否清空u子树对即时数据的影响
if(not keep){
/// 清空
for(int i=n.nid;i<=n.mdes;++i){
Bt.modify(W[New2Old[i]], -1);
}
}
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 i=1;i<=n;++i) cin >> Tree.W[i];
for(int a,b,i=1;i<n;++i){
cin >> a >> b;
Tree.mkBiEdge(a, b);
}
int q; cin >> q;
Tree.Questions.assign(n + 1, {});
Tree.Ans.assign(q, {});
for(int u,a,i=0;i<q;++i){
cin >> u >> a;
Tree.Questions[u].push_back({a, i});
}
Tree.dfsHeavy(1, 0);
Tree.dfs(1, 0, true);
for(const auto & i : Tree.Ans) cout << i << "\n";
return 0;
}
小结
总体而言,DSUonTree提供了一个离线查询的框架,其基础复杂度为 O ( N log N ) O(N\log{N}) O(NlogN)。在此基础上需要解决数据的单点更新、按需查询以及清空三种操作。如果这三种操作都能在 log N \log{N} logN及其以内时间完成,应该说DSUonTree就是可用的。