分块入门专题

http://hzwer.com/8053.html 先上大神博客。

https://loj.ac/problems/tag/207 题目链接

分块,是一种能过题的暴力,一般复杂度为n^(1/2),可以处理很多区间的问题的数据结构,因此很有必要去学习分块这里有入门级别的九道题,我觉得做题要自己去摸索,而不是一味得去读别人的题解,否则到时候就算做过,也有可能wa成狗...

1.给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。

分块的基本操作,将元素分为m块,每一块有n/m个元素,如果涉及的区间有不完整块的,那么直接对不完整块进行暴力遍历,对于完整块,用一个标记去维护,例如这里区间加法,利用一个atag去标记这个块增加了多少,初始化操作为将每一个元素进行块的分类blo[i]=(i-1)/m+1,blo[i]意味着i属于哪一块,单点查询直接v[i]+atag[blo[i]]就好了

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
void add(int a,int b,int c)
{
    int i;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        v[i]+=c;
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            v[i]+=c;
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        atag[i]+=c;
    }
}
int main()
{
  // freopen("date.txt","r",stdin);
   // freopen("ans.txt","w",stdout);
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
    }
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) add(a,b,c);
        else printf("%d\n",v[b]+atag[blo[b]]);
    }
}

2.给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。

区间加法和上题一样,这里求小于某个x的值,所以要对元素进行排序,所以要用到容器,这里使用vector,下面的一道题使用set,其实都一样,而且如果参入了set,那么对于增加删除操作很更加方便,把每一块的元素加入相应的vector中,那么最后一定要对所有的vector进行一次sort排序,接着是暴力修改,暴力修改完后需要对该块的所有元素重新放入,重新排序,所以利用reset()(这里要注意!!!!你传入函数的参数到底是什么!!!!),那么如果对于整个块修改呢,那么就维护atag就好了,接着查询的时候,不完整的块就直接暴力,完整的块就先把要查询的数c-atag[i],然后进行lower_bound(c)(容器的二分函数,lower_bound()返回第一个大于等于c的元素,upper_bound()返回第一个大于c的元素),作累加即可(其实这道题用multiset更合适,这是后面想到的)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <map>
#include <cmath>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
vector<int >ve[509];
void reset(int x)
{
    int i;
    ve[x].clear();
    for(i=(x-1)*m+1;i<=min(x*m,n);i++)
    {
        ve[x].push_back(v[i]);
    }
    sort(ve[x].begin(),ve[x].end());
}
void add(int a,int b,int c)
{
    int i;
    for(i=a;i<=min(blo[a]*m,b);i++)
    {
        v[i]+=c;
    }
    reset(blo[a]);
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            v[i]+=c;
        }
        reset(blo[b]);
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        atag[i]+=c;
    }
}
int query(int a,int b,int c)
{
    int i,ans=0;
    for(i=a;i<=min(blo[a]*m,b);i++)
    {
        if(v[i]+atag[blo[a]]<c) ans++;
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            if(v[i]+atag[blo[b]]<c) ans++;
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        int x=c-atag[i];
        ans+=lower_bound(ve[i].begin(),ve[i].end(),x)-ve[i].begin();
    }
    return ans;
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        ve[blo[i]].push_back(v[i]);
    }
    for(i=1;i<=blo[n];i++)//第n块!!!!
    {
        sort(ve[i].begin(),ve[i].end());
    }
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) add(a,b,c);
        else printf("%d\n",query(a,b,c*c));
    }
}

给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。

这题目和题2差不多,不过这里用了set,要注意的是大神博客里面是之间分了1000个块,我这里是分类sqrt(n)个块,所以我用了reset(),还要注意的是!!!!!!边界条件,边界条件,边界条件!!!!例如说,set在lower_bound()中如果第一个元素就大于等于c了怎么办?所以it=s[i].begin(),那么就不能进行it--了!!!所以要特判一下。还要注意对块进行查找的时候,要减去atag[i]  !!

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int n,m;
set<int>s[1009];
void reset(int x)
{
    int i;
    s[x].clear();
    for(i=(x-1)*m+1;i<=x*m;i++)
    {
        s[x].insert(v[i]);
    }
}
void add(int a,int b,int c)
{
    int i;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        v[i]+=c;
    }
    reset(blo[a]);
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            v[i]+=c;
        }
        reset(blo[b]);
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        atag[i]+=c;
    }
}
int query(int a,int b,int c)
{
    int ans=-1,i;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        if(v[i]+atag[blo[a]]<c) ans=max(ans,v[i]+atag[blo[a]]);
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            if(v[i]+atag[blo[b]]<c) ans=max(ans,v[i]+atag[blo[b]]);
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        int x=c-atag[i];
        set<int >::iterator it;
        it=s[i].lower_bound(x);
        if(it==s[i].begin()) continue;
        it--;
        ans=max(ans,(*it)+atag[i]);
    }
    return ans;
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        s[blo[i]].insert(v[i]);
    }
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) add(a,b,c);
        else printf("%d\n",query(a,b,c));
    }
}

4.给出一个长为n的数列,以及n个操作,操作涉及区间加法,区间求和。

数据结构经典操作,树状数组,线段树都可以做,这里用分块,和问题1一样,加一个sum数组,注意溢出问题就好了

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
ll v[50999];//原数值
int blo[50999];//块的数组
ll atag[50999];//每一个块的标记数组
int n,m;
set<int>s[1009];
ll sum[50009];
void add(int a, int b,int c)
{
    int i;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        v[i]+=c;
        sum[blo[a]]+=c;
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            v[i]+=c;
            sum[blo[b]]+=c;
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        atag[i]+=c;
    }
}
ll query(int a,int b)
{
    int i;
    ll ans=0;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        ans+=v[i]+atag[blo[a]];
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            ans+=v[i]+atag[blo[b]];
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        ans+=sum[i]+atag[i]*m;
    }
    return ans;
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%lld",&v[i]);
        blo[i]=(i-1)/m+1;
        sum[blo[i]]+=v[i];
    }
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) add(a,b,c);
        else printf("%lld\n",query(a,b)%(c+1));
    }
}

5.给出一个长为n的数列,以及n个操作,操作涉及区间开方,区间求和。

区间求和就和上面的一样利用一个sum数组去做,而区间开方就要去思考一下了,50000最多也就开方4次就到1了,到了1之后,无论怎么开方都还是1,因此我们要标记这个块是否还可以开方就好了,atag==1表示还可以开方,所以一开始要初始化的所有块都是1(wa了好久才发现),其它和上面的题目差不多

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#define INF 99999999
typedef long long ll;
using namespace std;
ll v[50999];//原数值
int blo[50999];//块的数组
int atag[50999];//每一个块的标记数组
int n,m;
set<int>s[1009];
ll sum[50009];
void reset(int x)
{
    int flag=0,i;
    sum[x]=0;
    for(i=(x-1)*m+1;i<=x*m;i++)
    {
        v[i]=sqrt(v[i]);
        sum[x]+=v[i];
        if(v[i]>1) flag=1;
    }
    atag[x]=flag;
}
void sq(int a,int b)
{
    int i;
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        sum[blo[a]]-=v[i];
        v[i]=sqrt(v[i]);
        sum[blo[a]]+=v[i];
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            sum[blo[b]]-=v[i];
            v[i]=sqrt(v[i]);
            sum[blo[b]]+=v[i];
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        if(atag[i])
        {
            reset(i);
        }
    }
}
ll query(int a,int b)
{
    int i;
    ll ans=0;
    for(i=a; i<=min(b,blo[a]*m); i++)
    {
        ans+=v[i];
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1; i<=b; i++)
        {
            ans+=v[i];
        }
    }
    for(i=blo[a]+1; i<=blo[b]-1; i++)
    {
        ans+=sum[i];
    }
    return ans;
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1; i<=n; i++)
    {
        scanf("%lld",&v[i]);
        blo[i]=(i-1)/m+1;
        sum[blo[i]]+=v[i];
    }
    for(i=1;i<=blo[n];i++)
    {
        atag[i]=1;
    }
    for(i=1; i<=n; i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) sq(a,b);
        else printf("%lld\n",query(a,b));
    }
}

6.给出一个长为 nnn 的数列,以及 nnn 个操作,操作涉及单点插入,单点询问,数据随机生成。

分块插入操作,这里学会用pair的用法。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int n,m,last;
int temp[200099];
vector<int>ve[1009];
pair<int ,int>query(int a)
{
    int i=1;
    while(a>ve[i].size())
    {
        a-=ve[i].size();
        i++;
    }
    return make_pair(i,a-1);
}
void reset()
{
    int top=1,i;
    for(i=1;i<=last;i++)
    {
        for(vector<int>::iterator it=ve[i].begin();it!=ve[i].end();it++)
        {
            temp[top++]=*it;
        }
        ve[i].clear();
    }
    m=sqrt(top-1);
    for(i=1;i<=top-1;i++)
    {
        blo[i]=(i-1)/m+1;
        ve[blo[i]].push_back(temp[i]);
    }
    m=blo[top-1];
}
void insert(int a,int b)
{
    pair<int,int>t=query(a);//
    ve[t.first].insert(ve[t.first].begin()+t.second,b);
    if(ve[t.first].size()>m*m) reset();
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        ve[blo[i]].push_back(v[i]);
    }
    last=blo[n];
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0) insert(a,b);
        else
        {
            pair<int,int>t=query(b);
            printf("%d\n",ve[t.first][t.second]);
        }
    }
}

7.给出一个长为n的数列,以及n个操作,操作涉及区间乘法,区间加法,单点询问。

莫名其妙出bug,最后发现自己b写成了blo[b]...这里学会维护两个标记,应该是乘法优先,如果加法优先的话,会被覆盖

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
#define mod 10007
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int mtag[100999];
int n,m,last;
void reset(int x)
{
    int i;
    for(i=(x-1)*m+1;i<=min(n,x*m);i++)
    {
        v[i]=(v[i]*mtag[x]+atag[x])%mod;
    }
    atag[x]=0;
    mtag[x]=1;
}
void add(int a,int b,int c,int op)
{
    int i;
    reset(blo[a]);
    for(i=a;i<=min(b,blo[a]*m);i++)
    {
        if(op==0) v[i]+=c;
        else v[i]*=c;
        v[i]%=mod;
    }
    if(blo[a]!=blo[b])
    {
        reset(blo[b]);
        for(i=(blo[b]-1)*m+1;i<=b;i++)
        {
            if(op==0) v[i]+=c;
            else v[i]*=c;
            v[i]%=mod;
        }
    }
    for(i=blo[a]+1;i<=blo[b]-1;i++)
    {
        if(op==0)
        {
            atag[i]=(atag[i]+c)%mod;
        }
        else
        {
            atag[i]=(atag[i]*c)%mod;
            mtag[i]=(mtag[i]*c)%mod;
        }
    }
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        mtag[i]=1;
    }
    for(i=1;i<=n;i++)
    {
        int op,a,b,c;
        scanf("%d%d%d%d",&op,&a,&b,&c);
        if(op==0||op==1) add(a,b,c,op);
        else printf("%d\n",(v[b]*mtag[blo[b]]+atag[blo[b]])%mod);
    }
}

8.给出一个长为n的数列,以及n个操作,操作涉及区间询问等于一个数c的元素,并将这个区间的所有元素改为c。

如果区间是不完整块,那么先把这个块全部变成标记的值,并且将标记变为-1,证明这个块是有不同值得,暴力去比较并且修改,对于完整块,如果c和标记不同,一个个去比较,然后改变值,如果相等直接加上块元素的数量。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 99999999
#define mod 10007
typedef long long ll;
using namespace std;
int v[100999];//原数值
int blo[100999];//块的数组
int atag[100999];//每一个块的标记数组
int mtag[100999];
int n,m,last;
void reset(int x)
{
    int i;
    for(i=(x-1)*m+1; i<=min(n,x*m); i++)
    {
        v[i]=atag[x];
    }
    atag[x]=-1;
}
int add(int a,int b,int c)
{
    int i;
    int ans=0;
    if(atag[blo[a]]!=-1)reset(blo[a]);
    for(i=a; i<=min(b,blo[a]*m); i++)
    {
        if(v[i]==c) ans++;
        else v[i]=c;
    }
    if(blo[a]!=blo[b])
    {
        if(atag[blo[b]]!=-1)reset(blo[b]);
        for(i=(blo[b]-1)*m+1; i<=b; i++)
        {
            if(v[i]==c) ans++;
            else v[i]=c;
        }
    }
    for(i=blo[a]+1; i<=blo[b]-1; i++)
    {
        if(atag[i]!=-1)
        {
            if(atag[i]==c) ans+=m;
            else atag[i]=c;
        }
        else
        {
            for(int j=(i-1)*m+1;j<=i*m;j++)
            {
                if(v[j]==c) ans++;
                else v[j]=c;
            }
            atag[i]=c;
        }
    }
    return ans;
}
int main()
{
    int i;
    scanf("%d",&n);
    m=sqrt(n);
    for(i=1; i<=n; i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        atag[i]=-1;
    }
    for(i=1; i<=n; i++)
    {
        int op,a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        printf("%d\n",add(a,b,c));
    }
}

9.给出一个长为n的数列,以及n个操作,操作涉及询问区间的最小众数。

这是一道经典的难题(我不知道,我也是听大牛说的,反正我不会...)还有区间修改的操作,以后有实力在去搞搞吧...题目先对没一个元素去映射一下,然后把每一个值都标记上序号,这就是v[i]了,然后val[i]表示该序号的值,然后把相同序号的元素放到一个vector中,接着就是预处理了,这个预处理是以块为单位,[1,blo[n]]  [2,blo[n]] [3,blo[n]]...这样就把所有的区间都覆盖了f[i][j]表示第i块到第j块的最小众数。然后对于不完整块进行暴力查询,查询的方法就是在该序号里面找到第一个大于等于l,和第一个大r的元素,然后两个作作差就是这个数在[l,r]区间的个数了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <vector>
#include <cmath>
#include <set>
#include <map>
#define INF 0x3f3f3f3f
#define mod 10007
#define maxn 100009
#define L rt<<1
#define R rt<<1|1
typedef long long ll;
using namespace std;
int n,m;
int val[maxn];
int v[maxn];
int blo[maxn];
int cnt[maxn];
vector<int>ve[1009];
map<int,int>mp;
int f[1005][1005];
void pre(int x)
{
    memset(cnt,0,sizeof(cnt));
    int i,ans=0,mx=0;
    for(i=(x-1)*m+1; i<=n; i++)
    {
        cnt[v[i]]++;
        if(cnt[v[i]]>mx||cnt[v[i]]==mx&&val[v[i]]<val[ans])
        {
            ans=v[i];
            mx=cnt[v[i]];
        }
        f[x][blo[i]]=ans;
    }
}
int query(int l,int r,int x)
{
    int t=upper_bound(ve[x].begin(),ve[x].end(),r)-lower_bound(ve[x].begin(),ve[x].end(),l);
    return t;
}
int Q(int a,int b)
{
    int ans=f[blo[a]+1][blo[b]-1],mx=query(a,b,ans),i;
    for(i=a; i<=min(b,blo[a]*m); i++)
    {
        int temp=query(a,b,v[i]);
        if(temp>mx||temp==mx&&val[v[i]]<val[ans])
        {
            mx=temp;
            ans=v[i];
        }
    }
    if(blo[a]!=blo[b])
    {
        for(i=(blo[b]-1)*m+1; i<=b; i++)
        {
            int temp=query(a,b,v[i]);
            if(temp>mx||temp==mx&&val[v[i]]<val[ans])
            {
                mx=temp;
                ans=v[i];
            }
        }
    }
    return val[ans];
}
int main()
{
    scanf("%d",&n);
    m=sqrt(n);
    int id=0,i;
    for(i=1; i<=n; i++)
    {
        scanf("%d",&v[i]);
        blo[i]=(i-1)/m+1;
        if(!mp[v[i]])
        {
            mp[v[i]]=++id;
            val[id]=v[i];//该编号的值
        }
        v[i]=mp[v[i]];//将该值变成编号
        ve[v[i]].push_back(i);//这是id那个点的序号
    }
    for(i=1; i<=blo[n]; i++) pre(i); //这是将所有块区间的众数都遍历一遍
    //如[1,blo[n]] [2,3]表示第1-blo[n],2-3的众数
    for(i=1;i<=n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",Q(a,b));
    }
}




最后说一下感想吧,大牛的代码风格真的很好,很值得我去学习,我要是能学到他一半我就已经知足了...最近思考了很多,也很迷茫,今年发生了很多事情,这些事情足以改变我的人生了,对于acm,之前有一段时间是比较厌恶反感的,因为有的人思想太过功利了,弄得好像二进制一样不是0就是1,世界哪有分的那么清的,突然我又喜欢上了acm,喜欢上了学算法,是单纯的想要去学习,而并不是想要拿奖拿奖云云,可能以后会打一堆破铜烂铁,但又如何?当心胸放宽广了,好像这些也不什么大事了。

猜你喜欢

转载自blog.csdn.net/keepcoral/article/details/80743559
今日推荐