分块+莫队+根号分治

分块思想

通过对原数据的适当划分,在划分后的每一个块上预处理并维护一些信息,从而在处理区间操作时,用“大段维护、小段朴素”的方式,取得比纯暴力更优的时间复杂度。

进行区间操作时,将区间分成中间整块和两边散块, 对于整块直接对块操作,对于散块需要暴力操作

一般来说对于长度为n的数列分成sqrt(n)个块(特殊的待补充)

区间加+区间查 : P2801 教主的魔法 - 洛谷

首先,先考虑怎么分块?

先计算:块数,块长,块的编号,块的左端点、右端点

int B;//块数
int tot;//块长
int belong[N];//记录块的编号
int L[N];//左端点
int R[N];//右端点
//下面分块
B=sqrt(n);
tot=(n+B-1)/B;
for(int i=1;i<=n;i++){
    belong[i]=(i-1)/B+1;//下标i所在块的编号
}
for(int i=1;i<=tot;i++){
    L[i]=(i-1)*B+1;//第i块的左端点
    R[i]=min(B*i,n);//第i块的右端点
}

然后,考虑区间加法的问题

想要区间[l,r]加一个数v,首先判断l,r是否在同一块内?

如果是,说明[l,r]是一个散块,比如[1,4]是一个整块,[2,3]则是其中的散块,那么对[l,r]暴力修改就好;

如果l,r不在同一块内,那么将[l,r]分成了两部分,即中间的整块和两边散块,对散块和上边一样暴力,对中间的整块则直接进行懒标记即可

void modify_part(int id,int l,int r,int v){
    for(int i=l;i<=r;i++){
        a[i]+=v;
    }
    /*
    
    */
}
void modify(int l,int r,int v){
	if(belong[l]==belong[r]){ //l,r在同一块内
		modify_part(belong[l],l,r,v);
        return;
	}
    //如果l,r不在同一块内
    modify_part(belong[l],l,R[belong[l]],v);//左边散块
    modify_part(belong[r],L[belong[r]],r,v);//右边散块
    //下面处理中间的整块
    for(int i=belong[l]+1;i<=belong[r]-1;i++){
        lazy[i]+=v;//懒标记
    }
}

接着,考虑怎么查询?

另外,我们想要查询的是[l,r]中 >=c 的个数,我们对整块是直接进行的懒标记,但是仍然不知道有多少数 >=c ,如果进行枚举的话时间复杂度最坏为O(n),那么分块的作用就没了

所以想要快速的查询,我们可以维护一个有序的数组,对其二分查找

对于整块,我们想要找 a[i]+lazy[i] >=c 的个数,直接二分查找最小的>=c的数的下标然后用块的右端点减掉+1就好了

对于散块,我们已经暴力修改原数组,然后对于一个有序的副本,我们再一次将散块中的修改后的数排序,然后二分查找

完整代码:

int a[N],b[N];//原数组,副本
int tot,B;
int belong[N];
int L[N],R[N];
int n,m;
int lazy[N];
void modify_part(int id,int l,int r,int v){
    for(int i=l;i<=r;i++){
        a[i]+=v;
    }
    int ql=L[id];
    int qr=R[id];
    //下面是对散块的重新排序
    for(int i=ql;i<=qr;i++){
        b[i]=a[i];
    }
    sort(b+ql,b+1+qr);
}
void modify(int l,int r,int w){
    if(belong[l]==belong[r]){
        modify_part(belong[l],l,r,w);
        return;
    }
    modify_part(belong[l],l,R[belong[l]],w);
    modify_part(belong[r],L[belong[r]],r,w);
    int ql=belong[l]+1;
    int qr=belong[r]-1;
    for(int i=ql;i<=qr;i++){
        lazy[i]+=w;
    }
}
int query_part(int id,int l,int r,int c){
    int ans=0;
    for(int i=l;i<=r;i++){
        if(a[i]+lazy[id]>=c){
            ans++;
        }
    }
    return ans;
}
int query(int l,int r,int c){
    if(belong[l]==belong[r]){//如果[l,r]是散块
        return query_part(belong[l],l,r,c);
    }
    int ansL=query_part(belong[l],l,R[belong[l]],c);//左散块的个数
    int ansR=query_part(belong[r],L[belong[r]],r,c);//右散块的个数
    int ans=0;//记录中间整块中的个数
    int ql=belong[l]+1;
    int qr=belong[r]-1;
    for(int i=ql;i<=qr;i++){
        int pos=lower_bound(b+L[i],b+1+R[i],c-lazy[i])-b;
        ans+=R[i]-pos+1;
    }
    return ansL+ansR+ans;
}
void solve(){
    cin>>n>>m;
    B=sqrt(n);
    tot=(n+B-1)/B;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        b[i]=a[i];//副本
        belong[i]=(i-1)/B+1;
    }
    for(int i=1;i<=tot;i++){
        L[i]=(i-1)*B+1;
        R[i]=min(i*B,n);
        sort(b+L[i],b+1+R[i]);//对每个块进行排序
    }

    while(m--){
        char op;
        int l,r,c;
        cin>>op>>l>>r>>c;
        if(op=='M'){
            modify(l,r,c);
        }
        if(op=='A'){
            cout<<query(l,r,c)<<endl;
        }
    }
}

莫队算法

用来处理离线区间查询问题

打破朴素纯暴力的瓶颈,优化效率

其核心思想是:分块排序

给一个长度为N的数组,M次查询[l,r]中有多少种数字至少出现2次?

例如:n=5,m=4
a:1 2 3 2 1
[l,r]: [1,3],[1,5],[3,4],[2,5]

如果是纯暴力的话,我们每次查询时双指针会重复跳很多次,时间复杂度O(n*m),如果数据很大会TLE

而利用莫队思想,我们先用结构体将所有查询的区间存起来,然后按l,r的块两层排序

先对l所在的块升序排序,然后对l在同一块中的再按r升序排序,这是标准的排序

struct ss{
	int l,r,id,cnt;//左端点,右端点,编号(输出顺序),个数
};

vector<ss> q;
for(int i=1;i<=m;i++){
	int l,r;
	cin>>l>>r;
	q.push_back({l,r,i,0});
}
sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
	if(belong[a.l]!=belong[b.l]) return belong[a.l]<belong[b.l];
	return a.r<b.r;
})

当然还有优化的排序方法,时间复杂度大大降低,就是奇偶性排序优化

对l在奇数号块中的按r的升序排,对l在偶数号块中的按r的降序排

在从奇块跳到偶块的时候,右端点直接跳到最大的r,然后令右端点回跳计数,这样就可以避免大幅回跳

sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
	if(belong[a.l]!=belong[b.l]) return belong[a.l]<belong[b.l];
	if(belong[a.l]&1) return a.r<b.r;
	else return a.r>b.r;
})

一般来说,最好的块数是 n/sqrt(m)+1

[P1494 国家集训队] 小 Z 的袜子 - 洛谷

通过上边我们已经大致知道莫队的板子了,敲一下吧

int n,m;
int B,tot;
int a[N];
int belong[N];
int ans;
int cnt[N];
struct ss{
    int l,r,id,fz,fm;
};
void bui(){//分块
    for(int i=1;i<=n;i++){
        belong[i]=(i-1)/B+1;
    }
}
void add(int x){//加点
    //扩张的内容
}
void del(int x){//删点
    //收缩的内容
}
void solve(){
    cin>>n>>m;
    B=n/sqrt(m)+1;
    tot=(n+B-1)/B;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }    
    bui();
    vector<ss> q;
    for(int i=1;i<=m;i++){
        int l,r;
        cin>>l>>r;
        q.push_back({l,r,i,0,0});
    }
    sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
        if(belong[a.l]!=belong[b.l]){
            return belong[a.l]<belong[b.l];
        }
        if(belong[a.l]&1){
            return a.r>b.r;
        }else{
            return a.r<b.r;
        }   
    });
    int L=1,R=0;
    for(int i=0;i<m;i++){
    /* 前置条件
    	*/
        //这里实现双指针
        //一般是先扩张后收缩
        while(L>q[i].l){
            add(--L);
        }
        while(R<q[i].r){
            add(++R);
        }
        while(L<q[i].l){
            del(L++);
        }
        while(R>q[i].r){
            del(R--);
        }
        /*得出答案
        	*/
    }
    sort(q.begin(),q.end(),[&](const ss &a,const ss &b){
        return a.id<b.id;
    });
    for(int i=0;i<m;i++){
        cout<</*答案*/<<endl;
    }
}

然后我们在考虑将注释中的内容补充,这一部分我不想写了QAQ

加点的时候就是将原点的内容(如果满足条件已经计算在内的)删掉,然后加入新点的

删点同理

反正莫队算法大多都是可以直接套板子的,主要的就是怎么补充注释内容啦~

根号分治

其实是一种管理学思想:根据问题规模大小,选择不同策略

对于一个大数据管理,我们取一个阈值,超过这个阈值的用一种方法,小于阈值的用另一种方法,以此来达到最大效率

假设你现在要管理一个送快递平台,需要运送两种包裹:

 1. **“大客户”包裹**:一次性要送几百件到同一个小区的包裹。

 	 2.  **“散客”包裹**:地址五花八门,遍布全城,每个地址只有一两件。

你手下有两种派送员:

A型员工(专线司机):开着大货车,适合跑专线。让他去送“大客户”的包裹效率极高,但如果让 他去送“散客”包裹,开着大车在小巷里穿梭,油费和时间成本都受不了。

B型员工(外卖小哥):骑着电瓶车,灵活机动。送“散客”包裹非常快,但如果让他去送“大客户” 的几百件包裹,他得来回跑无数趟,效率极低。

显而易见,我们对“大客户”包裹用专线司机,“散客”包裹用外卖小哥

一般取这个阈值为sqrt(n)

对于小于阈值的进行DP预处理,O(1)查询即可

超过阈值的暴力即可

因为很少做到根号分治的题,而且内容主要需要看题意,所以只给个板子:

int n,q;
int B;
int a[N];
void solve(){
    cin>>n;
    B=sqrt(n);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    vector<vector<int>> f(n+1,vector<int>(B+1,0));//dp数组
    //下面进行dp
    /*
    	*/
    cin>>q;
    while(q--){
        int p,k;
        cin>>p>>k;
        int ans=0;
        if(k>=B){
            /*
            	*/
        }else{
            ans=f[][];
        }
        cout<<ans<<endl;
    }
}

Problem - 797E - Codeforces

完整代码:

#include <bits/stdc++.h>
using namespace std;
//-------------------------------------------------------------------------------------------
#define int long long 
#define R ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define P pair<int,int>
#define lowbit(x) (x&(-x))
#define dbg1(x) cout<<"# "<<x<<endl
#define dbg2(x,y) cout<<"# "<<x<<" "<<y<<endl
#define endl '\n'
const int mod=998244353;
const int N=1e5+10;
const int INF=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
//--------------------------------------------------------------------------------------
int n,q;
int B;
int a[N];
void solve(){
    cin>>n;
    B=sqrt(n);
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    vector<vector<int>> f(n+1,vector<int>(B+1,0));
    for(int i=1;i<=B;i++){
        f[n][i]=1;
    }
    for(int i=n-1;i>=1;i--){
        for(int j=B;j>=1;j--){
            if(i+a[i]+j>n) f[i][j]++;
            else f[i][j]=f[i+a[i]+j][j]+1;
        }
    }
    cin>>q;
    while(q--){
        int p,k;
        cin>>p>>k;
        int ans=0;
        if(k>=B){
            while(p<=n){
                p+=a[p]+k;
                ans++;
            }
        }else{
            ans=f[p][k];
        }
        cout<<ans<<endl;
    }
}
signed main(){
    R;
    // freopen("jia.in","r",stdin);
    // freopen("jia.out","w",stdout);
    int T=1;
    //cin>>T;
    for(int i=1;i<=T;i++){
        solve();
    }
    return 0;
}

posted @ 2025-07-03 21:10  RYRYR  阅读(13)  评论(0)    收藏  举报