数列分块入门

分块是 莫队 算法的前置知识,也是一种十分 暴力 的数据结构。

分块的核心思想是把要操作的数列 \(a_i\) 分成若干长度相等的“块”;修改/查询时对于整一块都在指定区间 \([L,R]\) 内的块整体修改/查询,对于只有块的一部分在指定区间内的暴力修改/查询。

由于不需要操作/查询具有 区间加法 等性质,分块比线段树、树状数组、ST表等数据结构具有更加灵活的应用。



先来看一道例题 数列分块入门 4,简而言之,就是要求实现区间加法&区间查询;线段树可以很轻松地实现这两个操作,但是我们尝试使用分块:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

const int maxn = 50007;
int n, a[maxn], bl[maxn], blo;
int sum[307], atag[307];

inline void add(int l, int r, int val){ //区间加法
    for (int i = bl[l] + 1; i <= bl[r] - 1; ++i) //处理完整的块
        atag[i] += val;
    for (int i = l; i <= min(bl[l] * blo, r); ++i) //处理第一块
        a[i] += val, sum[bl[l]] += val;
    if (bl[l]!=bl[r])
        for (int i = (bl[r]-1) * blo + 1; i <= r; ++i) //处理最后一块
            a[i] += val, sum[bl[r]] += val;
}

inline long long query(int l, int r){ //区间查询,同区间加法
    long long res = 0;
    for (int i = bl[l] + 1; i <= bl[r] - 1; ++i)
        res = res + sum[i] + (long long)atag[i] * blo;
    for (int i = l; i <= min(bl[l] * blo, r); ++i)
        res = res + a[i] + atag[bl[l]];
    if (bl[l]!=bl[r])
        for (int i = (bl[r]-1) * blo + 1; i <= r; ++i)
            res = res + a[i] + atag[bl[r]];
    return res;
}

int main(){
    scanf("%d", &n);
    blo = sqrt(n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]), bl[i] = (i-1) / blo + 1, sum[bl[i]] += a[i];

    for (int i = 1; i <= n; ++i){
        int opt, l, r, c;
        scanf("%d%d%d%d", &opt, &l, &r, &c);
        if (opt==0) add(l,r,c);
        else printf("%d\n", query(l,r)%(c+1));
    }
    return 0;
}

addquery 操作分别是 区间加法区间查询,从这份代码可以看出分块操作的一般思路:

  1. 处理整块
  2. 处理非完整块

需要注意的是,处理非完整块和完整块的时候都需要注意处理标记,这和线段树是一样的;对于那些不便合并的标记,修改非完整块时可以先把标记作用到每个元素上再修改单个元素,这样处理起来会更加方便。



再来看一道有趣的题:数列分块入门8,简而言之,就是给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)。

查询一个区间内有多少个元素等于 \(c\) 这个操作并不容易实现,我刚开始想到的方法是分块后对于每个块内排序,这样就可以通过二分实现查询某个元素的个数。这样时间复杂度是 \(O(n \sqrt n log n)\). 会超时,不够优秀。

然后就有一个大(暴)胆(力)的算法,每次查询某个元素的个数的时候不用二分,而是直接暴力扫描,这样也不用排序,成功去掉了 \(O(logn)\),但是会有一个最大 \(O(n)\) 的额外时间复杂度。当然,如果一个块内都是同一个元素,就不用暴力扫描了。

但真的会每次有 \(O(n)\) 的额外时间复杂度吗?

我们可以把初始的数列看成全部都是同一个元素 + 暴力修改 \(n\) 个点,由于每次暴力修改的都是一段区间,最多会产生 \(2\) 个块内有不止一个元素。这样每次修改的额外花费就是 \(O(\sqrt n)\),暴力扫描的时间复杂度被摊还分析掉了。所以总时间复杂度就是 \(O(n \sqrt n)\)。

利用相似的证明,可以得到线段树暴力修改也能得到正确的复杂度。

线段树暴力修改也是线段树的常用技巧,通常可以通过证明暴力修改的次数有限来确保时间复杂度。

(分块实现)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

const int MAXN = 100007;

int n, blo, a[MAXN], bl[MAXN], tag[407];

void reset(int id){
    if (tag[id]!=-1){
        for (int i = (id-1)*blo+1; i <= min(id*blo,n); ++i)
            a[i] = tag[id];
        tag[id] = -1;
    }
}

inline int read(){
    int val = 0; bool f = false; char ch = getchar();
    while ((ch < '0' || ch > '9') && (ch != '-')) ch = getchar();
    if (ch == '-') f = true, ch = getchar();
    while (ch >= '0' && ch <= '9')
        val = (val<<3) + (val<<1) + ch - '0', ch = getchar();
    return f ? (-val) : val;
}

int solve(int l, int r, int c){
    int ans = 0;
    reset(bl[l]);
    for (int i = l; i <= min(bl[l] * blo, r); ++i)
        if (a[i] == c) ++ans;
        else a[i] = c;

    for (int i = bl[l] + 1; i < bl[r]; ++i)
        if (tag[i] != -1){
            if (tag[i] == c) ans += blo;
            else tag[i] = c;
        }else{
            for (int j = (i-1)*blo+1; j <= i * blo; ++j)
                if (a[j] == c) ++ans;
                else a[j] = c;
            tag[i] = c;
        }

    if (bl[l] != bl[r]){
        reset(bl[r]);
        for (int i = (bl[r]-1)*blo+1; i <= r; ++i)
            if (a[i] == c) ++ans;
            else a[i] = c;
    }
    return ans;
}

int main(){
    memset(tag, -1, sizeof(tag));
    n = read(); blo = sqrt(n);
    for (int i = 1; i <= n; ++i) a[i] = read();
    for (int i = 1; i <= n; ++i) bl[i] = (i-1)/blo + 1;
    for (int i = 1; i <= n; ++i){
        int l = read(), r = read(), c = read();
        printf("%d\n", solve(l, r, c));
    }
    return 0;
}

(线段树实现)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

const int MAXN = 100007;

int n, a[MAXN], tag[MAXN<<2];

inline void PushUp(int rt){
    tag[rt] = (tag[rt<<1] == tag[rt<<1|1]) ? tag[rt<<1] : -1;
}

inline void PushDown(int rt){
    if (tag[rt]==-1) return;
    tag[rt<<1] = tag[rt];
    tag[rt<<1|1] = tag[rt];
}

void build(int rt, int l, int r){
    if (l == r){
        tag[rt] = a[l];
        return;
    }
    int m = (l + r) >> 1;
    build(rt<<1, l, m);
    build(rt<<1|1, m+1, r);
    PushUp(rt);
}

int solve(int rt, int l, int r, int L, int R, int c){
    if (L <= l && r <= R && tag[rt] != -1){
        if (tag[rt] == c) return (r-l+1);
        else {tag[rt]=c; return 0;}
    }
    int m = l + r >> 1, ans = 0;
    PushDown(rt);
    if (L <= m) ans += solve(rt<<1, l, m, L, R, c);
    if (R > m) ans += solve(rt<<1|1, m+1, r, L, R, c);
    PushUp(rt);
    return ans;
}

inline int read(){
    int val = 0; bool f = false; char ch = getchar();
    while ((ch < '0' || ch > '9') && (ch != '-')) ch = getchar();
    if (ch == '-') f = true, ch = getchar();
    while (ch >= '0' && ch <= '9')
        val = (val<<3) + (val<<1) + ch - '0', ch = getchar();
    return f ? (-val) : val;
}

int main(){
    n = read();
    for (int i = 1; i <= n; ++i) a[i] = read();
    build(1, 1, n);
    for (int i = 1; i <= n; ++i){
        int l = read(), r = read(), c = read();
        printf("%d\n", solve(1, 1, n, l, r, c));
    }
    return 0;
}

原文地址:https://www.cnblogs.com/YJZoier/p/9803176.html

时间: 2024-11-09 20:39:55

数列分块入门的相关文章

loj 6278 6279 数列分块入门 2 3

参考:「分块」数列分块入门1 – 9 by hzwer 2 Description 给出一个长为\(n\)的数列,以及\(n\)个操作,操作涉及区间加法,询问区间内小于某个值\(x\)的元素个数. 思路 每个块内保持升序排列. 则块外暴力统计,块内二分查找分界点. 一些注意点,如: 要记录下标: 块外暴力修改完之后需要再排序: 在块内二分查找的值是\(c-tag[i]\)而非\(c\). Code #include <bits/stdc++.h> #define maxn 50010 #def

loj 6277 6280 数列分块入门 1 4

参考:「分块」数列分块入门1 – 9 by hzwer 1 Description 给出一个长为\(n\)的数列,以及\(n\)个操作,操作涉及区间加法,单点查值. 思路 用\(tag\)记录每个块整体的增量. Code #include <bits/stdc++.h> #define maxn 50010 #define F(i, a, b) for (int i = (a); i < (b); ++i) #define F2(i, a, b) for (int i = (a); i

LOJ#6284. 数列分块入门 8

#6284. 数列分块入门 8 内存限制:256 MiB时间限制:500 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: hzwer 提交提交记录统计讨论 1 测试数据 题目描述 给出一个长为 nnn 的数列,以及 nnn 个操作,操作涉及区间询问等于一个数 ccc 的元素,并将这个区间的所有元素改为 ccc. 输入格式 第一行输入一个数字 nnn. 第二行输入 nnn 个数字,第 i 个数字为 aia_ia?i??,以空格隔开. 接下来输入 nnn 行询问,每行输入三个数字 ll

LOJ6277~6285 数列分块入门

Portals 分块需注意的问题 数组大小应为\(N+\sqrt N\),因为最后一个块可能会超出\(N\)的范围.改成记录\(blk,fr,to\)就不用担心这个了 当操作的区间在一个块内时,要特判成暴力修改. 要清楚什么时候应该+tag[t] 最后一个块是越界的,注意是否有影响 数列分块入门 1 给出一个长为\(n\)的数列,以及\(n\)个操作,操作涉及区间加法,单点查值. //数列分块入门 1 #include <cstdio> #include <cmath> inlin

数列分块入门5 解题报告

占坑QAQ 数列分块系列目录 数列分块入门1 数列分块入门2 数列分块入门3 数列分块入门4 数列分块入门5 <- 数列分块入门6 数列分块入门7 数列分块入门8 数列分块入门9 蒲公英 公主的朋友 原文地址:https://www.cnblogs.com/louhancheng/p/10051160.html

数列分块入门2 解题报告

题意概括 区间加法,区间询问小于一个数的个数. 正题 对于每个块,除原数组之外用一个vector来有序地存储所有数.当区间加时,对于每个完整块维护共同加数,对于不完整的块直接暴力加上再重新排序.当询问时,对于每个完整块在vector中二分,对于不完整的,直接暴力计数. 代码 #include<cstdio> #include<vector> #include<algorithm> #include<cmath> using namespace std; #d

数列分块入门1-9 LibreOJ

数列分块入门1-9 LibreOJ 我也不知道为什么一个大二的ACM选手没学分块. 我怎么记得大一的时候,学长教给我的分块就只有 block 和 num 两个变量来着...好吧,应该是我没认真学.正好前两天朋友给学弟开课,乘机去蹭了一节课.然后...我还是不会哇,菜的一逼塌糊涂. 还是卿学姐好哇,多听几遍,睡得贼香. 分块原理 分块嘛,其实就是优雅的暴力,和莫队(不会)有点异曲同工的赶脚.通过将数组分成小块以降低复杂度. 通常情况下: 每个块的大小(block)为 \(\sqrt{n}\) 块数

LibreOJ6279. 数列分块入门 3 题解

题目链接:https://loj.ac/problem/6279 题目描述 给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的前驱(比其小的最大元素). 输入格式 第一行输入一个数字 \(n\). 第二行输入 \(n\) 个数字,第 \(i\) 个数字为 \(a_i\),以空格隔开. 接下来输入 \(n\) 行询问,每行输入四个数字 \(opt\).\(l\).\(r\).\(c\),以空格隔开. 若 \(opt=0\),表示将位于

Loj 6279. 数列分块入门 3

题目描述 给出一个长为 nnn 的数列,以及 nnn 个操作,操作涉及区间加法,询问区间内小于某个值 xxx 的前驱(比其小的最大元素). 输入格式 第一行输入一个数字 nnn. 第二行输入 nnn 个数字,第 iii 个数字为 aia_iai?,以空格隔开. 接下来输入 nnn 行询问,每行输入四个数字 opt\mathrm{opt}opt.lll.rrr.ccc,以空格隔开. 若 opt=0\mathrm{opt} = 0opt=0,表示将位于 [l,r][l, r][l,r] 的之间的数字