树状结构--主席树(可持久化线段树)区间第K大 区间不重复元素个数

感谢大佬博客:
https://www.cnblogs.com/zyf0163/
https://blog.csdn.net/pengwill97
预备知识:线段树

如果不知道请看我的另一篇博文:
https://blog.csdn.net/flymoyu/article/details/88745867

整体描述:

主席树又称函数式线段树或者可持久化线段树,顾名思义,也就是通过函数来实现的线段树。主席树就是利用函数式编程的思想来使线段树支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗的增强版的线段树。

简单来讲就是对每一个 [1-i] ( i>=1&&i<=n) 建立一个线段树,但是限于内存限制,这些线段树会有一些重复部分就不会再建立(详情后边会提到)

引入一个例子:如有数列:2 1 2 5 1 1 1 3,不难统计出,数列中数字1出现了4次,数字2出现了2次,数字3、数字5都出现了1次,对于这个数列,我们可以说 a[1]=4,a[2]=2,a[3]=1,a[4]=0,a[5]=1a[1]=4,a[2]=2,a[3]=1,a[4]=0,a[5]=1。如果放在权值线段树种,a[i]中的下标i不再是对应的数值,而是对应叶子节点的编号了。但是道理是一样的。

为了实现可持久化,就要保存树的历史版本。最自然的想法当然是每进行一次修改,就新建一颗线段树,这样的空间复杂度显然是不能够接受的。通过观察不难发现,每次进行单点修改,发生变化的只有从叶子节点到根节点这一条链上的节点,换句话说,只有log2n个节点发生了变化,而其他的节点都可以重用,没有必要新建。所以有了如下的思路:

1.新建根节点是必要的。首先是为了区分不同的版本,修改前是一个版本,修改后是另一个版本。
  如果做了修改,那么根节点的值一定发生了变化,也必须新建一个根节点。
2.对于新建有不同的理解。自然想到的是去动态的开点,即new一个新的节点,这在ACM中并不是
 一个很方便的选择,使用不当会造成内存溢出等等问题。所以会选择根据题目的数据范围提前申请好
 对应变量。所以新建的含义也就变成了为对应的节点分配编号。这在代码中会有更好的体现。

主席树应用一 区间第K大问题

原理

 区间第K大问题,即给定一个数列a,每次询问给定询问的区间[l,r]和想要得到的区间  
 第K大,求返回结果。如有数列a=[4,1,3,2],有一询问l=2,r=4,K=2,观察不难得出答案为2。

插入

未插入数值
未插入数值之前,树为空树,但是仍需要一个根节点,如图所示。
在这里插入图片描述

插入数值4
我们知道,根节点维护的区间是[1,4],根节点右儿子维护的区间是[3,4],所以应该向儿子右方向继续递归,知道到叶子节点。并且我们知道上一版本是空树,只有一个根节点,所以没有节点什么可以利用的(请忽略图片中右方的连线!)。

在这里插入图片描述
插入数值1
下面插入数值1,根据上面的经验,应该向左子树递归。但是问题是,他的右子树呢?明显插入数值1对其右子树没有发生影响,所以我们将右子树指向上一版本的右子树。指向之后,我们再进入左子树继续递归操作,直到叶子节点。

在这里插入图片描述
下面的插入操作原理是一样的,不再赘述。

另一种图像描述

对于样例( 4,2,5,3,1)
假设我们建完了T[3] 这棵树,我们加入第 4 个元素:
在这里插入图片描述
在这里插入图片描述
那么我们就这样自建立:
在这里插入图片描述
查询:
这样我们得到区间[l, r]的数要查询第k大便很容易了,设左节点中存的个数为cnt,当k<=cnt时,我们直接查询左儿子中第k小的数即可,如果k>cnt,我们只要去查右儿子中第k-cnt小的数即可,这边是一道很简单的线段树了。就如查找[1, 3]的第2小数(图上为了方便,重新给节点标号),从根节点1向下搜,发现左儿子2的个数为1,1<2,所有去右儿子3中搜第2-1级第1小的数,然后再往下搜,发现左儿子6便可以了,此时已经搜到底端,所以直接返回节点6维护的值3即可就可以了。
代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100000 + 5;

int a[N], b[N], rt[N * 20], ls[N * 20], rs[N * 20], sum[N * 20];

int n, k, tot, sz, ql, qr, x, q, T;

void Build(int& o, int l, int r){
    o = ++ tot;
    sum[o] = 0;
    if(l == r) return;
    int m = (l + r) >> 1;
    Build(ls[o], l, m);
    Build(rs[o], m + 1, r);
}

void update(int& o, int l, int r, int last, int p){
    o = ++ tot;
    ls[o] = ls[last];
    rs[o] = rs[last];
    sum[o] = sum[last] + 1;
    if(l == r) return;
    int m = (l + r) >> 1;
    if(p <= m)  update(ls[o], l, m, ls[last], p);
    else update(rs[o], m + 1, r, rs[last], p);
}

int query(int ss, int tt, int l, int r, int k){
    if(l == r) return l;
    int m = (l + r) >> 1;
    int cnt = sum[ls[tt]] - sum[ls[ss]];
    if(k <= cnt) return query(ls[ss], ls[tt], l, m, k);
    else return query(rs[ss], rs[tt], m + 1, r, k - cnt);
}

void work(){
    scanf("%d%d%d", &ql, &qr, &x);
    int ans = query(rt[ql - 1], rt[qr], 1, sz, x);
    printf("%d\n", b[ans]);
}

int main(){
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &q);
        for(int i = 1; i <= n; i ++) scanf("%d", a + i), b[i] = a[i];
        sort(b + 1, b + n + 1);
        sz = unique(b + 1, b + n + 1) - (b + 1);
        tot = 0;
        Build(rt[0],1, sz);
        //for(int i = 0; i <= 4 * n; i ++)printf("%d,rt =  %d,ls =  %d, rs = %d, sum = %d\n", i, rt[i], ls[i], rs[i], sum[i]);
        for(int i = 1; i <= n; i ++)a[i] = lower_bound(b + 1, b + sz + 1, a[i]) - b;
        for(int i = 1; i <= n; i ++)update(rt[i], 1, sz, rt[i - 1], a[i]);
        for(int i = 0; i <= 5 * n; i ++)printf("%d,rt =  %d,ls =  %d, rs = %d, sum = %d\n", i, rt[i], ls[i], rs[i], sum[i]);
        while(q --)work();
    }
    return 0;
}

主席树应用二 区间不重复元素个数
题目大意
给你 n 个数,然后有 q 个询问,每个询问会给你[l,r],输出[l,r]之间有多少种数字。
题目分析
首先我们还是思考对于右端点固定的区间(即R确定的区间),我们如何使用线段树来解决这个问题。

我们可以记录每个数字最后一次出现的位置。比如,5这个数字最后一次出现在位置3上,就把位置3记录的信息++(初始化为0)。比如有一个序列 1 2 2 1 3 那么我们记录信息的数列就是 0 0 1 1 1 (2最后出现的位置是位置3 1最后出现的位置是位置4 3最后出现的位置是位置5)。那么对区间 [1,5] , [2,5] , [3,5] , [4,5] , [5,5]的数字种数,我们都可以用sum[5]-sum[x-1]来求(sum数组记录的是前缀和)(前缀和之差可以用线段树或者树状数组来求)。

那么对着区间右端点会变化的题目,我们应该怎么办呢?先思考一下如果右端点有序的话,我们可以怎么做。对R不同的区间,向线段树或者树状数组中添加元素,知道右端点更新为新的R,在添加的过程中,如果这个元素之前出现过,就把之前记录的位置储存的信息 -1,然后在新的位置储存的信息 +1,这样就可以保证在新的右端点固定的区间里,记录的是数字最后一次出现的位置的信息,这样题目就解决了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=998244353;
const int inf=0x3f3f3f3f;
const int maxn=1e5+5;
const int maxm=2e6+5;

int tot,rt[maxn],ls[maxm],rs[maxm],sum[maxm];
void update(int x,int &y,int l,int r,int p) {
    y=++tot;
    if (l==r) { sum[y]=sum[x]+1; return; }
    int m=(l+r)>>1;
    if (p<=m) rs[y]=rs[x],update(ls[x],ls[y],l,m,p);
    else      ls[y]=ls[x],update(rs[x],rs[y],m+1,r,p);
    sum[y]=sum[ls[y]]+sum[rs[y]];
}
int query(int x,int y,int l,int r,int L,int R) {
    if (L<=l&&r<=R) return sum[y]-sum[x];
    int m=(l+r)>>1,res=0;
    if (L<=m) res+=query(ls[x],ls[y],l,m,L,R);
    if (m<R)  res+=query(rs[x],rs[y],m+1,r,L,R);
    return res;
}

int n,x;
int a[maxn],b[maxn];
int main () {
    int T;
    scanf("%d",&T);
    while (T--) {
        scanf("%d",&n); tot=0;
        for (int i=1;i<=n;i++) a[i]=b[i]=0;
        for (int i=1;i<=n;i++) {
            scanf("%d",&x);
            update(rt[i-1],rt[i],1,n,a[x]);
            if (a[x]) b[query(rt[ a[x] ],rt[i-1],1,n,0,a[x])+1]++;
            a[x]=i;
        }
        int sum=0;
        for (int i=1;i<=n;i++) {
            sum+=b[i];
            printf("%d%c",n-sum," \n"[i==n]);
        }
    }
    return 0;
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值