• 【OI】简单的分块


    介绍下简单的分块:

    当我们遇到区间类问题的时候,如何保证我们快速而高效地完成操作?

    答案是线段树分块。

    所谓分块,就是把一个序列分成许多块分别维护。是不是想起了树状数组 这样能大大提高效率:

    例如,我们需要查询l,r中所有元素的和

    利用分块,我们可以把1 2 3 4 5 6 7 8 9 10

    分为

    [1 2 3] [4 5 6] [7 8 9] [10]

    比如,我想要3~10的元素和。这是,我们拿出3~10的区间:

    ...[3] [4 5 6] [7 8 9] [10] 

    而我们已经预处理了 [4 5 6]与 [7 8 9]的区间和,我们只需要算出两端的和就可以。这样,就可以大大提高查询的速度。

    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    
    using namespace std;
    
    const int MaxN = 100010, MaxB = 1010;
    
    int n, B, q, Cnt;
    int a[MaxN], Left[MaxB], Right[MaxB], Pos[MaxN];
    long long sum[MaxN];
    
    int main()
    {
    	scanf("%d", &n);
    	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    	// 分块预处理 
    	B = max((int)sqrt(n), 1);
    	for (int i = 1; i <= n; i++)
    	{
    		if (i % B == 1 % B) Cnt++; // 每块块首 Cnt++ 
    		Pos[i] = Cnt; // i 处在哪一块 
    		if (Left[Cnt] == 0) Left[Cnt] = i; // 第Cnt块的左边 
    		Right[Cnt] = i; // 第Cnt块的右边
    	}
    
    // for (int i = 1; i <= n; i++) printf("%d ", Pos[i]); puts("");
    // for (int i = 1; i <= Cnt; i++) printf("%d %d
    ", Left[i], Right[i]);
    
    	for (int i = 1; i <= Cnt; i++)
    		for (int j = Left[i]; j <= Right[i]; j++)
    			sum[i] += a[j];	// sum[i] 表示的是第i块的和 
    
    	// 处理询问 
    	scanf("%d", &q);
    	//	a[l] + a[l + 1] + ... + a[r] 变成 sum[r] - sum[l - 1]
    	//	这样我们可以只用求 l=1 的情况 
    	//	但是我现在不这样做 
    	for (int i = 1; i <= q; i++)
    	{
    		int l, r;
    		scanf("%d %d", &l, &r);
    		long long ans = 0;
    		if (Pos[l] == Pos[r])
    		{
    			// 1   l,r 在同一个块内
    			// [  l     r  ]
    			for (int j = l; j <= r; j++) ans += a[j];
    		}
    		else
    		{
    			// 2   l,r 不在同一个块内
    			// [  l   ] [      ] [       ] [r      ]
    			for (int j = l; j <= Right[Pos[l]]; j++) ans += a[j];
    			for (int j = Right[Pos[l]] + 1; j <= Left[Pos[r]] - 1; j++) ans += sum[j];
    			for (int j = Left[Pos[r]]; j <= r; j++) ans += a[j];
    		}
    		printf("%lld
    ", ans);
    	}
    	return 0;
    }
    

    (不是我写的,但是的确十分简洁易懂对吧)

    例题:luogu P3203,LCT板子题可是可以用分块来过

    #include<iostream>
    #include<vector>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<cstdlib>
    #include<cmath>
    #include<algorithm>
    #include<set>
    #include<cstring>
    using namespace std;
    typedef long long ll;
    const ll INF=99999999;
    const int MaxN = 200010;
    int a[MaxN];
    struct nd{
        int t,to;//弹几次出此块 
        int from;//属于第几个块 
        
    }t[MaxN];
    int n,m,I,J; 
    int sqr,cnt = -1;
    int Left[MaxN],Right[MaxN];
    
    void findT(int x,int right,int &t,int &to){
        int re = 0;
        int i = x;
        for(;i <= right;){
            int ad = a[i];
            i += ad;
            re++;
            
        }
        t = re;
        to = i;
    //printf("需要%d步跳出,跳到%d
    ",t,to); 
        
    } 
    
    inline int Max(int a,int b){
        if(a>b) return a;
        return b;
        
    }
    
    int main()
    {
        //freopen("testdata.in","r",stdin);
        //freopen("testdata.out","w",stdout);
        scanf("%d",&n);
        for(int i = 0;i < n; i++){
            scanf("%d",&a[i]);
            
        }    
        
        sqr = Max(sqrt(n),1);
        
        /*for(int i = 0;i < n; i++){
            if(i%sqr == 0%sqr) {
                cnt++;
                Left[cnt-1] = i;
    //printf("第%d个块左边是%d
    ",cnt-1,i);
            }
            Right[cnt-1] = i;
            t[i].from = cnt-1;
    //printf("右边界:%d
    ",sqr*cnt-1);        
            findT(i,sqr*cnt-1,t[i].t,t[i].to);
            
        }*/
        
        for(int i = 0;i < n; i++){
            if(i%sqr == 0%sqr){
                Left[++cnt] = i;
                
            }
            t[i].from = cnt;
            Right[cnt] = i;
        }
        
        for(int i = n-1;i >= 0;i--){
            t[i].to = i+a[i];
            if(t[i].to >= Right[t[i].from]) t[i].t = 1;
            else t[i].t=t[t[i].to].t+1,t[i].to=t[t[i].to].to;
    //printf("%d
    ",t[i].to);
        }
    /*
    for(int i = 0;i < n; i++){
        printf("第%d个元素:跳%d次跳出此块(%d~%d)到%d,属于第%d个块
    ",i,t[i].t,Left[t[i].from],Right[t[i].from],t[i].to,t[i].from); 
    }*/
        scanf("%d",&m);
        for(int i = 0;i < m; i++){
            scanf("%d%d",&I,&J);
            int k;
            if(I == 2){
                
                
                scanf("%d",&k);
                a[J] = k;
                for(int j = Right[t[J].from];j >= Left[t[J].from]; j--){
                    t[j].to = j+a[j];
                    if(t[j].to >= Right[t[J].from]) t[j].t = 1;
                    else t[j].t=t[t[j].to].t+1,t[j].to=t[t[j].to].to;
                    
                }
            }
            else{
                int ans = 0;
                while(J < n){
                    ans += t[J].t;
                    J = t[J].to;
                    
                }
                printf("%d
    ",ans);
                
            }
            
            
        }
        
        
        return 0;
    }
    P3203

    利用分块思想,每一个元素维护跳几次跳出这个块以及跳到哪里。

    这样,就可以大大提高查询的速度,但是问题在于如何O(n)的预处理。

    预处理:从后往前枚举,就好像穿起一条链。比如:

    设t[i]为需要跳t[i]次跳出某个块,to[i]为跳出某个块到to[i]。

    假设这个序列为:

    ... 1 1

    第n个元素,处于第x块,则需要1次跳出块到n+a[i]。(t[i] = 1,to[i] = 4)

    第n-1个元素,处于第x块(假设处于同一个块),则需要2次跳出块到4。(t[i] = t[to[i]]+1 = 2,to[i] = to[to[i]],相信不难理解)

    以此类推。

    这样,只要在修改的时候对于修改元素所在的块进行我们写好的预处理。

  • 相关阅读:
    伯努利数学习笔记
    贝尔数学习笔记
    LuoguP5075 [JSOI2012]分零食
    LuoguP5748 集合划分计数
    LuoguP3338 [ZJOI2014]力
    LuoguP5488 差分与前缀和
    BZOJ4833 [Lydsy1704月赛]最小公倍佩尔数
    FFT&NTT学习笔记
    csp2019游记
    与图论的邂逅09:树上启发式合并
  • 原文地址:https://www.cnblogs.com/dudujerry/p/10337181.html
Copyright © 2020-2023  润新知