码迷,mamicode.com
首页 > 其他好文 > 详细

数列分块入门

时间:2018-10-17 14:43:47      阅读:295      评论:0      收藏:0      [点我收藏+]

标签:lse   \n   有趣的   加法   产生   memset   ble   ref   前置   

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

分块的核心思想是把要操作的数列 \(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;
}

数列分块入门

标签:lse   \n   有趣的   加法   产生   memset   ble   ref   前置   

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

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!