模板总结——主席树

任务

求静态区间第k小数。
即给定数组a和一个区间,求[l,r]中第k小的数。

简化问题

假设大小为n的数组内的值恰好为1~n的排列。比如大小为4的数组为:4,2,1,3.
要想将其他数组转化为上述简化问题,只需进行离散即可。即将原数组排序,将对应的值与排序后的下标对应起来。用下标来表示原来的值。

vector<int> v;
int getid(int k)
{
    return lower_bound(v.begin(),v.end(),k)-v.begin()+1;
}
for(int i=1;i<=n;i++)
 {
       scanf("%d",&a[i]);
       v.push_back(a[i]);
 }

 sort(v.begin(),v.end());
 v.erase(unique(v.begin(),v.end()),v.end());

变量含义

设rt[i]表示由数组前i个元素组成的线段树的根结点。结点为一个结构体,内含l,r,sum。l、r表示该结点的左右子结点,sum表示以该结点为根的子树所含数组内元素的个数。

算法流程

(1)建立一颗空树,默认数组值都为0

void build(int &o,int l,int r)//建立一颗空树
{
    o=++tot;
    T[o].sum=0;
    if(l==r) return;
    int mid=(l+r)/2;
    build(T[o].l,l,mid);
    build(T[o].r,mid+1,r);
}

(2)根据上一个颗树,更新线段树的值
每次更新都只是将数组第i个元素的值由默认的0更新为a[i],体现在线段树中只改变了一条链的结点值(从叶子结点到根结点)。只需要重新开logn个结点来存放被更新的结点值,其他结点可以与上一个版本的线段树共用。

void update(int l,int r,int &now,int last,int k)
{
    T[++tot]=T[last];//复制线段树的根结点
    //更新当前线段树的根结点
    now=tot;
    T[tot].sum++;
    if(l==r) return;//修改到叶子结点为止
    //根据需要修改的k来确定是修改上一个版本的左子树还是修改右子树
    int mid=(l+r)/2;
    if(k<=mid)
        update(l,mid,T[now].l,T[last].l,k);
    else
        update(mid+1,r,T[now].r,T[last].r,k);
}

(3)根据第x-1个版本的线段树和第y个版本的线段树来确定区间[x,y]的第k小值
设cnt=第y个版本的线段树的左子树的sum值 - 第x-1个版本的线段树的左子树的sum值。则cnt表示从第x个版本到第y个版本线段树的左子树所增加的值的数目。即区间[x,y]中的值在线段树的左子树上的个数。如果cnt值大于k,说明第k小的数在左子树上,递归去左子树求第k小的即可;否则,说明答案在右子树上,递归去右子树求第k-cnt小的数即可。

int query(int l,int r,int x,int y,int k)//查询区间【x,y】中第小的数
{
    if(l==r) return l;//查询到叶子结点为止
    int mid=(l+r)/2;
    int cnt=T[T[y].l].sum-T[T[x].l].sum;//第y颗树比第x颗树在左子树上多的结点数
    if(cnt>=k)//答案在左子树上
        return query(l,mid,T[x].l,T[y].l,k);
    else
        return query(mid+1,r,T[x].r,T[y].r,k-cnt);
}

模板

//poj2104
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn=1e5+100;

int a[maxn];
int rt[maxn];//rt[i]表示由数组前i个元素组成的线段树的根结点
struct node
{
    int l,r;//线段树左右子结点点
    int sum;//结点信息,表示这颗子树存在的元素的数目
}T[maxn*20];
int tot=0;//结点编号
vector<int> v;
int getid(int k)
{
    return lower_bound(v.begin(),v.end(),k)-v.begin()+1;
}
void build(int &o,int l,int r)//建立一颗空树
{
    o=++tot;
    T[o].sum=0;
    if(l==r) return;
    int mid=(l+r)/2;
    build(T[o].l,l,mid);
    build(T[o].r,mid+1,r);
}
void update(int l,int r,int &now,int last,int k)
{
    T[++tot]=T[last];//复制线段树
    //更新当前线段树的根结点
    now=tot;
    T[tot].sum++;
    if(l==r) return;//修改到叶子结点为止
    //根据需要修改的k来确定是修改左子树还是修改右子树
    int mid=(l+r)/2;
    if(k<=mid)
        update(l,mid,T[now].l,T[last].l,k);
    else
        update(mid+1,r,T[now].r,T[last].r,k);
}
int query(int l,int r,int x,int y,int k)//查询区间【x,y】中第小的数
{
    if(l==r) return l;//查询到叶子结点为止
    int mid=(l+r)/2;
    int cnt=T[T[y].l].sum-T[T[x].l].sum;//第y颗树比第x颗树在左子树上多的结点数
    if(cnt>=k)//答案在左子树上
        return query(l,mid,T[x].l,T[y].l,k);
    else
        return query(mid+1,r,T[x].r,T[y].r,k-cnt);
}
int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            v.push_back(a[i]);
        }
        //build(,1,n);
        sort(v.begin(),v.end());
        v.erase(unique(v.begin(),v.end()),v.end());
        build(rt[0],1,n);
        for(int i=1;i<=n;i++)
            update(1,n,rt[i],rt[i-1],getid(a[i]));
        while(m--)
        {
            int x,y,k;
            scanf("%d%d%d",&x,&y,&k);
            printf("%d\n",v[query(1,n,rt[x-1],rt[y],k)-1]);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_37685156/article/details/80350385