zoukankan      html  css  js  c++  java
  • 「笔记」分块

    分块思想

    引用一下 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 的讲解

    还会回来的!

  • 相关阅读:
    [LeetCode] Power of Three 判断3的次方数
    [LeetCode] 322. Coin Change 硬币找零
    [LeetCode] 321. Create Maximum Number 创建最大数
    ITK 3.20.1 VS2010 Configuration 配置
    VTK 5.10.1 VS2010 Configuration 配置
    FLTK 1.3.3 MinGW 4.9.1 Configuration 配置
    FLTK 1.1.10 VS2010 Configuration 配置
    Inheritance, Association, Aggregation, and Composition 类的继承,关联,聚合和组合的区别
    [LeetCode] Bulb Switcher 灯泡开关
    [LeetCode] Maximum Product of Word Lengths 单词长度的最大积
  • 原文地址:https://www.cnblogs.com/luckyblock/p/13629547.html
Copyright © 2011-2022 走看看