分块算法 常见题型学习

学习路线和内容来源于hzwer的博客

为了方便叙述和总结,对于hzwer大佬中的分块1~9题型我会进行重新排序。

题型1: 区间加法/区间开方/区间乘法/区间求和

对于这类分块问题,我们应该采取“大段预处理维护、局部暴力朴素”的思路!

分块入门1 区间加法 + 单点查询

整块Block修改:只需要维护一个add标记

局部小块修改:一个一个直接修改即可

单点查询:直接输出 Add + Num[Pos]即可

#include <bits/stdc++.h>

typedef long long ll;
const int MAXN = 50005;

//Block是每块的大小 Size是有多少块 
int n, Block, Size;
int Belong[MAXN], L[MAXN], R[MAXN];
ll Num[MAXN], Add[MAXN];

void Charge(int l,int r,ll Val){
    int X = Belong[l];
    int Y = Belong[r];
    if(X == Y){
        //如果属于同一块 直接暴力修改 
        for(int i = l;i <= r; i++) Num[i] += Val;
    }
    else{
        //先处理整块 
        for(int i = X + 1; i <= Y - 1; i++) Add[i] += Val;
        
        //再处理不完整的区域 
        for(int i = l; i <= R[X]; i++) Num[i] += Val;
        for(int i = r; i >= L[Y]; i--) Num[i] += Val;
    }
}

void preBlock(){
    Block = sqrt(n);
    Size = n / Block;
    if(n % Block != 0) Size++;
    //L[i]是第i块的左端点 R[i]是第i块的右端点 
    for(int i = 1; i <= Size; i++){
        L[i] = (i - 1) * Block + 1;
        R[i] = i * Block;
    }    
    R[Size] =  n;
    //Belong[i] 是 判断数字i属于哪一块 
    for(int i = 1; i <= n; i++) Belong[i] = (i - 1) / Block + 1;
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &Num[i]);
    preBlock();
    while(n--){
        int opt, l, r, c;
        scanf("%d %d %d %d", &opt, &l, &r, &c);
        if(opt == 0) Charge(l, r, c);
        else printf("%lld\n", Add[Belong[r]] + Num[r]);
    }
    return 0;
} 
Block 1

分块入门4 区间加法 + 区间求和

这道题目相比较上道题目,我们需要再多预处理一个块的区间和数组Sum,方便修改查询。

整块Block修改:维护一个Add标记

局部小块修改:一个一个直接修改,并同时对当前大块Block的区间和数组进行修改。

整块Block求和:直接用 Sum[i]数组 和 Add[i] * Block 求出答案。

局部小块求和:一个一个加和,并加上Add[i] * 当前块的影响长度。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>

typedef long long ll;
const int MAXN = 100005;

int n, m, Block, Size;
int Belong[MAXN], L[MAXN], R[MAXN];
ll Num[MAXN], Add[MAXN], Sum[MAXN];

void Charge(int l,int r,ll Val){
    int X = Belong[l];
    int Y = Belong[r];
    if(X == Y){
        for(int i = l;i <= r; i++) Num[i] += Val;
        Sum[X] += (r - l + 1) * Val;
    }
    else{
        for(int i = X + 1; i <= Y - 1; i++) Add[i] += Val;
        for(int i = l; i <= R[X]; i++) Num[i] += Val;
        Sum[X] += (R[X] - l + 1) * Val;
        for(int i = r; i >= L[Y]; i--) Num[i] += Val;
        Sum[Y] += (r - L[Y] + 1) * Val;
    }
}

ll Ask(int l,int r){
    int X = Belong[l];
    int Y = Belong[r];
    ll ans = 0;
    if(X == Y){
        for(int i = l; i <= r; i++) ans += Num[i];
        ans += Add[X] * (r - l + 1);
    }
    else{
        for(int i = X + 1; i <= Y - 1; i++) ans += (Sum[i] + Add[i] * Block);
        for(int i = l; i <= R[X]; i++) ans += Num[i];
        ans += Add[X] * (R[X] - l + 1);
        for(int i = r; i >= L[Y]; i--) ans += Num[i];
        ans += Add[Y] * (r - L[Y] + 1);
    }
    return ans;
}

void preBlock(){
    Block = sqrt(n);
    Size = n / Block;
    if(n % Block != 0) Size++;
    for(int i = 1; i <= Size; i++){
        L[i] = (i - 1) * Block + 1;
        R[i] = i * Block;
    }    
    R[Size] =  n;
    for(int i = 1; i <= n; i++){
        Belong[i] = (i - 1) / Block + 1;
        Sum[Belong[i]] += Num[i];
    }
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%lld", &Num[i]);
    preBlock();
    while(n--){
        ll opt, l, r, c;
        scanf("%lld%lld%lld%lld", &opt, &l, &r, &c);
        if(opt == 0) Charge(l, r, c);
        else printf("%lld\n", Ask(l, r) % (c + 1));
    }
    return 0;
} 
Block 4

分块入门5 区间开方 + 区间求和

本题要用到一个很重要的性质,就是一个数多次(最多五次)开方并向下取整之后一定会变成1.所以本题我们需要预处理块内数组Sum,并对整块进行判断是否全为1.

整块Block开方:先判断当前块是否都为1,如果是直接跳过,否则对于块内每个数都进行开方操作并更新区间和Sum.

局部小块开方:直接暴力开方,并同时修改当前块的区间和Block[i]

整块Block求和: 直接累加每个块的区间和Sum.

局部小块求和:直接暴力计算

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>

typedef long long ll;
const int MAXN = 50005;

int n, m, Block, Size;
int Belong[MAXN], L[MAXN], R[MAXN];
ll Num[MAXN], Sum[MAXN];
bool not_cal[MAXN];

void Judge(int Pos){
    if(not_cal[Pos] == true) return;
    not_cal[Pos] = 1;
    Sum[Pos] = 0;
    for(int i = L[Pos]; i <= R[Pos]; i++){
        Num[i] = sqrt(Num[i]);
        Sum[Pos] += Num[i];
        if(Num[i] > 1) not_cal[Pos] = 0;
    }
}

void sectionSqrt(int l,int r){
    int X = Belong[l];
    int Y = Belong[r];
    if(X == Y){
        for(int i = l;i <= r; i++){
            Sum[X] -= Num[i];
            Num[i] = sqrt(Num[i]);
            Sum[X] += Num[i];
        }
    }
    else{
        for(int i = l; i <= R[X]; i++){
            Sum[X] -= Num[i];
            Num[i] = sqrt(Num[i]);
            Sum[X] += Num[i];
        }
        for(int i = r; i >= L[Y]; i--){
            Sum[Y] -= Num[i];
            Num[i] = sqrt(Num[i]);
            Sum[Y] += Num[i];
        }
        for(int i = X + 1; i <= Y - 1; i++) Judge(i);
    }
}

ll Ask(int l,int r){
    int X = Belong[l];
    int Y = Belong[r];
    ll ans = 0;
    if(X == Y) for(int i = l; i <= r; i++) ans += Num[i];
    else{
        for(int i = l; i <= R[X]; i++) ans += Num[i];
        for(int i = r; i >= L[Y]; i--) ans += Num[i];
        for(int i = X + 1; i <= Y - 1; i++) ans += Sum[i];
    }
    return ans;
}

void preBlock(){
    Block = sqrt(n);
    Size = n / Block;
    if(n % Block != 0) Size++;
    for(int i = 1; i <= Size; i++){
        L[i] = (i - 1) * Block + 1;
        R[i] = i * Block;
    }    
    R[Size] =  n;
    for(int i = 1; i <= n; i++){
        Belong[i] = (i - 1) / Block + 1;
        Sum[Belong[i]] += Num[i];
    }
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%lld", &Num[i]);
    preBlock();
    while(n--){
        ll opt, l, r, c;
        scanf("%lld%lld%lld%lld", &opt, &l, &r, &c);
        if(opt == 0) sectionSqrt(l, r);
        else printf("%lld\n", Ask(l, r));
    }
    return 0;
} 
Blcok 5

分块入门7 区间乘法 + 区间加法 + 单点查询

 很容易想到本题需要预处理区间乘法标记Mul 和 区间加法标记Add.但本题主要的难点在于 乘法比加法的优先级要更高,所以很多操作实现细节需要特别注意。

首先是在处理局部问题的时候 需要将标记直接下放,因为如果不这么做的话。 假设对于当前数 Num[Pos] 加上一个数 Val,那么本来这个数应该 Num[Pos] * Mul * Val + Add * Val 但是会计算成 (Num[Pos] + Val ) * Mul + Add。

第二点是当出现 整块先加Val1后乘Val2的情况时,需要用Val2去乘上加法标记。 因为对于当前区间的数 Num,在进行(Num + Val1) * Val2的操作后,每个数都变成了 Num * Val2 + Val1* Val2。

整块Block乘法/加法: 加法直接加,乘法需要特殊处理一下

局部乘法/加法: 先把标记下放后,再乘/加

单点查询: 对于当前数只需要 ( Num[i] * Mul[belong[i]] + Add[belong[i]] ) % MOD 即可。

#include <bits/stdc++.h>

typedef long long ll;
const int MAXN = 100005;
const ll MOD = 10007;

int n, Block, Size;
int Belong[MAXN], L[MAXN], R[MAXN];
ll Num[MAXN], Add[MAXN], Mul[MAXN];

inline ll Read()
{
    ll x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}

void updateBlock(int Pos){
    for(int i = L[Pos];i <= R[Pos]; i++){
        Num[i] = (Num[i] * Mul[Pos] + Add[Pos]) % MOD;
    }
    Mul[Pos] = 1;
    Add[Pos] = 0;
}

void sectionCharge(int Opt, int l, int r, ll Val){
    int X = Belong[l];
    int Y = Belong[r];
    if(X == Y){
        updateBlock(X);
        for(int i = l;i <= r; i++){
            if(Opt == 0) Num[i] += Val;
            else Num[i] *= Val;
            Num[i] %= MOD;
        }
    }
    else{        
        updateBlock(X);
        for(int i = l; i <= R[X]; i++){
            if(Opt == 0) Num[i] += Val;
            else Num[i] *= Val;
            Num[i] %= MOD;
        }
        updateBlock(Y);
        for(int i = r; i >= L[Y]; i--){
            if(Opt == 0) Num[i] += Val;
            else Num[i] *= Val;
            Num[i] %= MOD;
        }
        for(int i = X + 1; i <= Y - 1; i++){
            if(Opt == 0) Add[i] = (Add[i] + Val) % MOD;
            else{
                Mul[i] = (Mul[i] * Val) % MOD;
                Add[i] = (Add[i] * Val) % MOD;
            }
        }
    }
}


void preBlock(){
    Block = sqrt(n);
    Size = n / Block;
    if(n % Block != 0) Size++;
    for(int i = 1; i <= Size; i++){
        L[i] = (i - 1) * Block + 1;
        R[i] = i * Block;
    }    
    R[Size] =  n;
    for(int i = 1; i <= n; i++){
        Belong[i] = (i - 1) / Block + 1;
        Mul[i] = 1;
    }
}

int main(){
    //freopen("a1.in","r",stdin);
    //freopen("my.out", "w", stdout);
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        Num[i] = Read();
        Num[i] %= MOD;
    }
    preBlock();
    while(n--){
        ll opt, l, r, c;
        opt = Read();
        l = Read();
        r = Read();
        c = Read();
        if(opt == 2) printf("%lld\n",(Num[r] * Mul[Belong[r]] + Add[Belong[r]]) % MOD);
        else sectionCharge(opt, l, r, c);
    }
    return 0;
} 
Block 7

题型2: 查询区间前驱 / 查询区间特定数个数

分块入门2 区间加法 +  询问区间小于x的数的个数

为了能在整块Block中快速查询小于x的数我们可以保证每块Block块内的有序性,从而快速二分查找。如果你很想当然地以为在每次区间加法操作后直接sort排序就行了,那么就会有问题!!! 因为排序后你打乱了原序列的顺序,导致你影响了之后的区间加和操作。所以这就要求我们多开一个vector数组存储并维护它的块内有序性,而每次加法操作都是在原序列进行。

整块Block加法:维护Add标记即可

局部加法: 一个一个暴力加和,但是加完记得排序保证其块内有序性。

整块Block查询: 直接二分查找

局部查询: 一个一个判断即可

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 50005;

int n, Block, Size;
int Belong[MAXN], L[MAXN], R[MAXN];
ll Num[MAXN], Add[MAXN];
vector<int>Re[2333];

void ReSort(int Pos){
    Re[Pos].clear();
    for(int i = L[Pos]; i <= R[Pos]; i++) Re[Pos].push_back(Num[i]);
    sort(Re[Pos].begin(), Re[Pos].end());
    /*
    printf("Debug Time:");
    for(auto Out : Re[Pos]) printf("%d ", Out);
    printf("\n");
    */
}

void Charge(int l,int r,ll Val){
    int X = Belong[l];
    int Y = Belong[r];
    if(X == Y){
        for(int i = l;i <= r; i++) Num[i] += Val;
        ReSort(X);
    }
    else{
        for(int i = X + 1; i <= Y - 1; i++) Add[i] += Val;
        for(int i = l; i <= R[X]; i++) Num[i] += Val;
        for(int i = r; i >= L[Y]; i--) Num[i] += Val;
        ReSort(X);
        ReSort(Y);
    }
}

ll ask(int l,int r,ll Val){
    int X = Belong[l];
    int Y = Belong[r];
    ll ans = 0;
    if(X == Y){
        for(int i = l;i <= r; i++) if(Num[i] < Val - Add[X]) ans++;
    }
    else{
        for(int i = X + 1; i <= Y - 1; i++) ans += lower_bound(Re[i].begin(), Re[i].end(), Val - Add[i]) - Re[i].begin();
        for(int i = l; i <= R[X]; i++) if(Num[i] < Val - Add[X]) ans++;
        for(int i = r; i >= L[Y]; i--) if(Num[i] < Val - Add[Y]) ans++;
    }
    return ans;
}

void preBlock(){
    //sort(Num + 1, Num + 1 + n);
    Block = sqrt(n);
    Size = n / Block;
    if(n % Block != 0) Size++;
    for(int i = 1; i <= Size; i++){
        L[i] = (i - 1) * Block + 1;
        R[i] = i * Block;
    }    
    R[Size] =  n;
    for(int i = 1; i <= n; i++) Belong[i] = (i - 1) / Block + 1;
    for(int i = 1; i <= Size; i++) ReSort(i);
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%lld", &Num[i]);
    preBlock();
    while(n--){
        ll opt, l, r, c;
        scanf("%lld %lld %lld %lld", &opt, &l, &r, &c);
        if(opt == 0) Charge(l, r, c);
        else printf("%lld\n", ask(l, r, c * c));
    }
    return 0;
} 
Blcok 2

分块入门3 区间加法 + 询问区间内某个数的前驱

待更

猜你喜欢

转载自www.cnblogs.com/LYFer233/p/12702535.html