树状数组套权值树学习笔记
【前言】
树状数组套权值树是众多树套树中的一种。
我个人认为它是“性价比”最高的一种,不论从代码实现复杂度还是常数大小来看。
它主要用于解决二维偏序问题,可以发现这同样是 整体二分 / CDQ 分治 的功能。
与这两者相比它的优点是容易理解,代码实现简单无脑。
缺点是相对来讲空间复杂度高,常数也比较大,经常被上面两种方法吊打。
前置芝士:
- 树状数组 / 线段树等具有同样功能的数据结构。
- 权值线段树。
不会的或许可以康康这里。
值得提一句的是,有人将这种方法称为树状数组套主席树,因为主席树的定义不是很明确。
甚至有 Dalao 真的套主席树(可持久化权值树),实际上是完全没有必要的。
因为树状数组本身就记录了 \(\log n\) 棵权值树的状态,就不再需要主席树维护每一个历史状态了。
再使用主席树只会徒增空间开销,这个后面会有详细分析。
【主要思想】
权值树是满足可 加/减 性的数据结构。
先来看一个问题:
求区间第 \(k\) 小,支持单点修改。
一个 native 的思想是每个点建立一个权值线段树,维护数值上的前缀和。
然后每次修改时修改序列 \(i\sim n\) 上的权值树,然后直接类似主席树查询。
可惜这样是 \(O(n^2\log n)\) 的,时空都不太行。
但是有了树状数组,一切都好办了。
我们对树状数组上每个位置所代表的的区间建立一棵权值树(动态开点)。
在修改时对应树状数组的修改,查询时也类似树状数组的查询。
关键主要思想它就这么多。
【基本操作】
那一道我认为比较简单的题入门:动态逆序对。
求一个序列的逆序对,支持删除元素。
某一位形成的逆序对数是:下标在这个数之前,且比这个数大的数的个数。
这形成了天然的二维偏序关系,可以转化为二维数点的模型。
主要思路是,先求出总的逆序对数量,然后每删去一个数,就 减去前面比它大 的和 后面比它小的 数的数量。
我们来一个一个看实现技巧。
【单点修改】
void push_up(int p){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat;
}
int Update(int p, int l, int r, int k, int v){
if(!p) p = ++ tot;
if(l == r) {t[p].dat += v; return p;}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p); return p;
}
void Change(int p, int v){
for(int i = p; i <= n; i += i & -i)
rt[i] = Update(rt[i], 1, n, a[p], v);
}
注意要动态开点,然后在树状数组的对应位置修改即可。
其中 \(p\) 是需要修改的位置,可以发现其实就是用树状数组维护第一维偏序,权值树维护第二维偏序。
【区间查询】
int Query(int p, int l, int r, int ql, int qr){
if(ql > qr || !p) return 0;
if(ql <= l && r <= qr) return t[p].dat;
int mid = (l + r) >> 1, sum = 0;
if(ql <= mid) sum += Query(t[p].l, l, mid, ql, qr);
if(qr > mid) sum += Query(t[p].r, mid + 1, r, ql, qr);
return sum;
}
int Get(int l, int r, int ql, int qr){
cnt1 = cnt2 = 0; int sum = 0;
for(int i = r; i; i -= i & -i)
sum += Query(rt[i], 1, n, ql, qr);
for(int i = l - 1; i; i -= i & -i)
sum -= Query(rt[i], 1, n, ql, qr);
return sum;
}
每道题的代码都略有不同,但是主体相似。
利用树状数组天然维护了第一维偏序,然后在权值树上查询第二维偏序。
这里利用了差分思想,就是用区间 \([1,r]\) 减去区间 \([1,l-1]\) 就是区间 \([l,r]\) 的答案。
有些题目不是统计,而是查询区间 \(k\) 小相关问题,那么就需要提前把所有树状数组上的相关位存下来。
然后在权值树上二分,这些之后都会见到。
【代码实现】
总体代码贴一下。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
typedef long long LL;
int n, m, cnt1, cnt2, tot, a[N], tmp1[N], tmp2[N], pos[N], rt[N];
struct Tree{int l, r, dat;} t[N * 100];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
void push_up(int p){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat;
}
int Update(int p, int l, int r, int k, int v){
if(!p) p = ++ tot;
if(l == r) {t[p].dat += v; return p;}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p); return p;
}
void Change(int p, int v){
for(int i = p; i <= n; i += i & -i)
rt[i] = Update(rt[i], 1, n, a[p], v);
}
int Query(int p, int l, int r, int ql, int qr){
if(ql > qr || !p) return 0;
if(ql <= l && r <= qr) return t[p].dat;
int mid = (l + r) >> 1, sum = 0;
if(ql <= mid) sum += Query(t[p].l, l, mid, ql, qr);
if(qr > mid) sum += Query(t[p].r, mid + 1, r, ql, qr);
return sum;
}
int Get(int l, int r, int ql, int qr){
cnt1 = cnt2 = 0; int sum = 0;
for(int i = r; i; i -= i & -i)
sum += Query(rt[i], 1, n, ql, qr);
for(int i = l - 1; i; i -= i & -i)
sum -= Query(rt[i], 1, n, ql, qr);
return sum;
}
int main(){
n = read(), m = read();
LL ans = 0;
for(int i = 1; i <= n; i ++){
a[i] = read(), Change(i, 1);
ans += Get(1, i - 1, a[i] + 1, n);
pos[a[i]] = i;
}
for(int i = 1; i <= m; i ++){
printf("%lld\n", ans);
int x = read();
ans -= Get(1, pos[x] - 1, x + 1, n);
ans -= Get(pos[x] + 1, n, 1, x - 1);
Change(pos[x], -1);
}
return 0;
}
【时空复杂度】
这里假设查询、序列长度、值域大小都同级。(值域偏大时可以离散化变为同级)
那么显然时间复杂度是树状数组和权值树的总和时间 \(O(n\log^2 n)\),还是很优秀的。
对于空间复杂度,我们需要使用动态开点。
对于每次插入和修改操作,树状数组预处理涉及到 \(\log n\) 棵树,
然后每一步修改只涉及到一条链,也就是只需要 \(\log n\) 的空间。
在最坏情况下,空间的复杂度为 \(O(n\log^2 n)\)。
当然实际情况远小于这一上界。
值得注意的是,在未知值域的情况下,这一算法是无法在线的,这是相比线段树套平衡树它的劣势。
同时,这一算法的空间开销很大,比赛时需要权衡利弊,谨慎开空间。
【简单例题】
【模板题】
有 \(n\) 个元素,第 \(i\) 个元素有 \(a_i,b_i,c_i\) 三个属性。
设 \(f(i)\) 表示满足 \(a_j \leq a_i\) 且 \(b_j \leq b_i\) 且 \(c_j \leq c_i\) 且 \(j \ne i\) 的 \(j\) 的数量。
对于 \(d \in [0, n)\),求 \(f(i) = d\) 的数量。
通过排序可以将一维偏序去掉,然后就是模板式的二维偏序裸题了。
而且这里只需要单点查询即可。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 200010;
int n, T, tot, cnt, f[N], ans[N], rt[N], tmp[N];
struct node{int a, b, c;} p[N];
struct Tree{int l, r, dat;} t[N*50];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
bool cmp(node x, node y) {return x.c < y.c;}
void push_up(int p){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat;
}
int Update(int p, int l, int r, int k, int v){
if(!p) p = ++ tot;
if(l == r) {t[p].dat += v; return p;}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p); return p;
}
void Insert(int p, int k, int v){
for(int i = p; i <= T; i += i & -i)
rt[i] = Update(rt[i], 1, T, k, v);
}
int Query(int l, int r, int k){
if(l == r){
int sum = 0;
for(int i = 1; i <= cnt; i ++)
sum += t[tmp[i]].dat;
return sum;
}
int mid = (l + r) >> 1, sum = 0;
if(k <= mid){
for(int i = 1; i <= cnt; i ++) tmp[i] = t[tmp[i]].l;
return Query(l, mid, k);
}
else{
for(int i = 1; i <= cnt; i ++) sum += t[t[tmp[i]].l].dat, tmp[i] = t[tmp[i]].r;
return sum + Query(mid + 1, r, k);
}
}
int Get(int p, int k){
cnt = 0;
for(int i = p; i; i -= i & -i)
tmp[++ cnt] = rt[i];
return Query(1, T, k);
}
int main(){
n = read(), T = read();
for(int i = 1; i <= n; i ++)
p[i].a = read(), p[i].b = read(), p[i].c = read();
sort(p + 1, p + n + 1, cmp);
for(int i = 1; i <= n;){
int j = i;
while(p[i].c == p[j].c) Insert(p[j].a, p[j].b, 1), j ++;
j = i;
while(p[i].c == p[j].c) f[j] = Get(p[j].a, p[j].b), j ++;
i = j;
}
for(int i = 1; i <= n; i ++)
ans[f[i]] ++;
for(int i = 1; i <= n; i ++)
printf("%d\n", ans[i]);
return 0;
}
【简单题】
支持单点修改,查区间前驱后继,以及区间 \(k\) 小和区间排名。
这道题的传统做法是 \(O(n\log^3 n)\) 的线段树套平衡树,然而本算法可以更优秀地解决。
本质上还是和上一题一样,但是因为有区间 \(k\) 小,要用到 提前把所有树状数组上的相关位存下来 的方法。
参考代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N = 50010;
int n, m, tot, T, cnt1, cnt2;
int tmp1[N], tmp2[N], rt[N], a[N], b[N*2];
struct Tree{int l, r, dat;} t[N * 100];
struct Query{int opt, l, r, k;} q[N];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
void push_up(int p){t[p].dat = t[t[p].l].dat + t[t[p].r].dat;}
int Update(int p, int l, int r, int k, int v){
if(!p) p = ++ tot;
if(l == r) {t[p].dat += v; return p;}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p);
return p;
}
void Change(int p, int v){
for(int i = p; i <= n; i += i & -i)
rt[i] = Update(rt[i], 1, T, a[p], v);
}
int Query_Num(int l, int r, int k){
if(l == r) return l;
int mid = (l + r) >> 1, sum = 0;
for(int i = 1; i <= cnt1; i ++) sum += t[t[tmp1[i]].l].dat;
for(int i = 1; i <= cnt2; i ++) sum -= t[t[tmp2[i]].l].dat;
if(k <= sum){
for(int i = 1; i <= cnt1; i ++) tmp1[i] = t[tmp1[i]].l;
for(int i = 1; i <= cnt2; i ++) tmp2[i] = t[tmp2[i]].l;
return Query_Num(l, mid, k);
}
else{
for(int i = 1; i <= cnt1; i ++) tmp1[i] = t[tmp1[i]].r;
for(int i = 1; i <= cnt2; i ++) tmp2[i] = t[tmp2[i]].r;
return Query_Num(mid + 1, r, k - sum);
}
}
// 提前存储。
int Get_Num(int l, int r, int k){
cnt1 = cnt2 = 0;
for(int i = r; i; i -= i & -i)
tmp1[++ cnt1] = rt[i];
for(int i = l - 1; i; i -= i & -i)
tmp2[++ cnt2] = rt[i];
return Query_Num(1, T, k);
}
int Query_Rank(int l, int r, int v){
if(l == r) return 1;
int mid = (l + r) >> 1, sum = 0;
if(v <= mid){
for(int i = 1; i <= cnt1; i ++) tmp1[i] = t[tmp1[i]].l;
for(int i = 1; i <= cnt2; i ++) tmp2[i] = t[tmp2[i]].l;
return Query_Rank(l, mid, v);
}
else{
for(int i = 1; i <= cnt1; i ++) sum += t[t[tmp1[i]].l].dat, tmp1[i] = t[tmp1[i]].r;
for(int i = 1; i <= cnt2; i ++) sum -= t[t[tmp2[i]].l].dat, tmp2[i] = t[tmp2[i]].r;
return sum + Query_Rank(mid + 1, r, v);
}
}
// 提前存储。
int Get_Rank(int l, int r, int v){
cnt1 = cnt2 = 0;
for(int i = r; i; i -= i & -i)
tmp1[++ cnt1] = rt[i];
for(int i = l - 1; i; i -= i & -i)
tmp2[++ cnt2] = rt[i];
return Query_Rank(1, T, v);
}
int Get_Pre(int l, int r, int v){
int now = Get_Rank(l, r, v);
if(now == 1) return 0;
return Get_Num(l, r, now - 1);
}
// 这里查后继有些技巧。
// 因为题目不保证查询的数存在,导致造成很多麻烦。
int Get_Nxt(int l, int r, int v){
if(v == T) return T + 1;
int now = Get_Rank(l, r, v + 1);
if(now == r - l + 2) return T + 1;
return Get_Num(l, r, now);
}
int main(){
n = read(), m = read();
for(int i = 1; i <= n; i ++)
a[i] = read(), b[++ T] = a[i];
for(int i = 1; i <= m; i ++){
q[i].opt = read();
if(q[i].opt == 3)
q[i].l = read(), q[i].k = read(), b[++ T] = q[i].k;
else if(q[i].opt == 4 || q[i].opt == 5)
q[i].l = read(), q[i].r = read(), q[i].k = read(), b[++ T] = q[i].k;
else
q[i].l = read(), q[i].r = read(), q[i].k = read();
}
// 离散化是必备技巧。
sort(b + 1, b + T + 1);
T = unique(b + 1, b + T + 1) - (b + 1);
for(int i = 1; i <= n; i ++){
a[i] = lower_bound(b + 1, b + T + 1, a[i]) - b;
Change(i, 1);
}
b[0] = -2147483647, b[T + 1] = 2147483647;
for(int i = 1; i <= m; i ++){
if(q[i].opt == 1){
q[i].k = lower_bound(b + 1, b + T + 1, q[i].k) - b;
printf("%d\n", Get_Rank(q[i].l, q[i].r, q[i].k));
}
else if(q[i].opt == 2){
printf("%d\n", b[Get_Num(q[i].l, q[i].r, q[i].k)]);
}
else if(q[i].opt == 3){
Change(q[i].l, -1);
a[q[i].l] = lower_bound(b + 1, b + T + 1, q[i].k) - b;
Change(q[i].l, 1);
}
else if(q[i].opt == 4){
q[i].k = lower_bound(b + 1, b + T + 1, q[i].k) - b;
printf("%d\n", b[Get_Pre(q[i].l, q[i].r, q[i].k)]);
}
else{
q[i].k = lower_bound(b + 1, b + T + 1, q[i].k) - b;
printf("%d\n", b[Get_Nxt(q[i].l, q[i].r, q[i].k)]);
}
}
return 0;
}
【思维题】
给定 \(A,B\) 两个序列,支持 \(B\) 序列中的两数交换位置,求 \(A,B\) 各选一个区间的交集大小。
考虑令 \(pos(a_i)=i\),令 \(a_i=pos(a_i),b_i=pos(b_i)\),这一定是个一一映射。
好处是 \(A\) 序列有序,那么就变成了查询 \([b_l,b_r]\) 中在 \(a_l\sim a_r\) 范围内的数,就变成了二维偏序裸题了。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 200010;
int n, m, tot, a[N], b[N], pos[N], rt[N];
struct Tree{int l, r, dat;} t[N*100];
vector<int> can;
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
int New_Point(){
if(can.empty()) return ++ tot;
else{
int now = can.back();
can.pop_back();
return now;
}
}
void Delet(int p){
t[p].l = t[p].r = t[p].dat = 0;
can.push_back(p);
}
void push_up(int p){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat;
}
int Update(int p, int l, int r, int k, int v){
if(!p) p = New_Point();
if(l == r) {
t[p].dat += v;
if(!t[p].dat) Delet(p), p = 0;
return p;
}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p);
if(!t[p].dat) Delet(p), p = 0;
return p;
}
void Change(int p, int v){
for(int i = p; i <= n; i += i & -i)
rt[i] = Update(rt[i], 1, n, b[p], v);
}
int Query(int p, int l, int r, int L, int R){
if(!p) return 0;
if(L <= l && r <= R) return t[p].dat;
int mid = (l + r) >> 1, sum = 0;
if(L <= mid) sum += Query(t[p].l, l, mid, L, R);
if(R > mid) sum += Query(t[p].r, mid + 1, r, L, R);
return sum;
}
int Get(int l, int r, int bl, int br){
int ans = 0;
for(int i = br; i; i -= i & -i)
ans += Query(rt[i], 1, n, l, r);
for(int i = bl - 1; i; i -= i & -i)
ans -= Query(rt[i], 1, n, l, r);
return ans;
}
int main(){
n = read(), m = read();
for(int i = 1; i <= n; i ++)
a[i] = read(), pos[a[i]] = i;
for(int i = 1; i <= n; i ++)
b[i] = read(), b[i] = pos[b[i]], Change(i, 1);
for(int i = 1; i <= m; i ++){
int opt = read();
if(opt == 2){
int x = read(), y = read();
Change(x, -1), Change(y, -1);
swap(b[x], b[y]);
Change(x, 1), Change(y, 1);
}
else{
int l = read(), r = read(), bl = read(), br = read();
printf("%d\n", Get(l, r, bl, br));
}
}
return 0;
}
【树上简单题】
树上点修路径点权 \(k\) 大。
路径看到 \(k\) 大考虑使用本算法,显然每次修改时只会修改整个子树。
于是在 dfs 序上跑就解决了,变为区间修改和单点查询。
利用树状数组天生的差分操作变为单点修改单点查询即可。
注意差分是四个点一起跑,所以代码看上去挺壮观的。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 80010;
const int M = 10000000;
int n, m, T, cnt, num, tot;
int rt[N], a[N], b[N*2], dep[N], fa[N][30];
int id[N], sz[N], head[N], tmp[4][N], sum[4];
struct Tree{int l, r, dat;} t[M];
struct Query{int k, a, b;} q[N];
struct Edge{int nxt, to;} ed[N*2];
int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
void add(int u, int v){
ed[++ cnt] = (Edge){head[u], v};
head[u] = cnt;
}
void dfs(int u, int Fa){
id[u] = ++ num;
dep[u] = dep[Fa] + 1; fa[u][0] = Fa; sz[u] = 1;
for(int i = 1; (1 << i) <= dep[u]; i ++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for(int i = head[u]; i; i = ed[i].nxt){
int v = ed[i].to;
if(v != Fa) dfs(v, u), sz[u] += sz[v];
}
}
int Lca(int x, int y){
if(dep[x] > dep[y]) swap(x, y);
for(int i = 17; i >= 0; i --)
if(dep[fa[y][i]] >= dep[x]) y = fa[y][i];
if(x == y) return x;
for(int i = 17; i >= 0; i --)
if(fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
void push_up(int p){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat;
}
int Update(int p, int l, int r, int k, int v){
if(!p) p = ++ tot;
if(l == r) {t[p].dat += v; return p;}
int mid = (l + r) >> 1;
if(k <= mid) t[p].l = Update(t[p].l, l, mid, k, v);
else t[p].r = Update(t[p].r, mid + 1, r, k, v);
push_up(p); return p;
}
void Change(int p, int k, int v){
for(int i = p; i <= n; i += i & -i)
rt[i] = Update(rt[i], 1, T, k, v);
}
int Query(int l, int r, int k){
if(l == r) return l;
int mid = (l + r) >> 1, C = 0;
for(int i = 1; i <= sum[0]; i ++) C += t[t[tmp[0][i]].r].dat;
for(int i = 1; i <= sum[1]; i ++) C += t[t[tmp[1][i]].r].dat;
for(int i = 1; i <= sum[2]; i ++) C -= t[t[tmp[2][i]].r].dat;
for(int i = 1; i <= sum[3]; i ++) C -= t[t[tmp[3][i]].r].dat;
if(k > C){
for(int i = 1; i <= sum[0]; i ++) tmp[0][i] = t[tmp[0][i]].l;
for(int i = 1; i <= sum[1]; i ++) tmp[1][i] = t[tmp[1][i]].l;
for(int i = 1; i <= sum[2]; i ++) tmp[2][i] = t[tmp[2][i]].l;
for(int i = 1; i <= sum[3]; i ++) tmp[3][i] = t[tmp[3][i]].l;
return Query(l, mid, k - C);
}
else{
for(int i = 1; i <= sum[0]; i ++) tmp[0][i] = t[tmp[0][i]].r;
for(int i = 1; i <= sum[1]; i ++) tmp[1][i] = t[tmp[1][i]].r;
for(int i = 1; i <= sum[2]; i ++) tmp[2][i] = t[tmp[2][i]].r;
for(int i = 1; i <= sum[3]; i ++) tmp[3][i] = t[tmp[3][i]].r;
return Query(mid + 1, r, k);
}
}
int Get(int a, int b, int c, int d, int k){
memset(sum, 0, sizeof(sum));
for(int i = a; i; i -= i & -i) tmp[0][++ sum[0]] = rt[i];
for(int i = b; i; i -= i & -i) tmp[1][++ sum[1]] = rt[i];
for(int i = c; i; i -= i & -i) tmp[2][++ sum[2]] = rt[i];
for(int i = d; i; i -= i & -i) tmp[3][++ sum[3]] = rt[i];
return Query(1, T, k);
}
int main(){
n = read(), m = read();
for(int i = 1; i <= n; i ++)
a[i] = read(), b[++ T] = a[i];
for(int i = 1; i < n; i ++){
int u = read(), v = read();
add(u, v), add(v, u);
}
dfs(1, 0);
for(int i = 1; i <= m; i ++){
q[i].k = read(), q[i].a = read(), q[i].b = read();
if(!q[i].k) b[++ T] = q[i].b;
}
sort(b + 1, b + T + 1);
T = unique(b + 1, b + T + 1) - (b + 1);
for(int i = 1; i <= n; i ++){
a[i] = lower_bound(b + 1, b + T + 1, a[i]) - b;
Change(id[i], a[i], 1);
Change(id[i] + sz[i], a[i], -1);
}
for(int i = 1; i <= m; i ++){
if(!q[i].k){
int u = q[i].a;
Change(id[u], a[u], -1);
Change(id[u] + sz[u], a[u], 1);
a[u] = lower_bound(b + 1, b + T + 1, q[i].b) - b;
Change(id[u], a[u], 1);
Change(id[u] + sz[u], a[u], -1);
}
else{
int u = q[i].a, v = q[i].b, w = Lca(u, v);
if(dep[u] + dep[v] - 2 * dep[w] + 1 < q[i].k)
puts("invalid request!");
else
printf("%d\n", b[Get(id[u], id[v], id[w], id[fa[w][0]], q[i].k)]);
}
}
return 0;
}
【其他技巧】
【内外反套】
我们看看这道题。
序列中插入某个数,查询区间 \(k\) 大时。
直接用本算法会很不方便。
这是可以考虑内外互换,用权值树套普通线段树,相应位置的线段树维护的是该节点代表权值区间的位置下标。
但是为了区间考虑,还需要标记永久化,要注意 push_up 和 Query 时的区别,\(rt[]\) 数组一定要开四倍!!!
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 50010;
const int M = 2e7;
int n, m, T, tot, rt[4*N], b[N];
struct Tree{int l, r, tag; LL dat;} t[M];
struct Query{int opt, l, r; LL c;} q[N];
LL read(){
LL x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
}
void push_up(int p, int l, int r){
t[p].dat = t[t[p].l].dat + t[t[p].r].dat + 1LL * t[p].tag * (r - l + 1);
}
int Modify(int p, int l, int r, int L, int R){
if(!p) {p = ++ tot; t[p].dat = t[p].tag = t[p].l = t[p].r = 0;}
if(L <= l && r <= R){
t[p].tag ++;
t[p].dat += 1LL * (r - l + 1);
return p;
}
int mid = (l + r) >> 1;
if(L <= mid) t[p].l = Modify(t[p].l, l, mid, L, R);
if(R > mid) t[p].r = Modify(t[p].r, mid + 1, r, L, R);
push_up(p, l, r); return p;
}
void Change(int p, int l, int r, int L, int R, int v){
rt[p] = Modify(rt[p], 1, n, L, R);
if(l == r) return;
int mid = (l + r) >> 1;
if(v <= mid) Change(p << 1, l, mid, L, R, v);
else Change(p << 1 | 1, mid + 1, r, L, R, v);
}
LL Query(int p, int l, int r, int L, int R, LL Add){
if(L <= l && r <= R)
return t[p].dat + 1LL * Add * (r - l + 1);
int mid = (l + r) >> 1; LL sum = 0;
if(L <= mid) sum += Query(t[p].l, l, mid, L, R, Add + t[p].tag);
if(R > mid) sum += Query(t[p].r, mid + 1, r, L, R, Add + t[p].tag);
return sum;
}
int Ask(int p, int l, int r, int L, int R, LL k){
if(l == r) return l;
int mid = (l + r) >> 1; LL sum = Query(rt[p << 1 | 1], 1, n, L, R, 0);
if(k > sum)
return Ask(p << 1, l, mid, L, R, k - sum);
else
return Ask(p << 1 | 1, mid + 1, r, L, R, k);
}
int main(){
n = read(), m = read();
for(int i = 1; i <= m; i ++){
q[i].opt = read(), q[i].l = read(), q[i].r = read(), q[i].c = read();
if(q[i].opt == 1) b[++ T] = q[i].c;
}
sort(b + 1, b + T + 1);
T = unique(b + 1, b + T + 1) - (b + 1);
for(int i = 1; i <= m; i ++){
if(q[i].opt == 1){
q[i].c = lower_bound(b + 1, b + T + 1, q[i].c) - b;
Change(1, 1, T, q[i].l, q[i].r, q[i].c);
}
else
printf("%d\n", b[Ask(1, 1, T, q[i].l, q[i].r, q[i].c)]);
}
return 0;
}
【垃圾回收】
上面第三题就用了这个技巧。
在空间不够时可以使用,考试时如果有时间尽量考虑写个垃圾回收,反正就几行代码。
int New_Point(){
if(can.empty()) return ++ tot;
else{
int now = can.back();
can.pop_back();
return now;
}
}
void Delet(int p){
t[p].l = t[p].r = t[p].dat = 0;
can.push_back(p);
}
在需要新节点时调用 New_Point()
,在某个节点的为空(例如 t[p].dat == 0
)时调用 Delet(p)
。
记得一定要清空这个节点。
【总结】
树状数组套权值树是个比较优秀的算法。
特别鸣谢&引用资料:
完结撒花。