分块科技 没学会

前言

  • 本来是莫二离的学习笔记,改成科技学习了因为学了科技三没地放a。。。
  • lxl的题解

Yuno loves sqrt technology I

2026-09-14 15-03-42屏幕截图

  • 平常我们怎么求逆序对,一般是扫描线或者归并,权值树状数组也行,但是这题要求在线,所有离线算法全部木大,但是我们可以从中发现:逆序对是可以分开求的
  • 会有四部分贡献
  1. 整块对整块
  2. 整块对散块
  3. 散块对整块
  4. 散块对散块
  • 考虑分块,预处理四个数组 \(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\)
  • 大分块学不会。。。
posted @ 2026-08-27 19:50  msjing  阅读(13)  评论(0)    收藏  举报