SPOJ3946 MKTHNUM - K-th Number
题意应该都知道吧,就是查询区间第 \(k\) 小。
这里介绍一下整体二分:
-
所谓整体二分,需要数据结构题满足以下性质:
1.询问的答案具有可二分性。
2.修改对判定答案的贡献相对独立,修改之间互不影响效果。
3.修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值。
4.贡献满足交换律,结合律,具有可加性。
5.题目允许离线操作。
-
询问的答案有可二分性质显然是前提,我们发现,因为修改对判定标准的贡献相对独立,且贡献的值(如果有的话)与判定标准无关,所以如果我们已经计算过某一个些修改对询问的贡献,那么这个贡献永远不会改变,我们没有必要当判定标准改变时再次计算这部分修改的贡献,只要记录下当前的总贡献,再进一步二分时,直接加上新的贡献即可。
-
这样的话,我们发现,处理的复杂度可以不再与序列总长度直接相关了,而可能只与当前待处理序列的长度相关。
接下来考虑本题。
对于单个查询而言,我们可以采用预处理+二分答案的方法解决,但现在我们要回答的是一系列的查询,对于查询而言我们都要重新预处理然后二分,时间复杂度无法承受,但是我们仍然希望通过二分答案的思想来解决,整体二分就是基于这样一种想法,我们将所有操作(包括修改和查询)一起二分,进行分治。
我们时刻维护一个操作序列和对应的可能答案区间 \([L,R]\),我们先求的一个判定答案 \(mid=(L+R)>>1\),然后我们考虑操作序列的修改操作,将其中符合条件的修改对各个询问的贡献统计出来,然后我们对操作序列进行划分。
先用一个结构体数组存下所有输入。
struct node
{
int x,y,k,type,id;
//修改中 x:数值, type:操作类型(1), id:在数组中的位置
//查询中 x,y,k:输入, type:操作类型(2), id:询问编号
};
node q[N+M],q1[N+M],q2[N+M]; //q:当前二分区间的操作, q1:当前二分区间的左区间的操作, q2:右区间的操作
int t1,t2; //t1:q1长度, t2:q2长度
\([ql,qr]\) 表示操作区间,\([L,R]\)表示答案区间。
如果 \(L=R\) 就说明找到答案了,将 \([ql,qr]\) 中所有查询操作的答案赋为 \(L\)。
if(L==R) //找到答案了
{
for(int i=ql; i<=qr; i++)
if(q[i].type==2)
ans[q[i].id]=L;
return;
}
第一类操作是修改,也就是输入的 \(n\) 个数。
这里用树状数组维护,最普通的吧,维护比一个数小的数的个数。
int c[N];
int lowbit(int t)
{
return t&(-t);
}
void add(int x,int y)
{
for(int i=x; i<=n; i+=lowbit(i))
c[i]+=y;
}
int getsum(int x)
{
int res=0;
for(int i=x; i>0; i-=lowbit(i))
res+=c[i];
return res;
}
但是并不是直接全都 \(add\) 进去。在二分的时候,如果 \(q[i].x<=mid\),也就是会影响第 \(k\) 小的值,就 \(add(q[i].id,1)\),并将这个操作存进 \(q1\),因为在左区间也会用到,否则直接存进 \(q2\),因为在右区间可能会用到。
if(q[i].type==1)
{
if(q[i].x<=mid)
{
add(q[i].id,1);
q1[++t1]=q[i];
}
else
{
q2[++t2]=q[i];
}
}
第二类操作是查询。
如果当前累计贡献 \(cnt\) 比要求贡献大,也就是数的个数大于 \(k\),说明 \(mid\) 过大,满足标准的修改过多,我们需要给这个查询设置更小的答案区间,于是二分到答案区间 \([L,mid]\),否则二分到区间 \([mid+1,R]\),并将查询第 \(k\) 小改为查询第 \(k-cnt\) 小。
else //if(q[i].type==2)
{
int cnt=getsum(q[i].y)-getsum(q[i].x-1);
if(cnt>=q[i].k)
{
q1[++t1]=q[i];
}
else
{
q[i].k-=cnt;
q2[++t2]=q[i];
}
}
最后把 \(q1\) 和 \(q2\) 再存回 \(q\),进行二分就可以了。注意要把树状数组清空。
for(int i=1; i<=t1; i++)
if(q1[i].type==1) add(q1[i].id,-1);
for(int i=1; i<=t1; i++)
q[ql+i-1]=q1[i];
for(int i=1; i<=t2; i++)
q[ql+t1+i-1]=q2[i];
solve(ql,ql+t1-1,L,mid);
solve(ql+t1,qr,mid+1,R);
Code
#include<iostream>
#include<cstdio>
#define INF 1e9
using namespace std;
const int N=1e5+10;
const int M=5e3+10;
struct node
{
int x,y,k,type,id;
//修改中 x:数值, type:操作类型(1), id:在数组中的位置
//查询中 x,y,k:输入, type:操作类型(2), id:询问编号
};
int n,m,tot; //tot:操作个数
int ans[N],c[N];
node q[N+M],q1[N+M],q2[N+M]; //q:当前二分区间的操作, q1:当前二分区间的左区间的操作, q2:右区间的操作
int lowbit(int t)
{
return t&(-t);
}
void add(int x,int y)
{
for(int i=x; i<=n; i+=lowbit(i))
c[i]+=y;
}
int getsum(int x)
{
int res=0;
for(int i=x; i>0; i-=lowbit(i))
res+=c[i];
return res;
}
void solve(int ql,int qr,int L,int R) //ql,qr:操作区间; L,R:答案区间
{
if(ql>qr) return;
if(L==R) //找到答案了
{
for(int i=ql; i<=qr; i++)
if(q[i].type==2)
ans[q[i].id]=L;
return;
}
int mid=(L+R)>>1,t1=0,t2=0;
for(int i=ql; i<=qr; i++)
{
if(q[i].type==1)
{
if(q[i].x<=mid)
{
add(q[i].id,1);
q1[++t1]=q[i];
}
else
{
q2[++t2]=q[i];
}
}
else //if(q[i].type==2)
{
int cnt=getsum(q[i].y)-getsum(q[i].x-1);
if(cnt>=q[i].k)
{
q1[++t1]=q[i];
}
else
{
q[i].k-=cnt;
q2[++t2]=q[i];
}
}
}
for(int i=1; i<=t1; i++)
if(q1[i].type==1) add(q1[i].id,-1);
for(int i=1; i<=t1; i++)
q[ql+i-1]=q1[i];
for(int i=1; i<=t2; i++)
q[ql+t1+i-1]=q2[i];
solve(ql,ql+t1-1,L,mid);
solve(ql+t1,qr,mid+1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
int x;
scanf("%d",&x);
tot++;
q[tot].x=x,q[tot].type=1,q[tot].id=i;
}
for(int i=1; i<=m; i++)
{
int x,y,k;
scanf("%d%d%d",&x,&y,&k);
tot++;
q[tot].x=x,q[tot].y=y,q[tot].k=k,q[tot].type=2,q[tot].id=i;
}
solve(1,tot,-INF,INF);
for(int i=1; i<=m; i++)
printf("%d\n",ans[i]);
return 0;
}