分块 - 树分块
分块 - 树分块
Luogu P3203 [HNOI2010] 弹飞绵羊
经典的 \(LCT\) 板子题,十分滴规范,但我们今天不讲 \(LCT\)
我们用 树分块 这个俎法哈,又短又快的咩(好像不是很快哈,同学们要认真学习哈
我们把一个点能到的点当成 它的老子,那 \(N + 1\) 就是 根 的咩
显然它的编号 越跳越大 撒,我们就按 编号来分这个块块儿哈,每 \(\sqrt N\) 分一块
然后 我们想要一块一块的跳 哈,就要处理下 每个点能跳到下一块的哪个点 哈
这个东西 倒起处理,\(O(\sqrt N)\) 就能俎一块的咩
然后我们还需要记录一哈 每个点跳几下能出块,算答案的时候每跳一块就加一哈
单点修改就 暴力重构 一块就行了哈,反正最后都是 \(O(N \sqrt N)\) 的复杂度的咩!
#include <bits/stdc++.h>
const int MAXN = 200005;
const int MAXB = 900;
using namespace std;
int N, M, B;
int K[MAXN], Blk[MAXN];
int BL[MAXB], BR[MAXB];
struct Block {
int T[MAXB];
short F[MAXB];
inline void Init (const int x) {
int Tmp;
short Cnt;
memset (F, 0, sizeof F);
memset (T, 0, sizeof T);
for (int i = BR[x]; i >= BL[x]; -- i) {
Tmp = i, Cnt = 0;
while (Tmp <= BR[x] && !F[Tmp - BL[x] + 1]) Tmp = Tmp + K[Tmp], ++ Cnt;
if (Tmp > BR[x]) T[i - BL[x] + 1] = Tmp, F[i - BL[x] + 1] = Cnt;
else T[i - BL[x] + 1] = T[Tmp - BL[x] + 1], F[i - BL[x] + 1] = Cnt + F[Tmp - BL[x] + 1];
}
}
} P[MAXB];
inline void Modify (const int x, const int y) {
K[x] = y, P[Blk[x]].Init (Blk[x]);
}
inline int Query (int x) {
int Ret = 0;
while (x <= N) Ret += P[Blk[x]].F[x - BL[Blk[x]] + 1], x = P[Blk[x]].T[x - BL[Blk[x]] + 1];
return Ret;
}
int Opt, x, y;
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N, B = sqrt (N) * 2;
for (int i = 1; i <= N; ++ i) cin >> K[i];
for (int i = 1; i <= N; ++ i) {
Blk[i] = i / B + 1;
if (Blk[i] != Blk[i - 1]) BL[Blk[i]] = i, BR[Blk[i - 1]] = i - 1;
} BR[N / B + 1] = N;
for (int i = 1; i <= N / B + 1; ++ i) P[i].Init (i);
cin >> M;
for (int i = 1; i <= M; ++ i) {
cin >> Opt >> x, ++ x;
if (Opt == 1) cout << Query (x) << '\n';
if (Opt == 2) cin >> y, Modify (x, y);
}
return 0;
}
Luogu P7446 [Ynoi2007] rfplca
和 上一道题 很像的,只是求的是 \(LCA\),然后 单点修改 变成 区间减,强制在线
注意到每跳一次 所在结点编号必然变小,于是同样按照编号分块,每 \(\sqrt N\) 一块
我们发现这个 区间减 又 可以被摊掉
具体来说,当一块被减超过 \(\sqrt N\)(区间长)次的时候
那个块里的 任意点直接跳父亲必然出块,此时再对那个块区间减时 就不必要重构块乐
只需要打个 \(Tag\) 就行,单块处理复杂度 \(O(\sqrt N) \to O(1)\)
也就是每块最多 重构 \(\sqrt N\) 次,一共 \(\sqrt N\) 块,故重构部分复杂度 \(O(N \sqrt N)\)
查询的话和 倍增找 \(LCA\) 有点像,但需要 注意一些细节
以下我们称 跳出块的操作称为 跳块,跳父亲的操作称为 跳步
具体来说,我们一直执行以下三个步骤,直到跳到同一个点
- 跳块的结果 不在同一块的时候,跳块后 块编号更大的 跳块
- 跳块的结果 在同一块的时候 但不是同一结点时,跳块后 结点编号更大的 跳块
- 跳块的结果 在同一块的时候 且是是同一结点时,当前编号更大的 跳步
我们注意到我们 不能 直接判断 当前在不在同一块内 来决定 跳块还是跳步
因为可能 当前在同一块,而 跳块后的结果不在同一块
这意味着 \(LCA\) 并不在这一块,但如果此时因为 当前在同一块内 就选择 跳步
那么最后就可能被卡到 \(O(N)\) 一次查询,假掉
#include <bits/stdc++.h>
const int MAXN = 400005;
const int MAXB = 700;
using namespace std;
int N, M, B, LA = 0;
int Blk[MAXN];
int T[MAXN], F[MAXN];
int BL[MAXB], BR[MAXB], C[MAXB], Mns[MAXB];
#define Bx Blk[x]
#define By Blk[y]
inline void Update (const int x) {
if (C[Bx] <= B) T[x] = Blk[F[x]] != Bx ? F[x] : T[F[x]];
}
inline int FOB (const int x) {
return C[Bx] > B ? max (1, F[x] - Mns[Bx]) : T[x];
}
inline int FIB (const int x) {
return C[Bx] > B ? max (1, F[x] - Mns[Bx]) : F[x];
}
inline void ModBlk (const int x, const int v) {
if (C[x] > B) return Mns[x] = min (Mns[x] + v, N), void ();
for (int i = BL[x]; i <= BR[x]; ++ i) F[i] = max (F[i] - v, 1), Update (i), void ();
}
int Opt, L, R, x;
inline void Modify (const int x, const int y, const int v) {
if (Bx == By) {
for (int i = x; i <= y; ++ i) F[i] = max (F[i] - v, 1);
for (int i = x; i <= BR[Bx]; ++ i) Update (i);
return ;
}
for (int i = x; i <= BR[Bx]; ++ i) F[i] = max (F[i] - v, 1);
for (int i = x; i <= BR[Bx]; ++ i) Update (i);
for (int i = BL[By]; i <= y; ++ i) F[i] = max (F[i] - v, 1);
for (int i = BL[By]; i <= BR[By]; ++ i) Update (i);
for (int i = Bx + 1; i < By; ++ i) ModBlk (i, v), ++ C[i];
}
inline int Query2 (int x, int y) {
while (x != y) {
if (x > y) swap (x, y);
if (Blk[x] < Blk[y]) y = FOB (y);
else if (x < y) y = FIB (y);
}
return x;
}
inline int Query (int x, int y) {
if (x == y) return x;
int fx, fy;
while (x != y) {
fx = FOB (x), fy = FOB (y);
if (Blk[fx] != Blk[fy]) Blk[fx] < Blk[fy] ? y = fy : x = fx;
else if (fx ^ fy) fx > fy ? x = fx : y = fy;
else x < y ? y = FIB(y) : x = FIB (x);
}
return x;
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M, B = sqrt (N);
F[1] = T[1] = 1;
for (int i = 1; i <= N; ++ i) {
Blk[i] = i / B + 1;
if (Blk[i] != Blk[i - 1]) BL[Blk[i]] = i, BR[Blk[i - 1]] = i - 1;
} BR[N / B + 1] = N, BL[N / B + 2] = N + 1;
for (int i = 2; i <= N; ++ i) cin >> F[i], Update (i);
for (int i = 1; i <= M; ++ i) {
cin >> Opt >> L >> R;
L ^= LA, R ^= LA;
if (Opt == 1) cin >> x, x ^= LA, Modify (L, R, x);
if (Opt == 2) cout << (LA = Query (L, R)) << '\n';
}
return 0;
}
Luogu P6779 [Ynoi2009] rla1rmdq
感觉比上一个题要强一点
给定一棵树,给定任意节点序列,区间跳父亲,区间查询最小深度
感觉不好标记维护,大抵还是能想到 分块
我们先对给定的 序列分块,对每块考虑如何处理 跳父亲 和 询问 的操作
注意到我们只要 最小深度,这意味着 一个点被到达过,另一个点再跳到它时是 无意义的
若前面有一个点 \(u\) 到过 \(u'\),那么当这个点 \(v\) 到 \(u'\) 时,\(u\) 的深度严格不大于 \(v\),\(v\) 无贡献
当深度等于时意味着 \(u, v\) 共点,之后也会 一起跳,那也不需要两个分别考虑
也就是 对于每一块的操作,每个点至多被访问一次,\(O(N)\) 时间
而一共 \(O(\sqrt N)\) 块,故复杂度 \(O(N \sqrt N)\) 正确
实际实现上有一些细节,但还是简单的
注意到空间只有 \(64 ~ MB\),故空间不能用 \(O(N \sqrt N)\) 的
于是我们可以离线操作,然后 依次对每一块操作,同时合并答案,这样空间就是 \(O(N)\) 的
对于一个询问,其 区间的最小深度
必然等于 区间两端散块部分的最小深度 和 **中间整块每块的最小深度 **的 最小值
我们只需要分别算出 每块对应时刻的最小深度,暴力跑散块最小深度即可
具体来说,我们每次处理一块,维护一个 当前有意义的点集
然后 遍历所有操作,判断 当前块 是 当前操作 的 整块 / 散块 / 无交集块
当前块 是 当前操作 的 整块 即 当前块被当前操作区间 完全包含
当前块 是 当前操作 的 散块 即 当前块被当前操作区间 部分包含
当前块 是 当前操作 的 无交集块 即 当前块与当前操作区间 无交集
如果当前操作是 跳父亲操作
- 若当前块是整块,遍历 有意义点集,跳父亲,同时将 新的无意义点踢出点集,更新当前答案
就是某个点跳父亲之后发现 他的父亲被跳到过,那么这个点就无意义了
-
若当前块是散块,遍历对应区间(当前操作的一个端点 和 当前块的一个端点 组成的区间)
暴力跳 若干次 父亲,同时将 新的有意义点加入点击,更新当前答案
这里的 若干次 具体指什么?
我们记录其 整块跳跃的次数 \(Tag\),而实际上我们每次只对 当前有意义点集的点 跳父亲
也就是 无意义的点 被忽略了若干次,但我们在这里 可能需要要这部分点的真实深度
于是就得 补跳,我们记录 每个点实际上(被当作有意义点)跳的次数 \(Val_i\)
那么对于当前点,\(Tag - Val_i\) 就是它要 补跳 的次数,再加上当前跳父亲的操作
于是这里的 若干次 其实就是 \(Tag - Val_i + 1\) 次
注意到为什么会有 新的有意义点 呢?
由于这个散块操作 并不是整个块一起跳,就可能出现 原来无意义的点跳了很多次
跳到了 没有被访问过的地方,那么这个点就 变得有意义了
我们仍然保证 树上每个点只被访问一次,故变有意义这个操作 并没有破坏复杂度
- 若当前块是无交集块,显然直接忽略即可
如果当前操作是 询问最小深度操作
-
若当前块是整块,将这个询问更新到当前的答案 和 当前块 当前最小深度 取
min
即可 -
若当前块是散块,我们暴力 补跳,使得所有点更新到 当前深度,然后更新答案即可
由于这里 不需要额外的 跳父亲操作,所以跳 \(Tag - Val_i\) 次即可,没有 \(+1\)
大体做完了,唯一需要考虑的就是 \(k\) 级祖先的求法
容易发现,这道题是 有性质的,对于每个点,我们将 一路往上跳 而不会返回
于是采用 树剖求 \(k\) 级祖先,从一个点到顶 一共需要切换至多 \(\log N\) 次重链
而由于这个性质,我们就把这个 \(\log\) 给 摊掉了
也就是询问的总时间复杂度是 \(O(N \sqrt N + N \log N)\) 而不是 \(O(N \sqrt N \log N)\)
但是如果使用 倍增求 \(k\) 级祖先,显然我们就 摊不掉 这个 \(\log\),于是复杂度 \(O(N \sqrt N \log N)\)
其实卡卡常也能过,但是复杂度就会有点假,跑的慢一点
树剖实现
#include <bits/stdc++.h>
const int MAXN = 200005;
const int LOGN = 18;
const long long INF = 1e18;
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 N, M, B, rt;
int Top = 0, Tag = 0, Tot = 0;
int F[MAXN];
int SIZ[MAXN], DFN[MAXN], DEP[MAXN], SON[MAXN], TOP[MAXN], IDF[MAXN];
int LOG[MAXN], Val[MAXN], A[MAXN], Que[MAXN];
long long Dep[MAXN];
inline void DFS1 (const int x, const int f) {
DEP[x] = DEP[f] + 1, F[x] = f, SIZ[x] = 1;
for (int i = H[x]; i; i = E[i].nxt) {
if (E[i].to != f) {
Dep[E[i].to] = Dep[x] + E[i].w, DFS1 (E[i].to, x);
if (SIZ[E[i].to] > SIZ[SON[x]]) SON[x] = E[i].to;
SIZ[x] += SIZ[E[i].to];
}
}
}
inline void DFS2 (const int x, const int top) {
TOP[x] = top, DFN[x] = ++ Tot, IDF[Tot] = x;
if (!SON[x]) return ;
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);
}
inline int KthF (int x, int k) {
if (k >= DEP[x]) return rt;
while (k >= DEP[x] - DEP[TOP[x]] + 1)
k -= DEP[x] - DEP[TOP[x]] + 1, x = F[TOP[x]];
return IDF[DFN[x] - k];
}
struct Ques {
int Opt, L, R;
} Q[MAXN];
long long Ans[MAXN];
bool Vis[MAXN], InQ[MAXN];
inline void Solve (const int L, const int R) {
Tag = Top = 0;
int l = L, r = R, Tmp;
long long ans = INF;
for (int i = 1; i <= N; ++ i) Vis[i] = InQ[i] = 0, Val[i] = Que[i] = 0;
for (int i = L; i <= R; ++ i) if (!Vis[A[i]])
Que[++ Top] = i, InQ[i] = 1, Vis[A[i]] = 1, ans = min (ans, Dep[A[i]]);
for (int i = 1; i <= M; ++ i) {
l = max (Q[i].L, L), r = min (Q[i].R, R);
if (l > r) continue ;
if (Q[i].Opt == 1) {
if (l == L && r == R) {
++ Tag, Tmp = Top, Top = 0;
for (int j = 1; j <= Tmp; ++ j) {
Val[Que[j]] ++ ;
if (Vis[A[Que[j]] = F[A[Que[j]]]]) InQ[Que[j]] = 0;
else Que[++ Top] = Que[j], Vis[A[Que[j]]] = 1;
ans = min (ans, Dep[A[Que[j]]]);
}
} else {
for (int j = l; j <= r; ++ j) {
if (!Vis[A[j] = KthF (A[j], Tag - Val[j] + 1)]) {
ans = min (ans, Dep[A[j]]), Vis[A[j]] = 1;
if (!InQ[j]) Que[++ Top] = j, InQ[j] = 1;
}
Val[j] = Tag;
}
}
} else {
if (l == L && r == R) Ans[i] = min (ans, Ans[i]);
else for (int j = l; j <= r; ++ j) Ans[i] = min (Ans[i], Dep[A[j] = KthF (A[j], Tag - Val[j])]), Val[j] = Tag;
}
}
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M >> rt, B = sqrt (N) * 2;
int u, v, w;
for (int i = 1; i < N; ++ i)
cin >> u >> v >> w, Add_Edge (u, v, w);
DFS1 (rt, rt), DFS2 (rt, rt);
for (int i = 1; i <= N; ++ i) cin >> A[i];
for (int i = 1; i <= M; ++ i)
cin >> Q[i].Opt >> Q[i].L >> Q[i].R, Ans[i] = INF;
for (int i = 0; i <= N / B; ++ i)
Solve (i * B + 1, min ((i + 1) * B, N));
for (int i = 1; i <= M; ++ i)
if (Q[i].Opt == 2) cout << Ans[i] << '\n';
return 0;
}
倍增实现
#include <bits/stdc++.h>
const int MAXN = 200005;
const int LOGN = 18;
const long long INF = 1e18;
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 N, M, rt;
int Top = 0, Tag = 0;
int F[MAXN][LOGN];
int LOG[MAXN], Val[MAXN], A[MAXN], Que[MAXN];
long long Dep[MAXN];
inline void DFS (const int x, const int f, const int w) {
F[x][0] = f, Dep[x] = Dep[f] + w;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].to != f) DFS (E[i].to, x, E[i].w);
}
inline void Init () {
for (int i = 0; i <= 17; ++ i) F[rt][i] = rt;
for (int j = 1; j <= 17; ++ j)
for (int i = 1; i <= N; ++ i)
F[i][j] = F[F[i][j - 1]][j - 1];
for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
}
inline int KthF (int x, int k) {
while (k) x = F[x][LOG[k]], k = k - (1 << LOG[k]);
return x;
}
struct Ques {
int Opt, L, R;
} Q[MAXN];
long long Ans[MAXN];
bool Vis[MAXN], InQ[MAXN];
int B;
inline void Solve (const int L, const int R) {
Tag = Top = 0; // 初始化清零
int l = L, r = R, Tmp;
long long ans = INF;
for (int i = 1; i <= N; ++ i) Vis[i] = InQ[i] = 0, Val[i] = Que[i] = 0;
for (int i = L; i <= R; ++ i) if (!Vis[A[i]]) // 最开始有意义的点都加入(第一次去重)
Que[++ Top] = i, InQ[i] = 1, Vis[A[i]] = 1, ans = min (ans, Dep[A[i]]);
// 这里不考虑有没有 父子关系 的问题,因为如果存在父子均被加人有意义的集合,那么儿子跳一次之后就会被踢出
for (int i = 1; i <= M; ++ i) {
l = max (Q[i].L, L), r = min (Q[i].R, R); // 判断时 整块,散块,还是没有交
// cerr << "Now : " << i << " - Blk |" << L << "," << R << "|" << endl;
if (l > r) continue ;
if (Q[i].Opt == 1) {
if (l == L && r == R) {
++ Tag, Tmp = Top, Top = 0; // Tag 是整块跳的总次数
for (int j = 1; j <= Tmp; ++ j) {
Val[Que[j]] ++ ; // 某个点跳了的次数
if (Vis[A[Que[j]] = F[A[Que[j]]][0]]) InQ[Que[j]] = 0;
else Que[++ Top] = Que[j], Vis[A[Que[j]]] = 1;
ans = min (ans, Dep[A[Que[j]]]);
}
} else {
for (int j = l; j <= r; ++ j) {
if (!Vis[A[j] = KthF (A[j], Tag - Val[j] + 1)]) {
ans = min (ans, Dep[A[j]]), Vis[A[j]] = 1;
if (!InQ[j]) Que[++ Top] = j, InQ[j] = 1;
}
Val[j] = Tag;
}
}
} else {
if (l == L && r == R) Ans[i] = min (ans, Ans[i]);
else for (int j = l; j <= r; ++ j) Ans[i] = min (Ans[i], Dep[A[j] = KthF (A[j], Tag - Val[j])]), Val[j] = Tag;
}
}
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M >> rt, B = sqrt (N) * 2;
int u, v, w;
for (int i = 1; i < N; ++ i)
cin >> u >> v >> w, Add_Edge (u, v, w);
DFS (rt, 0, 0), Init (); // 初始化 2 ^ k 级祖先
for (int i = 1; i <= N; ++ i) cin >> A[i];
for (int i = 1; i <= M; ++ i)
cin >> Q[i].Opt >> Q[i].L >> Q[i].R, Ans[i] = INF;
for (int i = 0; i <= N / B; ++ i)
Solve (i * B + 1, min ((i + 1) * B, N)); // 离线每个块单独处理
for (int i = 1; i <= M; ++ i)
if (Q[i].Opt == 2) cout << Ans[i] << '\n';
return 0;
}