• 「笔记」分块


    分块思想

    引用一下 oi-wiki 的话:

    分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。

    一些有意思的暴论:

    分块就是分治!

    没有什么错误,考虑分治思想的抽象过程:

    1. 分解问题,对应对原数据的划分。
    2. 解决问题,对应查询预处理的信息。
    3. 合并贡献,对应将得到的信息合并。

    分块完全与上述过程相符合,它实际上就是一种分治思想的具体应用。
    特别地,对于不在完整块中的元素,需要暴力统计它们的贡献。

    “线段树也是分块!” :

    线段树实质上也是通过对数据的划分,并预处理每块数据的信息。
    但线段树可以多次划分数据,直至数据规模为 (1),这点与分块只划分一次有所不同。

    线段树与分块都是分治思想的具体应用,因此上面这句话有一定道理。
    但过于绝对,于是归到暴论里。

    “数组也是分块!”

    可看做块大小为 1 的分块。
    从形式上来说,完全符合分块的定义,但分块后并没有降低复杂度,严格上说并不能叫分块。

    “int 型变量也是分块!”

    显然,能提出这么深刻道理的万古神犇,实在是太厉害了!
    我专程写博客来膜拜它!

    数列分块

    引入

    Loj6280. 数列分块入门 4

    给定一长度为 (n) 的数列,有 (n) 次操作。
    操作分为两种:区间加,查询区间和。
    (1le nle 5 imes 10^4)

    划分

    首先将序列按每 (T) 个元素一块进行分块,并记录每块的区间和。
    (T) 的大小需要根据实际要求选择,下文复杂度分析部分会详细解析。
    划分过程的常用模板:

    void PrepareBlock() {
      block_size = T; //根据实际要求选择一个合适的大小。
      block_num = n / block_size; 
      for (int i = 1; i <= block_num; ++ i) { //分配块左右边界。
        L[i] = (i - 1) * block_size + 1;
        R[i] = i * block_size;
      }
      if (R[block_num] < n) { //最后的一个较小的块。
        ++ block_num;
        L[block_num] = R[block_num - 1] + 1;
        R[block_num] = n;
      }
      //分配元素所属的块编号。
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = L[i]; j <= R[i]; ++ j) {
          bel[j] = i;
        }
      }
    }
    

    查询

    设查询区间 ([l,r]) 的区间和。

    • (l,r) 在同一块内,直接暴力求和即可。块长为 (T),单次查询复杂度上界 (O(T))

    • 否则答案由三部分构成:

      1. (l) 开头的不完整块的和。
      2. (r) 结尾的不完整块的和。
      3. 中间的完整块的和。

      对于 1、2 两部分,可暴力求和,复杂度上界 (O(T))
      对于 3,直接查询预处理的完整块和即可,复杂度上界 (O(frac{n}{T}))(查询所有的完整块)。
      单次查询总复杂度上界 (O(frac{n}{T}+T))

    修改

    修改操作的过程与查询操作相同,设修改区间为 ([l,r])

    • (l,r) 在同一块内,直接暴力修改,并更新区间和。块长为 (T),单次修改复杂度上界 (O(T))

    • 否则将修改区间划分成三部分:

      1. (l) 开头的不完整块。
      2. (r) 结尾的不完整块。
      3. 中间的完整块。

      1、2 直接暴力修改序列,3 给完整的块打上区间加的标记。
      单次修改复杂度上界 (O(frac{n}{T} +T))

    复杂度分析

    总复杂度为 (O(n imes (frac{n}{T} + T)))
    由均值不等式,(frac{n}{T} + Tge 2sqrt{frac{n}{T} imes T} = sqrt{n}),当且仅当 (frac{n}{T} = T) 时等号成立,复杂度最低。
    此时块大小为 (sqrt{n}),总复杂度 (O(nsqrt{n}))

    代码

    上古时期出土代码,经现代手段修复后勉强能看。

    //知识点:分块
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #define LL long long
    const int kN = 5e4 + 10;
    //=============================================================
    int n, m;
    int block_size, block_num, L[kN], R[kN], bel[kN];
    LL a[kN], sum[kN], tag[kN];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void Chkmin(LL &fir, LL sec) {
      if (sec < fir) fir = sec;
    }
    void PrepareBlock() {
      block_size = sqrt(n); //根据实际要求选择一个合适的大小。
      block_num = n / block_size; 
      for (int i = 1; i <= block_num; ++ i) { //分配块左右边界。
        L[i] = (i - 1) * block_size + 1;
        R[i] = i * block_size;
      }
      if (R[block_num] < n) { //最后的一个较小的块。
        ++ block_num;
        L[block_num] = R[block_num - 1] + 1;
        R[block_num] = n;
      }
      //分配元素所属的块编号。
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = L[i]; j <= R[i]; ++ j) {
          bel[j] = i;
          sum[i] += a[j]; //预处理区间和
        }
      }
    }
    void Modify(int L_, int R_, int val_) {
      int bell = bel[L_], belr = bel[R_];
      if (bell == belr) {
        for (int i = L_; i <= R_; ++ i) {
          a[i] += val_;
          sum[bell] += val_;
        }
        return ;
      }
      for (int i = bell + 1; i <= belr - 1; ++ i) tag[i] += val_;
      for (int i = L_; i <= R[bell]; ++ i) {
        a[i] += val_;
        sum[bell] += val_;
      }
      for (int i = L[belr]; i <= R_; ++ i) {
        a[i] += val_;
        sum[belr] += val_;
      }
    }
    LL Query(int L_, int R_, int mod_) {
      int bell = bel[L_], belr = bel[R_], ret = 0;
      if (bell == belr) {
        for (int i = L_; i <= R_; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
        return ret;
      }
      for (int i = bell + 1; i <= belr - 1; ++ i) {
        ret = (ret + sum[i] + (R[i] - L[i] + 1) * tag[i] % mod_) % mod_;
      }
      for (int i = L_; i <= R[bell]; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
      for (int i = L[belr]; i <= R_; ++ i) ret = (ret + a[i] + tag[belr]) % mod_;
      return ret;
    }
    //=============================================================
    int main() { 
      n = read();
      for (int i = 1; i <= n; ++ i) a[i] = read();
      PrepareBlock();
      for (int i = 1; i <= n; ++ i) {
        int opt = read(), l = read(), r = read(), c = read();
        if (opt == 0) {
          Modify(l, r, c);
        } else {
          printf("%lld
    ", Query(l, r, c + 1));
        }
      }
      return 0; 
    }
    

    练习

    建议阅读:LibreOJ 数列分块入门 1 ~ ⑨
    包含下列 9 个经典题目,涉及了数列分块的大部分重要思想:

    1. 区间加,单点查询:基础模板。
    2. 区间加,区间查询小于给定值 元素数:维护单调性。
    3. 区间加,区间查询小于给定值 最大元素:维护单调性。
    4. 区间加,区间求和:基础模板,最简单的区间信息合并。
    5. 区间开方,区间求和:复杂度分析。
    6. 单点插入,单点查询:复杂度分析,块状数组。
    7. 区间加,区间乘,单点查询:较复杂的区间信息合并。
    8. 区间赋值,查询区间等于给定值元素数:复杂度分析。
    9. 查询区间最小众数:复杂区间信息的预处理,复杂的区间信息合并。

    均值法复杂度分析

    引入

    在上述例题区间和中,修改操作与查询操作的复杂度是平衡的,因此可以简单地将块大小设为 (sqrt{n})
    但在某些数列分块的题目中,会出现修改查询操作复杂度并不平衡的状况,此时需要通过计算得到最优的块大小。

    设数列长度为 (n),查询次数为 (m),单次修改复杂度 (O(p)),单次查询复杂度 (O(q))(p,q) 的大小均与块大小相关。
    总复杂度一般可以表示成这种形式:(O(np + mq))
    根据高中必修 1 的均值不等式,可知 (np + mqge 2sqrt{nmpq}),当且仅当 (np=mq) 时,等号成立,复杂度最低。
    即可解得最优的块大小。

    确定最优块大小

    [Ynoi]舌尖上的由乃

    给定一长度为 (n) 的数列,有 (n) 次操作。
    操作分为两种:区间加,查询区间第 (k) 大。
    (1le n,mle 10^5)

    先吐槽一句,这题原题是树上的:bzoj4867. [Ynoi2017]舌尖上的由乃,为方便讲解换成了序列,但树上版本核心做法与序列上相同。

    首先有个超级暴力的做法:
    区间加不影响区间内大小关系,考虑维护每块排序后的序列。
    修改时整块打标记,散块修改后重构排序后序列。
    查询时二分答案判定,散块暴力,整块内二分。

    设块大小为 (T),单次修改复杂度为 (O(frac{n}{T} + Tlog n)),单次查询复杂度为 (O((frac{n}{T}log n+T)log n))
    单次询问复杂度严格大于修改复杂度,则总复杂度为 (O(n(frac{n}{T}log^2 n +Tlog n)))
    由均值不等式,(frac{n}{T}log^2 n +Tlog nge 2sqrt{nlog n}log n),当且仅当 (frac{n}{T}log^2 n =Tlog n) 时等号成立,复杂度最低。

    此时块大小 (T = sqrt{nlog n}),总复杂度 (O(nsqrt{nlog n}log n))


    当然这个做法是过不去的,考虑更优秀的做法。

    发现修改操作时,重构散块不需要重新排序。
    将端点所在块分为两部分:需要被修改的,不需要被修改的。
    修改后,各部分内部分别有序,元素大小关系不变。
    考虑直接归并,单次修改复杂度变为 (O(frac{n}{T} + T))

    询问时,考虑将两个散块先归并成一个有序的数列,再进行二分。
    这样对散块部分的查询也可以二分,单次查询复杂度变为 (O(frac{n}{T}log^2 n + log^2 n + T))

    单次询问复杂度仍严格大于修改复杂度,则总复杂度为 (O(n(frac{n}{T}log^2 n +T)))
    由均值不等式,(frac{n}{T}log^2 n +Tge 2sqrt{n}log n),当且仅当 (frac{n}{T}log^2 n =T) 时等号成立,复杂度最低。

    此时块大小 (T = sqrt{n}log n),总复杂度 (O(nsqrt{n}log n))
    然后卡亿点常就可以过啦!

    莫队的复杂度

    设序列长度为 (n),询问次数为 (m),假设可以 (O(1)) 地处理端点的移动 和 回答询问。
    考虑莫队算法中对询问的排序过程:
    先按照左端点的块编号升序排序,左端点在同一块中的按右端点升序排序。
    设块大小为 (T),排序后将询问分为了 (frac{n}{T}) 块,每块内右端点单调递增。

    考虑每一块的右端点单调递增,移动的复杂度为 (O(n))
    右端点的垮块移动量上界为 (O(n)),则右端点移动的总复杂度为 (O(frac{n^2}{T}))
    对于块内的每一次询问,左端点移动量 (le T)
    垮块移动左端点的改变量为 (T),则左端点移动的总复杂度为 (O(mT))

    算法的总复杂度为 (O(frac{n^2}{T}+mT + m)),由均值不等式,(frac{n^2}{T}+mTge 2sqrt{n^2m}),当且仅当 (frac{n^2}{T}=mT) 时,复杂度最低。
    解得最优块大小为 (frac{n}{sqrt{m}}),算法总复杂度为 (O(nsqrt{m} + m))

    如果简单将块大小设为 (sqrt n),得算法总复杂度为 (O(nsqrt n + msqrt n + m))
    与上面得到的最优复杂度作差,得:

    [nsqrt{n} + msqrt{n} - nsqrt{m}= n(sqrt{n} + frac{m}{sqrt{n}} - sqrt{m}) ]

    考虑括号内的三项,由均值不等式,有:

    [sqrt{n} + dfrac{m}{sqrt{n}} -sqrt{m} ge 2sqrt{m} - sqrt{m} = sqrt{m} > 0 ]

    得块大小设为 (sqrt n) 劣于块大小为 (frac{n}{sqrt{m}})

    平衡结合

    引入

    考虑这样一个问题:维护数列,支持单点修改,区间求和。
    显然可以线段树,树状数组求解,它们查询修改的是平衡的,均为 (O(log n)) 级别。
    可以在修改,查询次数同级时获得较优的时间复杂度。

    但修改与查询次数可能是不平衡的,这时可以通过别的手段来处理该问题:
    考虑分块解法,修改 (O(1)),查询 (O(sqrt{n})) ,在修改数大于查询数时,可以获得更优的时间复杂度。
    若查询次数远大于修改次数,甚至可以考虑直接维护前缀和,修改 (O(n)),但查询的复杂度仅为 (O(1))

    这就是一种平衡结合。
    根据修改与查询次数的关系,通过调整维护的手段,使总的复杂度达到更低级别。

    例一

    bzoj3809. Gty的二逼妹子序列

    给定一长度为 (n) 的数列 (a)(m) 次询问。
    每次询问给定参数 (l,r,a,b),求区间 ([l,r]) 内权值 (in [a,b]) 的数的种类数。
    (1le a_ile nle 10^5)(1le mle 10^6)

    发现莫队比较便于维护种类数,套一个莫队消去区间的限制。
    考虑值域的限制,权值线段树进行维护。
    单次 修改/查询 复杂度均为 (O(log n))
    设块大小为 (dfrac{n}{sqrt{m}}),总复杂度为 (O(nsqrt{m}log n + mlog n))

    发现修改和查询的次数并不平均,而线段树查询修改的复杂度是平均的。
    考虑能否替换成另一种 修改复杂度较小,查询复杂度较大的数据结构。
    想到直接使用数组进行O1修改On查询

    只有单点修改,考虑对值域分块,维护块内不同的数的个数,可在莫队左右端点移动顺便维护。
    查询时,在查询值域内的完整块直接统计答案,不完整块暴力查询。
    设块大小为 (dfrac{n}{sqrt{m}}),单次修改复杂度 (O(1)),查询复杂度 (sqrt{n}),总复杂度为 (O(nsqrt{m} +msqrt{n}))

    代码

    //知识点:莫队,分块 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #define ll long long
    const int kMaxn = 1e5 + 10;
    const int kMaxm = 1e6 + 10;
    const int kMaxSqrtn = 320 + 10;
    //=============================================================
    struct Que {
      int l, r, a, b, id;
    } q[kMaxm];
    int n, m, a[kMaxn];
    int block_size, block_num, L[kMaxn], R[kMaxn], bel[kMaxn];
    int nowl = 1, nowr, cnt[kMaxn], sum[kMaxSqrtn];
    int ans[kMaxm];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void GetMax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void GetMin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    bool CompareQuery(Que fir, Que sec) {
      if (bel[fir.l] != bel[sec.l]) return bel[fir.l] < bel[sec.l];
      return fir.r < sec.r;
    }
    void Prepare() {
      n = read(), m = read();
      for (int i = 1; i <= n; ++ i) a[i] = read();
      for (int i = 1; i <= m; ++ i) {
        q[i] = (Que) {read(), read(), read(), read(), i};
      }
      block_size = (int) sqrt(n), block_num = n / block_size;
      for (int i = 1; i <= block_num; ++ i) {
        L[i] = (i - 1) * block_size + 1;
        R[i] = i * block_size;
      }
      if (R[block_num] < n) {
        L[++ block_num] = R[block_num - 1] + 1;
        R[block_num] = n;
      } 
      
      for (int j = 1; j <= block_num; ++ j) {
        for (int i = L[j]; i <= R[j]; ++ i) {
          bel[i] = j;
        }
      }
      std :: sort(q + 1, q + m + 1, CompareQuery);
    }
    
    int Query(int l_, int r_) {
      int ret = 0;
      if (bel[l_] == bel[r_]) {
        for (int i = l_; i <= r_; ++ i) ret += (cnt[i] > 0);
        return ret;
      }
      for (int i = bel[l_] + 1; i <= bel[r_] - 1; ++ i) {
        ret += sum[i];
      }
      for (int i = l_; i <= R[bel[l_]]; ++ i) ret += (cnt[i] > 0);
      for (int i = L[bel[r_]]; i <= r_; ++ i) ret += (cnt[i] > 0);
      return ret;
    }
    void Delete(int now_) {
      cnt[a[now_]] --;
      if (! cnt[a[now_]]) sum[bel[a[now_]]] --;
    }
    void Add(int now_) {
      if (! cnt[a[now_]]) sum[bel[a[now_]]] ++;
      cnt[a[now_]] ++;
    }
    //=============================================================
    int main() {
      Prepare();
      for (int i = 1; i <= m; ++ i) {
        int l = q[i].l, r = q[i].r, a = q[i].a, b = q[i].b, id = q[i].id;
        while (nowl < l) Delete(nowl), nowl ++;
        while (nowl > l) nowl --, Add(nowl);
        while (nowr > r) Delete(nowr), nowr --;
        while (nowr < r) nowr ++, Add(nowr);
        ans[q[i].id] = Query(a, b);
      }
      for (int i = 1; i <= m; ++ i) printf("%d
    ", ans[i]);
      return 0;
    }
    

    例二

    可能是集训题的无出处题

    给定一长度为 (n) 的数列 (a),参数 (w)(q) 次询问。
    每次询问给定参数 (l,r),求忽略出现次数 (>w) 的数后,区间 ([l,r]) 内第 (k) 小值。
    (1le n,q,a_i,wle10^5)

    忽略出现次数 (>w) 的数,没法上主席树。
    但显然可以莫队套平衡树,当枚举的区间内某个数首次出现时插入,出现次数 (=0)(> w) 时删除。

    (n,a_i) 同级,修改和查询的复杂度相同,均为 (O(log n))
    块大小为 (T) 时,总复杂度为 (O((frac{n^2}{T}+qT)log n + qlog n))
    块大小为 (frac{n}{sqrt{q}}) 时最优,总复杂度为 (O(nsqrt{q}log n+ qlog n)),过不了。

    发现块大小为 (T) 时,莫队中一共有 (frac{n^2}{T}+qT) 次修改操作,但只有 (q) 次查询操作。
    考虑平衡结合,按照上述方法,使用分块维护区间第 (k) 小值。
    单次修改复杂度变为 (O(1)),查询变为 (O(sqrt{n})),总复杂度 (O(frac{n^2}{T}+qT + qsqrt{n}))
    块大小取 (frac{n}{sqrt{q}}) 时最优,总复杂度为 (O(nsqrt{q} + qsqrt{n}))

    代码见这个金牌爷的博客:WerKeyTom_FTD

    预处理之王

    引入

    字面意思,通过预处理,合并完整块的信息,从而避免在查询时对信息的合并,以降低时间复杂度。
    因为有些预处理手段十分神仙,处理对象复杂,处理方式神奇,而且为了总的时间复杂度不择手段。
    所以把这种处理手段称为 预处理之王

    例一

    简单的预处理

    「hackerrank」Range Modular Queries

    给定一长度为 (n) 的数列 (a)(q) 次询问。
    每次询问给定参数 (l,r,x,y),求:

    [sum_{i=l}^{r} [a_i mod x = y] ]

    (1le n,q,a_ile 5 imes 10^4)

    数据范围比较喜人,(n,q,a_i) 同级,先考虑暴力。
    考虑莫队搞掉区间限制,维护当前区间内各权值出现的次数。
    (cnt_{i}) 表示当前区间内权值 (i) 出现的次数,显然答案为:

    [sum_{k = 1}cnt_{y+kx} ]

    复杂度 (O( ext{Unknowen})),过不了。


    发现上述的算法查询复杂度与 (x) 有关。
    (x) 较小时,查询复杂度较高,可达到 (O(n)) 级别。(x) 较大时复杂度较优秀。
    考虑根号分治,对模数 (xle 200)(x > 200) 的询问分开考虑。

    对于 (xle 200) 的询问,考虑分块。
    设块大小为 (sqrt{n}),预处理 (f_{i,j,k}) 表示前 i 个块中,% j = k 的数的个数。
    预处理复杂度上界 (O(200^3) approx O(nsqrt{n}))
    询问时整块直接 (O(1)) 查询预处理的前缀和,散块暴力。
    单次查询复杂度 (O(sqrt{n}))

    对于 (x> 200) 的询问,套用上述莫队算法即可。
    单次查询复杂度上界为 (O(200) approx O(sqrt{n})) 级别。

    (n,q) 同级,总复杂度约为 (O(nsqrt{n})) 级别,可过。

    这同时也是一个平衡结合的例子,通过根号分治使时间复杂度达到平衡。

    代码

    //知识点:分块,莫队 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #define ll long long
    const int kMaxn = 4e4 + 10;
    const int kMaxSqrtn = 210;
    //=============================================================
    struct Query {
      int l, r, mod, val, id;
    } q[kMaxn];
    int n, m, qnum, maxa, a[kMaxn], ans[kMaxn];
    int block_size, block_num, sqrt_maxa, L[kMaxSqrtn], R[kMaxSqrtn], bel[kMaxn];
    int f[kMaxSqrtn][kMaxSqrtn][kMaxSqrtn]; //f[i][j][k]:前 i 个块,% j = k 的数的个数。 
    int nowl = 1, nowr, cnt[kMaxn]; //注意端点的初值 
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void Getmax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void Getmin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    void Debug() {
      printf("%d %d
    ***********
    ", block_size, block_num);
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = 1; j <= block_size; ++ j) {
          for (int k = 0; k < j; ++ k) {
            printf("1~%d mod %d = %d appear %d times
    ", i, j, k, f[i][j][k]);
          }
        }
      }
    }
    void PrepareBlock() {
      block_size = (int) sqrt(n);
      block_num = n / block_size;
      for (int i = 1; i <= block_num; ++ i) {
        L[i] = (i - 1) * block_size + 1;
        R[i] = i * block_size;
      }
      if (R[block_num] < n) {
        ++ block_num;
        L[block_num] = R[block_num - 1] + 1;
        R[block_num] = n;
      }
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = L[i]; j <= R[i]; ++ j) {
          bel[j] = i;
        }
      }
      
      sqrt_maxa = (int) sqrt(maxa);
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = 1; j <= sqrt_maxa; ++ j) {
          for (int k = L[i]; k <= R[i]; ++ k) {
            f[i][j][a[k] % j] ++;
          }
        }  
      }
      for (int i = 1; i <= block_num; ++ i) {
        for (int j = 1; j <= sqrt_maxa; ++ j) {
          for (int k = 0; k < j; ++ k) {
            f[i][j][k] += f[i - 1][j][k];
          }
        }
      }
    //  Debug();
    }
    bool CompareQuery(Query fir_, Query sec_) {
      if (bel[fir_.l] != bel[sec_.l]) return bel[fir_.l] < bel[sec_.l];
      return fir_.r < sec_.r;
    }
    void Solve(int l_, int r_, int mod_, int val_, int id_) {
      if (bel[l_] == bel[r_]) {
        for (int i = l_; i <= r_; ++ i) {
          ans[id_] += (a[i] % mod_ == val_);
        }
        return ;
      }
      int bell = bel[l_], belr = bel[r_];
      ans[id_] += f[belr - 1][mod_][val_] - f[bell][mod_][val_];
      for (int i = l_; i <= R[bell]; ++ i) {
        ans[id_] += (a[i] % mod_ == val_);
      }
      for (int i = L[belr]; i <= r_; ++ i) {
        ans[id_] += (a[i] % mod_ == val_);
      }
    }
    void Add(int pos_) {
      cnt[a[pos_]] ++;
    }
    void Del(int pos_) {
      cnt[a[pos_]] --;
    }
    //=============================================================
    int main() {
      n = read(), m = read();
      for (int i = 1; i <= n; ++ i) {
        a[i] = read();
        Getmax(maxa, a[i]); 
      }
      PrepareBlock();
      
      for (int i = 1; i <= m; ++ i) {
        int l = read() + 1, r = read() + 1;
        int mod = read(), val = read();
        if (mod <= sqrt_maxa) {
          Solve(l, r, mod, val, i);
        } else {
          q[++ qnum] = (Query) {l, r, mod, val, i};  
        }
      }
      
      std :: sort(q + 1, q + qnum + 1, CompareQuery);
      for (int i = 1; i <= qnum; ++ i) {
        int l = q[i].l, r = q[i].r, mod = q[i].mod, val = q[i].val;
        while (nowl > l) -- nowl, Add(nowl);
        while (nowr < r) ++ nowr, Add(nowr);
        while (nowl < l) Del(nowl), ++ nowl;
        while (nowr > r) Del(nowr), -- nowr;
        for (int j = val; j <= maxa; j += mod) {
          ans[q[i].id] += cnt[j];
        }
      }
      for (int i = 1; i <= m; ++ i) printf("%d
    ", ans[i]);
      return 0;
    }
    

    例二

    bzoj3744. Gty的妹子序列

    给定一长度为 (n) 的数列 (a)(m) 次查询操作。
    每次查询给定参数 (l,r),求区间 ([l,r]) 内的逆序对数。
    强制在线。
    (1le n,mle 5 imes 10^4)(1le a_ile 2^{31}-1)

    不强制在线是 CDQ 傻逼题。
    强制在线,没有什么方便的数据结构维护,考虑分块。

    这是一开始的想法:
    按块大小为 (sqrt{n}) 分块,树状数组暴力预处理每块的逆序对数,复杂度 (O(sqrt {n}sqrt {n}log sqrt{n}) = O(nlog sqrt{n}))
    考虑单次查询的过程,发现可拆分为下面几个部分:

    1. 整块内的逆序对数,已预处理得到,查询复杂度 (O(dfrac{n}{sqrt{n}}) = O(sqrt{n}))
    2. 散块内的逆序对数,树状数组暴力即可,复杂度 (O(sqrt{n}log sqrt{n}))
    3. 整块与整块间的逆序对。
    4. 散块与整块间的逆序对。

    考虑如何得到上述 3,4 两项的贡献。

    发现预处理整块之间的逆序对数时,需要遍历整个序列。
    太浪费了!考虑顺便预处理第 3 项。
    (f_{l,r}) 表示整块 (lsim r) 的逆序对数。
    对每个块都维护一个权值树状数组,在枚举到块 (j) 的某元素时,枚举之前块的树状数组。
    设枚举到块 (i),查询比该元素大的数的个数,即为当前元素 与 块 (i) 的逆序对数,累加到 (f_{j,i}) 中。
    这样做完后,(f_{i,j}) 存的只是块 (i) 和块 (j) 的逆序对数,DP 处理 (f),有转移:

    [f_{i,j} = f_{i,j} + f_{i,j-1} + sum_{k=i+1}^{j}{f_{k,j}} ]

    需要枚举每块的每一个数再枚举所有之前的块再在树状数组中查询。
    总复杂度为 (O(sqrt{n}sqrt{n}sqrt{n}logsqrt{n}) = O(nsqrt{n}logsqrt{n}))
    是不是很扯淡


    考虑第 4 项,由于散块较小,考虑直接枚举散块元素。
    对于右侧散块,即查询整块中比它大的数的个数。
    考虑主席树维护,左侧散块同理,查询比它小的数的个数即可。
    复杂度 (O(sqrt{n}log n))

    累计一下,总复杂度为 (O(nsqrt{n}logsqrt{n} +msqrt{n}log n))(n,m) 同级,为 (O(nsqrt{n}log n)) 级别,瓶颈似乎在主席树上。

    细节比较多,调调调过了几组手玩的数据,交上去 T 了。
    常数过大被卡了。


    发现预处理并不是复杂度瓶颈。
    且得到的信息并不丰富,仅求出了连续的块的逆序对,dp 数组的大小仅为 (O(sqrt{n}sqrt{n})= O(n)) 级别。
    而预处理过程中枚举到了所有元素,且求出了它与之前所有块的逆序对数,却把信息压缩成这样,这显然不大合适。

    考虑修改预处理对象,设 (f_{l,r}) 表示,从块 (l) 的开头,到 位置 (r) 的逆序对数。
    预处理时枚举所有块,从它的开头,一直扫到整个数列的尾部,暴力用树状数组求逆序对。
    复杂度 (O(nsqrt{n}log n))

    再考虑单次查询时的 4 部分:

    1. 整块内的逆序对数。
    2. 散块内的逆序对数。
    3. 整块与整块间的逆序对。
    4. 散块与整块间的逆序对。

    查询时,可直接得到第一个整块到查询区间右端点的逆序对数,第 1,3 项复杂度变为 (O(1)),且同时得到了右侧散块与它自身,与整块的逆序对数。

    发现仅剩左侧散块与它自身,与整块的逆序对了。
    考虑合并 2,4 两项,直接用主席树求。
    枚举左侧散块中所有数,查询从 该数到右端点 内,比它大的数的个数。
    复杂度 (O(sqrt{n}log n))

    总复杂度不变,仍为 (O(nsqrt{n}log n)),但通过增大预处理的复杂度,减小了查询的巨大常数。

    然后就可过了。

    还有 (O(nsqrt n)) 的预处理之王做法,通过归并来进行实现。
    感觉比较神,建议阅读题解学习,之后再补。

    代码

    //知识点:分块 
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cmath>
    #include <cstdlib>
    #include <cstdio>
    #include <cstring>
    #define ll long long
    const int kMaxm = 5e5;
    const int kMaxn = 5e5 + 10;
    const int kMaxSqrtn = 230 + 10;
    //=============================================================
    int n, m, block_size, block_num;
    int root[kMaxn];
    int maxa, map[kMaxn], a[kMaxn], data[kMaxn];
    int bel[kMaxn], L[kMaxSqrtn], R[kMaxSqrtn], f[kMaxSqrtn][kMaxn];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void GetMax(int &fir_, int sec_) {
      if (sec_ > fir_) fir_ = sec_;
    }
    void GetMin(int &fir_, int sec_) {
      if (sec_ < fir_) fir_ = sec_;
    }
    //ScientificConceptOfDevelopmentTree
    namespace HjtTree{
      #define ls (lson[now_])
      #define rs (rson[now_])
      #define mid (L_+R_>>1)
      int node_num, lson[kMaxn << 4], rson[kMaxn << 4], size[kMaxn << 4];
      void Insert(int &now_, int pre_, int L_, int R_, int val_) {
    	  now_ = ++ node_num;
    	  size[now_] = size[pre_] + 1;
    	  ls = lson[pre_], rs = rson[pre_];
    	  if (L_ == R_) return ;
    	  if (val_ <= mid) Insert(ls, lson[pre_], L_, mid, val_);
    	  else Insert(rs, rson[pre_], mid + 1, R_, val_);
      }
      int Query(int r_, int l_, int L_, int R_, int ql_, int qr_) {
    	  if (ql_ > qr_) return 0;
        if (ql_ <= L_ && R_ <= qr_) return size[r_] - size[l_];
    	  int ret = 0;
        if (ql_ <= mid) ret += Query(lson[r_], lson[l_], L_, mid, ql_, qr_);
    	  if (qr_ > mid) ret += Query(rson[r_], rson[l_], mid + 1, R_, ql_, qr_);
        return ret;
      }
    }
    struct TreeArray {
      #define lowbit(x) (x&-x)
      int time, ti[kMaxm + 10], t[kMaxm + 10];
      void Clear () {
        time ++;
      }
      void Add(int pos_, int val_) {
        for (; pos_ <= maxa; pos_ += lowbit(pos_)) {
          if (ti[pos_] != time) {
            t[pos_] = 0;
            ti[pos_] = time;
          }
          t[pos_] += val_;
        }
      }
      int Sum(int pos_) {
        int ret = 0;
        for (; pos_; pos_ -= lowbit(pos_)) {
          if (ti[pos_] != time) {
            t[pos_] = 0;
            ti[pos_] = time;
          }
          ret += t[pos_];
        }
        return ret;
      }
    } tmp;
    void Prepare() {
      n = read();
      for (int i = 1; i <= n; ++ i) {
        a[i] = data[i] = read();
      }
      data[0] = - 0x3f3f3f3f;
      std :: sort(data + 1, data + n + 1);
      for (int i = 1; i <= n; ++ i) {
        if (data[i] != data[i - 1]) maxa ++;
        data[maxa] = data[i];
      }
      for (int i = 1; i <= n; ++ i) {
        int ori = a[i];
        a[i] = std :: lower_bound(data + 1, data + maxa + 1, ori) - data;
        map[a[i]] = ori;
      }
    }
    void PrepareBlock() {
      int i = 1;
      block_size = (int) sqrt(n);
      block_num = n / block_size;
      for (i = 1; i <= block_num; ++ i) {
        L[i] = (i - 1) * block_size + 1;
        R[i] = i * block_size;
      }
      if (R[block_num] < n) {
        L[++ block_num] = R[block_num - 1] + 1;
        R[block_num] = n;
      } 
      
      for (int j = 1; j <= block_num; ++ j) {
        for (int i = L[j]; i <= R[j]; ++ i) {
          bel[i] = j;
        }
      }
      for (int l = 1; l <= block_num; ++ l) {
        tmp.Clear();
        for (int r = L[l]; r <= n; ++ r) {
          f[l][r] = f[l][r - 1] + tmp.Sum(maxa) - tmp.Sum(a[r]);
          tmp.Add(a[r], 1);
        }
      }
    }
    //=============================================================
    int main() {
      Prepare();
      PrepareBlock();
      for (int i = 1; i <= n; ++ i) {
        HjtTree :: Insert(root[i], root[i - 1], 1, maxa, a[i]);
      }
      m = read();
      int lastans = 0;
      while (m --) {
        int l = read() ^ lastans, r = read() ^ lastans;
        lastans = 0;
        if (l > r) {
          printf("0
    ");
          continue ;
        }
        if (bel[l] == bel[r]) {
          tmp.Clear();
          for (int i = l; i <= r; ++ i) {
            lastans += tmp.Sum(maxa) - tmp.Sum(a[i]);
            tmp.Add(a[i], 1);
          }
          printf("%d
    ", lastans);
          continue ;
        }
        lastans = f[bel[l] + 1][r]; 
        for (int i = l; i <= R[bel[l]]; ++ i) {
          lastans += HjtTree :: Query(root[r], root[i], 1, maxa, 1, a[i] - 1);
        }
        printf("%d
    ", lastans);
      }
      return 0;
    }
    

    写在最后

    参考资料:
    分块思想 - OI Wiki
    学长 zbq 的讲解

    还会回来的!

  • 相关阅读:
    009-Python-面向对象
    008-Python-模块
    007-Python函数-装饰器
    006-Python函数
    005-Python字典
    003-python列表
    PyCharm之python书写规范--消去提示波浪线
    001-python基础
    Java基础总结(一)
    High ASCII字符从bat文件到dos控制台的转化问题
  • 原文地址:https://www.cnblogs.com/luckyblock/p/13629547.html
Copyright © 2020-2023  润新知