题解 P5356 【[Ynoi2017]由乃打扑克】
2018heyuyang · · 题解
前言
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;
}
可以看到,
修改
大段打
小段里面有两类数,一部分是不需要修改的数,另一部分则需要
我们知道
于是我们只要把
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--];//归并排序基本操作
}
查询(重点)
二分答案,设二分值域为
然后查询区间内 小于等于
若
若
如何得知区间内 小于等于
再套一个二分就行,正常思路是小段暴力,大段用
(优化1)
后来我发现,哎,为什么小段要暴力呢?
其实我们可以再弄一个
然后我们就可以“在小段上二分了”
我原来的题解里是没有这个优化的,两端都是暴力统计(我太菜了)
当我想起来时,然后去看别人的题解,发现他们都加了。。。
(优化2)
预先确定二分的值域范围,不要直接
因为题目的数据有一句“每次加上的数和原序列的数在
(优化3)
这也是我在旧题解里面提出的一个大优化
假设我们某次枚举了一个
这一轮的
若
那么对于上述的块
我们便可以把左端点拉过来,减少块内二分位置的区域
若
那么对于上述的块
我们便可以把右端点拉过来,减少块内二分位置的区域
在随机数据下,这个优化快到离谱,但是也不是不能卡掉,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;
}
不过很抱歉,这玩意是可以处理的
因为“每次加上的数和原序列的数在
这样会导致部分区间的值域范围大小很小
我们只要在块内二分前先判断一下,是否整个块都比
这样可以使得 优化3 变快不少,其他题解也提到了这个,不过lxl的数据很水,随便使用几个优化即可AC
复杂度
空间复杂度
我的代码时间复杂度随机情况下是
这里的最优复杂度块长取
也就是说数据越随机,块长我就取得越小,代码就能跑得更快,但是数据毒瘤一点的话复杂度就会退化
至此,我们得出了一个空间复杂度为
但这个代码可以卡掉,经过与大佬的讨论,我们只需构造出多个相似的段,比如说
于是这种做法就宣告失败了,但是这道题还是能过的
关于本题数据
lxl的数据水到什么程度呢?
如果你把上面的数据生成器弄出来的数据给目前所有题解测一下,你会发现都过不了(截至2020.11.30),TLE也就算了(最快的大概跑到5秒),但是有几个题解是WA?
然后看了下WA掉的题解,发现他们二分答案的初始范围是错的
下面给出我的代码(抄就抄吧,抄不走我对分块的热情)
#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...
还记得我们二分答案后要求什么吗?区间有多少数小于等于
现在来这么一道经典题目,有两种操作
但是这里有个小瑕疵,就是块长比块的数量大一些
每一个大段的询问均摊不是
复杂度也许是
上面这部分本来是口糊的
但与大佬交流了一下,他说这样做还真的可以
于是我们的新做法:时空复杂度都略大于
虽然不及神仙的
结语
跟大佬交流,虽然看起来被D飞了,但是学到的真的不少
如果还有什么好的意见,欢迎提出