题目描述
给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题。你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令。
对于每一个询问指令,你必须输出正确的回答。
输入格式
第一行有两个正整数n(1≤n≤100000),m(1≤m≤100000)。分别表示序列的长度和指令的个数。
第二行有n个数,表示a[1],a[2]……a[n],这些数都小于10^9。接下来的m行描述每条指令,每行的格式是下面两种格式中的一种。 Q i j k 或者 C i t
-
Q i j k (i,j,k是数字,1≤i≤j≤n, 1≤k≤j-i+1)表示询问指令,询问a[i],a[i+1]……a[j]中第k小的数。
-
C i t (1≤i≤n,0≤t≤10^9)表示把a[i]改变成为t。
输出格式
对于每一次询问,你都需要输出他的答案,每一个输出占单独的一行。
输入输出样例
输入 #1复制
5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3
输出 #1复制
3
6
说明/提示
10%的数据中,m,n≤100;
20%的数据中,m,n≤1000;
50%的数据中,m,n≤10000。
对于所有数据,m,n≤100000
请注意常数优化,但写法正常的整体二分和树套树都可以以大约1000ms每个点的时间过。
来源:bzoj1901
本题数据为洛谷自造数据,使用CYaRon耗时5分钟完成数据制作。
学完主席树入门后,发现那只是静态主席树模板,无法进行动态修改操作,或者说如果要修改的话,每次修改就要重新建树,那对于多次修改多次查询就会超时,而本题需要动态修改和查询,这就要用到可修改的动态主席树了(静态主席树套上树状数组--简称“树套树”)。一开始听着好像很复杂,后来学会了才发现,说是两个“树”,其实就是静态主席树的每棵权值线段树按照树状数组的结构和存储方式来建树罢了(然后把每棵树用树状数组的结构联系起来)。
这里每棵树的根结点所示值域区间中树的个数不再是静态主席树那样的简单的前缀和了,而是按树状数组那样的前logn个数的个数和,所以主要就难在建树上,建树时:每棵树的编号相当于树状数组的编号,树的根结点所示值域区间(也就是[1,n])中数的个数就相当于树状数组中保存的值(如果当前根结点编号为i,i转化为二进制后末尾连续的0的个数为n,那这棵树的根结点所示值域区间(也就是[1,n])中数的个数也就是a[i]前2^n个数(包括a[i])的总个数,整个树维护的数就是a[i]前2^n个数(包括a[i]),在维护每个结点所示的值域区间中数的个数时和静态主席树的维护方法一样,只不过这里的每棵树没有像静态主席树一样的共用的边了,而是一棵一棵的独立的权值线段树,后面建与前面有关联的包含关系的树时(这里说的相关联就是树状数组的那种相互包含结构:每次加上一个lowbit),直接将前面那一整棵树都复制过来,然后再插入新的数建树,树的根结点所示区间都是[1,n],只建权值不为0的结点),建根结点下面的子结点时就和线段树那样递归创建就行了。
这样按照树状数组的结构和存储方式来建树完成后,修改的话只需要先删去那个结点再在相应的位置加上新的结点,查询时按照树状数组的查询方式来查询就可以了,如果所查区间为[l.r],那只要用与第l-1棵树连着的前logn个树(每次减一个lowbit)中数的个数的累加和减去与第r棵树连着的前logn个树中数的个数的累加和,就可以得到[l,r]这个区间中数的个数了,因此每次查询的时间复杂度就和树状数组查询的时间复杂度一样,最后查询和修改一次的时间复杂度就降到了o(logn),问题就解决了!
完整代码:
#include <cstdio>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int maxn=1e5+5;
struct
{
int l,r,sum;
}hjt[maxn*800];
struct option
{
char kind;
int u,v,k;
}q[maxn];
int n,m,N,a[maxn],b[maxn<<1],root[maxn],cnt,Lroots[100],Rroots[100];
int getid(int x)
{
return lower_bound(b+1,b+N+1,x)-b;
}
int lowbit(int x)
{
return x&(-x);
}
void build(int p,int l,int r,int key)
{
if(l>r) return;
hjt[p].sum++;
if(l==r) return;
int mid=(l+r)>>1;
if(key>=l&&key<=mid)
{
if(hjt[p].l==0)
{
hjt[p].l=++cnt;
}
build(hjt[p].l,l,mid,key);
}
else if(key>=mid+1&&key<=r)
{
if(hjt[p].r==0)
{
hjt[p].r=++cnt;
}
build(hjt[p].r,mid+1,r,key);
}
}
void del(int p,int l,int r,int num)
{
if(l>r) return;
hjt[p].sum--;
if(l==r) return;
int mid=(l+r)>>1;
if(num>=l&&num<=mid)
{
del(hjt[p].l,l,mid,num);
}
else if(num>=mid+1&&num<=r)
{
del(hjt[p].r,mid+1,r,num);
}
}
int query(int l,int r,int ln,int rn,int k)
{
if(l==r) return l;
int Lsum=0,Rsum=0;
for(int i=1;i<=ln;i++)
{
Lsum+=hjt[hjt[Lroots[i]].l].sum;
}
for(int i=1;i<=rn;i++)
{
Rsum+=hjt[hjt[Rroots[i]].l].sum;
}
int t=Rsum-Lsum;
int mid=(l+r)>>1;
if(k<=t)
{
for(int i=1;i<=ln;i++)
{
Lroots[i]=hjt[Lroots[i]].l;
}
for(int i=1;i<=rn;i++)
{
Rroots[i]=hjt[Rroots[i]].l;
}
return query(l,mid,ln,rn,k);
}
else
{
for(int i=1;i<=ln;i++)
{
Lroots[i]=hjt[Lroots[i]].r;
}
for(int i=1;i<=rn;i++)
{
Rroots[i]=hjt[Rroots[i]].r;
}
return query(mid+1,r,ln,rn,k-t);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[++N]=a[i];
}
for(int i=1;i<=m;i++)
{
cin>>q[i].kind>>q[i].u>>q[i].v;
if(q[i].kind=='Q')
{
cin>>q[i].k;
}
else if(q[i].kind=='C')
{
b[++N]=q[i].v;
}
}
sort(b+1,b+N+1);
N=unique(b+1,b+N+1)-b-1;
for(int i=1;i<=n;i++)
{
int key=getid(a[i]);
for(int j=i;j<=n;j+=lowbit(j))
{
if(root[j]==0)
{
root[j]=++cnt;
}
build(root[j],1,N,key);
}
}
for(int i=1;i<=m;i++)
{
if(q[i].kind=='Q')
{
int ln=0,rn=0;
for(int j=q[i].u-1;j;j-=lowbit(j))
{
Lroots[++ln]=root[j];
}
for(int j=q[i].v;j;j-=lowbit(j))
{
Rroots[++rn]=root[j];
}
int id=query(1,N,ln,rn,q[i].k);
int ans=b[id];
cout<<ans<<endl;
}
else if(q[i].kind=='C')
{
int pre=getid(a[q[i].u]);
a[q[i].u]=q[i].v;
int now=getid(q[i].v);
for(int j=q[i].u;j<=n;j+=lowbit(j))
{
del(root[j],1,N,pre);
build(root[j],1,N,now);
}
}
}
return 0;
}
下面对此模板进行一下简单的说明:
#include <cstdio>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int maxn=1e5+5;
struct
{
int l,r,sum;
}hjt[maxn*800];//主席树的结点
struct option
{
char kind;
int u,v,k;
}q[maxn];//用来记录每次要进行的操作
int n,m,N,a[maxn],b[maxn<<1],root[maxn],cnt,Lroots[100],Rroots[100];//查询时:如果所查区间为[l,r],Lroots[]记录与第l-1个树连着的前logn个树(每次减一个lowbit)的根结点的编号,Rroors[]记录第r个的树连着的的前logn个数的根结点的编号
//a[]依次记录初始序列中的每个数,用b[]进行离散化,root[i]表示第i棵树根结点的编号,cnt为每个结点依次编号
int getid(int x)//得x对应的离散化后的值
{
return lower_bound(b+1,b+N+1,x)-b;
}
int lowbit(int x)//得与x有关系的数(树状数组的操作)
{
return x&(-x);
}
void build(int p,int l,int r,int key)//p是根结点编号,l和r为区间左右界,key为要在树中插入的数(这个数影响到的区间也就是这棵树要建的结点)
{
if(l>r) return;
hjt[p].sum++;//该结点中数的个数+1
if(l==r) return;
int mid=(l+r)>>1;
if(key>=l&&key<=mid)//要插入的数再左区间
{
if(hjt[p].l==0)
{
hjt[p].l=++cnt;//左子树编号为0的话要给左子树编号
}
build(hjt[p].l,l,mid,key);//递归建立往左子树建
}
else if(key>=mid+1&&key<=r)//要插入的数再左右区间
{
if(hjt[p].r==0)
{
hjt[p].r=++cnt;//右子树编号为0的话要给左子树编号
}
build(hjt[p].r,mid+1,r,key);//递归建立往右子树建
}
}
void del(int p,int l,int r,int num)//删除结点函数,与建树形式一致,p,l,r同上,num是要删除的数
{
if(l>r) return;
hjt[p].sum--;//这里是与建树时唯一的不同,因为删除结点,个数-1
if(l==r) return;
int mid=(l+r)>>1;
if(num>=l&&num<=mid)
{
del(hjt[p].l,l,mid,num);
}
else if(num>=mid+1&&num<=r)
{
del(hjt[p].r,mid+1,r,num);
}
}
int query(int l,int r,int ln,int rn,int k)//l和r是整个区间左右界,ln是与第l-1个树连着的前logn个树(每次减一个lowbit)的根结点的个数,rn是与第r个树连着的前logn个树(每次减一个lowbit)的根结点的个数,k是找的第k小
{
if(l==r) return l;
int Lsum=0,Rsum=0;
for(int i=1;i<=ln;i++)
{
Lsum+=hjt[hjt[Lroots[i]].l].sum;//Lsum计算与第l-1棵树连着的前logn个树(每次减一个lowbit)的左子树中数的个数的累加和
}
for(int i=1;i<=rn;i++)
{
Rsum+=hjt[hjt[Rroots[i]].l].sum;//Rsum计算与第r棵树连着的前logn个树(每次减一个lowbit)的左子树中数的个数的累加和
}
int t=Rsum-Lsum;//这个差值即为当前区间左子树中数的个数和
int mid=(l+r)>>1;
if(k<=t)//k<=为当前区间左子树中数的个数和,所以在左子树
{
for(int i=1;i<=ln;i++)
{
Lroots[i]=hjt[Lroots[i]].l;//这时因为要继续往下面的左子树中找,所以要把与第l-1个树连着的前logn个树(每次减一个lowbit)的根结点的编号更新为其左子树的根结点(也就是它的左孩子结点)的编号
}
for(int i=1;i<=rn;i++)
{
Rroots[i]=hjt[Rroots[i]].l;//把与第r个树连着的前logn个树(每次减一个lowbit)的根结点的编号更新为其左子树的根结点(也就是它的左孩子结点)的编号
}
return query(l,mid,ln,rn,k);//递归查询左子树
}
else//在右子树
{
for(int i=1;i<=ln;i++)
{
Lroots[i]=hjt[Lroots[i]].r;//与上面原理一样,这时就要把与第l-1个树连着的前logn个树(每次减一个lowbit)的根结点的编号更新为其右子树的根结点(也就是它的右孩子结点)的编号
}
for(int i=1;i<=rn;i++)
{
Rroots[i]=hjt[Rroots[i]].r;//把与第r个树连着的前logn个树(每次减一个lowbit)的根结点的编号更新为其右子树的根结点(也就是它的右孩子结点)的编号
}
return query(mid+1,r,ln,rn,k-t);//递归查询右子树
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[++N]=a[i];
}
for(int i=1;i<=m;i++)
{
cin>>q[i].kind>>q[i].u>>q[i].v;
if(q[i].kind=='Q')
{
cin>>q[i].k;
}
else if(q[i].kind=='C')
{
b[++N]=q[i].v;
}
}
//注意这里是把本组测试要需改的数加进来后再统一离散化
sort(b+1,b+N+1);
N=unique(b+1,b+N+1)-b-1;//N取去重后数组中数据的个数
//接下来就是建树:
for(int i=1;i<=n;i++)
{
int key=getid(a[i]);//key用来取a[i]离散化后的值
for(int j=i;j<=n;j+=lowbit(j))//向上后找与第i个数关联的也就是包含离散化后的a[i]的树的根结点的编号
{//比如i为1时遍历到的树编号依次为1 2 4 8 16 32...,i为3时3 4 8 16...,i为5时5 6 8 16 32...i为6时6 8 16 32...这就是树状数组的结构
if(root[j]==0)//root[j]表示第j棵树的根结点的编号
{
root[j]=++cnt;//根结点不存在时要新分配一个编号
}
build(root[j],1,N,key);//以当前编号为root[j]的根结点(与第i个数关联的根结点)为根,插入离散化后的a[i](也就是key)建树
}
}
//建完树,接下来就可以进行相应操作了:
for(int i=1;i<=m;i++)
{
if(q[i].kind=='Q')
{
int ln=0,rn=0;
for(int j=q[i].u-1;j;j-=lowbit(j))
{
Lroots[++ln]=root[j];//将每棵与第q[i].u-1(也就是要查询的区间左界l-1)棵树连着的前logn棵树的根结点的编号(每次减去个lowbit)依次记录在Lroots里
}
for(int j=q[i].v;j;j-=lowbit(j))
{
Rroots[++rn]=root[j];//将每棵与第q[i].(也就是要查询的区间左右界r)棵树连着的前logn棵树的根结点的编号(每次减去个lowbit)依次记录在Rroots里
}
int id=query(1,N,ln,rn,q[i].k);
int ans=b[id];
cout<<ans<<endl;
}
else if(q[i].kind=='C')
{
int pre=getid(a[q[i].u]);//pre得到离散化后的初始序列中的第q[i].u个数
a[q[i].u]=q[i].v;
int now=getid(q[i].v);//now得到离散化后的要改成的新数
for(int j=q[i].u;j<=n;j+=lowbit(j))
{
del(root[j],1,N,pre);//删除以前的数的相应的结点
build(root[j],1,N,now);//建立现在插入的数的相应的结点
//并且,用以上这两个函数操作时,可以同时维护影响到的结点
}
}
}
return 0;
}