主席树
概念
主席树,可持久化(值域)线段树。
时间复杂度 \(O(\log n)\),总空间复杂度 \(O(m \log n + 4n)\)
支持维护值域线段树的操作,并且在任意历史版本的树上查询。
思想
动态开点。
暴力复制树是 \(O(4nm)\) 的。容易发现相邻版本的线段树上只有根结点到被修改的叶子结点的路径是不同的,考虑只复制这一条链,每次的复杂度是 \(O(\log n)\)
例题
P3834 【模板】可持久化线段树 2
主席树求静态区间第 \(k\) 小。
考虑用主席树维护前缀 \([1, i]\) 内值域中每一个值的个数。容易看到两棵树的结构一致(含共用结点),则对于同一位置的结点 \(k_r, k_{l - 1}\),只含 \([l, r]\) 贡献时 \(k\) 的值为 \(k_r\) 的值减去 \(k_l\) 的值。考虑将 \([1, r]\) 的树和 \([1, l]\) 的树相减,然后在线段树上二分。
注意离散化。
P3157 [CQOI2011]动态逆序对
带修主席树模板。
考虑修改第 \(p\) 个位置。容易发现普通主席树维护的是区间信息的前缀和,因此暴力修改的复杂度高。不妨用树状数组的套路,每次修改第 \(p\) 棵线段树,第 \(p + \operatorname{lowbit}(p)\) 棵线段树……以此类推。本质上是对于每一个结点的不同版本用树状数组维护前缀和。然后查询时直接查询第 \(p\) 棵线段树,第 \(p - \operatorname{lowbit}(p)\) 棵线段树……以此类推。
回到此题。可以先求出原序列的逆序对数量。每次删去一个数,前面比它大的数和后面比它小的数都会和它构成一组逆序对。直接减去原数组中符合条件的数会统计到已经删除的数,因此可以用主席树维护区间内被删除的某一值域内的树,然后加上其贡献即可。
时间复杂度 \(O(n \log^2 n)\)
P2633 Count on a tree
树上问题套主席树。
考虑用主席树维护树上从根到结点 \(i\) 的路径。令 \(f_u\) 为结点 \(u\) 的父亲,则 \(u, v\) 间的树上路径贡献为:
\(g(u) + g(v) - g(\operatorname{lca}(u, v)) - g(f_{\operatorname{lca}(u, v)})\)
然后直接在主席树上询问区间第 \(k\) 大。
P3168 [CQOI2015]任务查询系统
区修单查主席树。
考虑维护差分数组,转化成单点修改区间求和。
HDU4348 To the Moon
题意:
-
将区间 \([l, r]\) 中所有值加上 \(d\)
-
查询当前版本 \([l, r]\) 值的和
-
查询第 \(t\) 个版本 \([l, r]\) 值的和
-
回溯到第 \(t\) 个版本
区间修改主席树。
标记永久化。考虑每次仅在被完全覆盖的结点处更新值和 \(lazy\),每次查询时累加从线段树根结点到当前父结点的 \(lazy\) 之和。
int update(int pre, int l, int r, int ql, int qr, int d)
{
int k = ++cnt;
ls[k] = ls[pre];
rs[k] = rs[pre];
sum[k] = sum[pre];
lazy[k] = lazy[pre];
if ((l >= ql) && (r <= qr))
{
lazy[k] += d;
sum[k] += (1ll * d * (r - l + 1));
return k;
}
int mid = (l + r) >> 1;
if (ql <= mid) ls[k] = update(ls[pre], l, mid, ql, qr, d);
if (qr > mid) rs[k] = update(rs[pre], mid + 1, r, ql, qr, d);
sum[k] = sum[ls[k]] + sum[rs[k]] + (r - l + 1) * lazy[k];
return k;
}
ll query(int k, int l, int r, int ql, int qr, ll lazys)
{
if ((l >= ql) && (r <= qr)) return sum[k] + (r - l + 1) * lazys;
int mid = (l + r) >> 1;
ll res = 0;
if (ql <= mid) res += query(ls[k], l, mid, ql, qr, lazys + lazy[k]);
if (qr > mid) res += query(rs[k], mid + 1, r, ql, qr, lazys + lazy[k]);
return res;
}
代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 2e5 + 5;
int n, m, q, cnt;
int a[maxn], b[maxn], rk[maxn], t[maxn];
int lson[maxn << 5], rson[maxn << 5], sum[maxn << 5];
inline int read()
{
int res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (flag == '-')
flag = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
inline void write(int x)
{
if (x < 0)
{
putchar('-');
x = -x;
}
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
inline int build(int l, int r)
{
cnt++;
if (l < r)
{
int mid = (l + r) >> 1;
lson[cnt] = build(l, mid);
rson[cnt] = build(mid + 1, r);
}
return cnt;
}
inline int update(int pre, int l, int r, int x)
{
int rt = ++cnt;
lson[rt] = lson[pre];
rson[rt] = rson[pre];
sum[rt] = sum[pre] + 1;
if (l < r)
{
int mid = (l + r) >> 1;
if (x <= mid)
lson[rt] = update(lson[pre], l, mid, x);
else
rson[rt] = update(rson[pre], mid + 1, r, x);
}
return rt;
}
inline int query(int x, int y, int l, int r, int k)
{
if (l >= r)
return l;
int size = sum[lson[x]] - sum[lson[y]];
int mid = (l + r) >> 1;
if (k <= size)
return query(lson[x], lson[y], l, mid, k);
else
return query(rson[x], rson[y], mid + 1, r, k - size);
}
int main()
{
int l, r, k;
n = read();
q = read();
for (int i = 1; i <= n; i++)
{
a[i] = read();
b[i] = a[i];
}
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
t[0] = build(1, m);
for (int i = 1; i <= n; i++)
{
rk[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
t[i] = update(t[i - 1], 1, m, rk[i]);
}
for (int i = 1; i <= q; i++)
{
l = read();
r = read();
k = read();
write(b[query(t[r], t[l - 1], 1, m, k)]);
puts("");
}
return 0;
}