1 条题解

  • 0
    @ 2025-8-24 22:09:55

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar 2018heyuyang
    **

    搬运于2025-08-24 22:09:55,当前版本为作者最后更新于2019-11-22 16:56:04,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    前言

    UPD by 2020.11.30,找到了更优的解法

    旧题解已转移至这里

    一年前,我写这道题时弄出了一个“缩减二分长度”的优化

    这项剪枝表现出了优异的效果,我没加什么优化就过了这题甚至没加快读

    然后写了篇题解,通俗易懂,码风清新,备受好评

    但是现在看来那篇题解写得太烂了,表述不严谨且仍有优化空间,于是我决定重新投一份((配合旧题解食用更佳))

    正片

    一些变量/预处理

    分块算法,首先我们要给块内排序,这里我们不使用结构体((可以减小常数且代码更美观))

    int a[N],lazy[N/block+5];//原数组,整体加标记 
    int p[N];bool cmp(int x,int y){return a[x]<a[y];}//排序数组 
    int main()
    {
    	int n,m;scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    	B=(n-1)/block+1;
    	for(int i=1;i<=B;i++)
    	{
    		bl[i]=br[i-1]+1;
    		br[i]=br[i-1]+block;
    	}br[B]=n;//分块基本操作 
    	for(int i=1;i<=B;i++)
    	{
    		for(int j=bl[i];j<=br[i];j++)w[p[j]=j]=i;
    		sort(p+bl[i],p+br[i]+1,cmp);
    	}//对于一个块内的p数组,其对应的a数组是从小到大的 
    	/*
    	......
    	*/
    	return 0;
    }
    

    可以看到,pp数组 反映了 aa数组 的大小关系

    修改

    大段打 lazylazy标记,小段暴力修改 aa数组,然后用归并排序的思想来维护 pp数组

    小段里面有两类数,一部分是不需要修改的数,另一部分则需要 +k+k

    我们知道 pp数组 存的是一堆下标,对应到 aa数组 是排好序的

    于是我们只要把 pp数组 按上述规则划分开来,然后来个简单的归并即可维护

    int c1[N],t1;
    int c2[N],t2;//tmp数组 
    void msort(int L,int R,int l,int r,int k)
    {//块左端,块右端,区间加范围 
    	t1=t2=0;//搞完以后c里面是一堆下标,对应a数组是从小到大的 
    	for(int i=L;i<=R;i++)(l<=p[i]&&p[i]<=r)?a[c1[++t1]=p[i]]+=k:c2[++t2]=p[i];
    	while(t1&&t2)p[R--]=(a[c1[t1]]>a[c2[t2]])?c1[t1--]:c2[t2--];//从大到小丢进去 
    	while(t1)p[R--]=c1[t1--];
    	while(t2)p[R--]=c2[t2--];//归并排序基本操作 
    }
    

    查询(重点)

    二分答案,设二分值域为LLRR,每次枚举midmid

    然后查询区间内 小于等于midmid 的数 的个数 ((记为s)s)

    s<ks<k,则 midmid 显然不为答案,令L=mid+1L=mid+1

    s>=ks>=k,则令 ans=midans=midR=mid1R=mid-1

    如何得知区间内 小于等于midmid 的数 的个数 呢?

    再套一个二分就行,正常思路是小段暴力,大段用 pp数组 二分

    (优化1)

    后来我发现,哎,为什么小段要暴力呢?

    其实我们可以再弄一个 cc数组,把小段像修改操作那样归并一下

    然后我们就可以“在小段上二分了”

    我原来的题解里是没有这个优化的,两端都是暴力统计(我太菜了)

    当我想起来时,然后去看别人的题解,发现他们都加了。。。

    (优化2)

    预先确定二分的值域范围,不要直接L=2147483648L=-2147483648R=2147483647R=2147483647

    因为题目的数据有一句“每次加上的数和原序列的数在[2×104,2×104][-2 \times 10^4 , 2 \times 10^4]

    (优化3)

    这也是我在旧题解里面提出的一个大优化

    假设我们某次枚举了一个midmid,然后其中有一个块 [l,r][l,r],我们得出 [l,pos][l,pos] 这些东西 小于等于midmid

    这一轮的 midmid 枚举结束后

    s<ks<k ,则说明 midmid 比正确答案小了,那么以后我们枚举的MIDMID就比这个midmid

    那么对于上述的块 [l,r][l,r],我们以后得出的位置一定大于等于pospos

    我们便可以把左端点拉过来,减少块内二分位置的区域

    s>=ks>=k ,那么以后我们枚举的MIDMID就比这个midmid

    那么对于上述的块 [l,r][l,r],我们以后得出的位置一定小于等于pospos

    我们便可以把右端点拉过来,减少块内二分位置的区域

    在随机数据下,这个优化快到离谱,但是也不是不能卡掉,hack数据很简单,然后可以把我的代码卡到10s

    以下为hack数据生成代码

    #include<ctime>
    #include<cstdio>
    #include<cstdlib>
    #include<random>
    using namespace std;
    mt19937 rnd;
    int random(int x){return (rnd()%x+x)%x;}//[0,x-1]
    int main()
    {
    	freopen("1.in","w",stdout);
    	rnd.seed(time(0));
    	int n=100000,m=100000;printf("%d %d\n0",n,m);
    	for(int i=2;i<=n;i++)printf(" 20000");
    	puts("");
    	for(int i=1;i<=50000;i++)puts("2 2 100000 20000");
    	for(int i=50001;i<=100000;i++)puts("1 1 100000 100000");
    	return 0;
    }
    

    不过很抱歉,这玩意是可以处理的

    因为“每次加上的数和原序列的数在[2×104,2×104][-2 \times 10^4 , 2 \times 10^4]

    这样会导致部分区间的值域范围大小很小

    我们只要在块内二分前先判断一下,是否整个块都比midmid//((优化4))

    这样可以使得 优化3 变快不少,其他题解也提到了这个,不过lxl的数据很水,随便使用几个优化即可AC

    复杂度

    空间复杂度O(N)O(N)是跑不掉的

    我的代码时间复杂度随机情况下是O(NNΔ)O(N\sqrt{NΔ}),最坏是O(NNΔ)O(N\sqrt{N}Δ),其中ΔΔ是二分次数,你大概可以看成20到30

    这里的最优复杂度块长取(NΔ)(\sqrt{NΔ}),最坏复杂度块长取(NΔ)(\sqrt{N}Δ)

    也就是说数据越随机,块长我就取得越小,代码就能跑得更快,但是数据毒瘤一点的话复杂度就会退化

    至此,我们得出了一个空间复杂度为O(N)O(N),时间复杂度是O(NNΔ)O(N\sqrt{N}Δ)的在线做法,但是因为剪枝多,跑得非常快,测了一下上面的hack数据,没加读入输出优化(只有O2)跑了2秒

    但这个代码可以卡掉,经过与大佬的讨论,我们只需构造出多个相似的段,比如说N\sqrt{N}个块长得一模一样而且里面11N\sqrt{N}每个数都出现一次,当然这种数据不常见,但是我们这是学术研究,不能随便口糊过去

    于是这种做法就宣告失败了,但是这道题还是能过的

    关于本题数据

    lxl的数据水到什么程度呢?

    如果你把上面的数据生成器弄出来的数据给目前所有题解测一下,你会发现都过不了(截至2020.11.30),TLE也就算了(最快的大概跑到5秒),但是有几个题解是WA?

    然后看了下WA掉的题解,发现他们二分答案的初始范围是错的((会爆int)int)

    下面给出我的代码(抄就抄吧,抄不走我对分块的热情

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    const int N=100005;
    const int block=700;
    int B,bl[N/block+5],br[N/block+5],w[N];//左,右,从属块 
    
    int a[N],lazy[N/block+5];//原数组,整体加标记 
    int p[N];bool cmp(int x,int y){return a[x]<a[y];}//排序数组 
    
    int c1[N],t1;
    int c2[N],t2;//tmp数组 
    void msort(int L,int R,int l,int r,int k)
    {//块左端,块右端,区间加范围 
    	t1=t2=0;//搞完以后c里面是一堆下标,对应a数组是从小到大的 
    	for(int i=L;i<=R;i++)(l<=p[i]&&p[i]<=r)?a[c1[++t1]=p[i]]+=k:c2[++t2]=p[i];
    	while(t1&&t2)p[R--]=(a[c1[t1]]>a[c2[t2]])?c1[t1--]:c2[t2--];//从大到小丢进去 
    	while(t1)p[R--]=c1[t1--];
    	while(t2)p[R--]=c2[t2--];//归并排序基本操作 
    }
    
    int c[N],t;
    
    int asl[N/block+5],asr[N/block+5],as[N/block+5];
    
    int main()
    {
    //	freopen("1.in","r",stdin);
    //	freopen("1.out","w",stdout);
    	int n,m;scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    	B=(n-1)/block+1;
    	for(int i=1;i<=B;i++)
    	{
    		bl[i]=br[i-1]+1;
    		br[i]=br[i-1]+block;
    	}br[B]=n;//分块基本操作 
    	for(int i=1;i<=B;i++)
    	{
    		for(int j=bl[i];j<=br[i];j++)w[p[j]=j]=i;
    		sort(p+bl[i],p+br[i]+1,cmp);
    	}//对于一个块内的p数组,其对应的a数组是从小到大的 
    	
    	while(m--)
    	{
    		int cz,l,r,k;scanf("%d%d%d%d",&cz,&l,&r,&k);
    		int ql=w[l],qr=w[r];
    		if(cz==1)//查询 
    		{
    			if(k>r-l+1){puts("-1");continue;}
    			if(ql==qr)
    			{
    				for(int i=bl[ql],j=0;i<=br[ql];i++)
    				{
    					if(l<=p[i]&&p[i]<=r&&++j==k)
    					{
    						printf("%d\n",a[p[i]]+lazy[ql]);break;
    					}//不要漏掉lazy标记... 
    				}
    			}
    			else
    			{
    				t1=t2=t=0;//把小段取出来归并,不过要注意此时c数组的处理方式和修改操作不一样 
    				for(int i=br[ql];i>=bl[ql];i--)if(l<=p[i])c1[++t1]=a[p[i]]+lazy[ql];//左边 
    				for(int i=br[qr];i>=bl[qr];i--)if(p[i]<=r)c2[++t2]=a[p[i]]+lazy[qr];//右边 
    				while(t1&&t2)c[++t]=(c1[t1]<c2[t2])?c1[t1--]:c2[t2--];//从小到大丢进去 
    				while(t1)c[++t]=c1[t1--];
    				while(t2)c[++t]=c2[t2--];
    				
    				if(ql+1==qr){printf("%d\n",c[k]);continue;}
    				
    				//然后的情况是从c数组和很多个大段里面找第k小,注意此时c数组是确定的值,且无需lazy标记 
    				int L=c[1],R=c[t],ans;
    				for(int i=ql+1;i<qr;i++)
    				{
    					L=min(L,a[p[bl[i]]]+lazy[i]);
    					R=max(R,a[p[br[i]]]+lazy[i]);
    					as[i]=asl[i]=bl[i]-1;asr[i]=br[i]+1;
    				}as[0]=asl[0]=0;asr[0]=t+1;//找到最值以及预处理,这个下标0是给c数组用的 
    				while(L<=R)//二分开始! 
    				{
    					int mid=(L+R)/2,s=0,fucl,fucr,fucmid;//mid为二分值,s为比mid小或者等于mid的个数 
    					fucl=asl[0]+1;fucr=asr[0]-1;
    					if(fucl<=fucr)
    					{
    						if(c[fucr]<=mid){as[0]=fucr;fucl=fucr+1;}//优化 
    						else if(c[fucl]>mid)fucr=fucl-1;
    						while(fucl<=fucr)
    						{
    							fucmid=(fucl+fucr)/2;
    							if(c[fucmid]<=mid){as[0]=fucmid;fucl=fucmid+1;}
    							else fucr=fucmid-1;
    						}
    					}
    					s+=as[0];
    					if(s<k)
    					{
    						for(int i=ql+1;i<qr;i++)
    						{
    							fucl=asl[i]+1;fucr=asr[i]-1;//asl和asr是不可以踩到的地方 
    							if(fucl<=fucr)
    							{
    								if(a[p[fucr]]+lazy[i]<=mid){as[i]=fucr;fucl=fucr+1;}//优化 
    								else if(a[p[fucl]]+lazy[i]>mid)fucr=fucl-1;
    								while(fucl<=fucr)//合法范围 
    								{
    									fucmid=(fucl+fucr)/2;
    									if(a[p[fucmid]]+lazy[i]<=mid){as[i]=fucmid;fucl=fucmid+1;}//bl[i]~fucmid都小于等于mid 
    									else fucr=fucmid-1;
    								}
    							}
    							s+=as[i]-bl[i]+1;
    							if(s>=k)//你s都大于等于k了说明什么,ans必能更新啊,还二分个锤子 
    							{//mid的排名一定>=s,当s>=k时,mid>=k,此时可以更新答案ans 
    								while(i>ql){asr[i]=as[i]+1;as[i]=asl[i];i--;}
    								break;
    							}
    						}
    					}
    					if(s>=k)
    					{
    						asr[0]=as[0]+1;as[0]=asl[0];
    						ans=mid;R=mid-1;//ans只会越来越小 
    					}
    					else
    					{
    						L=mid+1;asl[0]=as[0];
    						for(int i=ql+1;i<qr;i++)asl[i]=as[i];
    					}
    				}
    				printf("%d\n",ans);
    			}
    		}
    		else //修改 
    		{
    			if(ql==qr)msort(bl[ql],br[ql],l,r,k);//使用归并思想处理小块 
    			else
    			{
    				msort(bl[ql],br[ql],l,br[ql],k);
    				msort(bl[qr],br[qr],bl[qr],r,k);
    				for(int i=ql+1;i<qr;i++)lazy[i]+=k;//打标记 
    			}
    		}
    	}
    	return 0;
    }
    

    难道你觉得这份题解已经结束了吗?

    并不,这题还剩一个条件没有用过,就是“离线”

    有的人可能会问了,区间加还能离线?

    那你可以康康这题(话说这题怎么时限变750ms了

    在某个奇妙的冬夜,我裹着被子翻来覆去,然后脑子里冒出了一个idea...

    还记得我们二分答案后要求什么吗?区间有多少数小于等于midmid

    现在来这么一道经典题目,有两种操作

    1.1.区间加 kk

    2.2.求区间内小于等于kk的数的个数

    其实这题是可以O(NN)O(N\sqrt{N})离线做的(块长取N\sqrt{N}),套用P4118的思想即可

    那么我们只要应用这项黑科技,就可以直接大力二分答案

    然后复杂度是O(NNΔ)O(N\sqrt{N}Δ)的,这种做法似乎和众多题解的复杂度相当

    但是这里又能优化,就很离谱

    以下内容不难,但是涉及上面的思想,而且我不会过于详细地解释

    设块长为BB,我们有NN个小段修改,N2B\frac{N^2}{B}个大段询问,

    因为二分答案的缘故,我们要搞ΔΔ次,每次重构会浪费很多复杂度

    我们对比二分操作中询问的两个不同的midmid,然后发现修改操作完全一致

    于是我们可以先预处理出小段修改后的序列,这样可以将复杂度从O(NΔ(B+NB))O(NΔ(B+\frac{N}{B}))变成O(N(B+ΔNB))O(N(B+Δ\frac{N}{B}))

    然后块长取(NΔ)(\sqrt{NΔ}),复杂度似乎就是O(NNΔ)O(N\sqrt{NΔ})

    但是这里有个小瑕疵,就是块长比块的数量大一些

    每一个大段的询问均摊不是O(1)O(1)的,最终我们选取块长略大于(NΔ)(\sqrt{NΔ})

    复杂度也许是O(NNkΔ)O(N\sqrt{NkΔ}),其中 kk 近似于 loglogNloglogN 级别

    上面这部分本来是口糊的

    但与大佬交流了一下,他说这样做还真的可以O(NNkΔ)O(N\sqrt{NkΔ})

    于是我们的新做法:时空复杂度都略大于O(NNΔ)O(N\sqrt{NΔ}),但是肯定比O(NNΔ)O(N\sqrt{N}Δ)小不少

    虽然不及神仙的O(NNlogN)O(N\sqrt{NlogN})做法,但是这也不失为一种优秀的做法

    结语

    跟大佬交流,虽然看起来被D飞了,但是学到的真的不少

    如果还有什么好的意见,欢迎提出

    • 1

    信息

    ID
    4337
    时间
    2000ms
    内存
    128MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者