暑假集训日记——6.30(分块)

本文详细解析数列分块与莫队算法,涵盖区间加法、区间求和、区间乘法等操作,通过实例代码讲解算法实现与复杂度分析,适合初学者入门与进阶学习。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分块,莫队

数列分块入门 1

//区间加法单点查询
/*
我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块
直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。
每次询问时返回元素的值加上其所在块的加法标记。
*/
#include <iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<cmath>
using namespace std;

typedef long long ll;
const int N=1e6+5;

int a[N];
int vis[N];
int bl[N];//分块
int blo;//分块大小
int sum[N];//加法标记

void update(int L,int R,int x)
{
    if(bl[L]==bl[R])
    {
        for(int i=L;i<=R;i++)
            a[i]+=x;
    }
    else
    {
    for(int i=L;i<=bl[L]*blo;i++)
            a[i]+=x;
    for(int i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]+=x;
    for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            a[i]+=x;
    }
}

int query(int x)
{
    return a[x]+sum[bl[x]];
}

int main()
{
    ios::sync_with_stdio(false);
    int n,ty,l,r,c;
    cin>>n;
    blo=sqrt(n);
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
        bl[i]=(i-1)/blo+1;
    for(int i=0;i<n;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            update(l,r,c);
        }
        else
        {
            cout<<a[r]+sum[bl[r]]<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 2

//区间加法,询问区间内小于某个值x的元素个数。
/*
我们先来思考只有询问操作的情况,不完整的块枚举统计即可;而要在每个整块内寻找小于一个值的
元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度O(nlogn),每次查询在√n个块内二分,以及暴力2√n个元素。
那么区间加怎么办呢?
套用第一题的方法,维护一个加法标记,略有区别的地方在于,不完整的块修改后可能会使得该块内
数字乱序,所以头尾两个不完整块需要重新排序,复杂度分析略。
在加法标记下的询问操作,块外还是暴力,
查询小于(x – 加法标记)的元素个数,块内用(x – 加法标记)作为二分的值即可。
*/
#include <iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<cmath>
#include<vector>
using namespace std;

typedef long long ll;
const int N=1e6+5;


vector<int>vis[600];//比较动态数组
int a[N];
int bl[N];//分块
int blo;//分块大小
int sum[N];//加法标记
int n;

void update(int x)//不完全区间每次修改值,原有顺序被改变,需要重新排序
{
    vis[x].clear();
    for(int i=(x-1)*blo+1;i<=min(x*blo,n);i++)
        vis[x].push_back(a[i]);
    sort(vis[x].begin(),vis[x].end());
}

void add(int L,int R,int x)
{
    if(bl[L]==bl[R])
    {
        for(int i=L;i<=R;i++)
            a[i]+=x;
            update(bl[L]);
    }
    else
    {
    for(int i=L;i<=min(bl[L]*blo,n);i++)
                a[i]+=x;
                update(bl[L]);

    for(int i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]+=x;
    for(int i=(bl[R]-1)*blo+1;i<=R;i++)
                a[i]+=x;
                update(bl[R]);

    }
}

int query(int L,int R, int x)
{
    int ans=0;
    if(bl[L]==bl[R]){
        for(int i=L;i<=R;i++)
        if(a[i]+sum[bl[i]]<x)
            ans++;
        return ans;
    }
        for(int i=L;i<=bl[L]*blo;i++)
            if(a[i]+sum[bl[i]]<x)
            ans++;
        for(int i=bl[L]+1;i<=bl[R]-1;i++)
            {
                int tag=x-sum[i];
                ans+=lower_bound(vis[i].begin(),vis[i].end(),tag)-vis[i].begin();
            }
        for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            if(a[i]+sum[bl[i]]<x)
            ans++;
        return ans;
}

int main()
{
    ios::sync_with_stdio(false);
    int ty,l,r,c;
    cin>>n;
    blo=sqrt(n);
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
        {
            bl[i]=(i-1)/blo+1;
            vis[bl[i]].push_back(a[i]);//入队
        }
    for(int i=1;i<bl[n];i++)
        sort(vis[i].begin(),vis[i].end());//每块排序
    for(int i=0;i<n;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            add(l,r,c);//修改区间值
        }
        else
        {
            cout<<query(l,r,c*c)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 3

//操作涉及区间加法,询问区间内小于某个值  的前驱(比其小的最大元素)
/*
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,
这样如果还有插入、删除元素的操作,会更加的方便。

区间加法,求某个数的前驱
用set维护方便很多(其实相当于一颗splay把我觉得,手动艾特lynkin同学用的二分查找前驱过了)
依旧是完整的块直接lower_bound
不完整的块还是继续暴力
不过要注意set的使用方法 删掉再进行暴力修改 再加回去

分块的调试检测技巧:
可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
*/
#include <iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<cmath>
#include<vector>
#include<set>
using namespace std;

typedef long long ll;
const int N=1e6+5;


set<int>vis[6000];//set相当于一颗树,红黑平衡二叉树
int a[N];
int bl[N];//分块
int blo;//分块大小
int sum[N];//加法标记
int n;

void add(int L,int R,int x)
{
    if(bl[L]==bl[R])
    {
        for(int i=L;i<=R;i++)
            {
                vis[bl[L]].erase(a[i]);
                a[i]+=x;
                vis[bl[L]].insert(a[i]);
            }

    }
    else
    {
    for(int i=L;i<=min(bl[L]*blo,n);i++)
            {
                vis[bl[L]].erase(a[i]);
                a[i]+=x;
                vis[bl[L]].insert(a[i]);
            }

    for(int i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]+=x;
    for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            {
                vis[bl[R]].erase(a[i]);
                a[i]+=x;
                vis[bl[R]].insert(a[i]);
            }

    }
}

int query(int L,int R, int x)
{
    int ans=-1;
    if(bl[L]==bl[R]){
        for(int i=L;i<=R;i++)
        if(a[i]+sum[bl[i]]<x)
            ans=max(ans,a[i]+sum[bl[i]]);
        return ans;
    }
        for(int i=L;i<=min(bl[L]*blo,n);i++)
            if(a[i]+sum[bl[i]]<x)
            ans=max(ans,a[i]+sum[bl[i]]);
        for(int i=bl[L]+1;i<=bl[R]-1;i++)
            {
                int tag=x-sum[i];
                set<int>::iterator k=vis[i].lower_bound(tag);
                //定义一个k的指针 一开始指向t的后继
        //upper_bound(i) 返回的是键值为i的元素可以插入的最后一个位置(上界) 
        //lower_bound(i) 返回的是键值为i的元素可以插入的位置的第一个位置(下界)。
                if(k==vis[i].begin()) continue;
                k--;//所以要减一
                ans=max(ans,*k+sum[i]);
            }
        for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            if(a[i]+sum[bl[i]]<x)
            ans=max(ans,a[i]+sum[bl[i]]);
        return ans;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int ty,l,r,c;
    cin>>n;
    blo=sqrt(n);
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
        {
            bl[i]=(i-1)/blo+1;
            vis[bl[i]].insert(a[i]);//入队
        }
    for(int i=0;i<n;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            add(l,r,c);//修改区间值
        }
        else
        {
            cout<<query(l,r,c)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 4

//区间加法,区间求和。
/*
这题的询问变成了区间上的询问,不完整的块还是暴力;而要想快速统计完整块的答案,需要维护每个块的元素和,先要预处理一下。

考虑区间修改操作,不完整的块直接改,顺便更新块的元素和;完整的块类似之前标记的做法,直接根据块的元素和所加的值计算元素和的增量。
*/
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<set>
#include<map>
#include<iterator>
#include<queue>
#include<vector>
#include<string>
using namespace std;

typedef long long ll;
const int N=1e6+10;
const long long INF=1e18;
const double eps=0.0000001;
const ll mod=1e9+7;
int n;
ll a[N];
ll vis[N];
int bl[N];//分块
int blo;//分块大小
ll sum[N];//加法标记
ll add[N];

void init()
{
    for(ll i=1;i<=n;i++)
    {
        add[bl[i]]+=a[i];
    }
}

void update(ll L,ll R,ll x)
{
    if(bl[L]==bl[R])
    {
        for(ll i=L;i<=R;i++)
            a[i]+=x,add[bl[L]]+=x;
    }
    else
    {
    for(ll i=L;i<=bl[L]*blo;i++)
            a[i]+=x,add[bl[L]]+=x;
    for(ll i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]+=x;
    for(ll i=(bl[R]-1)*blo+1;i<=R;i++)
            a[i]+=x,add[bl[R]]+=x;
    }
}

ll query(ll L,ll R,ll c)
{
    ll summ=0;
    if(bl[L]==bl[R])
    {
        for(ll i=L;i<=R;i++)
            summ=(summ%c+a[i]%c)%c,summ=(summ%c+sum[bl[L]]%c)%c;
    }
    else
    {
    for(ll i=L;i<=bl[L]*blo;i++)
        summ=(summ%c+a[i]%c)%c,summ=(summ%c+sum[bl[L]]%c)%c;
    for(ll i=bl[L]+1;i<=bl[R]-1;i++)
        summ=(summ%c+add[i]%c+sum[i]*blo%c)%c;
        //sum[i]*blo最好不要在加法的时候加到add里,由于数字太大可能会爆掉
    for(ll i=(bl[R]-1)*blo+1;i<=R;i++)
        summ=(summ%c+a[i]%c)%c,summ=(summ%c+sum[bl[R]]%c)%c;
    }
    return summ;
}

int main()
{
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    ll ty,l,r,c;
    cin>>n;
    blo=sqrt(n);
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    for(ll i=1;i<=n;i++)
        bl[i]=(i-1)/blo+1;
    init();
    for(ll i=0;i<n;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            update(l,r,c);
            //cout<<add[1]<<" "<<add[2]<<endl;
        }
        else
        {
            cout<<query(l,r,c+1)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 5

//操作涉及区间开方,区间求和。
/*
给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。
稍作思考可以发现,开方操作比较棘手,主要是对于整块开方时,必须要知道每一个元素,才能知道他们开方后的和,也就是说,难以快速对一个块信息进行更新。
看来我们要另辟蹊径。不难发现,这题的修改就只有下取整开方,而一个数经过几次开方之后,它的值就会变成 0 或者 1。
如果每次区间开方只不涉及完整的块,意味着不超过2√n个元素,直接暴力即可。
如果涉及了一些完整的块,这些块经过几次操作以后就会都变成 0 / 1,于是我们采取一种分块优化的暴力做法,只要每个整块暴力开方后,记录一下元素是否都变成了 0 / 1,区间修改时跳过那些全为 0 / 1 的块即可。
这样每个元素至多被开方不超过4次,显然复杂度没有问题。
*/

#include<algorithm>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<set>
#include<map>
#include<iterator>
#include<queue>
#include<vector>
#include<string>
using namespace std;

typedef long long ll;
const int N=1e6+10;
const long long INF=1e18;
const double eps=0.0000001;
const ll mod=1e9+7;
int n;
ll a[N];
ll vis[N];
int bl[N];//分块
int blo;//分块大小
ll sum[N];//加法标记
ll add[N];

void init()
{
    for(ll i=1;i<=n;i++)
    {
        add[bl[i]]+=a[i];
    }
}

void update(ll L,ll R,ll x)
{
    if(bl[L]==bl[R])
    {
        for(ll i=L;i<=R;i++)
            add[bl[L]]-=a[i],a[i]=floor(sqrt(a[i])),add[bl[L]]+=a[i];
    }
    else
    {
    for(ll i=L;i<=bl[L]*blo;i++)
            add[bl[i]]-=a[i],a[i]=floor(sqrt(a[i])),add[bl[i]]+=a[i];
    for(ll i=bl[L]+1;i<=bl[R]-1;i++)
    {
        if(vis[i]==0)
        {
            vis[i]=1;
            for(int j=(i-1)*blo+1;j<=i*blo;j++)
            {
                add[bl[j]]-=a[j],a[j]=floor(sqrt(a[j])),add[bl[j]]+=a[j];
                if(a[j]>1)
                    vis[i]=0;
            }
        }
    }
    for(ll i=(bl[R]-1)*blo+1;i<=R;i++)
            add[bl[i]]-=a[i],a[i]=floor(sqrt(a[i])),add[bl[i]]+=a[i];
    }
}

ll query(ll L,ll R,ll c)
{
    ll summ=0;
    if(bl[L]==bl[R])
    {
        for(ll i=L;i<=R;i++)
            summ+=a[i];
    }
    else
    {
    for(ll i=L;i<=bl[L]*blo;i++)
        summ+=a[i];
    for(ll i=bl[L]+1;i<=bl[R]-1;i++)
        summ+=add[i];
    for(ll i=(bl[R]-1)*blo+1;i<=R;i++)
        summ+=a[i];
    }
    return summ;
}

int main()
{
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    ll ty,l,r,c;
    cin>>n;
    blo=sqrt(n);
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    for(ll i=1;i<=n;i++)
        bl[i]=(i-1)/blo+1;
    init();
    for(ll i=0;i<n;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            update(l,r,c);
        }
        else
        {
            cout<<query(l,r,c)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}


数列分块入门 6

//操作涉及单点插入,单点询问
/*
给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。
先说随机数据的情况
之前提到过,如果我们块内用数组以外的数据结构,能够支持其它不一样的操作,比如此题每块内可以放一个动态的数组,每次插入时先找到位置所在的块,再暴力插入,把块内的其它元素直接向后移动一位,当然用链表也是可以的。
查询的时候类似,复杂度分析略。
但是这样做有个问题,如果数据不随机怎么办?
如果先在一个块有大量单点插入,这个块的大小会大大超过√n,那块内的暴力就没有复杂度保证了。
还需要引入一个操作:重新分块(重构)
每根号n次插入后,重新把数列平均分一下块,重构需要的复杂度为O(n),重构的次数为√n,所以重构的复杂度没有问题,而且保证了每个块的大小相对均衡。
当然,也可以当某个块过大时重构,或者只把这个块分成两半。
*/
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<set>
#include<map>
#include<iterator>
#include<queue>
#include<vector>
#include<string>
using namespace std;

typedef long long ll;
const int N=1e6+10;
const long long INF=1e18;
const double eps=0.0000001;
const ll mod=1e9+7;
int n,mx;
ll a[N];
ll vis[N];
int bl[N];//分块
int blo;//分块大小
ll sum[N];//加法标记
ll add[N];
vector<int>ans[2000];

void init()
{
    for(ll i=1;i<=n;i++)
    {
        add[bl[i]]+=a[i];
    }
}

///重构分块
void res()
{
    int p=0;
    for(int i=1;i<=mx;i++)
    {
        for(int j=0;j<ans[i].size();j++)
        a[++p]=ans[i][j];
        ans[i].clear();
    }
    n=p;

}

void dev()
{
        blo=sqrt(n);
        for(ll i=1;i<=n;i++)
            bl[i]=(i-1)/blo+1,ans[bl[i]].push_back(a[i]),mx=max(mx,bl[i]);
}

void update(ll L,ll R,ll x)
{
    for(int i=1;i<=mx;i++)
    {
        if(L>ans[i].size())
            L-=ans[i].size();
        else
        {
            ans[i].insert(ans[i].begin()+L-1,R);
            if(ans[i].size()>2*mx) res(),dev();//重新分块
            return;
        }
    }
}

ll query(ll L,ll R,ll c)
{
    for(int i=1;i<=mx;i++)
    {
        if(R>ans[i].size())
            R-=ans[i].size();
        else
           return ans[i][R-1];
    }
}

int main()
{
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    ll ty,l,r,c,m;
    cin>>n;
    m=n;
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    //for(ll i=1;i<=n;i++)
    //    bl[i]=(i-1)/blo+1;
    dev();
    for(ll i=0;i<m;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            update(l,r,c);
        }
        else
        {
            cout<<query(l,r,c)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 7

//操作涉及区间乘法,区间加法,单点询问。
/*
很显然,如果只有区间乘法,和分块入门 1 的做法没有本质区别,但要思考如何同时维护两种标记。
我们让乘法标记的优先级高于加法(如果反过来的话,新的加法标记无法处理)
若当前的一个块乘以m1后加上a1,这时进行一个乘m2的操作,则原来的标记变成m1*m2,a1*m2
若当前的一个块乘以m1后加上a1,这时进行一个加a2的操作,则原来的标记变成m1,a1+a2

对于不完整的块,直接 a[i] = a[i] × tg2[b[i]] + tg1[b[i]] 将这个块的所有元素都还原,
也就是将该块的标记下传(这里比较像线段树
*/

#include<algorithm>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<set>
#include<map>
#include<iterator>
#include<queue>
#include<vector>
#include<string>
using namespace std;

typedef long long ll;
const int N=1e6+10;
const long long INF=1e18;
const double eps=0.0000001;
const ll mod=10007;
int n,mx;
ll a[N];
ll vis[N];
int bl[N];//分块
int blo;//分块大小
ll sum[N];//加法标记
ll pre[N];//乘法标记
ll add[N];
vector<int>ans[2000];

void init()
{
    for(ll i=1;i<=n;i++)
    {
        add[bl[i]]+=a[i];
    }
}

void res()
{
    int p=0;
    for(int i=1;i<=mx;i++)
    {
        for(int j=0;j<ans[i].size();j++)
        a[++p]=ans[i][j];
        ans[i].clear();
    }
    n=p;

}

void dev()
{
        blo=sqrt(n);
        for(ll i=1;i<=n;i++)
            bl[i]=(i-1)/blo+1,ans[bl[i]].push_back(a[i]),mx=max(mx,bl[i]);

}

void push_down(int k)
{
    for(int i=(bl[k]-1)*blo+1;i<=bl[k]*blo;i++)
        a[i]=(a[i]*pre[bl[i]]%mod+sum[bl[i]]%mod)%mod;
    pre[bl[k]]=1,sum[bl[k]]=0;
}

void update(int L,int R,int x)
{
    if(bl[L]==bl[R])
    {
        push_down(L);
        for(int i=L;i<=R;i++)
            a[i]=(x%mod+a[i]%mod)%mod;
    }
    else
    {
    push_down(L);
    for(int i=L;i<=bl[L]*blo;i++)
            a[i]=(x%mod+a[i]%mod)%mod;
    for(int i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]=(x%mod+sum[i]%mod)%mod;
    push_down(R);
    for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            a[i]=(x%mod+a[i]%mod)%mod;
    }
}

void updates(int L,int R,int x)
{
    if(bl[L]==bl[R])
    {
        push_down(L);
        for(int i=L;i<=R;i++)
            a[i]=(x%mod*a[i]%mod)%mod;
    }
    else
    {
    push_down(L);
    for(int i=L;i<=bl[L]*blo;i++)
            a[i]=(x%mod*a[i]%mod)%mod;
    for(int i=bl[L]+1;i<=bl[R]-1;i++)
            sum[i]=(x%mod*sum[i]%mod)%mod,pre[i]=(x%mod*pre[i]%mod)%mod;
    push_down(R);
    for(int i=(bl[R]-1)*blo+1;i<=R;i++)
            a[i]=(x%mod*a[i]%mod)%mod;
    }
}

ll query(ll L,ll R,ll c)
{
    return (a[R]*pre[bl[R]]%mod+sum[bl[R]]%mod)%mod;
}

int main()
{
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    ll ty,l,r,c,m;
    cin>>n;
    m=n;
    blo=sqrt(n);
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    for(ll i=1;i<=n;i++)
        bl[i]=(i-1)/blo+1,pre[bl[i]]=1;

    for(ll i=0;i<m;i++)
    {
        cin>>ty>>l>>r>>c;
        if(ty==0)
        {
            update(l,r,c);
        }
        else if(ty==1)
        {
            updates(l,r,c);
        }
        else
        {
            cout<<query(l,r,c)<<endl;
        }
    }

    //cout << "Hello world!" << endl;
    return 0;
}

数列分块入门 8
下代码为错误的,debug4个小时都没找出来,放弃治疗了
修改的时候还记得标记,查询的时候把标记这个事情给忘了…这记性

//操作涉及区间询问等于一个数  的元素,并将这个区间的所有元素改为 。
/*
区间修改没有什么难度,这题难在区间查询比较奇怪,因为权值种类比较多,似乎没有什么好的维护方法。
模拟一些数据可以发现,询问后一整段都会被修改,几次询问后数列可能只剩下几段不同的区间了。
我们思考这样一个暴力,还是分块,维护每个分块是否只有一种权值,区间操作的时候,对于同权值的一个块就O(1)统计答案,否则暴力统计答案,并修改标记,不完整的块也暴力。
 
这样看似最差情况每次都会耗费O(n)的时间,但其实可以这样分析:
假设初始序列都是同一个值,那么查询是O(√n),如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是O(√n)。
换句话说,要想让一个操作耗费O(n)的时间,要先花费√n个操作对数列进行修改。
初始序列不同值,经过类似分析后,就可以放心的暴力啦。
*/
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<set>
#include<map>
#include<iterator>
#include<queue>
#include<vector>
#include<string>
using namespace std;

typedef long long ll;
const int N=1e6+10;
const long long INF=1e18;
const double eps=0.0000001;
const ll mod=10007;
ll n,mx;
ll a[N];
ll vis[N];
ll bl[N];//分块
ll blo;//分块大小
ll su[N];//加法标记
ll pre[N];//乘法标记
ll tag[N];//区间标记
ll add[N];
vector<ll>ans[2000];

void update(ll L,ll R,ll x)
{
    if(bl[L]==bl[R])
    {
        if(vis[bl[L]])
        {
            for(ll i=(bl[L]-1)*blo+1;i<L;i++) a[i]=tag[bl[L]];
            for(ll i=R+1;i<=min(bl[R]*blo,n);i++) a[i]=tag[bl[R]];
            vis[bl[L]]=0;
        }
        for(ll i=L;i<=R;i++)
            a[i]=x;
    }
    else
    {
        if(vis[bl[L]])
        {
            for(ll i=(bl[L]-1)*blo+1;i<L;i++) a[i]=tag[bl[L]];
            vis[bl[L]]=0;
        }
        for(ll i=L;i<=bl[L]*blo;i++)
                a[i]=x;
        for(ll i=bl[L]+1;i<=bl[R]-1;i++)
                tag[i]=x,vis[i]=1;
        for(ll i=(bl[R]-1)*blo+1;i<=R;i++)
                a[i]=x;
        if(vis[bl[R]])
        {
            for(ll i=R+1;i<=min(bl[R]*blo,n);i++) a[i]=tag[bl[R]];
            vis[bl[R]]=0;
        }
    }
}

ll query(ll L,ll R,ll c)
{
    ll sum=0;
    if(bl[L]==bl[R])
    {
        if(vis[bl[L]])
        {
            if(tag[bl[L]]==c)
                sum+=R-L+1;
        }
        else
         for(int i=L;i<=R;i++)
            sum+=(ll)(a[i]==c);
    }
    else
    {
        if(vis[bl[L]])
        {
            if(tag[bl[L]]==c)
                sum+=bl[L]*blo-L+1;
        }
        else
        for(int i=L;i<=bl[L]*blo;i++)
                sum+=(ll)(a[i]==c);
        for(int i=bl[L]+1;i<=bl[R]-1;i++)
        {
            if(vis[i])
            {
                if(tag[i]==c)
                    sum+=blo;
            }
            else
            {
                for(int j=(i-1)*blo+1;j<=i*blo;j++)
                    sum+=(ll)(a[j]==c);
            }
        }
        if(vis[bl[R]])
        {
            if(tag[bl[R]]==c)
                sum+=R-(bl[R]-1)*blo;
        }
        else
        for(int i=(bl[R]-1)*blo+1;i<=R;i++)
                sum+=(ll)(a[i]==c);
    }
    return sum;
}

int main()
{
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    ll ty,l,r,c,m;
    cin>>n;
    m=n;
    blo=sqrt(n);
    for(ll i=1;i<=n;i++)
        cin>>a[i];
    for(ll i=1;i<=n;i++)
        bl[i]=(i-1)/blo+1;

    for(ll i=0;i<m;i++)
    {
        cin>>l>>r>>c;
        cout<<query(l,r,c)<<"\n";
        update(l,r,c);
    }
    //cout << "Hello world!" << endl;
    return 0;
}

莫队算法:复杂度的证明
例题: 小Z的袜子

#include <bits/stdc++.h>

using namespace std;
//莫队算法主要处理离线问题,查询只给出L,R
//当[L,R]很容易向[L-1,R],[L+1,R],[L,R-1],[L,R+1]转移时可用莫队
//注意转移的时候先扩张再收缩,L先向右,L再向左,最后再收缩
//add就是当前区间添加某元素时要做的操作
//remove就是当前区间删除某元素时要做的操作
//add,remove函数写的时候都要注意结构顺序
struct node
{
    int l,r,id;
}Q[maxn];

int bl[maxn];//保存所在块

bool cmp(const node &a,const node &b)
{
    if(bl[a.l]==bl[b.l]) return a.r<b.r; return bl[a.l]<bl[b.l];
}

int a[maxn];
ll ans[maxn]; //保存每个查询得答案
int L=0,R=0;  //多组记得重置
ll Ans=0;     //多组记得重置
void Add(int x);
void Remove(int x);
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    int sz=sqrt(n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i]=a[i]^a[i-1];
        bl[i]=i/sz;
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&Q[i].l,&Q[i].r);
        Q[i].id=i;
    }
    sort(Q+1,Q+1+m,cmp);
    for(int i=1;i<=m;i++)
    {
        while(L+1<Q[i].l) Remove(L++);
        while(L+1>Q[i].l) Add(L--);
        while(R<Q[i].r) Add(R++);
        while(R>Q[i].r) Remove(R--);
        ans[Q[i].id]=Ans;
    }
    for(int i=1;i<=m;i++)
        printf("%lld\n",ans[i]);
    return 0;
}

莫队算法的应用

带修莫队

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=2*1e6+10;
inline int read()
{
    char c=getchar();int x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0',c=getchar();}
    return x*f;
}
int N,M;
int a[MAXN],where[MAXN];
struct Query
{
    int x,y,pre,id;
}Q[MAXN];
int Qnum=0;
struct Change
{
    int pos,val;
}C[MAXN];
int Cnum=0;
int color[MAXN],ans=0,base,out[MAXN];
int comp(const Query &a,const Query &b)
{
    if(a.x!=b.x) return where[a.x]<where[b.x];
    if(a.y!=b.y) return where[a.y]<where[b.y];
    return a.pre<b.pre;
}
void Add(int val)
{    
    if(++color[val]==1) ans++;
} 
void Delet(int val)
{
    if(--color[val]==0) ans--;
}
void Work(int now,int i)
{
    if(C[now].pos>=Q[i].x&&C[now].pos<=Q[i].y)//注意:只有修改在查询的区间内才会对查询的结果产生影响 
    {
        if( --color[a[C[now].pos]] == 0 ) ans--;
        if( ++color[C[now].val] == 1)      ans++; 
    }
    swap(C[now].val,a[C[now].pos]);
    //这里有个很巧妙的操作
    //对于一个操作,下一次需要为的颜色是本次被改变的颜色
    //比如,我把颜色3改为了7,那么再进行这次修改的时候就是把7改为3
    //所以直接交换两种颜色就好 
}
void MoQueue()
{
    int l=1,r=0,now=0; 
    for(int i=1;i<=Qnum;i++)
    {
        while(l<Q[i].x)    Delet(a[l++]);
        while(l>Q[i].x) Add(a[--l]);
        while(r<Q[i].y) Add(a[++r]);
        while(r>Q[i].y) Delet(a[r--]);//以上四句为莫队模板 
        while(now<Q[i].pre) Work(++now,i);//改少了,改过去 
        while(now>Q[i].pre) Work(now--,i);//改多了,改回来 
        out[Q[i].id]=ans;//统计答案 
    }
    for(int i=1;i<=Qnum;i++)
        printf("%d\n",out[i]);
}
int main()
{
    N=read();M=read();
    base=sqrt(N);
    for(int i=1;i<=N;i++) a[i]=read(),where[i]=(i-1)/base+1;
    while(M--)
    {
        char opt[5];
        scanf("%s",opt);
        if(opt[0]=='Q')
        {
            Q[++Qnum].x=read();
            Q[Qnum].y=read();
            Q[Qnum].pre=Cnum;//别忘了记录最近的修改位置 
            Q[Qnum].id=Qnum;        
        }
        else if(opt[0]=='R')
        {
            C[++Cnum].pos=read();
            C[Cnum].val=read();
        }
    }
    sort(Q+1,Q+Qnum+1,comp);//玄学排序 
    MoQueue();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值