可持久化线段树(静态)【学习笔记】

(静态)主席树入门

前置知识:动态开点线段树,权值线段树。

1)权值线段树:相当于将线段树当成一个桶,其中的每一个点所代表的区间相当于一段值域。维护的值为这段值域中的一些信息。
在这里插入图片描述

例如该图,节点2代表的是值域为[1,2]的区间,节点6代表值域为[3,4]的区间…
权值线段树的基本意义是一棵线段树。但它和普通线段树不同:
线段树,每个节点用来维护一段区间的最大值或总和等。
权值线段树,相当于一个桶,每个节点用来表示一个区间的数出现的次数

我们可以用它来维护一段区间的数出现的次数,从它的定义上来看,它可以快速计算一段区间的数的出现次数。
此外,它还有一个重要功能,在于它可以快速找到第k大或第k小值,下面会做详细解释。
其实,它就是一个桶,桶能做到的它都可以用更快的速度去完成。

基本操作

1.添加
就是找到对应位置将位置加加

inline void update(int rt, int l, int r, int pos)
{
    if(l == pos && r == pos)
    {
        tree[rt] ++;
        return ;
    }
    if(pos <= mid)
      update(Lson,pos);
    if(pos > mid)
      update(Rson,pos);
    tree[rt] = tree[rt << 1] + tree[rt << 1|1];
    return ;
}

2.查询所有数的第k大值
这是权值线段树的核心,思想如下:
到每个节点时,如果右子树的总和大于等于k,说明第k大值出现在右子树中,则递归进右子树;否则说明此时的第kkk大值在左子树中,则递归进左子树,注意:此时要将k的值减去右子树的总和。
为什么要减去?
如果我们要找的是第7大值,右子树总和为444,7−4=3,说明在该节点的第7大值在左子树中是第3大值。
最后一直递归到只有一个数时,那个数就是答案。

	int kth(int l,int r,int v,int k)
	{
		if(l==r) return l;
		else
		{
			int mid=(l+r)/2,s1=f[v*2],s2=f[v*2+1];
			if(k<=s2) return kth(mid+1,r,v*2+1,k); else return kth(l,mid,v*2,k-s2);
		}
	}

存一个板子

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cmath>
#include<queue>
using namespace std;
const int maxn=1e5+10;
int num[maxn*5];
//权值线段树: 
//区间的值是这段值域里的个数的线段树 
//叶子结点的值 是这个数在序列里出现的次数
//可以当平衡树用 比平衡树代码好写 
/* 
没有必要build
多组样例的时候
memset就搞定了 
void build(int p,int l,int r)
{
	if(l==r)
	{
		num[l]=0;
		return;
	}
	int mid=(l+r)>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
} 
*/
void update(int p,int l,int r,int v,int op)//op==1或-1,插入或删除 
{
	num[p]+=op;
	if(l==r)return;
	int mid=(l+r)>>1;
	if(v<=mid)update(p<<1,l,mid,v,op);
	else update(p<<1|1,mid+1,r,v,op); 
}
 
int Kth(int p,int l,int r,int rank)//k小值 
{
	if(l==r)return l;
	int mid=(l+r)>>1;
	if(num[p<<1]>=rank)return Kth(p<<1,l,mid,rank);//左子k小 
	return Kth(p<<1|1,mid+1,r,rank-num[p<<1]);//右子(k-左num)小 
} 
 
//求一个数的最小排名,排名从0起 
int Rank(int p,int l,int r,int v)//[1,v-1]的出现个数 即v-1>mid 即前面3个数v就rank3 
{
	if(r<v)return num[r];
	int mid=(l+r)>>1,res=0;
	if(v>l)res+=Rank(p<<1,l,mid,v);//左段区间得有比v小的值,才有加的意义,比如说rank[1]=0 
	if(v>mid+1)res+=Rank(p<<1|1,mid+1,r,v);//右段区间得有比v小的值,才有加的意义 
	return res;
} 
 
int Findpre(int p,int l,int r)
{
	if(l==r)return l;
	int mid=(l+r)>>1;
	if(num[p<<1|1])return Findpre(p<<1|1,mid+1,r);//右子树非空向右找 
	return Findpre(p<<1,l,mid);//否则向左找 
}
//找前驱 尽可能在小于v的右子树找 
int Pre(int p,int l,int r,int v)
{
	if(r<v)//maxr<v即在p的子树中 p区间内最右非空子树即答案 
	{
		if(num[p])return Findpre(p,l,r);
		return 0;
	}
	int mid=(l+r)>>1,Re;
	//如果v在右子树可能有前驱(至少mid+1比v小)就先查右子树,l=mid+1 
	if(mid+1<v&&num[p<<1|1]&&(Re=Pre(p<<1|1,mid+1,r,v)))return Re;
	//否则查左子树,r=mid,使r不断变小直至满足题意小于v 
	return Pre(p<<1,l,mid,v);
} 
 
int Findnext(int p,int l,int r)
{
	if(l==r)return l;
	int mid=(l+r)>>1;
	if(num[p<<1])return Findnext(p<<1,l,mid);
	return Findnext(p<<1|1,mid+1,r);
} 
 
//找后继 尽可能在大于v的左子树找 
int Next(int p,int l,int r,int v)
{
	if(v<l)//已找到大于v的最小完整区间 
	{
		if(num[p])return Findnext(p,l,r); 
		return 0;
	}
	int mid=(l+r)>>1,Re;
	//如果左子树里有比v大的(至少mid比v大)就查左子树 否则查右子树 
	if(v<mid&&num[p<<1]&&(Re=Next(p<<1,l,mid,v)))return Re;
	return Next(p<<1|1,mid+1,r,v);
}
 
int main()
{
	return 0;
} 

2)动态开点的线段树

动态开点线段树
用处:一般线段树开局直接4*N的空间,然而当N很大时,4倍空间会消耗很多,这时考虑用动态开点线段树,用多少开多少,跟c++的new差不多
预备:一个根节点root,存值数组c[N],左右儿子 lc[N],rc[N]

算法流程:
1.一开始只有一个根节点
2.update操作:类似串算法,每次传入节点root,区间范围[l,r]和更新点x,判断x在区间的哪边,在哪边就就往哪边走,如果遇到一个无标记的节点但又需要往这个节点走就以访问次序给这个节点标号,像正常线段树更新即可。
3.query操作:和一般线段树不同的是,当我们遇到一个没有访问过的节点,直接返回,其他与线段树查询一致。
假设我们要对 1 3 5 7 8 9这段序列操作,区间范围[1,9] 开的线段树如下可以少点2,4,6这3个儿子节点
如果这段区间没有5,我们甚至可以不用开7号和8号节点

一颗残缺的线段树
在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N=1e7+7;

int root,n,c[N],lc[N],rc[N],cnt;

void update(int &p,int l,int r,int x,int val)
{
    if(!p) p=++cnt; //遇到了,但没标号,标记
    if(l==r)
    {
        c[p]+=val;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid) update(lc[p],l,mid,x,val);
    else update(rc[p],mid+1,r,x,val);
    c[p]=c[lc[p]]+c[rc[p]];
}

int query(int p,int l,int r,int x,int y)
{
     if(!p) return 0;
     if(l==x&&r==y) return c[p];
     int mid=(l+r)>>1;
     if(y<=mid) return query(lc[p],l,mid,x,y);
     else if(x>mid) return query(rc[p],mid+1,r,x,y);
     return query(lc[p],l,mid,x,mid)+query(rc[p],mid+1,r,mid+1,y);
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    long long ans=0;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        ans+=query(root,1,1e9,x+1,1e9);
        update(root,1,1e9,x,1);
    }
    cout<<ans<<endl;
    return 0;
}

静态第k小数

在这里插入图片描述
首先我们先想想最原始的做法怎么搞,就是先把指定区间截出来再sort一下,假设m次询问,区间长度为n总时间复杂度为O(mnlogn)爆炸
正经的做法:我们知道区间问题一般可以转化为前缀和问题,那么这个问题怎么转呢?
1.首先我们看看暴力的做法,我们说sort的一下然后再找第k小,我们不如直接一颗权值线段树建树nlogn,查询logn复杂都还是比较高,关键是如何快速建出l到r区间内部的树。
2.我们可以考虑用差分的思想就说区间具有可加减的性质


一列数,可以对于每个点i都建一棵权值线段树,维护1~i这些数,每个不同的数出现的个数(权值线段树以值域作为区间)
现在,n棵线段树就建出来了,第i棵线段树代表1~i这个区间
例如,一列数,n为6,数分别为1 3 2 3 6 1
首先,每棵树都是这样的:

在这里插入图片描述
以第4棵线段树为例,1~4的数分别为1 3 2 3
在这里插入图片描述

注意

因为是同一个问题,n棵权值线段树的形状是一模一样的,只有节点的权值不一样
所以这样的两棵线段树之间是可以相加减的(两颗线段树相减就是每个节点对应相减)
想想,第x棵线段树减去第y棵线段树会发生什么?
第x棵线段树代表的区间是[1,x]
第y棵线段树代表的区间是[1,y]
两棵线段树一减
设x>y,[1,x]−[1,y]=[y+1,x]
所以这两棵线段树相减可以产生一个新的区间对应的线段树!
等等,这不是前缀和的思想吗
这样一来,任意一个区间的线段树,都可以由我这n个基础区间表示出来了!
因为每个区间都有一个线段树
然后询问对应区间,在区间对应的线段树中查找kth就行了

这就是主席树的一个核心思想:前缀和思想

具体做法待会儿再讲,现在还有一个严峻的问题,就是n棵线段树空间太大了!
如何优化空间,就是主席树另一个核心思想
我们发现这n棵线段树中,有很多重复的点,这些重复的点浪费了大部分的空间,所以考虑如何去掉这些冗余点
在建树中优化
假设现在有一棵线段树,序列往右移一位,建一棵新的线段树
对于一个儿子的值域区间,如果权值有变化,那么新建一个节点,否则,连到原来的那个节点上
现在举几个例子来说明
序列4 3 2 3 6 1
区间[1,1]的线段树(蓝色节点为新节点)

在这里插入图片描述
区间[1,2]的线段树(橙色节点为新节点)
在这里插入图片描述
区间[1,3]的线段树(紫色节点为新节点)
在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
#include <map>
#include <cstring>
#include <deque>
#include <cmath>
#include <iomanip>
#include <queue>
#include <algorithm>
#include <set>
#define mid ((l + r) >> 1) 
#define Lson rt << 1, l , mid
#define Rson rt << 1|1, mid + 1, r
#define ms(a,al) memset(a,al,sizeof(a))
#define sfx(x) scanf("%lf",&x)
#define sfxy(x,y) scanf("%lf%lf",&x,&y)
#define sdx(x) scanf("%d",&x)
#define sdxy(x,y) scanf("%d%d",&x,&y)
#define pfx(x) printf("%.0f\n",x)
#define pfxy(x,y) printf("%.6f %.6f\n",x,y)
#define pdx(x) printf("%d\n",x)
#define pdxy(x,y) printf("%d %d\n",x,y)
#define _for(i,a,b) for( int i = (a); i < (b); ++i)
#define _rep(i,a,b) for( int i = (a); i <= (b); ++i)
#define for_(i,a,b) for( int i = (a); i >= (b); -- i)
#define rep_(i,a,b) for( int i = (a); i > (b); -- i)
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define INF 0x3f3f3f3f-
#define hash Hash
#define next Next
#define pb push_back
#define f first
#define s second
#define lowbit(x) (x & (-x))
using namespace std;
const int N = 1e5 + 10, eps = 1e-10;
const int M = 10010;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<LL,LL> PII;
inline long long read()
{
    long long F=1,Num=0; 
    char ch=getchar();
    while(!isdigit(ch)) 
    {
        if(ch=='-') F=-1;
        ch=getchar();
    }
    while(isdigit(ch)) 
    {
        Num=Num*10+ch-'0'; 
        
        ch=getchar(); 
    }
    return Num*F; 
}
int a[N];
struct node {//类似可持续化trie树
    int l, r;//这里已经是不是传统意义上的左右儿子
    int cnt;//权值线段树上的数字个数
}tr[N * 4 + N * 17];
vector<int> nums;
int root[N]; int idx;//每个起始根节点的下标
int n, m;

inline int getid(int x)
{
    return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;
}

inline int build(int l, int r)
{
    //开辟一个内存池
    int now = ++ idx;
    if(l == r) return now;
    tr[now].l = build(l,mid), tr[now].r = build(mid + 1, r);
    return now;
}

inline int insert(int pre, int l, int r, int k)
{
    int point = ++ idx ;//动态开点
    tr[point] = tr[pre];//复制指针,因为这个线段树的存储方式是链式的,就是一个节点套一个节点只要存头节点的地址就会复制整颗棵子树
    if(l == r) 
    {
        tr[point].cnt ++;
        return point;
    }
    if(k <= mid)//注意左右儿子改变了,是递归求解左右儿子
    tr[point].l = insert(tr[pre].l,l,mid,k);
    else tr[point].r = insert(tr[pre].r,mid+1,r,k);
    tr[point].cnt = tr[tr[point].l].cnt + tr[tr[point].r].cnt;
    return point;
}

inline int query(int lpoint, int rpoint, int l, int r, int k)
{
    if(l == r) return l;
    int cnt = tr[tr[rpoint].l].cnt - tr[tr[lpoint].l].cnt;
    if(k <= cnt)
        return query(tr[lpoint].l,tr[rpoint].l, l, mid, k);
    else return query(tr[lpoint].r,tr[rpoint].r, mid+1,r,k-cnt);
}

int main()
{
    n = read(), m = read();
    _for(i,1,n+1)
      nums.pb(a[i] = read());
    
    sort(nums.begin(),nums.end());
    nums.erase(unique(nums.begin(),nums.end()),nums.end());
    
    root[0] = build(1,nums.size());
    
    _for(i,1,n+1)
      root[i] = insert(root[i - 1],1,nums.size(), getid(a[i]));
    
    while(m --)
    {
        int l, r, k;
        l = read(), r = read(), k = read();
        printf("%d\n",nums[query(root[l - 1], root[r], 1, nums.size(), k) - 1]);
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值