【洛谷6774】[NOI2020] 时代的眼泪(分块“入门”题)
- 给定一个长度为\(n\)的序列\(a\),每次询问\([l,r]\)区间内值在\([L,R]\)范围内的所有元素中存在多少顺序对。
- \(n\le10^5,m\le2\times10^5\)
我觉得我就是“时代的眼泪”,卡常卡到心态爆炸。。。
可以先看一看这道题的弱化版:【洛谷5046】[Ynoi2019 模拟赛] Yuno loves sqrt technology I(已经是道黑题了)。
听说这是道分块“入门”题,仔细想了想感觉思维难度的确不是很高?
序列分块:处理散块
首先我们考虑序列分块,那么就是要把一个询问区间拆成散块+若干整块+散块。
然后注意到一个重要性质,一个块中数的种数是\(O(\sqrt n)\)的,所以我们可以对每个块事先离散化,预处理出\(T_{i,x}\)表示第\(i\)个块中大于等于\(x\)的最小的数的离散化后的标号,并记录第\(i\)个数\(a_i\)在离散化后变成了\(p_i\)。
借助这个性质我们就可以预先求出\(pre_{i,v},suf_{i,v}\)分别表示\(i\)在所在块内的前缀/后缀中,离散化后值小于等于\(v\)的数的个数。
而根据块数\(\le\sqrt n\)的基本性质,我们还可以预处理出\(c_{i,x}\)表示前\(i\)个块中值小于等于\(x\)的数的个数。
接下来,我们分别讨论散块与不同主体之间产生的贡献。(设左右端点所在块编号分别为\(pl,pr\))
- 散块块内: 以左散块为例,直接暴力枚举散块内每个元素\(p_i\),判断是否在值域内,然后给答案加上块内从它向后的后缀中离散化后值在\([p_{i}+1,T_{pl,R+1}-1]\)范围内的数的个数。
- 散块与整块: 以左散块为例,同样直接暴力枚举散块内每个元素\(a_i\),判断是否在值域内,然后给答案加上整块中值在\([a_i+1,R]\)范围内的数的个数。
- 散块与散块: 暴力枚举左散块中的每个元素\(a_i\),判断是否在值域内,然后给答案加上右散块中从\(r\)向前的前缀中离散化后值在\([T_{pr,a_i},T_{pr,R+1}-1]\)范围内的数的个数
而整块与整块之间的贡献比较复杂了,对此我们需要再搞一个值域分块来求解。
值域分块:求解整块
考虑对值域分块,然后预处理出\(f_{i,j,k}\)表示第\(i\sim j\)个块中,值在前\(k\)个块内的所有数对于答案的贡献总和。
设值域上下界所在值域块编号分别为\(dl,dr\),则我们通过差分,用\(f_{pl+1,pr-1,dr-1}\)减去\(f_{pl+1,pr-1,dl}\)再减去\(pl+1\sim pr-1\)这些序列块中由前\(dl\)个值域块中的数和第\(dl+1\sim dr-1\)个值域块中的数组成的顺序对个数即可得出序列整块询问的值域整块的答案。
而对于要减去的这玩意儿,我们直接枚举每个序列块,则其中在前\(dl\)个值域块中的数的个数乘上其后在第\(dl+1\sim dr-1\)个值域块中的数的个数即为它与之后块产生的错误贡献。
而每个序列块内部的错误贡献,可以事先预处理一个\(g_{i,j,k}\)表示第\(i\)个块中,离散化后值小于等于\(j\)的数与离散化后值在\(j+1\sim k\)范围内的数组成的顺序对个数,那么把第\(dl\)个值域块的右端点和第\(dr-1\)个值域块的右端点离散化后询问即可。
最后还要求解序列整块询问的值域散块的答案,这其实和普通的分块差不多,散块直接暴力枚举,判断在序列上是否位于询问的序列整块中,然后考虑它的贡献即可。
一个奇怪的散块优化
对于一个散块,如果我们的询问区间小于块大小的一半就直接询问,否则我们用总贡献减去剩余区间的贡献。
这样可以保证每次处理的散块大小都不超过块大小的一半,实测有比较显著的优化。
代码:\(O(n\sqrt n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define BS 320
#define LL long long
using namespace std;
int n,a[N+5],id[N+5];
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
#define C(s,x1,x2) (s[x2]-s[x1-1])//一维数组差分
#define G(s,x1,x2,y1,y2) (C(s[x2],y1,y2)-C(s[x1-1],y1,y2))//二维数组两维分别差分
int pre[N+5][BS+5],suf[N+5][BS+5],c0[BS+5][N+5],c[BS+5][N+5],T[BS+5][N+5],g[BS+5][BS+5][BS+5];LL f[BS+5][BS+5][BS+5];
int sz,bl[N+5],LS[BS+5],RS[BS+5],p[N+5],s[N+5],o[BS+5];I void Build()//预处理
{
RI i,j,k;for(sz=sqrt(n),i=1;i<=n;++i) !LS[bl[i]=(i-1)/sz+1]&&(LS[bl[i]]=i),RS[bl[i]]=i,s[i]=a[i];LS[bl[n]+1]=n+1;//记录每个位置所在块及每个块的左右端点
for(i=1;i<=bl[n];++i)//枚举每个块预处理信息
{
for(sort(s+LS[i],s+RS[i]+1),j=LS[i];j<=RS[i];++j) T[i][s[j]]=j-LS[i]+1;T[i][n+1]=j-LS[i]+1;//离散化
for(j=n;j;--j) !T[i][j]&&(T[i][j]=T[i][j+1]);for(j=LS[i];j<=RS[i];++j) o[p[j]=T[i][a[j]]]=j;//求出每个数后继的离散化结果;记录每个位置离散化值
for(j=LS[i];j<=RS[i];++j) {if(j^LS[i]) for(k=1;k<=T[i][n+1];++k) pre[j][k]=pre[j-1][k];for(k=p[j];k<=T[i][n+1];++k) ++pre[j][k];}//所在前缀离散化后每个数的个数
for(j=RS[i];j>=LS[i];--j) {if(j^RS[i]) for(k=1;k<=T[i][n+1];++k) suf[j][k]=suf[j+1][k];for(k=p[j];k<=T[i][n+1];++k) ++suf[j][k];}//所在后缀离散化后每个数的个数
for(j=LS[i];j<=RS[i];++j) ++c0[i][a[j]];for(j=1;j<=n;++j) c[i][j]=c[i-1][j]+(c0[i][j]+=c0[i][j-1]);//统计前i个块每个数的个数
for(j=1;j<=T[i][n+1];++j) for(k=j+1;k<=T[i][n+1];++k) g[i][j][k]=g[i][j][k-1]+(k^T[i][n+1]?pre[o[k]][j]:0);//预处理块内离散化后1~j的数与j+1~k的数组成的顺序对数
}
for(i=1;i<=bl[n];++i) for(j=i;j<=bl[n];++j)//枚举i~j的块
{
for(k=LS[j];k<=RS[j];++k) f[i][j][bl[a[k]]]+=pre[k][p[k]-1]+c[j-1][a[k]]-c[i-1][a[k]];//计算加入数的贡献
for(k=1;k<=bl[n];++k) f[i][j][k]+=f[i][j][k-1];for(k=1;k<=bl[n];++k) f[i][j][k]+=f[i][j-1][k];//二维前缀和
}
}
I LL Q(CI l,CI r,CI L,CI R)//询问[l,r]中值在[L,R]范围内的顺序对数
{
RI i,j,pl=bl[l],pr=bl[r],vl,vr,t1=0,t2=0;LL t=0;
if(pl==pr) {for(vl=T[pl][L],vr=T[pl][R+1]-1,i=l;i<=r;++i) L<=a[i]&&a[i]<=R&&(t+=G(pre,i+1,r,p[i],vr));return t;}//同一序列块
for(vl=T[pl][L],vr=T[pl][R+1]-1,i=l;i<=RS[pl];++i) L<=a[i]&&a[i]<=R&&(t+=C(suf[i],p[i]+1,vr)+G(c,pl+1,pr-1,a[i],R));//左散块内部;左散块与整块
for(vl=T[pr][L],vr=T[pr][R+1]-1,i=LS[pr];i<=r;++i) L<=a[i]&&a[i]<=R&&(t+=C(pre[i],vl,p[i]-1)+G(c,pl+1,pr-1,L,a[i]));//右散块内部;右散块与整块
for(i=l;i<=RS[pl];++i) L<=a[i]&&a[i]<=R&&(t+=C(pre[r],T[pr][a[i]],vr));if(pl+1>pr-1) return t;//散块与散块;没有整块直接退出
RI k,dl=bl[L],dr=bl[R],tl=RS[dl]+1,tr=LS[dr]-1,gl=RS[pl],gr=LS[pr];
if(dl==dr) {for(i=L;i<=R;++i) gl<(k=id[i])&&k<gr&&(t+=C(pre[k],T[bl[k]][L],p[k]-1)+G(c,pl+1,bl[k]-1,L,i));return t;}//同一值域块
if(RS[dl]-L+1<sz/2) for(i=L;i<=RS[dl];++i) gl<(k=id[i])&&k<gr&&(t+=C(suf[k],p[k]+1,T[bl[k]][R+1]-1)+G(c,bl[k]+1,pr-1,i,R));//左侧值域块
else {for(i=LS[dl];i<L;++i) gl<(k=id[i])&&k<gr&&(t-=C(suf[k],p[k]+1,T[bl[k]][R+1]-1)+G(c,bl[k]+1,pr-1,i,R));tl=RS[--dl]+1;}//询问块过大则用总答案减剩余部分贡献
if(R-LS[dr]+1<sz/2) for(i=LS[dr];i<=R;++i) gl<(k=id[i])&&k<gr&&(t+=C(pre[k],T[bl[k]][tl],p[k]-1)+G(c,pl+1,bl[k]-1,tl,a[k]));//右侧值域块
else {for(i=R+1;i<=RS[dr];++i) gl<(k=id[i])&&k<gr&&(t-=C(pre[k],T[bl[k]][tl],p[k]-1)+G(c,pl+1,bl[k]-1,tl,a[k]));tr=LS[++dr]-1;}//询问块过大则用总答案减剩余部分贡献
for(t+=C(f[pl+1][pr-1],dl+1,dr-1),i=pl+1;i<pr;++i) t-=g[i][T[i][tl]-1][T[i][tr+1]-1]+1LL*c0[i][tl-1]*G(c,i+1,pr-1,tl,tr);return t;//先差分,然后减去错误贡献
}
int main()
{
RI Qt,i;for(read(n,Qt),i=1;i<=n;++i) read(a[i]),id[a[i]]=i;Build();//记录每个数所在位置,然后预处理
RI x1,x2,y1,y2;W(Qt--) read(x1,x2,y1,y2),writeln(Q(x1,x2,y1,y2));return clear(),0;//询问
}