Histcat's Blog
一个高中生的小窝
浅谈分块

分块含义

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

时间复杂度

分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。

均衡不等式

对于我们初中所学的完全平方公式 (x+y)20(x+y)^2 \ge 0 经过简单的变形可以得到 x2+y22xyx^2+y^2 \ge 2xy

我们把 x=ax = \sqrt{a}y=by = \sqrt{b} 带入可以得到 a+b2aba + b \ge 2\sqrt{ab} (a,b>0)(a, b > 0) ,在 a=ba = b 时取等号,这就是均衡不等式

分析

以一道题目为例:线段树1

没错,虽然是线段树的模板题,但也可以用分块来做,而且跑的贼快

我们设块长为 ss ,无论什么[l, r]操作都是

  1. l,rl, r 在同一块中,枚举: O(s)O(s)
  2. l,rl, r 不在同一块中:枚举 ll 所在块,枚举 ll 所在块的下一个 到 rr 所在块的上一个,枚举 rr 所在块: O(s+ns)O(s + \frac{n}{s})

共有 mm 次操作的话,总时间复杂度就是 O(m(s+ns))O(m(s + \frac{n}{s})) ,利用均衡不等式我们可知 当 s=nss = \frac{n}{s} 时该式子取得值最小,即时间复杂度最优,此时 s=ns = \sqrt{n}

(tips:对于不同的题目可能块长 ss 不一样,要具体分析,但一般来说类似此题这种操作的块长取得都是 n\sqrt{n}

模板

对于刚刚的那道线段树题目,我们可以写出分块的代码

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 5;

int n, m;
long long a[N], blk[N], b[N];
int pos[N];
int R;

void add(int l, int r, long long k)
{
    if(pos[l] == pos[r])
    {
        for(int i = l;i <= r;i++)
            a[i] += k;
        blk[pos[l]] += (r - l + 1) * k;
        return;
    }
    for(int i = l;pos[i] == pos[l];i++)
    {
        a[i] += k;
        blk[pos[i]] += k;
    }
    for(int i = pos[l] + 1;i <= pos[r] - 1;i++)
    {
        blk[i] += R * k;
        b[i] += k;
    }
    for(int i = r;pos[i] == pos[r];i--)
    {
        a[i] += k;
        blk[pos[i]] += k;
    }
}

long long query(int l, int r)
{
    long long ans = 0;
    if(pos[l] == pos[r])
    {
        for(int i = l;i <= r;i++)
        {
            ans += a[i] + b[pos[i]];
        }
        return ans;
    }
    for(int i = l;pos[i] == pos[l];i++)
    {
        ans += a[i] + b[pos[i]];
    }
    for(int i = pos[l] + 1;i <= pos[r] - 1;i++)
    {
        ans += blk[i];
    }
    for(int i = r;pos[i] == pos[r];i--)
    {
        ans += a[i] + b[pos[i]];
    }
    return ans;
}

int main()
{
    clock_t c1 = clock();
#ifdef LOCAL
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    //------------------------------
    scanf("%d%d", &n, &m);
    R = sqrt(n) + 1;

    for(int i = 1;i <= n;i++)
    {
        scanf("%lld", &a[i]);
        pos[i] = i / R + 1;
        blk[pos[i]] += a[i];
    }
    long long opt, x, y, z;
    while(m--)
    {
        scanf("%lld", &opt);
        if(opt == 1)
        {
            scanf("%lld%lld%lld", &x, &y, &z);
            add(x, y, z);
        }
        else if(opt == 2)
        {
            scanf("%lld%lld", &x, &y);
            printf("%lld\n", query(x, y));
        }
    }

  
  
    //------------------------------
end:
    cerr << "Time Used:" << clock() - c1 << "ms" << endl;
    return 0;
}

tips:

  1. pospos 数组记录的是每个元素所在的块的编号
  2. blkblk 数组记录的是每个块内实际的和
  3. bb 数组像是线段树里的 lazytaglazytag 记录的是已经加到 blkblk 数组里的,但还没有“下传”到原数组里的值(但是这里不用下传也可以,代码里就没有下传)

其他题目

单点修改+查询区间内小于(等于)或大于(等于)某数的个数

对于每一个块,我们可以建立一个vector并排序。当修改的时候,二分查到vector内这个数所在的位置,erase掉,插入然后再排序

查询的话,不是完整块就暴力枚举,是完整块用vector的 lower_boundlower\_bound 查询个数就行了