分块就是乱搞(确信
啥是分块
分块本质就是优雅的暴力,通过预处理和根号平衡(玄学地)让复杂度降低
比如我们考虑一个线段树裸题:
区间加,区间查询,\(n<=1e5\)
显然暴力的做法是\(n^2\)的,那么我们有没有什么优化方法呢?
我们可以将整个序列分为若干块,提前预处理出每个块的和,每次修改如果包含一个整块就直接打标记,非整块范围就暴力修改
时间复杂度
假设我们将\(k\)个元素分成一块,那么将一共分出\(\frac{n}{k}\)个块
对于修改和查询操作:整块\(O(1)\)标记或查询,散块暴力修改,那么一次操作复杂度最高为\(O(\frac{n}{k}+k)\)
根据均值不等式,当\(\frac{n}{k}=k\)时,\(\frac{n}{k}+k\)最小,也就是说当\(k=\sqrt{n}\)时总复杂度最低为\(O(n\sqrt{n})\)
虽然相比\(n^2\)很优秀,但是为啥我不写线段树呢???
相比其他数据结构优势
众所周知,线段树维护的信息需要能够区间快速合并,但是分块由于过于暴力,可以忽略这个条件
如这道例题:
区间加,区间小于\(k\)的个数
线段树已经去世,但是分块仍然可以胜任:
预先将每个块内数字排序,对于修改操作,显然不会影响整块的有序性,对于散块我们可以暴力重构,复杂度\(O(\sqrt{n}logn)\)
对于查询操作,可以散块暴力查询,整块二分查找,复杂度\(O(\sqrt{n}logn)\)
总复杂度\(O(n\sqrt{n}logn)\)
分块牺牲了部分复杂度,所以处理信息更加灵活,保留的信息量更多
入门例题
就放黄学长的分块入门吧
分块入门1:区间加,单点查询
没什么好说的了,放代码康康格式吧
展开查看
#include
using namespace std; #define int long long inline int read() { int x=0,f=1; char ch; for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar()); if(ch=='-') f=0,ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();} return f?x:-x; } int n,cnt; int a[100010]; int bel[100010],tag[100010]; inline void update(int l,int r,int k) { int lx=bel[l],rx=bel[r]; for(int i=l;bel[i]==lx&&i<=r;++i) a[i]+=k; if(lx==rx) return ; for(int i=r;bel[i]==rx;--i) a[i]+=k; for(int i=lx+1;i
分块入门2:区间加,区间小于\(k\)个数
刚才讲过惹\(qwq\)
分块入门3:区间加,区间\(k\)前驱
\(emmm\)和上一题方法一样,懒得讲了
分块入门4:区间加,区间求和
也讲过了……(这样看来这篇博客画风清奇
分块入门5:区间求和,区间开方(下取整)
这个有点意思
首先区间求和肯定要预处理出块内和,那对于区间开方怎么处理呢?
这个题目有个条件,\(a_i<=maxint\),我们可以算出对于每个数字最多开方\(6\)次就会变成\(1\)或\(0\),然后再也不会变化了
所以如果一个块内全部变为了\(1\)或者\(0\),就不用再处理了,那么对于块内存在非\(1\)或\(0\)的块,暴力重构,对于已经不会变化的块直接打\(continue\)标记然后跳过
分块入门6:单点插入,单点查询(数据随机)
考虑到数据随机,对每个块开个链表,记录下每个块内有目前多少个元素,然后模拟就好了
如果数据不随机怎么办?我们可以设置一个值s,每当插入s个数字就对整个序列重新分块,可以证明理论复杂度仍然是\(O(n\sqrt{n})\)级别的
分块入门7:区间加法,区间乘法,区间求和
这不线段树\(2\)吗(雾
我在这里再说明一下标记顺序的问题:一定要先乘再加!
先加后乘效果如下:
假设当前状态为\((a+add)*mul\),又添加了一组\([add2,mul2]\)标记
当前状态变为\(((a+add)*mul+add2)*mul2\)
展开\(((a+add)*mul*mul2+add2*mul2)\)
继续展开\((a*mul*mul2+add*mul*mul2+add2*mul2)\)
恢复标记格式:\((a+add+(\frac{add2}{mul}))*mul*mul2\)
发现了什么?分数!这样可能会丢精度\(qwq\)
如果我们先乘后加呢
\((a*mul+add)添加新标记\)[add2,mul2]$
当前状态变为\(((a*mul)+add)*mul2+add2\)
展开\((a*mul*mul2+add*mul2+add2)\)
恢复标记格式:\((a*mul*mul2)+add*mul2+add2\)
完美
注意细节:对散块处理之前要下放整个散块标记
分块入门8:查询区间等于\(k\)的个数,并将区间修改为\(k\)
似乎可以沿用分块入门\(2\)的套路,不过很不幸被卡了
考虑入门\(5\)的分析方法:若块内都是同一数字,可以打标记\(O(1)\)计算,如果不是就暴力统计
整个序列均摊\(O(n)\)就可以全部变为同一数字,每次操作最多破坏两个块的数字,带来\(O(\sqrt{n})\)的复杂度,总复杂度\(O(n\sqrt{n})\)
分块入门9:区间众数
介绍方法之前我们需要介绍一个引理:有两个数字集合\(A\)和\(B\),假设\(A\)的众数为\(x\),那么\(A\cup B\)的众数\(y \in x \cup B\)
证明:不会,详情请见陈立杰的《区间众数解题报告》
根据引理,我们只要找出散块的众数和每个整块的众数,求出其中出现次数最多的众数
我们知道枚举散块众数,预处理整块众数可以做到\(O(\sqrt{n})\),那么怎么快速求一个数在某段区间内出现次数呢?
我们可以用\(vector\)预处理出每个数字出现的位置,二分找到第一个大于区间坐端点的位置和最后一个小于区间右端点的位置
总复杂度\(O(n\sqrt{n}logn)\)
有点慢,得卡卡常
换个方法,发现我们设\(d_{ij}\)为第\(i\)个块到第\(j\)个块的众数,这样复杂度只有在散块贡献较多
然后我们进行调参,发现预处理只需要进行一次,但是询问有多次,我们设法让预处理复杂度稍大些,询问复杂度稍小些
设块大小为\(k\),则预处理复杂度为\(O(n*\frac{n}{k})\),每次询问复杂度为\(O(klogn)\),总复杂度\(O(n*\frac{n}{k}+nklogn)\)
根据均值不等于,\(k=\sqrt{\frac{n}{logn}}\)时,复杂度最低,不太好算懒得打了,算出来约为\(70-80\),是可以卡过本题的
不过我们其实有方法直接去掉\(log\)
再加一步预处理,\(p_{ij}\)表示前\(i\)个块中数字\(j\)出现的次数,就可以做到\(O(n\sqrt{n})\)了