首页 > 技术文章 > 分块

genshy 2020-07-10 21:38 原文

                             分块

分块,顾名思义,就是将序列分成一小块一小块的来处理和维护。

我们把一段当成一个整体,只记录整体有关的信息,这就是分块。

首先,我们考虑一道题。

1.区间修改,查询区间中一段区间有多少数 < k;

  一种朴素算法就是,暴力从区间 L 到 R 扫过去,每遇到小于k的位置加1

没错,是不是这种方法很傻。但分块就是从这里优化过来的。

假设,我们有一个长度为 n 的序列,然后我们把他切成 sqrt(n)块,把每一块中的东西当成一个整体。(至于为什么块的大小是sqrt(n),自己可以百度一下QAQ)

几个术语:      完整块  被操作区间完全覆盖的块

                        不完整块 不完全覆盖的块

然后我们考虑怎么得出答案

  1.    首先我们要预处理每个点在哪个块,以及每个块的左右端点。
  2.    然后, 对于不完整的块,我们可以考虑直接暴力修改,因为元素比较少。
  3.   那么, 对于完整的块,我们就可以考虑打上lazy标记。

总而言之,分块就是将小块暴力,大块处理的一种方法。

然后这就可以用分块——这种看似高大上的方法做完。。。。。。比之前傻傻的暴力是不是好了很多。

下面就是预处理代码

 

1 for(int i = 1; i <= n; i++){
2     scanf("%d",&a[i]);
3     pos[i] = (i-1)/len +1;//每个点所在的块
4     L[pos[i]] = 2333333;
5 }
6 for(int i = 1; i <= n; i++){
7       L[pos[i]] = min(L[pos[i]],i);//每个块的左右端点
8       R[pos[i]] = max(R[pos[i]],i);
9 }

 一个比较重要的问题就是,块的数量不一定是len+1

他正确的是n/len+1

 

思想讲完了,下面就该看几道题了。

P4145 花神游历各国

题意大概是给出一个长为n的序列,以及n个操作,操作有区间开方和区间求和。

我们会发现开方操作比较难,主要是维护整块时,必须一个个的去修改,那这样岂不是和不同的暴力复杂度不就差不多了吗.

然后我们接着往深里想,不难发现,一个数经过几次开方后,就会变成0或1.

如果每次区间开方只要修改不完整的块,直接暴力修改就可以了。

如果修改一些整块,我们可以只要在每个整块暴力开方后,记录一下元素是否变成了0或1,修改时直接跳过全为0或1的块

就okk了 ,这样每个元素至多开方不超过4次,复杂度刚好可以。

下面是代码;

 

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<cstring>
 6 using namespace std;
 7 const int N = 1e5+100;
 8 long long n,m,k,l,r;
 9 long long a[N],pos[N],L[N],R[N],sum[N];
10 bool vis[N];
11 void check(long long x){//检查每个块是否全为0或1
12     if(vis[x]) return;
13     vis[x] = 1;
14     sum[x] = 0;
15     for(int i = L[x]; i <= R[x]; i++){
16         a[i] = sqrt(a[i]);
17         sum[x] += a[i];
18         if(a[i] > 1) vis[x] = 0;
19     }
20 }
21 void chenge(long long l,long long r){
22     int p = pos[l],q = pos[r];
23     if(p == q){
24         for(int i = l; i <= r; i++){//同一个块直接暴力改
25             sum[p] -=a[i];
26             a[i] = sqrt(a[i]);
27             sum[p] += a[i];
28         }
29     } 
30     else {
31         for(int i = p+1; i <= q-1; i++) check(i);
32         for(int i = l; i <= R[p]; i++){//左边的散块
33             sum[p] -=a[i];
34             a[i] = sqrt(a[i]);
35             sum[p] += a[i];
36         }
37         for(int i = L[q]; i <= r; i++){
38             sum[q] -= a[i];
39             a[i] = sqrt(a[i]);
40             sum[q] += a[i];
41         }
42     }
43 }
44 long long ask(long long l,long long r){
45     long long ans = 0; 
46     int  p = pos[l],q = pos[r];
47     if(p == q) for(int i = l; i <= r; i++) ans += a[i];
48     else {
49         for(long long i = p+1; i <= q-1; i++) ans += sum[i];
50         for(int i =l; i <= R[p]; i++) ans+= a[i];
51         for(long long i  = L[q]; i <= r; i++) ans += a[i];
52     }
53     return ans;
54 }
55 int main(){
56     cin>>n;
57     memset(vis, false, sizeof(vis));
58     int len = sqrt(n);
59     for(long long i = 1; i <= n; i++) cin>>a[i];
60     for(long long i = 1; i <= n; i++){
61         pos[i] = (i-1)/len+1;
62         L[pos[i]] = 23333333;
63     }
64     for(long long i = 1; i <= n; i++){
65         L[pos[i]] = min(L[pos[i]],i);
66         R[pos[i]] = max(R[pos[i]],i);
67     }
68     for(int i = 1; i <= n/len+1; i++){//预处理每个块的和
69         for(int j = L[i]; j <= R[i]; j++){
70             sum[i]+=a[j];
71         }
72     }
73     cin>>m;
74     for(long long i = 1; i <= m; i++){
75         cin>>k>>l>>r;
76         if(l > r) swap(l,r);
77         if( k == 0) chenge(l,r);
78         else cout<<ask(l,r)<<endl;
79     }
80     return 0;
81 }

 

关于这道题,你还可以用线段树做,并且要比分块快QAQ。

 

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<cstring>
 6 using namespace std;
 7 const int N = 1e5+100;
 8 long long n,m,k,l,r;
 9 long long a[N],pos[N],L[N],R[N],sum[N];
10 bool vis[N];
11 void check(long long x){
12     if(vis[x]) return;
13     vis[x] = 1;
14     sum[x] = 0;
15     for(int i = L[x]; i <= R[x]; i++){
16         a[i] = sqrt(a[i]);
17         sum[x] += a[i];
18         if(a[i] > 1) vis[x] = 0;
19     }
20 }
21 void chenge(long long l,long long r){
22     int p = pos[l],q = pos[r];
23     if(p == q){
24         for(int i = l; i <= r; i++){
25             sum[p] -=a[i];
26             a[i] = sqrt(a[i]);
27             sum[p] += a[i];
28         }
29     } 
30     else {
31         for(int i = p+1; i <= q-1; i++) check(i);
32         for(int i = l; i <= R[p]; i++){
33             sum[p] -=a[i];
34             a[i] = sqrt(a[i]);
35             sum[p] += a[i];
36         }
37         for(int i = L[q]; i <= r; i++){
38             sum[q] -= a[i];
39             a[i] = sqrt(a[i]);
40             sum[q] += a[i];
41         }
42     }
43 }
44 long long ask(long long l,long long r){
45     long long ans = 0; 
46     int  p = pos[l],q = pos[r];
47     if(p == q) for(int i = l; i <= r; i++) ans += a[i];
48     else {
49         for(long long i = p+1; i <= q-1; i++) ans += sum[i];
50         for(int i =l; i <= R[p]; i++) ans+= a[i];
51         for(long long i  = L[q]; i <= r; i++) ans += a[i];
52     }
53     return ans;
54 }
55 int main(){
56     cin>>n;
57     memset(vis, false, sizeof(vis));
58     int len = sqrt(n);
59     for(long long i = 1; i <= n; i++) cin>>a[i];
60     for(long long i = 1; i <= n; i++){
61         pos[i] = (i-1)/len+1;
62         L[pos[i]] = 23333333;
63     }
64     for(long long i = 1; i <= n; i++){
65         L[pos[i]] = min(L[pos[i]],i);
66         R[pos[i]] = max(R[pos[i]],i);
67     }
68     for(int i = 1; i <= n/len+1; i++){
69         for(int j = L[i]; j <= R[i]; j++){
70             sum[i]+=a[j];
71         }
72     }
73     cin>>m;
74     for(long long i = 1; i <= m; i++){
75         cin>>k>>l>>r;
76         if(l > r) swap(l,r);
77         if( k == 0) chenge(l,r);
78         else cout<<ask(l,r)<<endl;

 

第二题 P2801 教主的魔法

这个题大致就是区间加和区间询问小于k的数。

对于每个询问,可以先将这个块排好序,然后二分查找k的值就行了。散块直接暴力查询没有排序的原序列。

怎么修改呢???

对于整块,我们可以打上 lazy 标记,查询时直接查 >= k - lazy 的值;

对于散块,直接暴力修改,在直接排个序就okk了。

 

例三 zcl的妹

 

例三 ZCL的妹子

  1 #include<iostream>
  2 #include<cstdio>
  3 #include<algorithm>
  4 #include<cmath>
  5 using namespace std;
  6 const int N = 1e6+10;
  7 #define LL long long
  8 char opt;
  9 int n,m,x,y,len;
 10 int L[N],R[N];
 11 LL sum[N],size[N],a[N],pos[N],cnt[N];
 12 inline int read()
 13 {
 14     int s = 0, w = 1; char ch = getchar();
 15     while(ch < '0' || ch > '9'){if(ch == '-') w = -1;ch = getchar();}
 16     while(ch >= '0'&&ch <= '9'){s = s * 10 +ch -'0'; ch = getchar();}
 17     return s * w;
 18 }
 19 void chenge1(int x,int y)
 20 {
 21     if(!cnt[x]) return ;
 22     int xx = pos[x];
 23     sum[xx] -= y;
 24     a[x] -= y;
 25 }
 26 void chenge2(int x,int y)
 27 {
 28     int xx = pos[x];
 29     if(cnt[x] == 1)
 30     {
 31         sum[xx] -= a[x];
 32         a[x] = y;
 33         sum[xx] += a[x];
 34      }
 35      else{
 36          cnt[x] = 1;
 37          sum[x] -= a[x];
 38          a[x] = y;
 39          size[xx]++;
 40          sum[xx] += a[x];
 41      }
 42 }
 43 void chenge3(int x)
 44 {
 45      int p = 1;
 46      while(x - size[p] > 0) x -= size[p], p++;
 47      for(int i = L[p]; i <= R[p]; i++)
 48      {
 49          x -= cnt[i];
 50          if(x == 0)
 51          {
 52              size[p]--;
 53              sum[p] -= a[i];
 54              cnt[i] = a[i] = 0;
 55              return ;
 56          }
 57      } 
 58 }
 59 LL ask()
 60 {
 61     LL ans = 0;
 62     for(int i = 1; i <= pos[500000]; i++) ans += sum[i];
 63     return ans;
 64 }
 65 int main()
 66 {
 67     n = read(); m = read();
 68     len = sqrt(500000);
 69     for(int i = 1; i <= n; i++) a[i] = read(),cnt[i] = 1;
 70     for(int i = 1; i <= 500000; i++)
 71     {
 72         pos[i] = (i-1)/len+1;
 73         L[pos[i]] = 23333333;
 74     }
 75     for(int i = 1; i <= 500000; i++)
 76     {
 77         size[pos[i]] += cnt[i];
 78         L[pos[i]] = min(L[pos[i]] , i);
 79         R[pos[i]] = max(R[pos[i]] , i);
 80         sum[pos[i]] += a[i]; 
 81     }
 82     for(int i = 1; i <= m; i++)
 83     {
 84         cin>>opt;
 85         if(opt == 'C')
 86         {
 87             x = read(); y = read();
 88             chenge1(x,y);
 89         }
 90         if(opt == 'I')
 91         {
 92             x = read(); y = read();
 93             chenge2(x,y);
 94         }
 95         if(opt == 'D')
 96         {
 97             x = read();
 98             chenge3(x);
 99         }
100         if(opt == 'Q') printf("%lld\n",ask());
101     }
102     return 0;
103 }
View Code

 

 

第N题:P4168 蒲公英  (这道题我也不会,贼恶心

这道题题意就是给你一段区间,然后询问区间的最小众数。

首先 a < 1e9先离散化;

然后预处理出两个数组 p[i][j] 表示第i个块到第j个块的最小众数。

                                    s[i][j] 前i个块中 j出现的次数

 如何处理呢

  1.  对于s, 直接每一个块扫一遍  。
  2.  对于 p ,双重枚举i,j开个数组暴力统计每个数出现多少次就行了。
  3. 对于询问操作,残块暴力搞,整块骚操作。所以对于posr - posl <= 2 的情况直接可以暴力搞出来

 4.对于其他的情况,答案要么是整块中的众数,或者残块中的数。对于残块中的数,我们已经预处理了之前数字出现的前缀和,

        所以能很快的求出这个数出现的次数。

       5.对于整块中的众数,我们也可以很快的求出ta在残块中的次数。这样答案就可以出来了。

        

void Treaker()//分块预处理 
{
    for(int i=1;i<=n;++i)
    for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++;
    
    for(int i=1;i<=pos[n];++i)
    {
        memset(tong,0,sizeof(tong));
        for(int j=i;j<=pos[n];++j)
        {
            p[i][j]=p[i][j-1];
            for(int k=L[j];k<=R[j];++k)
            {
                tong[a[k]]++;
                if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k]<p[i][j]))p[i][j]=a[k];
            }
        }
    }
    memset(tong,0,sizeof(tong));
}
ycl

比较重要的是

p[i][j]=p[i][j-1]

因为我们需要先继承先前的区间众数,然后才能更新,否则的话就会听取 WA 声一片(据说我们学长神犇就卡在这里好几回

if(pos[r]-pos[l]<=2)
    {
        int res=0;
        for(int i=l;i<=r;++i)
        {
            ++tong[a[i]];
            if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
        }
        for(int i=l;i<=r;++i)tong[a[i]]=0;
        return res;
    }
小于等于2的情况
int res=0;
    for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
    for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
    for(int i=l;i<=R[pos[l]];++i)
    {
        ++tong[a[i]];
        if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
    }
    for(int i=L[pos[r]];i<=r;++i)
    {
        ++tong[a[i]];
        if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
    }
    int k=p[pos[l]+1][pos[r]-1];
    int lin=s[pos[r]-1][k]-s[pos[l]][k];
    for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k);
    for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k);
    if(lin>tong[res]||(lin==tong[res]&&k<res))res=k;
    
    for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0;
    for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0;
    return res;
其他情况

最后有几点要注意

  1. 注意离散化,否则直接RE到原地起飞。
  2. 注意 p 数组的转移,否则 WA 到怀疑人生。
  3. 注意分块的边界,否则TLE 到直接原地爆炸。

由于本蒟蒻水平太差,此篇题解参考了神犇(wjlss)的博客,如果有人看不懂的话,这里有链接QAQ  wljss

最后本人代码一直炸,所以附上神犇的代码

#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,tot,len,l,r,ans;
const int N=40010,M=50010,K=510;
int a[N],b[N],pos[N],s[K][N],L[K],R[K],p[K][K],tong[N];
int read()
{
    char ch;int x=0,f=1;
    while(!isdigit(ch=getchar()))
    {(ch=='-')&&(f=-f);}
    while(isdigit(ch))
    {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    return x*f;
}
int cnt=0;
void yych()//离散化 
{
    len=sqrt(n);
    memset(L,0x3f,sizeof(L));
    for(int i=1;i<=n;++i)
    {
        pos[i]=(i-1)/len+1;
        L[pos[i]]=min(L[pos[i]],i);
        R[pos[i]]=max(R[pos[i]],i);
        a[i]=read();
        b[i]=a[i];  
    }
    sort(b+1,b+1+n);
    tot=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=n;++i)a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
}
void Treaker()//分块预处理 
{
    for(int i=1;i<=n;++i)
    for(int j=pos[i];j<=pos[n];++j)s[j][a[i]]++;

    for(int i=1;i<=pos[n];++i)
    {
        memset(tong,0,sizeof(tong));
        for(int j=i;j<=pos[n];++j)
        {
            p[i][j]=p[i][j-1];
            for(int k=L[j];k<=R[j];++k)
            {
                tong[a[k]]++;
                if((tong[a[k]]>tong[p[i][j]])||(tong[a[k]]==tong[p[i][j]]&&a[k]<p[i][j]))p[i][j]=a[k];
            }
        }
    }
    memset(tong,0,sizeof(tong));
}
int ask(int l,int r)
{
    if(pos[r]-pos[l]<=2)
    {
        int res=0;
        for(int i=l;i<=r;++i)
        {
            ++tong[a[i]];
            if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
        }
        for(int i=l;i<=r;++i)tong[a[i]]=0;
        return res;
    }
    int res=0;
    for(int i=l;i<=R[pos[l]];++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
    for(int i=L[pos[r]];i<=r;++i)if(!tong[a[i]])tong[a[i]]+=s[pos[r]-1][a[i]]-s[pos[l]][a[i]];
    for(int i=l;i<=R[pos[l]];++i)
    {
        ++tong[a[i]];
        if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
    }
    for(int i=L[pos[r]];i<=r;++i)
    {
        ++tong[a[i]];
        if(tong[a[i]]>tong[res]||(tong[a[i]]==tong[res]&&a[i]<res))res=a[i];
    }
    int k=p[pos[l]+1][pos[r]-1];
    int lin=s[pos[r]-1][k]-s[pos[l]][k];
    for(int i=l;i<=R[pos[l]];++i)lin+=(a[i]==k);
    for(int i=L[pos[r]];i<=r;++i)lin+=(a[i]==k);
    if(lin>tong[res]||(lin==tong[res]&&k<res))res=k;

    for(int i=l;i<=R[pos[l]];++i)tong[a[i]]=0;
    for(int i=L[pos[r]];i<=r;++i)tong[a[i]]=0;
    return res;
}
int main()
{
    cin>>n>>m;
    yych();
    Treaker();
    for(int i=1;i<=m;++i)
    {
        l=read();r=read();
        l=(l+ans-1)%n+1;r=(r+ans-1)%n+1;
        if(l>r)swap(l,r);
        ans=b[ask(l,r)];
        printf("%d\n",ans);
    }
    return 0;
}
神犇完整代码

然后你就可以AC 一道紫题(据说前几年这个还是一道黑题

最后,分块的东西也就这么多,主要理解小块暴力,整块瞎搞就可以了。

最后附上习题。

  1. Karen and coffee
  2. 蒲公英
  3. 线段树2
  4. 定价 (河北15年省选题,思考出怎么分块就可以轻松AC了)

 

 

 

 

 

 

 

推荐阅读