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;
}
posted @ 2020-07-02 00:11  Acestar  阅读(113)  评论(0编辑  收藏  举报