权值线段树及主席树的详解

这篇的重点是为各位简绍主席树的知识,当然在简绍主席树之前,还需要一些准备知识
第一:离散化
离散化:
把无限空间有限个体映射到有限空间里有限
白话:在不改变数据相对大小的条件下,对数据进行相应的缩小
例如:
原数据:7 1 4 3处理后:4 1 3 2
原数据:{100,250}{200,400}处理后{1,3}{2,4}
离散化需要函数:unique函数(去重函数)
low_bound(x,y,val)函数:
在(x,y)范围内返回>=val的第一个元素位置。如果所有元素都小于val,则返回last的位置
所谓离散化,就是为了防止当数字绝对值过大是,我们的线段树无法开出那么大的数组,同时降低我们的空间复杂度和时间复杂度。
下面是两种常见的离散化方法(个人推荐第一种,更为简单)
第一种:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
using namespace std;
#define maxl 1000;
//n原数组的大小
//num原数组的中的元素
//lsh离散化的数组
//cnt离散化后的大小
int lsh[maxl],n,num[maxl],cnt;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>num[i];
		lsh[i]=mun[i];
	}
	sort(lsh+1,lsh+n+1);
	cnt=unique(lsh+1,lsh+n+1)-lsh-1;
	for(int i=1;i<=n;i++)
		num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
}

第二种:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
#define maxl 100;
struct node
{
    int data,id;
    bool operator<(const node &a) const
    {
        return data < a.data;
    }
};
int main()
{
    node num[maxl];
    int rankl[maxl];
    int n=100;
    for(int i=1;i<=n;i++)
    {
        cin>>num[i].data;
        num[i].id=i;
    }
    sort(num+1,num+n+1);
    for(int i=1;i<=n;i++)
        rankl[num[i].id]=i;
}

第二种:权值线段树
权值线段树(一般用结点式存储struct node{int sumv,lc,rc;};) 😕/一定要是全局的!!!
1、维护全局的值域信息,每个结点记录的其实是该值域的值出现的总次数
2、用了二分的思想
3、支持查询全局K小值,全局rank,前驱,后继等
(前驱:小于x,且最大的数 后继:大于x,且最小的数)
4、单次操作时间复杂度O(log(n))
5、空间复杂度O(n)
6、相对于平衡二叉树的优势:代码简单、速度快
7、劣势:值域较大(10^9)??时,需要离散化,就变成了离线数结构
okk那我们接下来就来讲解权值线段树的代码
//插入删除

int tree[n<<2];
void update_tree(int p,int v,int rt,int l,int r)//当v=1为增加,当v=-1的时为减少
{
	tree[rt]+=v;
	if(l==r)
		return;
	int mid=(l+r)>>1;
	if(p<=m)
		update_tree(p,v,rt<<1,l,mid);
	else
		update_tree(p,v,rt<<1|1,mid+1,r);
}

//k小值

int kth(int k,int rt,int l,int r)
{
	if(l==r)
		return l;
	int mid=(l+r)>>1;
	if(tree[rt<<1]>=k)
		return kth(k,rt<<1,l,mid);
	else
		return kth(k-tree[rt<<1],rt<<1|1,mid+1,r);
}

排名(即区间和查询)

int rankl(int p,int rt,int l,int r)
{
	if(r<p)
		return tree[rt];
	int mid=(l+r)>>1;
	int res=0;
	res+=rankl(p,rt<<1,l,m);
	if(m<p-1)
		res+=rankl(p,rt<<1|1,m+1,r);
	return res;
}

在求排名的代码中,if和else的判断会显得有些不同,我们在函数中对于左子树的递归无需进行条件的判断,因为,我们排名默认为从左向右递增,所以我们只需要判断是否到达所求的叶子节点和是否需要向右子树进行递归。

前驱

int rankl(int p,int rt,int l,int r)
{
    if(r<p)
        return t[rt];
    int m=(l+r)>>1;
    int res=0;
    res+=rankl(p,rt<<1,l,m);
    if(m<p-l)
        res+=rankl(p,rt<<1|1,m+1,r);
    return res;
}
//前驱
int findl(int rt,int l,int r)
{
    if(l==r)
        return l;
    int m=(l+r)>>1;
    if(t[rt<<1|1])
        return findl(rt<<1|1,m+1,r);
    return findl(rt<<1,l,m);
}
int pre(int p,int rt,int l,int r)
{
    if(r<p)
    {
        if(t[rt])
            return findl(rt,l,r);//查询最靠右的数
        return 0;
    }
    int m=(l+r)>>1;
    int re;
    if(m<p-1&&t[rt<<1|1]&&(re=pre(p,rt<<1|1,m+1,r)))//先考虑右子树
        return re;
    return pre(p,rt<<1,l,m);
}

后继

int findl(int rt,int l,int r)
{
    if(l==r)
        return l;
    int m=(l+r)>>1;
    if(t[rt<<1])
        return findl(rt<<1,l,m);
    return findl(rt<<1|1,m+1,r);
}
int next(int p,int rt,intl,int r)
{
    if(p<l)
    {
        if(t[rt])
            return findl(rt,l,r);
        return 0;
    }
    int m=(l+r)>>1;
    int re;
    if(p<m&&t[rt<<1]&&(re=next(p,rt<<1,l,m)))
        return re;
    return next(p,rt<<1|1,m+1,r);
}

第三:主席树
好了,终于到了正题,我们要讲的主席树
主席树据说是黄嘉泰大佬在比赛场地上,因为写题的需求,在赛场上自创的一种线段树运用。
主席树
1、以前缀和形式建立的可持久化线段树
2、基于动态开结点的存储形式
3、每次插入一个值时,最多新开O(log(n))个结点
4、空间复杂度O(n*log(n))
5、单次操作时间复杂度O(log(n))
6、可以查询区间的值域信息
7、相对于线段树套平衡树的优势:代码简单、速度快
8、劣势:离线数据结构,难以修改。(可以采取对询问分块等方式弥补)
可持久化思想:部分重建
主席树的询问与权值线段树本质相同,只是要在区间作差

主要思想:
用主席树构造一颗可持久化权值线段树,对于每个数字,将其离散化后,新建一个版本的权值线段树,然后插入这个离散化后的数字。例如对于25957 6405 15770 26287 26465 离散化结果:3 1 2 4 5这五个数字的序列。我们就要依次建立五个版本的权值线段树,分别插入3 1 2 4 5这五个数。这就是“对原序列的每一个前缀建树”
对于查询操作L,R,我们利用主席树的函数性质,用R那个版本的权值线段树减去L-1那个版本的权值线段树,在得到的权值线段树中查找第K小值就可以了。
因为权值线段树存储的是值域上值的个数,我们用R版本的权值线段树减去L-1版本的权值线段树,得到的就是维护[L,R]上值的个数的权值线段树

如何存储主席树:
首先可以建立一个数组int root[manl],来储存各个根节点的编号。对于子结点,可以看出主席树不像线段树可以用当前节点编号乘2和乘2加1来得到左右儿子的节点编号。于是我们可以用struct存储当前节点的左右儿子节点编号。于是我们可以这样做:用一个struct开一个内存池,每新建一个主席树的结点,就从内存池里取一块新的空间送给这个结点,取空间从hjt[1]开始,hjt[0]充当NULL

线段树是读完所有数据后再执行操作,而主席树是先读入一个,然后剩下的,一边插入,一边建树即可
主席树一开始里面什么都没有,所以所有的节点l,r,sum都是0,而全局变量和数组都是默认赋值为0的。

构造节点

struct node
{
	int l;
	int r;
	int sum;
};//l是左子树,r是右子树,sum是当前的权值
struct node tree[maxl*4];

对于主席树,我们一般采用节点来实现,在节点中存储其左子树,右子树和当前节点的权值的信息。
离散化函数

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

通过int getid()函数我们获得x在存储的值中的相对位置

建树和对树的更新

void update_tree(int l,int r,int &x,int y,int pos)
{
	tree[++cnt]=tree[y];
	tree[cnt].sum++;
	x=cnt;
	if(l==r);
		return;
	int mid=(l+r)>>1;
	if(mid>=pos)
		update_tree(1,mid,tree[x].l,tree[y].l,pos);
	else
		update_tree(mid+1,r,tree[x].r,tree[y].r,pos);
}

主席树的建树和更新是一起的,每当输入一个值的时候,就会依托于最近的一颗主席树来进行新的一次构建。
cnt为内存池。

遍历

int query(int l,int r,int x,int y,int k)
{
	if(l==r)
		return l;
	int mid=(l+r)>>1;
	int sum=tree[tree[y].l].sum-tree[tree[x].l].sum;
	if(sum>=k)
		return query(l,mid,tree[x].l,tree[y].l,k);
	else
		return query(mid+1,r,tree[x].r,tree[y].r,k-sum);
}

完整代码来惹

/*
题意:
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
7个数字 3次询问, 7个数字分别是 1 5 2 6 3 7 4
三次询问分别:
从第二个到第五个数字中间 排第三个多的数字是
*/
/*
input
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
output
5
6
3
*/
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;
const int maxl=1e5+6;
int n,m,cnt,root[maxl],a[maxl],x,y,k;
//a数组用来存放数字
//root数组用来建树
struct node
{
    int l;
    int r;
    int sum;
    //l是左子树,r是右子树,sum是当前的权值
};
struct node tree[maxl*4];
vector<int>v;
int getid(int x)//得到位置,离散化的第二步
{
    return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
void update(int l,int r,int &x,int y,int pos)
{
    //每建立一个新的结点,都需要以前一个结点,建立当前结点
    tree[++cnt]=tree[y];
    tree[cnt].sum++;//由于新插入一个数 所以权值增加
    x=cnt;
    if(l==r)
        return;
    int mid=(l+r)/2;
    if(mid>=pos)
        update(l,mid,tree[x].l,tree[y].l,pos);
        //若插入在左边就用前一个的左子树为依托建立当前要建立的左子树
    else
        update(mid+1,r,tree[x].r,tree[y].r,pos);
        //若插入在右边就以前一个的右子树为依托建立当前右子树
}
int query(int l,int r,int x,int y,int k)
{
    // x是原来的y是现在的
    //从l到r维护当前节点的区间
    if(l==r)
        return l;
    int mid=(l+r)/2;
    int sum=tree[tree[y].l].sum-tree[tree[x].l].sum;
    if(sum>=k)
        return query(l,mid,tree[x].l,tree[y].l,k);
    else
        return query(mid+1,r,tree[x].r,tree[y].r,k-sum);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        v.push_back(a[i]);
    }
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());//通过离散化后,已经排好的数组,如题:v即为1,2,3,4,5,6,7
    for(int i=1;i<=7;i++)
        cout<<a[i]<<"   "<<getid(a[i])<<endl;
    for(int i=1;i<=n;i++)
    {
        update(1,n,root[i],root[i-1],getid(a[i]));//对每一个输入数字进行建树
    }
    for(int i=1;i<=m;i++)
    {
        cin>>x>>y>>k;
        cout<<v[query(1,n,root[x-1],root[y],k)-1]<<endl;
    }
    return 0;
}

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值