树上差分等操作
树链剖分 & 树上差分
有一些相关的 树上操作
主要是写 可持久化 的时候的时候
发现
树链剖分
主要记录 重链剖分,不讲基本原理,只是题解
CF536E Tavas on the Path
好像并没有 可持久化,但是 树剖
先考虑在 序列 上做这个问题,如果直接快进到
那么我们 维护一个线段树 就可以做了,具体一点,我们维护以下 四个内容
区间 前缀
合并 的时候需要小心 顺序不能反(显然 前后缀 这东西并 不符合交换律)
inline Node operator + (const Node &a) const {
Node x;
x.L = L, x.R = a.R;
x.Ans = Ans + a.Ans - Val[Suf] - Val[a.Pre] + Val[Suf + a.Pre];
x.Pre = Ful * a.Pre + Pre;
x.Suf = a.Ful * Suf + a.Suf;
x.Ful = Ful & a.Ful;
return x;
}
然后查区间答案就行,回到树上
注意这道题是 边权,一般树剖维护 点权,比较难泵
考虑 一个点 只有一个父亲,也只有一个父亲下来的边
可以把 边权下放到它连接的深度较深的点的点权,注意这个
注意最后 查答案的时候 要 除掉
(它代表它父亲连向它的边)
然后 正常树剖,正常线段树维护,查答案就行
注意一些细节,考虑 线段树节点合并时顺序是敏感的
所以查答案时 ::Query
中容易理解
然后
但是最终路径实际上是
所以需要 交换一边 的 ::Query
的末尾容易理解
最后注意不要把
混用了... 比如 点数是
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
struct Edge {
int to, nxt, w;
} E[MAXN << 1];
int H[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;
}
namespace Tree_Cut {
int Son[MAXN], Top[MAXN], Siz[MAXN], Dfn[MAXN], Idf[MAXN];
int F[MAXN], D[MAXN];
int Cnt = 0;
inline void DFS1 (const int x, const int f) {
F[x] = f, D[x] = D[f] + 1, 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];
if (Siz[E[i].to] > Siz[Son[x]]) Son[x] = E[i].to;
}
}
inline void DFS2 (const int x, const int top) {
Top[x] = top, Dfn[x] = ++ Cnt, Idf[Cnt] = x;
if (Son[x]) DFS2 (Son[x], top);
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != F[x] && E[i].to != Son[x])
DFS2 (E[i].to, E[i].to);
}
}
int Val[MAXN];
namespace SegTree {
struct Node {
int L = 0, R = 0;
int Pre = 0, Suf = 0, Ans = 0;
bool Ful = 0;
inline Node operator + (const Node &a) const {
Node x;
x.L = L, x.R = a.R;
x.Ans = Ans + a.Ans - Val[Suf] - Val[a.Pre] + Val[Suf + a.Pre];
x.Pre = Ful * a.Pre + Pre;
x.Suf = a.Ful * Suf + a.Suf;
x.Ful = Ful & a.Ful;
return x;
}
} 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] = T[LC] + T[RC];
}
inline void Build (const int L, const int R, const int x = 1) {
T[x].L = L, T[x].R = R;
if (L == R) return ;
Build (L, M, LC), Build (M + 1, R, RC), Maintain (x);
}
inline void Modify (const int p, const int x = 1) {
if (T[x].L == T[x].R) return T[x].Ans = Val[1], T[x].Pre = T[x].Suf = T[x].Ful = 1, void ();
if (p <= M) Modify (p, LC);
else Modify (p, RC);
Maintain (x);
}
inline Node Sum (const int L, const int R, const int x = 1) {
if (L <= T[x].L && T[x].R <= R) return T[x];
Node Ans = {0, 0, 0, 0, 0, 1};
if (L <= M) Ans = Sum (L, R, LC);
if (R > M) Ans = Ans + Sum (L, R, RC);
return Ans;
}
#undef M
}
using namespace Tree_Cut;
inline SegTree::Node Query (int u, int v) {
using namespace SegTree;
Node Ans1 = {0, 0, 0, 0, 0, 1}, Ans2 = {0, 0, 0, 0, 0, 1};
while (Top[u] != Top[v]) {
if (D[Top[u]] < D[Top[v]]) swap (u, v), swap (Ans1, Ans2);
Ans1 = Sum (Dfn[Top[u]], Dfn[u]) + Ans1, u = F[Top[u]];
}
if (D[u] < D[v]) swap (u, v), swap (Ans1, Ans2);
if (Dfn[u] != Dfn[v]) Ans1 = Sum (Dfn[v] + 1, Dfn[u]) + Ans1;
return swap (Ans1.Pre, Ans1.Suf), Ans1 + Ans2;
}
struct Que {
int u, v, w, id;
inline bool operator < (const Que &a) const {
return w > a.w;
}
} G[MAXN], Q[MAXN];
int N, M;
int u, v, w;
int Ans[MAXN];
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M;
for (int i = 1; i < N; ++ i)
cin >> Val[i];
for (int i = 1; i < N; ++ i)
cin >> G[i].u >> G[i].v >> G[i].w, Add_Edge (G[i].u, G[i].v, G[i].w);
for (int i = 1; i <= M; ++ i)
cin >> Q[i].u >> Q[i].v >> Q[i].w, Q[i].id = i;
DFS1 (1, 0), DFS2 (1, 1), SegTree::Build (1, N);
for (int i = 1; i < N; ++ i)
if (D[G[i].u] > D[G[i].v]) swap (G[i].u, G[i].v);
sort (Q + 1, Q + M + 1);
sort (G + 1, G + N);
for (int i = 1, Pos = 1; i <= M; ++ i) {
while (G[Pos].w >= Q[i].w) SegTree::Modify (Dfn[G[Pos].v]), ++ Pos;
Ans[Q[i].id] = Query (Q[i].u, Q[i].v).Ans;
}
for (int i = 1; i <= M; ++ i) cout << Ans[i] << '\n';
return 0;
}
树上差分
Luogu P1600 [NOIP2016 提高组] 天天爱跑步
十分经典的一个题了
感觉老是要和去年(
形式化题意,即 给定一棵树,每个玩家沿一条路径 用相等的速度匀速行进
相等是指 不同的人速度相等
所有同时出发,询问对于 树上每个节点,特定时刻经过的玩家有多少
既然 每个玩家是同时出发的,这个时间就显得没什么意义,考虑 时间转距离
询问变成 对于树上每个点
且
模拟每个玩家的过程是 行不通的,因为对于每个玩家,路径长至多为
而这路径上 任意两点的
也就是 贡献给路径上的任意两点 所需的条件可能都不一样,不便 区间操作
于是我们还是考虑 从树上的点,即 “观察员” 下手,考虑 有多少玩家给他贡献
容易发现(
一条路径
与 共点的情况另行考虑
且
但是修改一整条路径是 困难的,可以分成
我们建立 两个桶,实时更新 特定深度的点 个数
对于
对于
注意到这里通过 一次在
减,一次在 减 的方式,使 只算了一次 这样贡献就正确了
然后为什么
这段的 贡献如此奇特? 注意到由于 时间是从起点
开始记的,于是实际上要求的始终是 但是我们现在如果只知道
咋办呢?就需要转化一下了 注意到对于一个玩家
,我们容易求出 又有
于是
,于是 所以每次 插入的是
我们
当 进入一个点
当 退出这个点
两次的值 做差,就得到 这个点子树内符合条件的点 个数了
在
处查询是 容易理解的,显然这样查出来的起点 满足 那为什么在 第二个桶的
处 查呢? 通过上面我们推的式子
就容易理解了
注意到
也可以像 下面给出的代码 一样,建立指针指向数组中间 即可(这很好用)
实现还比较简单,一遍过了
#include <bits/stdc++.h>
const int MAXN = 300005;
const int LOGN = 20;
using namespace std;
int N, M;
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 F[MAXN][LOGN], D[MAXN], LOG[MAXN];
int W[MAXN];
namespace BINLCA {
bool Init = 0;
inline void DFS (const int x, const int f) {
F[x][0] = f, D[x] = D[f] + 1;
for (int i = 1; i < LOGN; ++ i) F[x][i] = F[F[x][i - 1]][i - 1];
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x);
}
inline int LCA (int u, int v) {
if (!Init) {
DFS (1, 0), Init = 1;
for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
}
if (D[u] < D[v]) swap (u, v);
while (D[u] > D[v]) u = F[u][LOG[D[u] - D[v]]];
if (u == v) return u;
for (int i = LOGN - 1; i >= 0; -- i)
if (F[u][i] != F[v][i])
u = F[u][i], v = F[v][i];
return F[u][0];
}
#define LCA BINLCA::LCA
}
int Ans[MAXN], buk[MAXN << 2];
int * Buk1 = buk, * Buk2 = buk + (MAXN << 1);
struct Node {
int Dep, Val;
};
vector <Node> Opt1[MAXN], Opt2[MAXN];
inline void DFS (const int x, const int f) {
int Tmp = Buk1[D[x] + W[x]] + Buk2[W[x] - D[x]];
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x);
for (int i = 0; i < (int) Opt1[x].size (); ++ i) Buk1[Opt1[x][i].Dep] += Opt1[x][i].Val ;
for (int i = 0; i < (int) Opt2[x].size (); ++ i) Buk2[Opt2[x][i].Dep] += Opt2[x][i].Val;
int Aft = Buk1[D[x] + W[x]] + Buk2[W[x] - D[x]];
Ans[x] += Aft - Tmp;
}
int Dis, u, v, l, f;
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, Add_Edge (u, v);
for (int i = 1; i <= N; ++ i) cin >> W[i];
for (int i = 1; i <= M; ++ i) {
cin >> u >> v;
l = LCA (u, v), f = F[l][0], Dis = D[u] + D[v] - (D[l] << 1);
Opt1[u].push_back ({D[u], + 1});
Opt1[l].push_back ({D[u], - 1});
Opt2[v].push_back ({Dis - D[v], + 1});
Opt2[f].push_back ({Dis - D[v], - 1});
}
DFS (1, 0);
for (int i = 1; i <= N; ++ i) cout << Ans[i] << ' ';
return 0;
}
Luogu P2680 [NOIP2015 提高组] 运输计划
一开始还理解错题意了...
大致来讲,给定一棵 带权的树 和 一些路径
现在可以 把树上一条边边权变成
考虑 最值转存在,即 二分答案
我们二分这个最小长度
显然,对于 所有原始长度大于
最终使得 所有路径长度均小于
于是,我们可以 预处理出所有路径的原始长度,这里需要求
得到 深度,然后 即可 求得
对其排序,每次二分到
显然 删边不会使路径变长,故 已经小于
的路径 均无需考虑
可以想到求出它们 边覆盖的并集,找到 被 所有路径 覆盖的 最长边
若有 任一路径没有覆盖这条边,那么 删去这条边 后
那条路径 长度不变,仍然大于
,那么这样的删边 是不优的
找的过程就可以考虑 树上差分,遍历路径
在
运用 边下放到点 的
(树上每个结点 到父亲的边是唯一的)
于是
然后加上 这个点的标记,就得到了这个点(对应边)被 经过的次数
于是
显然我们再找到 最长路径(即从大到小排序后的第一条路径)
若 最长路径长减去这个最长边 小于等于
反之增大
又是一个 好写好调 的题,注意 差分时先遍历儿子再加自身 即可
#include <bits/stdc++.h>
const int MAXN = 300005;
const int LOGN = 20;
using namespace std;
struct Edge {
int to, nxt, w;
} E[MAXN << 1];
int H[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;
}
int F[MAXN][LOGN], D[MAXN], LOG[MAXN];
int N, M;
namespace BINLCA {
bool Init = 0;
inline void DFS (const int x, const int f) {
F[x][0] = f, D[x] = D[f] + 1;
for (int i = 1; i < LOGN; ++ i) F[x][i] = F[F[x][i - 1]][i - 1];
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x);
}
inline int LCA (int u, int v) {
if (!Init) {
for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
DFS (1, 0), Init = 1;
}
if (D[u] < D[v]) swap (u, v);
while (D[u] > D[v]) u = F[u][LOG[D[u] - D[v]]];
if (u == v) return u;
for (int i = LOGN - 1; i >= 0; -- i)
if (F[u][i] != F[v][i])
u = F[u][i], v = F[v][i];
return F[u][0];
}
}
struct Path {
int u, v, l, d;
inline bool operator < (const Path &a) const {
return d > a.d;
}
} Q[MAXN];
int S[MAXN], P[MAXN], Dep[MAXN];
inline void DFS1 (const int x, const int f) {
int Tmp = 0;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS1 (E[i].to, x), Tmp += P[E[i].to];
Tmp += S[x], P[x] = Tmp;
}
inline void DFS2 (const int x, const int f) {
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) Dep[E[i].to] = Dep[x] + E[i].w, DFS2 (E[i].to, x);
}
inline bool Check (const int x) {
memset (S, 0, sizeof S);
memset (P, 0, sizeof P);
int Max = 0, Cnt = 0;
for (int i = 1; i <= M; ++ i) {
if (Q[i].d <= x) break ;
Cnt ++, S[Q[i].u] ++, S[Q[i].v] ++, S[Q[i].l] -= 2;
}
DFS1 (1, 0);
for (int i = 2; i <= N; ++ i)
if (P[i] == Cnt) Max = max (Max, Dep[i] - Dep[F[i][0]]);
if (Q[1].d - Max <= x) return 1;
return 0;
}
int u, v, w, lca;
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);
DFS2 (1, 0);
for (int i = 1; i <= M; ++ i)
cin >> Q[i].u >> Q[i].v, Q[i].l = BINLCA::LCA (Q[i].u, Q[i].v), Q[i].d = Dep[Q[i].u] + Dep[Q[i].v] - (Dep[Q[i].l] << 1);
sort (Q + 1, Q + M + 1);
int L = 0, R = Q[1].d, Mid, Ans = Q[1].d;
while (R - L >= 1) {
Mid = (L + R) >> 1;
if (Check (Mid)) R = Ans = Mid;
else L = Mid + 1;
}
cout << Ans << endl;
return 0;
}
其他操作
CF383C Propagating tree
注意到修改 子树信息,于是想到
容易想到 结点深度奇偶 对修改时的权值贡献正负 有影响
考虑记录
同时 标记 也按 **操作中修改的那个点的深度奇偶 **分开成两个
然后正常 区间和,单点查 线段树维护 即可
注意下传标记的时候 奇偶 贡献,不要反了
#include <bits/stdc++.h>
const int MAXN = 200005;
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 Dfn[MAXN], Rdf[MAXN], Idf[MAXN], Dep[MAXN], Val[MAXN], Cnt = 0;
namespace SegTree {
struct Node {
int L, R;
int Siz[2] = {0, 0}, Tag[2] = {0, 0};
long long v;
} 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].Siz[0] = T[LC].Siz[0] + T[RC].Siz[0];
T[x].Siz[1] = T[LC].Siz[1] + T[RC].Siz[1];
T[x].v = T[LC].v + T[RC].v;
}
inline void Update (const int x) {
T[LC].v += 1ll * (T[LC].Siz[0] - T[LC].Siz[1]) * (T[x].Tag[0] - T[x].Tag[1]);
T[RC].v += 1ll * (T[RC].Siz[0] - T[RC].Siz[1]) * (T[x].Tag[0] - T[x].Tag[1]);
T[LC].Tag[0] += T[x].Tag[0];
T[RC].Tag[0] += T[x].Tag[0];
T[LC].Tag[1] += T[x].Tag[1];
T[RC].Tag[1] += T[x].Tag[1];
T[x].Tag[0] = T[x].Tag[1] = 0;
}
inline void Build (const int L, const int R, const int x = 1) {
T[x].L = L, T[x].R = R;
if (L == R) return T[x].v = Val[Idf[L]], T[x].Siz[Dep[Idf[L]] & 1] = 1, void ();
Build (L, M, LC), Build (M + 1, R, RC), Maintain (x);
}
inline void Modify (const int L, const int R, const bool f, const int v, const int x = 1) {
if (T[x].L > R || L > T[x].R) return ;
if (L <= T[x].L && T[x].R <= R) return T[x].Tag[f] += v, T[x].v += v * (f ? T[x].Siz[1] - T[x].Siz[0] : T[x].Siz[0] - T[x].Siz[1]), void ();
Update (x), Modify (L, R, f, v, LC), Modify (L, R, f, v, RC), Maintain (x);
}
inline long long Query (const int p, const int x = 1) {
if (T[x].L == T[x].R) return T[x].v;
Update (x);
if (p <= M) return Query (p, LC);
else return Query (p, RC);
}
#undef M
}
using namespace SegTree;
inline void DFS (const int x, const int f) {
Dfn[x] = ++ Cnt, Idf[Cnt] = x, Dep[x] = Dep[f] + 1;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x);
Rdf[x] = Cnt;
}
int N, M;
int Opt, u, v;
signed main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M;
for (int i = 1; i <= N; ++ i) cin >> Val[i];
for (int i = 2; i <= N; ++ i)
cin >> u >> v, Add_Edge (u, v);
DFS (1, 0), Build (1, N);
for (int i = 1; i <= M; ++ i) {
cin >> Opt;
if (Opt == 1) cin >> u >> v, Modify (Dfn[u], Rdf[u], Dep[u] & 1, v);
if (Opt == 2) cin >> u, cout << Query (Dfn[u]) << '\n';
}
return 0;
}
但实际上这样写 比较唐氏
考虑如果我们 分奇偶建立两颗线段树 就可以完全按 普通区间加 来维护了
一棵只对 深度为奇数 的结点 有效,另一棵对 深度为偶数 的结点有效
然后按照 单点修改 的 那个点深度奇偶,给两棵树分别加 正负权值
最后一想,区间和甚至 不用线段树,两个 树状数组 就解决了,又短又快
几乎没有细节,直接实现即可
#include <bits/stdc++.h>
const int MAXN = 200005;
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 N, M, Cnt;
int Ldf[MAXN], Rdf[MAXN], Dep[MAXN], Val[MAXN];
namespace BIT {
int T[2][MAXN];
#define lowbit(x) (x & -x)
inline void add (int x, const int v, const bool f) {
while (x <= N) T[f][x] += v, x += lowbit (x);
}
inline void Add (const int L, const int R, const int v, const bool f) {
add (L, + v, f), add (R + 1, - v, f);
}
inline int Sum (int x, const int f) {
int Ret = 0;
while (x) Ret += T[f][x], x -= lowbit (x);
return Ret;
}
}
inline void DFS (const int x, const int f) {
Ldf[x] = ++ Cnt, Dep[x] = Dep[f] + 1;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x);
Rdf[x] = Cnt;
}
int Opt, L, R;
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M;
for (int i = 1; i <= N; ++ i) cin >> Val[i];
for (int i = 2; i <= N; ++ i) cin >> L >> R, Add_Edge (L, R);
DFS (1, 0);
for (int i = 1; i <= N; ++ i) BIT::Add (Ldf[i], Ldf[i], Val[i], 0), BIT::Add (Ldf[i], Ldf[i], Val[i], 1);
for (int i = 1; i <= M; ++ i) {
cin >> Opt;
if (Opt == 1) {
cin >> L >> R;
BIT::Add (Ldf[L], Rdf[L], + R, Dep[L] % 2 == 0);
BIT::Add (Ldf[L], Rdf[L], - R, Dep[L] % 2 == 1);
}
if (Opt == 2) {
cin >> L, cout << (Dep[L] & 1 ? BIT::Sum (Ldf[L], 0) : BIT::Sum (Ldf[L], 1)) << '\n';
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!