树状数组套权值树学习笔记

【前言】

树状数组套权值树是众多树套树中的一种。

我个人认为它是“性价比”最高的一种,不论从代码实现复杂度还是常数大小来看。

它主要用于解决二维偏序问题,可以发现这同样是 整体二分 / CDQ 分治 的功能。

与这两者相比它的优点是容易理解,代码实现简单无脑。

缺点是相对来讲空间复杂度高,常数也比较大,经常被上面两种方法吊打。

前置芝士:

  1. 树状数组 / 线段树等具有同样功能的数据结构。
  2. 权值线段树。

不会的或许可以康康这里

值得提一句的是,有人将这种方法称为树状数组套主席树,因为主席树的定义不是很明确。

甚至有 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;
}

【思维题】

Intersection of Permutations

给定 \(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)

记得一定要清空这个节点。

【总结】

树状数组套权值树是个比较优秀的算法。

特别鸣谢&引用资料:

  1. 浅谈树状数组套权值树

完结撒花。

posted @ 2021-04-01 15:06  LPF'sBlog  阅读(84)  评论(0编辑  收藏  举报