树上启发式合并
树上启发式合并
即
是一种很暴力的东西捏
可以说是 重儿子 性质的一种 利用手段,结构复杂度是
一般用于处理 不带修的子树查询 问题(
其利用了 儿子子树信息 可以 继承到父亲 上这个性质
但由于 儿子之间子树信息多是不可合并的,也就是说 切换儿子 时 统计的信息 需要清空
于是当遍历完一个结点的 所有儿子 时,最多 只能选择 其中一个儿子的子树信息 保留
这个选择就 十分重要,考虑到重儿子
而最终计算当前节点的答案时将其它子树的信息 重新统计
若对于 一个结点,计算答案 操作是
证明 可以从 每个点被访问了多少次 这个方向入手
注意到我们每次会保留 重儿子子树,重新统计 轻儿子子树
而一个点 到根的路径中 至多有
也就是说,每个点只会在至多
这
个点就是每次 重链链头的父亲,即 切换点
于是一共
CF600E Lomsat gelral
板子题,又调半天...
套用上方的流程,用数组记录 每个颜色出现的次数
同时两个变量记录 颜色最大出现次数 和 最大次数颜色编号和
先计算 轻儿子答案,删除贡献,然后算 重儿子答案,保留贡献
暴力加上轻儿子的贡献,计算当前点答案 即可
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
struct Edge {
int to, nxt;
} E[MAXN << 1];
int H[MAXN], tot;
inline void Add_Edge (const int u, const int v) {
E[++ tot] = {v, H[u]}, H[u] = tot;
E[++ tot] = {u, H[v]}, H[v] = tot;
}
int Siz[MAXN], Son[MAXN];
inline void DFS1 (const int x, const int f) {
Siz[x] = 1;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f)
DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Siz[E[i].to] > Siz[Son[x]] ? Son[x] = E[i].to : Son[x];
}
int Cnt[MAXN], Col[MAXN];
long long Ans[MAXN];
long long Sum;
int Max;
inline void Add (const int x, const int f) {
Cnt[Col[x]] ++ ;
if (Cnt[Col[x]] > Max) Sum = Col[x], Max = Cnt[Col[x]];
else if (Cnt[Col[x]] == Max) Sum += Col[x];
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Add (E[i].to, x);
}
inline void Del (const int x, const int f) {
Cnt[Col[x]] -- ;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Del (E[i].to, x);
}
inline void DFS2 (const int x, const int f) {
if (!x) return ;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
DFS2 (E[i].to, x), Del (E[i].to, x), Sum = Max = 0;
if (Son[x]) DFS2 (Son[x], x);
Cnt[Col[x]] ++ ;
if (Cnt[Col[x]] > Max) Sum = Col[x], Max = Cnt[Col[x]];
else if (Cnt[Col[x]] == Max) Sum += Col[x];
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
Add (E[i].to, x);
Ans[x] = Sum;
}
int N;
int u, v;
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N;
for (int i = 1; i <= N; ++ i) cin >> Col[i];
for (int i = 2; i <= N; ++ i) cin >> u >> v, Add_Edge (u, v);
DFS1 (1, 0), DFS2 (1, 0);
for (int i = 1; i <= N; ++ i) cout << Ans[i] << ' ';
return 0;
}
CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
据说是 树上启发式合并 的 发明者 出的题?确实 很启发
考虑 回文串 的性质,若想 重排得到回文串,则 至多有
要求 最长路径,显然把路径长差分成
可以想到 点分治的思想,开一个桶记录 每种情况下的最大深度,然后合并
为什么这道题不能直接用 点分治?
注意到我们要求 每个点子树内 的 最长路径
而 点分治 并 不能保证 按 原树的子树结构 来遍历(每次递归到子树的 重心)
不能正确求出答案
具体什么是 每种情况?
考虑
压缩成一个 unsigned int
表示,而这样的 字符出现情况 一共
为什么这么做?
我们设一条路径上 字符出现情况 表示为数
容易发现,若这条路径是
即
二进制下存在 至多一位为 ,即 至多 种字符出现奇数次
显然,异或可差分,于是我们可以记录 从根到每个点 路径的 字符出现情况
于是
又考虑
于是我们在
回到题目,由于 路径长 的计算需要
故容易想到 遍历每个点作为
先考虑 简单暴力,我们尝试 枚举当前点
遍历并查询 对应桶 内是否有值,即 能否与其它儿子子树中的点构成合法路径
更新答案,此处路径显然经过 当前点
然后用 这棵儿子子树所有点的
注意,我们需要更新所有满足
于是这一步操作的 时间复杂度 是
处理完 当前点
显然,这样的时间复杂度是
但是这个结构我们很熟悉,就是上面 启发式合并 的结构,可以用同样的办法优化
即 先递归求所有轻儿子答案,清空信息,然后 求重儿子答案,保留信息
暴力添加轻儿子贡献,计算当前点答案,即可把时间复杂度优化到
看上去挺卡的其实,启发式合并常数也蛮大,但是 实际上不卡常
细节极少,适合用来熟悉 启发式合并 的基本结构
#include <bits/stdc++.h>
const int MAXN = 500005;
const int MAXK = (1 << 22) + 5;
using namespace std;
struct Edge {
int to, nxt;
} E[MAXN << 1];
int H[MAXN], F[MAXN], tot;
inline void Add_Edge (const int u, const int v) {
E[++ tot] = {v, H[u]}, H[u] = tot;
E[++ tot] = {u, H[v]}, H[v] = tot;
}
char Val[MAXN];
int Siz[MAXN], Son[MAXN], Dep[MAXN];
unsigned int Dis[MAXN];
inline void DFS1 (const int x, const int f) {
Siz[x] = 1, Dep[x] = Dep[F[x] = f] + 1;
if (x > 1) Dis[x] = Dis[f] ^ (1 << (Val[x] - 'a'));
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f)
DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Siz[E[i].to] > Siz[Son[x]] ? Son[x] = E[i].to : Son[x];
}
int Max[MAXK], Ans[MAXN];
struct Node {
unsigned int dis;
int dep;
};
vector <Node> P;
inline void Del (const int x, const int f) {
for (int i = 0; i <= 21; ++ i) Max[Dis[x] ^ (1 << i)] = 0;
Max[Dis[x]] = 0;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Del (E[i].to, x);
}
inline void Add (const int x, const int f) {
P.push_back ({Dis[x], Dep[x]});
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Add (E[i].to, x);
}
inline void DFS2 (const int x, const int f) {
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
DFS2 (E[i].to, x), Del (E[i].to, x), Ans[x] = max (Ans[x], Ans[E[i].to]);
if (Son[x]) DFS2 (Son[x], x), Ans[x] = max (Ans[x], Ans[Son[x]]);
if (Max[Dis[x]]) Ans[x] = max (Ans[x], Max[Dis[x]] - Dep[x]);
for (int i = 0; i <= 21; ++ i) Max[Dis[x] ^ (1 << i)] = max (Max[Dis[x] ^ (1 << i)], Dep[x]);
Max[Dis[x]] = max (Max[Dis[x]], Dep[x]);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x]) {
P.clear (), Add (E[i].to, x);
for (auto k : P) if (Max[k.dis]) Ans[x] = max (Ans[x], k.dep + Max[k.dis] - (Dep[x] << 1));
for (auto k : P) {
for (int p = 0; p <= 21; ++ p)
Max[k.dis ^ (1 << p)] = max (Max[k.dis ^ (1 << p)], k.dep);
Max[k.dis] = max (Max[k.dis], k.dep);
}
}
}
int N;
int x;
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N;
for (int i = 2; i <= N; ++ i)
cin >> x >> Val[i], Add_Edge (x, i);
DFS1 (1, 0), DFS2 (1, 0);
for (int i = 1; i <= N; ++ i) cout << Ans[i] << ' ';
return 0;
}
支配对
考虑一些 点对统计 的问题
存在点对
并且它们对最终答案有 一样贡献时
我们可以声称 它们存在 支配关系,其中
通常称一个点对为 支配对 当且仅当 它没有被任何其它点对支配
我们可以只考虑 计数支配对 即可
树上的支配对问题常见的就是下面的 树上保留区间点 问题
一般树上问题与 编号在区间内的点两两间
常见思路即 启发式合并,加入点
显然,前驱后继产生的
最为接近
这样一共可以找出
具体可以看下面例题
Luogu P7880 [Ynoi2006] rldcot
给定一棵 带权树,多次询问 区间,求 区间内任意点对的
即询问
不妨设
则 统计到
显然,
容易证明,若
则
现在考虑如何求出
显然,我们可以遍历儿子子树,维护一个 std::set
去 存储之前儿子子树里的点
同时 拿出当前儿子子树的点,在 std::set
里查找 前驱后继 即可
但是 不同儿子子树 的 std::set
显然不能 继承,每次暴力遍历子树就是
于是又可以考虑 启发式合并 的结构,求解 轻儿子,清空,求解 重儿子,保留,暴力加轻儿子
得到所有支配对之后,离线询问,与 支配对 同样按 左端排序
扫描线扫左端点,每次对 所有相同深度支配对 的右端点取
我们刚刚求的是
的 支配对,可能有 深度相同的 情况
然后 直接查询问右端点 即可,注意深度需要预处理后 离散化,然后要开 long long
还是比较好写的,不卡常
#include <bits/stdc++.h>
const int MAXN = 100005;
const int MAXQ = 500005;
using namespace std;
struct Edge {
int to, nxt, w;
} E[MAXN << 1];
int H[MAXN], F[MAXN], tot;
inline void Add_Edge (const int u, const int v, const int w) {
E[++ tot] = {v, H[u], w}, H[u] = tot;
E[++ tot] = {u, H[v], w}, H[v] = tot;
}
struct Que {
int R, id;
};
struct Inv {
int R, C;
};
int N, M;
namespace BIT {
int T[MAXN << 1];
#define lowbit(x) (x & -x)
inline void Add (int x) {
while (x <= N + 2) ++ T[x], x += lowbit (x);
}
inline void Del (int x) {
while (x <= N + 2) -- T[x], x += lowbit (x);
}
inline int Sum (int x) {
int Ret = 0;
while (x) Ret += T[x], x -= lowbit (x);
return Ret;
}
}
int Siz[MAXN], Son[MAXN], Pos[MAXN], dep[MAXN], Ans[MAXQ];
long long Dep[MAXN];
inline void DFS1 (const int x, const int f, const int w = 0) {
Siz[x] = 1, Dep[x] = Dep[F[x] = f] + w;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f)
DFS1 (E[i].to, x, E[i].w), Siz[x] += Siz[E[i].to], Siz[E[i].to] > Siz[Son[x]] ? Son[x] = E[i].to : Son[x];
}
vector <Inv> P[MAXN];
vector <Que> Q[MAXN];
set <int> S;
inline void AddNode (const int x, const int lcadep) {
auto it = S.lower_bound (x);
if (it != S.end ()) P[x].push_back ({* it, lcadep});
if (it != S.begin ()) -- it, P[* it].push_back ({x, lcadep});
}
inline void Calc (const int x, const int f, const int lcadep) {
AddNode (x, lcadep);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Calc (E[i].to, x, lcadep);
}
inline void Add (const int x, const int f) {
S.insert (x);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Add (E[i].to, x);
}
inline void DFS2 (const int x, const int f) {
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
DFS2 (E[i].to, x), S.clear ();
if (Son[x]) DFS2 (Son[x], x), AddNode (x, dep[x]);
P[x].push_back ({x, dep[x]}), S.insert (x);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
Calc (E[i].to, x, dep[x]), Add (E[i].to, x);
}
int Cnt;
long long Tmp[MAXN];
inline void Unique (int * Des, long long * Src) {
for (int i = 1; i <= N; ++ i) Tmp[i] = Src[i];
sort (Src + 1, Src + N + 1);
Cnt = unique (Src + 1, Src + N + 1) - Src - 1;
for (int i = 1; i <= Cnt; ++ i) Pos[i] = N + 1;
for (int i = 1; i <= N; ++ i) Des[i] = lower_bound (Src + 1, Src + Cnt + 1, Tmp[i]) - Src;
}
int u, v, w;
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M;
for (int i = 2; i <= N; ++ i)
cin >> u >> v >> w, Add_Edge (u, v, w);
DFS1 (1, 0), Unique (dep, Dep), DFS2 (1, 0);
for (int i = 1; i <= M; ++ i)
cin >> u >> v, Q[u].push_back ({v, i});
for (int i = N; i >= 1; -- i) {
for (auto p : P[i]) BIT::Del (Pos[p.C]), Pos[p.C] = min (Pos[p.C], p.R), BIT::Add (Pos[p.C]);
for (auto q : Q[i]) Ans[q.id] = BIT::Sum (q.R);
}
for (int i = 1; i <= M; ++ i) cout << Ans[i] << '\n';
return 0;
}
Luogu P8528 [Ynoi2003] 铃原露露
和上面那个东西的套路很像啊
加了一个 排列 的套路,直接在 插入点集的时候插
AddNode
的时候往 std::vector
里面推
考虑
区间子区间问题,实质上限制在一个 二维平面的矩形 上(左右端点分别一个维度)
于是我们可以考虑 扫描线扫一维,然后用 线段树维护一维
扫 左右端点 是 等价的,但是扫 右端点 的话扫描线就是 从左往右的,赢!
我们考虑如下 三种 大小关系,可以钦定
,不用去单独考虑 ,显然只需要 即可 ,容易发现,右端点 时,左端点 不能取 ,容易发现,右端点 时,左端点 哪儿都不能取(不能取 )
于是我们对于每对
扫描线扫到对应点时,把不能取的部分在 线段树上区间标记
我们需要统计的就是 没有被标记到的部分,又由于我们实际上要一个 子矩形内的信息
于是线段树需要维护 历史信息(扫描线 + 时间维 = 矩形)
可以考虑把区间标记变成 区间加,容易发现,矩形内的数始终非负
于是统计 子矩形内 没有标记的点 就是统计 子矩形内
写一个板子就行
然后上面说的是 对于每对
于是考虑 支配对,和上面那道题 一模一样,枚举
然后找 不同儿子子树内的 前后缀点 即可(注意到这里的排序按
好写不卡常!但是代码较长
#include <bits/stdc++.h>
const int MAXN = 200005;
using namespace std;
int N, Q;
int Cnt = 0;
int A[MAXN], U[MAXN];
struct Edge {
int to, nxt;
} E[MAXN << 1];
int H[MAXN], F[MAXN], tot;
inline void Add_Edge (const int u, const int v) {
E[++ tot] = {v, H[u]}, H[u] = tot;
E[++ tot] = {u, H[v]}, H[v] = tot;
}
int Siz[MAXN], Son[MAXN];
inline void DFS1 (const int x, const int f) {
Siz[x] = 1;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f)
DFS1 (E[i].to, x), Siz[x] += Siz[E[i].to], Siz[E[i].to] > Siz[Son[x]] ? Son[x] = E[i].to : Son[x];
}
struct Node {
int L, R, v;
};
struct Ques {
int L, id;
};
std::set <int> S;
std::vector <Node> P[MAXN];
std::vector <Ques> I[MAXN];
inline void InsNode (const int x, const int y, const int z) {
if (z < x) P[y].push_back ({z + 1, x, 1}), ++ Cnt;
if (z > y) P[y].push_back ({1, x, 1}), P[z].push_back ({1, x, -1}), Cnt += 2;
}
inline void AddNode (const int x, const int z) {
auto it = S.lower_bound (A[x]);
if (it != S.end ()) InsNode (A[x], * it, A[z]);
if (it != S.begin ()) -- it, InsNode (* it, A[x], A[z]);
}
inline void Add (const int x, const int f, const int z) {
AddNode (x, z);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Add (E[i].to, x, z);
}
inline void Ins (const int x, const int f) {
S.insert (A[x]);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Ins (E[i].to, x);
}
inline void DFS2 (const int x, const int f) {
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
DFS2 (E[i].to, x), S.clear ();
if (Son[x]) DFS2 (Son[x], x);
S.insert (A[x]), AddNode (x, x);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f && E[i].to != Son[x])
Add (E[i].to, x, x), Ins (E[i].to, x);
}
namespace SegTree {
struct node {
int L, R;
int Min, Tag, Cnt, Ctg;
long long Sum;
} T[MAXN << 2];
#define LC (x << 1)
#define RC (x << 1 | 1)
#define M ((T[x].L + T[x].R) >> 1)
inline void Maintain (const int x) {
T[x].Sum = T[LC].Sum + T[RC].Sum;
T[x].Min = min (T[LC].Min, T[RC].Min);
T[x].Cnt = (T[LC].Min == T[x].Min) * T[LC].Cnt + (T[RC].Min == T[x].Min) * T[RC].Cnt;
}
inline void PutAdd (const int x, const int tag) {
T[x].Min += tag, T[x].Tag += tag;
}
inline void PutCnt (const int x, const int tag) {
T[x].Sum += 1ll * tag * T[x].Cnt, T[x].Ctg += tag;
}
inline void Update (const int x) {
if (T[x].Tag) PutAdd (LC, T[x].Tag), PutAdd (RC, T[x].Tag);
if (T[x].Ctg && T[x].Min == T[LC].Min) PutCnt (LC, T[x].Ctg);
if (T[x].Ctg && T[x].Min == T[RC].Min) PutCnt (RC, T[x].Ctg);
T[x].Tag = T[x].Ctg = 0;
}
inline void Build (const int L, const int R, const int x = 1) {
T[x].L = L, T[x].R = R, T[x].Cnt = R - L + 1;
if (L == R) return ;
Build (L, M, LC), Build (M + 1, R, RC), Maintain (x);
}
inline void Add (const int L, const int R, const int v, const int x = 1) {
if (L > T[x].R || T[x].L > R) return ;
if (L <= T[x].L && T[x].R <= R) return PutAdd (x, v);
Update (x), Add (L, R, v, LC), Add (L, R, v, RC), Maintain (x);
}
inline void Upd (const int L, const int R, const int x = 1) {
if (L > T[x].R || T[x].L > R) return ;
if (L <= T[x].L && T[x].R <= R) return (T[x].Min == 0) ? PutCnt (x, 1) : void ();
Update (x), Upd (L, R, LC), Upd (L, R, RC), Maintain (x);
}
inline long long Que (const int L, const int R, const int x = 1) {
if (L > T[x].R || T[x].L > R) return 0;
if (L <= T[x].L && T[x].R <= R) return T[x].Sum;
Update (x); return Que (L, R, LC) + Que (L, R, RC);
}
}
int L, R;
long long Ans[MAXN];
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> Q;
for (int i = 1; i <= N; ++ i)
cin >> A[i], U[A[i]] = i;
for (int i = 2; i <= N; ++ i)
cin >> F[i], Add_Edge (F[i], i);
DFS1 (1, 0), DFS2 (1, 0), SegTree::Build (1, N);
for (int i = 1; i <= Q; ++ i)
cin >> L >> R, I[R].push_back ({L, i});
for (int i = 1; i <= N; ++ i) {
for (auto p : P[i]) SegTree::Add (p.L, p.R, p.v);
SegTree::Upd (1, i);
for (auto p : I[i]) Ans[p.id] = SegTree::Que (p.L, i);
}
for (int i = 1; i <= Q; ++ i) cout << Ans[i] << '\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具