零、写在前面
可持久化线段树学习起来难度并不大,如果学过可持久化Trie,了解怎样复用旧版本的信息,只通过少量空间维护信息的修改,其实很容易理解可持久化线段树的原理。
对于普通线段树,我们可能是维护序列,维护权值,但是可持久化之后,其实我们掌握的信息更多了,可以解决很多看起来只能暴力解决的问题。
一、可持久化线段树
1.1 可持久化线段树
支持访问历史版本的线段树(基于动态开点)。
暴力实现:每次修改或查询都创建一颗新的线段树,这样空间复杂度O(4n * n),显然不行。
例如数组初值:8 5 7 2
把版本0 位置4 修改为6
把版本1 位置2 修改为1
但线段树之所以高效就在于每次修改涉及路径上的节点数目为O(log)级别, 也就是说,新版本的线段树和旧版本线段树有着大量的相同节点,如果我们和旧版本共用这些旧节点,只对被修改的节点开辟新结点。
这种策略下,前面的修改表示为:
这样我们的空间复杂度是否能够降低?时间复杂度如何?
空间复杂度:
- 每次只修改 logn+1 个节点,故只增加修改的节点即可。
- 初始建树需开 2n - 1个节点。n 次修改,每次修改最多开 logn + 1 个节点。所以节点总数为 2n + n(logn + 1) = n(logn + 3)
动态开点:
不再用 p * 2和 p * 2 + 1代表左右儿子,每个节点记录左右儿子的编号。
1.2 模板——静态区间第 k小
原题链接
P3834 【模板】可持久化线段树 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
就是给了你一个数组,没有修改操作,然后查询任意区间第k小。(普通莫队可过)
以这道题为例,来介绍一下可持久化线段树的实现代码
节点定义
- 每一个版本都是一颗权值线段树,这里采用动态开点
struct Node {
int l, r; // 左右儿子编号
int sum;
} pool[N * 22]; // 节点内存池
// 动态开点
int newNode() {
static int tot = 0;
return tot ++;
}
O(n)建树
void build(int &p, int l, int r) {
p = newNode();
if (l == r) {
return;
}
int m = (l + r) / 2;
build(pool[p].l, l, m);
build(pool[p].r, m + 1, r);
}
点修
- 每次修改都会新开一个版本的线段树,所以 x = newNode();
- y 是上一个版本线段树的节点
- x 继承 y
- 然后沿着路径修改,没有涉及到的节点和y 共用
- 时间复杂度: O(logn)
void modify(int &x, int y, int l, int r, int v) {
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (v <= m) {
modify(pool[x].l, pool[y].l, l, m, v);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, v);
}
}
区间第 k 小查询
- 查询 [L, R] 的第k 小
- 初始y 为 版本R 根节点,x 为版本L - 1 根节点
- 那么递归过程中,对于当前区间 [l, r] 内 的元素数目就是 pool[y].sum - pool[x].sum(前缀和)
- 我们根据 元素数目 和 k 的关系到对应区间查询即可
- 时间复杂度:O(logn)
// kth
int query(int x, int y, int l, int r, int k) {
if (l == r) {
return l;
}
int m = (l + r) / 2;
int sum = pool[pool[y].l].sum - pool[pool[x].l].sum;
if (sum >= k) {
return query(pool[x].l, pool[y].l, l, m, k);
} else {
return query(pool[x].r, pool[y].r, m + 1, r, k - sum);
}
}
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 200000;
struct Node {
int l, r;
int sum;
} pool[N * 22];
int newNode() {
static int tot = 0;
return tot ++;
}
void build(int &p, int l, int r) {
p = newNode();
if (l == r) {
return;
}
int m = (l + r) / 2;
build(pool[p].l, l, m);
build(pool[p].r, m + 1, r);
}
void modify(int &x, int y, int l, int r, int v) {
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (v <= m) {
modify(pool[x].l, pool[y].l, l, m, v);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, v);
}
}
// kth
int query(int x, int y, int l, int r, int k) {
if (l == r) {
return l;
}
int m = (l + r) / 2;
int sum = pool[pool[y].l].sum - pool[pool[x].l].sum;
if (sum >= k) {
return query(pool[x].l, pool[y].l, l, m, k);
} else {
return query(pool[x].r, pool[y].r, m + 1, r, k - sum);
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::vector<int> p(a);
std::ranges::sort(p);
p.resize(std::unique(p.begin(), p.end()) - p.begin());
for (int &x : a) {
x = std::lower_bound(p.begin(), p.end(), x) - p.begin();
}
int U = p.size();
std::vector<int> root(n + 1);
build(root[0], 0, U - 1);
for (int i = 1; i <= n; ++ i) {
modify(root[i], root[i - 1], 0, U - 1, a[i - 1]);
}
while (m --) {
int l, r, k;
std::cin >> l >> r >> k;
-- l, -- r;
std::cout << p[query(root[l], root[r + 1], 0, U - 1, k)] << '\n';
}
return 0;
}
二、OJ练习
2.1 P1383 高级打字机
原题链接
思路分析
显然每次修改 / undo 都要新开一个版本,每个版本的线段树维护当前的文本序列,节点内存了区间字符数目,叶子节点保存了这个位置的字符。
对于修改操作:
- 当前区间 [l, r],如果 [l, mid] 内有空位,即 sum < mid - l + 1,那么往左区间插入
- 否则往有区间插入
对于undo操作:
很简单,copy 一份 若干个版本前的线段树即可(只需copy 根节点)
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 1E5;
struct Node {
int sum;
char ch;
int l, r;
} pool[22 * N];
int newNode() {
static int tot = 0;
return ++ tot;
}
void pull(int p) {
pool[p].sum = pool[pool[p].l].sum + pool[pool[p].r].sum;
}
void modify(int &x, int y, int l, int r, char ch) {
x = newNode();
pool[x] = pool[y];
if (l == r) {
pool[x].sum = 1;
pool[x].ch = ch;
return;
}
int m = (l + r) / 2;
if (pool[x].sum < m - l + 1) {
modify(pool[x].l, pool[y].l, l, m, ch);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, ch);
}
pull(x);
}
char query(int x, int l, int r, int k) {
if (l == r) {
return pool[x].ch;
}
int m = (l + r) / 2;
if (pool[pool[x].l].sum >= k) {
return query(pool[x].l, l, m, k);
} else {
return query(pool[x].r, m + 1, r, k - pool[pool[x].l].sum);
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> root{0};
for (int i = 0; i < n; ++ i) {
char op;
std::cin >> op;
switch (op) {
case 'T': {
char ch;
std::cin >> ch;
root.emplace_back();
modify(root.back(), root[root.size() - 2], 0, n - 1, ch);
break;
}
case 'U': {
int x;
std::cin >> x;
root.push_back(root[root.size() - x - 1]);
break;
}
case 'Q': {
int x;
std::cin >> x;
std::cout << query(root.back(), 0, n - 1, x) << '\n';
break;
}
}
}
return 0;
}
2.2 P1972 [SDOI2009] HH的项链
原题链接
思路分析
本题非常经典,建议也写一下树状数组离线的做法,很有帮助。
和树状数组离线的处理方式类似,只不过可持久化seg的做法更为直观。
每个贝壳数组下标在前一个版本基础上新开一颗线段树。
第 i 个版本的线段树维护了每种贝壳在 [1, i] 中的最右位置
这样我们查询 [l, r] 内的贝壳种类,只需版本r 所有 >= l 位置的权值和即可。
因为版本r 线段树所有元素的最右位置不超过r,我们又限制搜索区间不会向左跨越,所以保证了正确性。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 1E6;
struct Node {
int sum;
int l, r;
} pool[40 * N];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k, int v) {
x = newNode();
pool[x] = pool[y];
pool[x].sum += v;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (k <= m) {
modify(pool[x].l, pool[y].l, l, m, k, v);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}
}
int query(int x, int l, int r, int k) {
if (l == r) {
return pool[x].sum;
}
int m = (l + r) / 2;
if (k <= m) {
return query(pool[x].l, l, m, k) + pool[pool[x].r].sum;
} else {
return query(pool[x].r, m + 1, r, k);
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> root(n + 1);
constexpr int M = 1E6;
std::vector<int> last(M + 1, -1);
for (int i = 1, t; i <= n; ++ i) {
int x;
std::cin >> x;
if (~last[x]) {
modify(t, root[i - 1], 0, n - 1, last[x], -1);
modify(root[i], t, 0, n - 1, i - 1, 1);
} else {
modify(root[i], root[i - 1], 0, n - 1, i - 1, 1);
}
last[x] = i - 1;
}
int m;
std::cin >> m;
while (m --) {
int l, r;
std::cin >> l >> r;
std::cout << query(root[r], 0, n - 1, l - 1) << '\n';
}
return 0;
}
2.3 P2468 [SDOI2010] 粟粟的书架
原题链接
思路分析
注意本题数据范围,只有 R = 1的时候,列数 会达到5E5量级
对于R > 1的测试点,我们二分+二位前缀和可以轻松解决
对于R = 1的测试点,矩阵退化成了序列,我们考虑可持久化线段树
每个下标对应一个版本的权值线段树,即维护每种厚度的书的出现次数
那么查询的时候我们贪心的优先拿取厚的书,这样可以保证书的数量最少。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 5E5;
struct Node {
int sum;
int siz;
int l, r;
} pool[25 * N];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k) {
x = newNode();
pool[x] = pool[y];
pool[x].sum += k;
++ pool[x].siz;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (k <= m) {
modify(pool[x].l, pool[y].l, l, m, k);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, k);
}
}
int query(int x, int y, int l, int r, int k) {
if (l == r) {
return (k + l - 1) / l;
}
int s = pool[pool[x].r].sum - pool[pool[y].r].sum;
int m = (l + r) / 2;
if (s >= k) {
return query(pool[x].r, pool[y].r, m + 1, r, k);
} else {
return query(pool[x].l, pool[y].l, l, m, k - s) + pool[pool[x].r].siz - pool[pool[y].r].siz;
}
}
void work1(int n, int m, int q) {
std::vector<int> a(m);
for (int i = 0; i < m; ++ i) {
std::cin >> a[i];
}
const int M = std::ranges::max(a);
std::vector<int> root(m + 1);
for (int i = 1; i <= m; ++ i) {
root[i] = newNode();
modify(root[i], root[i - 1], 1, M, a[i - 1]);
}
while (q --) {
int x1, y1, x2, y2, H;
std::cin >> x1 >> y1 >> x2 >> y2 >> H;
if (pool[root[y2]].sum - pool[root[y1 - 1]].sum < H) {
std::cout << "Poor QLW\n";
continue;
}
std::cout << query(root[y2], root[y1 - 1], 1, M, H) << '\n';
}
}
int psum[201][201][1001], psiz[201][201][1001];
void work2(int n, int m, int q) {
std::vector<std::vector<int>> g(n, std::vector<int>(m));
int M = 0;
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
std::cin >> g[i][j];
M = std::max(M, g[i][j]);
}
}
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
for (int k = 1; k <= M; ++ k) {
psum[i + 1][j + 1][k] = psum[i + 1][j][k] + psum[i][j + 1][k] - psum[i][j][k] + (g[i][j] >= k) * g[i][j];
psiz[i + 1][j + 1][k] = psiz[i + 1][j][k] + psiz[i][j + 1][k] - psiz[i][j][k] + (g[i][j] >= k);
}
}
}
auto get = [&](int x1, int y1, int x2, int y2, int k, int t) -> int {
-- x1, -- y1;
if (t == 0) {
return psum[x2][y2][k] - psum[x1][y2][k] - psum[x2][y1][k] + psum[x1][y1][k];
} else {
return psiz[x2][y2][k] - psiz[x1][y2][k] - psiz[x2][y1][k] + psiz[x1][y1][k];
}
};
while (q --) {
int x1, y1, x2, y2, H;
std::cin >> x1 >> y1 >> x2 >> y2 >> H;
int lo = 0, hi = M;
while (lo < hi) {
int x = (lo + hi + 1) / 2;
if (get(x1, y1, x2, y2, x, 0) >= H) {
lo = x;
} else {
hi = x - 1;
}
}
if (lo == 0) {
std::cout << "Poor QLW\n";
continue;
}
std::cout << get(x1, y1, x2, y2, lo, 1) - (get(x1, y1, x2, y2, lo, 0) - H) / lo << '\n';
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, q;
std::cin >> n >> m >> q;
if (n == 1) {
work1(n, m, q);
} else {
work2(n, m, q);
}
return 0;
}
2.4 P2633 Count on a tree
原题链接
思路分析
树上前缀和 + 可持久化seg
关于树上前缀和:树上前缀和详解-CSDN博客
我们对树做dfs,每个节点u以父亲节点p为旧版本开辟新版本线段树,线段树维护的是权值,即从根节点到 u 路径上点权出现次数
那么对于查询 u, v, k
我们怎样得知 元素 x 在u 到 v路径上的出现次数?根据树上前缀和:cnt(u, x) + cnt(v, x) - cnt(lca, x) - cnt(parent(lca), x)
但现在要查询第k小,我们类似于静态区间第k 小的方式在线段树上搜索即可:
- 四树联查:u, v, lca(u, v), parent(lca(u, v))
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 1E5;
struct Node {
int l, r;
int sum;
} pool[N * 22];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int v) {
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (v <= m) {
modify(pool[x].l, pool[y].l, l, m, v);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, v);
}
}
int query(int u, int v, int x, int y, int l, int r, int k) {
if (l == r) {
return l;
}
int s = pool[pool[v].l].sum + pool[pool[u].l].sum - pool[pool[x].l].sum - pool[pool[y].l].sum;
int m = (l + r) / 2;
if (k <= s) {
return query(pool[u].l, pool[v].l, pool[x].l, pool[y].l, l, m, k);
} else {
return query(pool[u].r, pool[v].r, pool[x].r, pool[y].r, m + 1, r, k - s);
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::vector<int> p(a);
std::ranges::sort(p);
p.resize(std::unique(p.begin(), p.end()) - p.begin());
const int M = p.size();
for (int &x : a) {
x = std::ranges::lower_bound(p, x) - p.begin();
}
std::vector<std::vector<int>> adj(n);
for (int i = 1; i < n; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
adj[u].push_back(v);
adj[v].push_back(u);
}
constexpr int B = 18;
std::vector<std::array<int, B>> f(n);
std::vector<int> dep(n), root(n);
auto dfs = [&](auto &&self, int u, int p) -> void{
modify(root[u], ~p ? root[p] : 0, 0, M - 1, a[u]);
for (int v : adj[u]) {
if (v != p) {
dep[v] = dep[u] + 1;
f[v][0] = u;
for (int i = 1; i < B; ++ i) {
f[v][i] = f[f[v][i - 1]][i - 1];
}
self(self, v, u);
}
}
};
dfs(dfs, 0, -1);
auto LCA = [&](int u, int v) -> int {
if (dep[u] < dep[v]) std::swap(u, v);
for (int i = B - 1; ~i; -- i)
if (dep[f[u][i]] >= dep[v])
u = f[u][i];
if (u == v)
return u;
for (int i = B - 1; ~i; -- i)
if (f[u][i] != f[v][i]) {
u = f[u][i];
v = f[v][i];
}
return f[u][0];
};
int last = 0;
while (m --) {
int u, v, k;
std::cin >> u >> v >> k;
u ^= last;
-- u, -- v;
int lca = LCA(u, v);
last = p[query(root[u], root[v], root[lca], lca ? root[f[lca][0]] : 0, 0, M - 1, k)];
std::cout << last << '\n';
}
return 0;
}
2.5 P3302 [SDOI2013] 森林
原题链接
思路分析
和上一题类似,对于树上路径的第k小查询我们可以处理,但是这道题多了加边操作。
对于加边操作我们:启发式合并,即小树合到大树身上,同时dfs维护倍增数组,用于求lca
合并途中,维护小树节点新版本线段树。
合并部分:每次O(log)
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 8E4;
struct Node {
int l, r;
int sum;
} pool[N * 150];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int v) {
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
if (v <= m) {
modify(pool[x].l, pool[y].l, l, m, v);
} else {
modify(pool[x].r, pool[y].r, m + 1, r, v);
}
}
int query(int u, int v, int x, int y, int l, int r, int k) {
if (l == r) {
return l;
}
int s = pool[pool[v].l].sum + pool[pool[u].l].sum - pool[pool[x].l].sum - pool[pool[y].l].sum;
int m = (l + r) / 2;
if (k <= s) {
return query(pool[u].l, pool[v].l, pool[x].l, pool[y].l, l, m, k);
} else {
return query(pool[u].r, pool[v].r, pool[x].r, pool[y].r, m + 1, r, k - s);
}
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int _;
std::cin >> _;
int n, m, t;
std::cin >> n >> m >> t;
std::vector<int> a(n + 1), root(n + 1);
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
}
std::vector<int> p(a);
std::ranges::sort(p);
p.resize(std::unique(p.begin(), p.end()) - p.begin());
const int M = p.size() - 1;
for (int &x : a) {
x = std::ranges::lower_bound(p, x) - p.begin();
}
std::vector<std::vector<int>> adj(n + 1);
for (int i = 0; i < m; ++ i) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
constexpr int B = 18;
std::vector<std::array<int, B>> f(n + 1);
std::vector<int> dep(n + 1), siz(n + 1), top(n + 1);
auto dfs = [&](auto &&self, int u, int p, int t) -> void {
top[u] = t;
++ siz[t];
modify(root[u], root[p], 1, M, a[u]);
f[u][0] = p;
dep[u] = dep[p] + 1;
for (int i = 1; i < B; ++ i) {
f[u][i] = f[f[u][i - 1]][i - 1];
}
for (int v : adj[u]) {
if (v == p) continue;
self(self, v, u, t);
}
};
auto LCA = [&](int u, int v) -> int {
if (dep[u] < dep[v]) std::swap(u, v);
for (int i = B - 1; ~i; -- i)
if (dep[f[u][i]] >= dep[v])
u = f[u][i];
if (u == v)
return u;
for (int i = B - 1; ~i; -- i)
if (f[u][i] != f[v][i]) {
u = f[u][i];
v = f[v][i];
}
return f[u][0];
};
for (int i = 1; i <= n; ++ i) {
if (!top[i]) {
dfs(dfs, i, 0, i);
}
}
int last = 0;
while (t --) {
char op;
std::cin >> op;
if (op == 'Q') {
int u, v, k;
std::cin >> u >> v >> k;
u ^= last;
v ^= last;
k ^= last;
int lca = LCA(u, v);
last = p[query(root[u], root[v], root[lca], root[f[lca][0]], 1, M, k)];
std::cout << last << '\n';
} else {
int u, v;
std::cin >> u >> v;
u ^= last;
v ^= last;
adj[u].push_back(v);
adj[v].push_back(u);
if (siz[top[u]] > siz[top[v]]) {
dfs(dfs, v, u, top[u]);
} else {
dfs(dfs, u, v, top[v]);
}
}
}
return 0;
}
2.6 P2839 [国家集训队] middle
原题链接
思路分析
本题中位数下标上取整
如果查询的是 [l, r] 内的中位数怎么做?
二分,中位数x满足 大于等于 x 的数目 - 小于x 的数目 ∈ [0, 1]
现在加上了左右端点的区间限制
我们考虑这么做:
将原数组排序(记得保存下标),遍历排序后的数组
每个数x开一个线段树,维护出现位置,x前面的数出现的位置权值为-1,剩下的其他为1,同时维护区间最大前后缀权值和
这样我们对于查询,仍然二分中位数
对于查询 a, b, c, d
只要 sum(b + 1, c - 1) + lsum(a, b) + rsum(c, d) >= 0,我们就右边界左移,否则左区间右移
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 2E4;
constexpr int inf = 1E9;
struct Info {
int sum = 0;
int lsum = -inf;
int rsum = -inf;
};
Info operator+ (const Info &x, const Info &y) {
return {
x.sum + y.sum,
std::max(x.lsum, x.sum + y.lsum),
std::max(y.rsum, x.rsum + y.sum)
};
}
struct Node {
int l, r;
Info info;
} pool[N * 25];
int newNode() {
static int tot = 0;
return ++ tot;
}
void pull(int p) {
pool[p].info = pool[pool[p].l].info + pool[pool[p].r].info;
}
void modify(int &x, int y, int l, int r, int k, int v) {
if (k < l || k > r) {
return;
}
x = newNode();
pool[x] = pool[y];
if (l == r) {
pool[x].info = {v, v, v};
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, k, v);
modify(pool[x].r, pool[y].r, m + 1, r, k, v);
pull(x);
}
Info rangeQuery(int p, int l, int r, int x, int y) {
if (x > r || y < l) {
return Info();
}
if (x <= l && r <= y) {
return pool[p].info;
}
int m = (l + r) / 2;
return rangeQuery(pool[p].l, l, m, x, y) + rangeQuery(pool[p].r, m + 1, r, x, y);
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> arr(n);
for (int i = 0; i < n; ++ i) {
std::cin >> arr[i];
}
std::vector<int> p(n);
std::iota(p.begin(), p.end(), 0);
std::ranges::sort(p, {}, [&](int x) {
return arr[x];
});
std::vector<int> root(n + 1);
auto build = [&](auto &&self, int &p, int l, int r){
if (!p) {
p = newNode();
}
if (l == r) {
pool[p].info = {1, 1, 1};
return;
}
int m = (l + r) / 2;
self(self, pool[p].l, l, m);
self(self, pool[p].r, m + 1, r);
pull(p);
};
build(build, root[0], 0, n - 1);
for (int i = 1; i <= n; ++ i) {
modify(root[i], root[i - 1], 0, n - 1, p[i - 1], -1);
}
int q;
std::cin >> q;
int last = 0;
while (q --) {
std::array<int, 4> Q;
std::cin >> Q[0] >> Q[1] >> Q[2] >> Q[3];
for (int &x : Q) {
x = (x + last) % n;
}
std::ranges::sort(Q);
int a = Q[0], b = Q[1], c = Q[2], d = Q[3];
int lo = 0, hi = n;
while (lo < hi) {
int x = (lo + hi + 1) / 2;
int s = 0;
if (b + 1 < c) {
s += rangeQuery(root[x], 0, n - 1, b + 1, c - 1).sum;
}
s += rangeQuery(root[x], 0, n - 1, a, b).rsum;
s += rangeQuery(root[x], 0, n - 1, c, d).lsum;
if (s >= 0) {
lo = x;
} else {
hi = x - 1;
}
}
last = arr[p[lo]];
std::cout << last << '\n';
}
return 0;
}
2.7 P3168 [CQOI2015] 任务查询系统
原题链接
思路分析
很简单,以时间为版本号维护可持久化线段树,每个线段树维护了当前时刻存在的任务优先级的出现次数
具体操作我们将输入区间按照开始时间分组,结束时间 + 1分组,类似差分的思想去建树即可
查询操作也非常简单,具体见代码。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 1E5;
std::vector<int> o;
struct Node {
int l, r;
int cnt;
i64 sum;
} pool[N * 40];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k, int v) {
if (l > k || r < k) {
return;
}
x = newNode();
pool[x] = pool[y];
pool[x].cnt += v;
pool[x].sum += v * o[k];
if (l == r) {
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, k, v);
modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}
i64 rangeQuery(int p, int l, int r, int k) {
if (pool[p].cnt <= k) {
return pool[p].sum;
}
if (k <= 0) {
return 0;
}
if (l == r) {
return pool[p].sum / pool[p].cnt * k;
}
int m = (l + r) / 2;
return rangeQuery(pool[p].l, l, m, k) + rangeQuery(pool[p].r, m + 1, r, k - pool[pool[p].l].cnt);
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int m, n;
std::cin >> m >> n;
std::vector<std::vector<int>> st(n + 1), ed(n + 1);
std::vector<int> a(m);
for (int i = 0; i < m; ++ i) {
int s, e, p;
std::cin >> s >> e >> p;
-- s, -- e;
st[s].push_back(i);
ed[e + 1].push_back(i);
a[i] = p;
o.push_back(p);
}
std::ranges::sort(o);
o.resize(std::unique(o.begin(), o.end()) - o.begin());
int U = o.size();
for (int &x : a) {
x = std::ranges::lower_bound(o, x) - o.begin();
}
std::vector<int> root(n);
for (int i = 0; i < n; ++ i) {
root[i] = i > 0 ? root[i - 1] : 0;
for (int x : st[i]) {
modify(root[i], root[i], 0, U - 1, a[x], 1);
}
for (int x : ed[i]) {
modify(root[i], root[i], 0, U - 1, a[x], -1);
}
}
i64 last = 1;
for (int i = 0; i < n; ++ i) {
int t, x, y, z;
std::cin >> t >> x >> y >> z;
-- t;
int k = 1 + (x * last % z + y) % z;
std::cout << (last = rangeQuery(root[t], 0, U - 1, k)) << '\n';
}
return 0;
}
2.8 P3293 [SCOI2016] 美味
原题链接
思路分析
先看看能不能不用字典树做出这个前置题目:421. 数组中两个数的最大异或值 - 力扣(LeetCode)
其实就是试填法求两个数最大异或值的 加强版。
按位考虑:
当前考虑到第 i 位
如果 bi 当前位为 0
前面已经确定的位为msk
我们异或后如果该位为1,那么 aj + xi 该位为 1,即 aj + xi ∈ [msk | 1 << i, msk | ((1 << (i + 1)) - 1)]
bi 当前位为 1 的情况类似。
可持久化树每个版本对应原序列下标,然后每个版本是一个权值线段树。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 2E5;
struct Node {
int l, r;
int sum;
} pool[N * 22];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int v) {
if (l > v || r < v) {
return;
}
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, v);
modify(pool[x].r, pool[y].r, m + 1, r, v);
}
int rangeQuery(int p, int q, int l, int r, int x, int y) {
if (x > y) {
return 0;
}
if (x > r || y < l) {
return 0;
}
if (x <= l && r <= y) {
return pool[p].sum - pool[q].sum;
}
int m = (l + r) / 2;
return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n), root(n + 1);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
const int M = std::ranges::max(a);
for (int i = 0; i < n; ++ i) {
modify(root[i + 1], root[i], 0, M, a[i]);
}
constexpr int B = 19;
for (int i = 0; i < m; ++ i) {
int b, x, l, r;
std::cin >> b >> x >> l >> r;
int msk = 0;
for (int j = B - 1; j >= 0; -- j) {
int lo = msk, hi = msk;
if (!(b >> j & 1)) {
lo |= 1 << j;
hi |= 1 << j;
}
hi |= ((1 << j) - 1);
lo -= x;
hi -= x;
if (rangeQuery(root[r], root[l - 1], 0, M, std::max(0, lo), std::min(hi, M))) {
msk |= (!(b >> j & 1)) << j;
} else {
msk |= (b >> j & 1) << j;
}
}
std::cout << (b ^ msk) << '\n';
}
return 0;
}
2.9 P3755 [CQOI2017] 老C的任务
原题链接
思路分析
CDQ 分治的方法已经介绍过:CDQ分治详解,一维、二维、三维偏序-CSDN博客
以x 坐标作为版本号,每个x 坐标开一个线段树,维护这条竖线上各个y 坐标的功率和
那么对于查询就很板了,就是在对应x 坐标区间的两个线段树在指定 y 区间内进行功率和的差分
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
constexpr int N = 1E5;
struct Node {
int l, r;
i64 sum;
} pool[N * 25];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k, int v) {
if (k < l || k > r) {
return;
}
x = newNode();
pool[x] = pool[y];
pool[x].sum += v;
if (l == r) {
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, k, v);
modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}
i64 rangeQuery(int p, int q, int l, int r, int x, int y) {
if (l > r || x > y) {
return 0;
}
if (y < l || x > r) {
return 0;
}
if (x <= l && r <= y) {
return pool[p].sum - pool[q].sum;
}
int m = (l + r) / 2;
return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> x(n), y(n), p(n);
for (int i = 0; i < n; ++ i) {
std::cin >> x[i] >> y[i] >> p[i];
}
auto dy = y;
std::ranges::sort(dy);
dy.resize(std::unique(dy.begin(), dy.end()) - dy.begin());
for (int &v : y) {
v = std::ranges::lower_bound(dy, v) - dy.begin();
}
const int M = dy.size();
std::vector<int> o(n);
std::iota(o.begin(), o.end(), 0);
std::ranges::sort(o, {}, [&](int i){
return x[i];
});
std::vector<int> root(n + 1);
for (int i = 0; i < n; ++ i) {
modify(root[i + 1], root[i], 0, M - 1, y[o[i]], p[o[i]]);
}
while (m --) {
int x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
int R = std::ranges::upper_bound(o, x2, {}, [&](int i){
return x[i];
}) - o.begin();
int L = std::ranges::lower_bound(o, x1, {}, [&](int i){
return x[i];
}) - o.begin();
int Y = std::ranges::upper_bound(dy, y2) - dy.begin() - 1;
int X = std::ranges::lower_bound(dy, y1) - dy.begin();
std::cout << rangeQuery(root[R], root[L], 0, M - 1, X, Y) << '\n';
}
return 0;
}
2.10 P3567 [POI2014] KUR-Couriers
原题链接
思路分析
以下标为版本号开权值线段树
对于查询二分即可
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
constexpr int N = 5e5;
struct Node{
int sum;
int l, r;
} pool[30 * N];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k) {
if (k < l || k > r) {
return;
}
x = newNode();
pool[x] = pool[y];
++ pool[x].sum;
if (l == r) {
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, k);
modify(pool[x].r, pool[y].r, m + 1, r, k);
}
int rangeQuery(int p, int q, int l, int r, int len) {
if (pool[p].sum - pool[q].sum <= len / 2) {
return 0;
}
if (l == r) {
return l;
}
int m = (l + r) / 2;
return std::max(rangeQuery(pool[p].l, pool[q].l, l, m, len), rangeQuery(pool[p].r, pool[q].r, m + 1, r, len));
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::vector<int> root(n + 1);
const int L = std::ranges::min(a);
const int R = std::ranges::max(a);
for (int i = 0; i < n; ++ i) {
modify(root[i + 1], root[i], L, R, a[i]);
}
while (m --) {
int l, r;
std::cin >> l >> r;
std::cout << rangeQuery(root[r], root[l - 1], L, R, r - l + 1) << '\n';
}
return 0;
}
2.11 P3899 [湖南集训] 更为厉害
原题链接
思路分析
可以线段树合并来做,该天写个线段树合并的博客。
对于查询 p,k
如果 b 为 p 的祖先,那么方案数就是 min(d[p] - 1, k) * (size[p] - 1)
如果 b 为 p 的后代,那么方案数为 所有距离合法的后代的 size - 1 的和
那么我们按dfs序开线段树,维护每个深度的权值和,因为子树内dfs序连续,第二部分贡献我们查询 dfn[p] 和 dfn[p] + size[p] - 1两个线段树关于指定深度区间的差分和即可。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
constexpr int N = 3E5;
struct Node{
i64 sum;
int l, r;
} pool[30 * N];
int newNode() {
static int tot = 0;
return ++ tot;
}
void modify(int &x, int y, int l, int r, int k, int v) {
if (k < l || k > r) {
return;
}
x = newNode();
pool[x] = pool[y];
pool[x].sum += v;
if (l == r) {
return;
}
int m = (l + r) / 2;
modify(pool[x].l, pool[y].l, l, m, k, v);
modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}
i64 rangeQuery(int p, int q, int l, int r, int x, int y) {
if (x > y || x > r || y < l) {
return 0;
}
if (x <= l && r <= y) {
return pool[p].sum - pool[q].sum;
}
int m = (l + r) / 2;
return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
std::cin >> n >> q;
std::vector<std::vector<int>> adj(n);
for (int i = 1; i < n; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
adj[u].push_back(v);
adj[v].push_back(u);
}
std::vector<int> root(n), siz(n, 1), d(n), dfn(n), seq(n);
int cur = 0;
auto dfs = [&](auto &&self, int u, int p) -> void {
dfn[u] = cur ++;
seq[dfn[u]] = u;
for (int v : adj[u]) {
if (v == p) {
continue;
}
d[v] = d[u] + 1;
self(self, v, u);
siz[u] += siz[v];
}
};
dfs(dfs, 0, -1);
const int M = std::ranges::max(d);
for (int i = 0; i < n; ++ i) {
modify(root[i], i > 0 ? root[i - 1] : 0, 0, M, d[seq[i]], siz[seq[i]] - 1);
}
while (q --) {
int p, k;
std::cin >> p >> k;
-- p;
std::cout << std::min(d[p], k) * (siz[p] - 1LL) + rangeQuery(root[dfn[p] + siz[p] - 1], root[dfn[p]], 0, M, d[p] + 1, std::min(M, d[p] + k)) << '\n';
}
return 0;
}