分块科技 没学会
前言
- 本来是莫二离的学习笔记,改成科技学习了因为学了科技三没地放a。。。
- lxl的题解
Yuno loves sqrt technology I

- 平常我们怎么求逆序对,一般是扫描线或者归并,权值树状数组也行,但是这题要求在线,所有离线算法全部木大,但是我们可以从中发现:逆序对是可以分开求的
- 会有四部分贡献
- 整块对整块
- 整块对散块
- 散块对整块
- 散块对散块
- 考虑分块,预处理四个数组 \(pre,nxt,f,g\)
- \(pre_i\) 表示每个元素到其所在块块首这段区间的逆序对个数,\(nxt_i\) 表示每个元素到其所在块块尾这段区间的逆序对个数,这个用权值树状数组扫一遍
点击查看代码
for (int i=1;i<=cnt;i++)
{
const int left=L[i],right=R[i];
for (int j=left;j<=right;j++)
pos[j]=i;
int ct=0;
for (int j=left;j<=right;j++)
{
add(a[j],1);
ct+=(j-left+1)-que(a[j]);
pre[j]=ct;
}
for (int j=left;j<=right;j++)
{
nxt[j]=ct;
add(a[j],-1);
ct-=que(a[j]-1);
}
}
- \(f_{i,j}\) 表示块 \(i\) 到块 \(j\) 区间的逆序对个数,方便查整块,\(f_{i,i} = pre_{R_i}\)
- \(g_{i,j}\) 表示 \(a_{1∼i}\) 能与第 \(j\) 块中的数字产生逆序对个数
- \(g\) 的话,对每块进行归并排序,之后前缀处理就行
点击查看代码
for (int i=1;i<=cnt;i++)
{
sort(b+L[i],b+R[i]+1);
for (int j=L[i];j<=R[i];j++)
c[j]=b[j];
}
sort(b+1,b+1+n);
for (int i=1;i<=cnt;i++)
{
const int left=L[i],right=R[i];
int le=L[i];
for (int j=1;j<=n;j++)
{
const int bjs=b[j].se;
while (le<=right && b[j].fi>c[le].fi)
le++;
if (bjs<left)
g[i][bjs]=le-left;
else if (bjs>right)
g[i][bjs]=right-le+1;
}
}
- \(f\) 就有 \(f_{i,j} = f_{i+1,j} + f_{i,j−1} − f_{i+1,j−1} + g_{R_i,j} - g_{L_i-1,j}\)
- 询问在同一块内,贡献为 \(pre\) 差分的值(需要特判查询的 \(l\) 是否为块的开头哦)减去两边的,两边归并求,就是总区间减去无法产生贡献的区间
点击查看代码
inline long long merge_sort(int l,int r)
{
int i=1,j=1;
long long res=0;
while (i<=l && j<=r)
{
if (x[i]<=y[j])++i;
else res+=l-i+1,++j;
}
return res;
}
if (pl == pr)
{
const int left=L[pl],right=R[pl];
for (int i=left;i<=right;i++)
{
if (l<=c[i].se && c[i].se<=r) y[++Y]=c[i].fi;
else if (c[i].se<l) x[++X]=c[i].fi;
}
ans=pre[r]-merge_sort(X,Y);
if (l!=left) ans-=pre[l-1];
}
- 不在一块内,就拆成三块,左右散块还是整个区间减去无法产生贡献的区间,散块对整块的贡献可以用前缀算了,就是lxl说的预处理前缀块到前缀的答案,然后每次可以大力差分为一堆已经预处理的东西,以及两个块的零散互相的贡献,散块还是归并
点击查看代码
else
{
ans=f[pl+1][pr-1]+pre[r]+nxt[l];
const int Rpl=R[pl],Lpr=L[pr];
for (int i=pl+1;i<pr;i++)
ans+=g[i][Rpl]-g[i][l-1]+g[i][r]-g[i][Lpr-1];
for (int i=L[pl];i<=R[pl];i++)
if (c[i].se>=l) x[++X]=c[i].fi;
for (int i=L[pr];i<=R[pr];i++)
if (c[i].se<=r) y[++Y]=c[i].fi;
ans+=merge_sort(X,Y);
}
- 代码不好写,细节挺多,写了 \(4.4K\)
- 你会发现代码的 \(g\) 和上面说的定义是反过来的,这是由于这题太™卡常了
- 给lxl跪了
- 所以介绍一个卡常技巧:内存转置,即通过更改便利顺序使内存访问快速,具体啥原理我也不知道
- 然后还有一些其他的常用卡常技巧,代码已经面目全非了。。。
点击查看代码
#pragma G++ optimize("O3")
#pragma G++ optimize("unroll-loops")
#include <bits/stdc++.h>
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
constexpr int maxn=1e5+10,B=316;
struct IO
{
static const int Size=(1<<20);
char buf[Size],*p1,*p2;
int st[25],Top;
~IO(){clear();}
inline void clear(){fwrite(buf,1,Top,stdout);Top=0;}
inline char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,Size,stdin),p1==p2)?EOF:*p1++;}
inline void pc(const char c){Top==Size&&(clear(),0);buf[Top++]=c;}
inline IO& operator >>(char& c){while(c=gc(),c==' ' || c=='\n' || c=='\r');return *this;}
template<typename T>inline IO& operator >>(T& x)
{
x=0;bool f=0;char c=gc();
while(!isdigit(c)){if(c=='-') f=1;c=gc();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
f?x=-x:0;
return *this;
}
inline IO& operator >>(string& s)
{
s="";char c=gc();
while(c==' ' || c=='\n' || c=='\r') c=gc();
while(c!=' ' && c!='\n' && c!='\r' && c!=EOF) s+=c,c=gc();
return *this;
}
inline IO& operator <<(const char c){pc(c);return *this;}
template<typename T> inline IO& operator <<(T x)
{
if(x<0) pc('-'),x=-x;
do st[++st[0]]=x%10,x/=10;while(x);
while(st[0]) pc(st[st[0]--]+'0');
return *this;
}
inline IO& operator <<(const string s){for(auto c:s) pc(c);return *this;}
inline IO& operator <<(const char* c){for(int i=0;c[i];i++) pc(c[i]);return *this;}
}fin,fout;
int n,m,a[maxn];
pii b[maxn],c[maxn];
long long ans;
int C[maxn];
inline int lb(int x) {return x&(-x);}
inline void add(int x,int v) {while (x<=n) C[x]+=v,x+=lb(x);}
inline int que(int x)
{
int res=0;
while (x)
res+=C[x],x-=lb(x);
return res;
}
int L[320],R[320],pos[maxn],cnt,pre[maxn],nxt[maxn];
long long f[320][320];
int g[320][maxn];
int x[320],y[320],X,Y;
inline long long merge_sort(int l,int r)
{
int i=1,j=1;
long long res=0;
while (i<=l && j<=r)
{
if (x[i]<=y[j])++i;
else res+=l-i+1,++j;
}
return res;
}
inline void init()
{
cnt=(n-1)/B+1;
for (int i=1;i<=cnt;++i)
L[i]=R[i-1]+1,R[i]=i*B;
R[cnt]=n;
for (int i=1;i<=cnt;i++)
{
const int left=L[i],right=R[i];
for (int j=left;j<=right;j++)
pos[j]=i;
int ct=0;
for (int j=left;j<=right;j++)
{
add(a[j],1);
ct+=(j-left+1)-que(a[j]);
pre[j]=ct;
}
f[i][i]=ct;
for (int j=left;j<=right;j++)
{
nxt[j]=ct;
add(a[j],-1);
ct-=que(a[j]-1);
}
}
for (int i=1;i<=cnt;i++)
{
sort(b+L[i],b+R[i]+1);
for (int j=L[i];j<=R[i];j++)
c[j]=b[j];
}
sort(b+1,b+1+n);
for (int i=1;i<=cnt;i++)
{
const int left=L[i],right=R[i];
int le=L[i];
for (int j=1;j<=n;j++)
{
const int bjs=b[j].se;
while (le<=right && b[j].fi>c[le].fi)
le++;
if (bjs<left)
g[i][bjs]=le-left;
else if (bjs>right)
g[i][bjs]=right-le+1;
}
}
for (int i=1;i<=cnt;i++)
for (int j=2;j<=n;j++)
g[i][j]+=g[i][j-1];
for (int k=1;k<=cnt;k++)
for (int i=1;i<=cnt;i++)
{
const int j=i+k;
if (j>cnt) break;
f[i][j]=f[i+1][j]+f[i][j-1]-f[i+1][j-1]+g[j][R[i]]-g[j][L[i]-1];
}
}
int main()
{
fin >> n >> m;
for (int i=1;i<=n;i++)
fin >> a[i],b[i]={a[i],i};
init();
while (m--)
{
int le,ri;
fin >> le >> ri;
const int l=le^ans,r=ri^ans;
X=Y=0;
const int pl=pos[l],pr=pos[r];
if (pl == pr)
{
const int left=L[pl],right=R[pl];
for (int i=left;i<=right;i++)
{
if (l<=c[i].se && c[i].se<=r) y[++Y]=c[i].fi;
else if (c[i].se<l) x[++X]=c[i].fi;
}
ans=pre[r]-merge_sort(X,Y);
if (l!=left) ans-=pre[l-1];
}
else
{
ans=f[pl+1][pr-1]+pre[r]+nxt[l];
const int Rpl=R[pl],Lpr=L[pr];
for (int i=pl+1;i<pr;i++)
ans+=g[i][Rpl]-g[i][l-1]+g[i][r]-g[i][Lpr-1];
for (int i=L[pl];i<=R[pl];i++)
if (c[i].se>=l) x[++X]=c[i].fi;
for (int i=L[pr];i<=R[pr];i++)
if (c[i].se<=r) y[++Y]=c[i].fi;
ans+=merge_sort(X,Y);
}
fout << ans << endl;
}
return 0;
}
Yuno loves sqrt technology II
莫队二次离线
- 莫二离是lxl发明的一种基于莫队的算法,代表题有:莫二离板子,科技2
- 莫二离这个科技听起来很 NB,实际上你会扫描线就差不多能理解,
能不能写出来另说,可以去我这学扫描线,应该没有人会没学扫描线学莫二离吧 - 莫二离主要处理一类问题:莫队的转移不能 \(O(1)\) 进行,转移会使复杂度变得很劣,无法保证莫队的复杂度,且询问的东西可以差分做
- 由于询问可以通过差分求解,离线下来询问后扫描线就可以很好处理,由于要在莫队的基础上再离线,所以称为莫队二次离线
- 我们开始说科技2
- 暴力还挺好打的,离散化,在莫队时直接用权值树状数组查,复杂度 \(n \sqrt n \log n\),给一下莫队的实现,拍了 \(3w\) 没拍出问题应该是真了
为啥会有用正解拍暴力的啊
点击查看代码
while (l>q[i].l) l--,as+=que(a[l]-1),add(a[l],1);
while (r<q[i].r) r++,as+=r-l-que(a[r]),add(a[r],1);
while (l<q[i].l) as-=que(a[l]-1),add(a[l++],-1);
while (r>q[i].r) as-=r-l+1-que(a[r]),add(a[r--],-1);
暴力写的比正解假- 暴力打得好会有 \(20pts\),但是msjing打的常数巨大然后拿滚木
- 我们考虑如何优化,正式开始介绍莫二离
- 我们考虑对于指针的移动,新加一个 \(x\) 会产生什么贡献
- 贡献为前面所有比 \(x\) 大的数的数量
- 这个明显可以差分,直接预处理出 \([1,x]\) 的比 \(x\) 大的数的数量
- 后面我们发现并不是很好维护差分减去的区间贡献,所以考虑将这些再次离线,对于这些右端点的移动,将其固定在左端点 \(l\) 上,做扫描线,固定一个处理另一堆
- 但是扫描线不太能用传统的树状数组或者线段树这一类的了,因为询问次数是 \(n \sqrt m\) 的,带只老哥就和暴力没区别了,但是我们注意到,由于扫描线只扫 \(n\) 次,这是不平衡的,所以我们可以选一个 \(DS\) 平衡一下,即 \(O(\sqrt n) - O(1)\) 的值域分块维护
- 对了,由于左右端点移动贡献不等价,左端点是小于,右端点是大于
- 我们就可以以 \(O(n \sqrt m + n \sqrt n)\) 的时间复杂度,空间 \(O(n \sqrt m)\) 做完
- 欸但是你发现lxl把内存就开了 \(31.25Mb\)(
有零有整),空间爆炸,但是我们发现固定一个端点,另一个移动连续,我们就可以只存一个移动区间左右端点就行,空间来到 \(O(m)\) - 由于我们算的是变化,不是答案,要做一次前缀
- 不卡常,记得挂
long long 理论我会,实现不会,找个马蜂好的看看,msjing实现看的 \(\mathcal{oiwiki}\)
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
constexpr int maxn=1e5+10;
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || ch>'9')
{
if (ch == '-') f=-1;
ch=getchar();
}
while (ch>='0' && ch<='9')
{
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
int n,m;
int a[maxn],b[maxn];
struct _ {int l,r,id,cnt;}q[maxn];
int L[maxn],R[maxn],pos[maxn],cnt;
void init()
{
cnt=sqrt(n);
for (int i=1;i<=cnt;i++)
L[i]=R[i-1]+1,R[i]=cnt*i;
if (R[cnt]<n)
cnt++,L[cnt]=R[cnt-1]+1,R[cnt]=n;
for (int i=1;i<=cnt;i++)
for (int j=L[i];j<=R[i];j++)
pos[j]=i;
}
int b1[maxn],b2[maxn],g1[maxn],g2[maxn];
int get(int x)
{return !x ? 0 : b1[pos[x]-1]+b2[x];}
struct __ {int l,r,id,x,y;};
vector<__> k[maxn];
int as;
int ans[maxn];
signed main()
{
n=read(),m=read();
for (int i=1;i<=n;i++) a[i]=b[i]=read();
sort(b+1,b+1+n);
int K=unique(b+1,b+1+n)-b-1;
for (int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+1+K,a[i])-b;
init();
for (int i=1;i<=n;i++)
{
g1[i]=get(a[i]-1),g2[i]=get(n)-get(a[i]);
for (int j=pos[a[i]];j<=cnt;j++) b1[j]++;
for (int j=a[i];j<=R[pos[a[i]]];j++) b2[j]++;
}
memset(b1,0,sizeof(b1)),memset(b2,0,sizeof(b2));
for (int i=1;i<=m;i++)
{
int l=read(),r=read();
q[i]={l,r,i,0};
}
sort(q+1,q+1+m,[](_ a,_ b)
{return pos[a.l] == pos[b.l] ? (pos[a.l]&1) ?
a.r<b.r : a.r>b.r : pos[a.l]<pos[b.l];});
int l=1,r=0;
for (int i=1;i<=m;i++)
{
if (l>q[i].l)
k[r].pb({q[i].l,l-1,i,1,0});
while (l>q[i].l) q[i].cnt-=g1[--l];
if (r<q[i].r)
k[l-1].pb({r+1,q[i].r,i,-1,1});
while (r<q[i].r) q[i].cnt+=g2[++r];
if (l<q[i].l)
k[r].pb({l,q[i].l-1,i,-1,0});
while (l<q[i].l) q[i].cnt+=g1[l++];
if (r>q[i].r)
k[l-1].pb({q[i].r+1,r,i,1,1});
while (r>q[i].r) q[i].cnt-=g2[r--];
}
for (int i=1;i<=n;i++)
{
for (int j=pos[a[i]];j<=cnt;j++) b1[j]++;
for (int j=a[i];j<=R[pos[a[i]]];j++) b2[j]++;
for (auto j:k[i])
for (int o=j.l;o<=j.r;o++)
{
if (!j.y)
q[j.id].cnt+=j.x*get(a[o]-1);
else
q[j.id].cnt+=j.x*(i-get(a[o]));
}
}
for (int i=1;i<=m;i++)
q[i].cnt+=q[i-1].cnt;
for (int i=1;i<=m;i++)
ans[q[i].id]=q[i].cnt;
for (int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
return 0;
}
Yuno loves sqrt technology III
- 科技三,强制在线区间众数
- 我们想到蒲公英,不过蒲公英处理的东西和这题不太一样,这题是求区间的众数的出现次数,不过我们可以利用蒲公英的思想,即处理块于块的关系
- 我们预处理出 \(f_{i,j}\) 表示从 \(i\) 块到 \(j\) 块的众数的出现次数,方便后面整块直接 \(O(1)\) 查,我们可以像蒲公英那样求一个前缀数组表示到第 \(i\) 块时 \(j\) 数出现的个数,但是题目是线性空间,所以没法这样做,由于我们仅需要求众数出现的个数,那我们直接开桶每次暴力算一个数出现次数就行
点击查看代码
for (int i=1;i<=cnt;i++)
{
memset(s,0,sizeof(s));
for (int j=i;j<=cnt;j++)
{
f[i][j]=f[i][j-1];
for (int k=L[j];k<=R[j];k++)
f[i][j]=max(f[i][j],++s[a[k]]);
}
}
- 有了 \(f\),我们可以方便算整块了,现在考虑散块
- 我们考虑左边块,右边块同理
- 先设整块直接查 \(f\) 得到的答案为 \(ans\)
- 对于一个左边块的元素 \(x\),,能使众数 \(+1\) 当且仅当 $这个数后面在 \(r\) 内的数大于等于 \(ans\),这样才有贡献
- 那我们可以开一个
vector记录等于一个值的所有数在原序列的下标,然后记一个 \(p\) 表示原数组下表为 \(i\) 的数在所对应的vector里的下标,对于左端点,我们在vector里查 \(p_i + ans\) 的值(即下标),看是否小于等于 \(r\),是就让 \(ans+1\),直接暴力统计就行,记得先判越界,复杂度应该是 \(O(\sqrt n)\) 的,带一个比较小的常数 - 如果 \(l\) 和 \(r\) 在同一块内,直接开桶统计就行了,需要回退桶而不是
memset要不然复杂度就是 \(O(nm \sqrt n)\) 的了 - 不卡常
点击查看代码
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
constexpr int maxn=5e5+10;
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || ch>'9')
{
if (ch == '-') f=-1;
ch=getchar();
}
while (ch>='0' && ch<='9')
{
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
int n,m;
int a[maxn],b[maxn];
int L[maxn],R[maxn],pos[maxn],cnt;
int s[maxn],f[777][777];
int p[maxn];
vector<int> g[maxn];
void init()
{
cnt=sqrt(n);
for (int i=1;i<=cnt;i++)
L[i]=R[i-1]+1,R[i]=cnt*i;
if (R[cnt]<n)
cnt++,L[cnt]=R[cnt-1]+1,R[cnt]=n;
for (int i=1;i<=cnt;i++)
for (int j=L[i];j<=R[i];j++)
pos[j]=i;
for (int i=1;i<=cnt;i++)
{
memset(s,0,sizeof(s));
for (int j=i;j<=cnt;j++)
{
f[i][j]=f[i][j-1];
for (int k=L[j];k<=R[j];k++)
f[i][j]=max(f[i][j],++s[a[k]]);
}
}
for (int i=1;i<=n;i++)
g[a[i]].pb(i),p[i]=g[a[i]].size()-1;
memset(s,0,sizeof(s));
}
int que(int l,int r)
{
if (pos[l] == pos[r])
{
int ans=0;
for (int i=l;i<=r;i++)
ans=max(ans,++s[a[i]]);
for (int i=l;i<=r;i++) s[a[i]]--;
return ans;
}
int ans=f[pos[l]+1][pos[r]-1];
for (int i=l;i<=R[pos[l]];i++)
{
int I=p[i];
while (I+ans<g[a[i]].size() && g[a[i]][I+ans]<=r) ans++;
}
for (int i=L[pos[r]];i<=r;i++)
{
int I=p[i];
while (I-ans>=0 && g[a[i]][I-ans]>=l) ans++;
}
return ans;
}
int main()
{
n=read(),m=read();
for (int i=1;i<=n;i++)
a[i]=read();
init();
int ans=0;
while (m--)
{
int l=read()^ans,r=read()^ans;
if (l>r) swap(l,r);
ans=que(l,r);
printf("%d\n",ans);
}
return 0;
}
后话
原莫二离学习笔记的后话
- 这个东西有啥用吗
装b- 其实过了今天就忘了
希望不会 - 不过我可能要考虑新开一个大分块的合集了
真的学的会吗
- 主观难度:科技 \(3\) \(<\) 科技 \(1\) \(<\) 科技 \(2\)
- 大分块学不会。。。
$\mathscr{msjing}$

浙公网安备 33010602011771号