//没用

分块与根号算法

update:

  • 2024.4.13:完工,修改与整理

0. 根号算法

一些无法以 \(polylog\) 复杂度实现的题,又不能暴力通过,这时根号算法就是一个不错的选择。

1. 整除分块

这一部分较为简单。

1.1 概念与解法

整除分块是要求形如 $$\sum_{i=1}^{n} f(i)\left\lfloor\dfrac{n}{i}\right\rfloor$$ 的式子,我们要求 \(f(n)\)前缀和是易求的。
暴力是 \(O(n)\) 的,不太好。
我们考虑把 \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 相同的 \(i\) 一起计算,可以发现 \(\left\lfloor\dfrac{n}{i}\right\rfloor\) 最多只有 \(2 \times \sqrt{n}\) 种数字。
证明:

  • 分类:\(i \leq \sqrt{n}\)\(\left\lfloor\dfrac{n}{i}\right\rfloor\) 显然只有 \(\sqrt{n}\) 个。
  • \(i > \sqrt{n}\)\(\left\lfloor\dfrac{n}{i}\right\rfloor < \sqrt{n}\) 也只有 \(\sqrt{n}\) 个。

得证。
所以我们这样计算复杂度为 \(O(\sqrt{n})\)

若有 \(l\),我们考虑求出最大的 \(r\),使 \(\left\lfloor\dfrac{n}{l}\right\rfloor = \left\lfloor\dfrac{n}{r}\right\rfloor\)
我们令 $k = \left\lfloor\dfrac{n}{l}\right\rfloor = \left\lfloor\dfrac{n}{r}\right\rfloor \leq \dfrac n r\
\Longrightarrow \left\lfloor\dfrac{n}{k}\right\rfloor \geq \left\lfloor\dfrac n {\frac n r} \right\rfloor = r $
\(r_{max} = \left\lfloor\dfrac n {\left\lfloor\frac n l\right\rfloor}\right\rfloor\) (不要忘了对 \(n\)\(min\))
这样只需累加 \((S(r) - S(l-1)) \times (r-l+1)\) 即可。(\(S\) 代表 \(f\) 前缀和)

1.2 拓展

1.2.1 多维整除分块

即求 $$\sum_{i=1}^{n} f(i)\prod_{j=1}^{m} \left\lfloor\frac {n_j} i\right\rfloor$$
只需令 \(r = \min\limits_{j=1}^m \left(\left\lfloor\dfrac {n_j} {\left\lfloor\frac {n_j} l\right\rfloor}\right\rfloor\right)\)
使所有 \(\left\lfloor\dfrac {n_j} l \right\rfloor\) 相等,复杂度 \(O(\sum \sqrt{n_j})\)

1.2.2 常用式子

\(\sum\limits_{i=1}^n i^2 = \dfrac{n(n+1)(2n+1)} 6\) , \(\sum\limits_{i=1}^n i^3 = \left(\dfrac{n(n+1)} 2\right)^2\),\(\left\lceil\dfrac n m\right\rceil = \left\lfloor\dfrac {n+m-1} m\right\rfloor\),\(n \bmod i = n - i \times \left\lfloor\dfrac n i\right\rfloor\)

1.3 例题

I P2424 约数和
拆分问题则答案为 \(S(r) - S(l-1)\),考虑求 \(S\)
求前 \(n\) 个数的约数和,我们考虑枚举约数 \(i\),可知前 \(n\) 个数中有 \(\left\lfloor\dfrac n i\right\rfloor\) 个数是 \(i\) 的倍数。
则每个约数 \(i\) 的贡献为 \(i\times\left\lfloor\dfrac n i\right\rfloor\)
答案就是 \(\sum\limits_{i=1}^n i\times\left\lfloor\dfrac n i\right\rfloor\)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll l,r;
ll ask(ll n){
    ll ans = 0;
    for(ll l = 1,r;l <= n;l = r + 1){
        r = min(n,n / (n / l));
        ans += (r - l + 1) * (l + r) / 2 * (n / l);
    }
    return ans;
}
int main(){
	scanf("%lld%lld",&l,&r);
	printf("%lld\n",ask(r)-ask(l-1));
	
	return 0;
}

II P2261 [CQOI2007] 余数求和 [cogs 3149]
\(\sum\limits_{i=1}^n k \bmod i\)
\(\sum\limits_{i=1}^n k \bmod i = \sum\limits_{i=1}^n k - i \times \left\lfloor\dfrac k i\right\rfloor = k\times n - \sum\limits_{i=1}^n \left\lfloor\dfrac k i\right\rfloor = k\times n - \sum\limits_{i=1}^{min(n,k)}\left\lfloor\dfrac k i\right\rfloor\)
直接求即可。
代码

IIIP2260 [清华集训2012] 模积和[cogs 3668]
\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m(n\bmod i) \times (m \bmod j),i\neq j\)
则原式可写为\(\sum\limits_{i=1}^n\sum\limits_{j=1}^m (n\bmod i) \cdot(m \bmod j) - \sum\limits_{i=1}^n(n\bmod i) \cdot(m \bmod i)\\\)
先考虑减号前的式子。

\[\sum\limits_{i=1}^n\sum\limits_{j=1}^m (n\bmod i) \cdot (m \bmod j) = (n^2 - \sum\limits_{i=1}^n i\cdot \left\lfloor\dfrac n i\right\rfloor)\cdot(m^2 - \sum\limits_{j=1}^m j\cdot \left\lfloor\dfrac n j\right\rfloor) \]

都可直接解决。减号后。

\[\sum\limits_{i=1}^n(n\bmod i) \times (m \bmod i) = \sum\limits_{i=1}^n(n - i\cdot\left\lfloor\dfrac n i\right\rfloor) \times (m - i\cdot\left\lfloor\dfrac m i\right\rfloor) = \sum_{i=1}^n(nm - n\cdot i\left\lfloor\dfrac m i\right\rfloor - m\cdot \left\lfloor\dfrac n i\right\rfloor + i^2\left\lfloor\dfrac n i\right\rfloor\left\lfloor\dfrac m i\right\rfloor) \]

相当于二维整除分块。复杂度 \(O(\sqrt{n})\)
注意 \(19940417\) 不是质数,需要别的方法求 \(2\)\(6\) 的逆元。
代码

2. 分块

2.1 算法简介

分块是一种思想,实质是将通过分成多块后再整体修改暴力重构的结合。
对于一个序列,我们把它分成 \(B\) 块,每块有 \(\dfrac n B\) 个元素,在修改时遇到整块就打标记,遇到散块就暴力重构,这样每次暴力重构的复杂度最多为 \(O(2\dfrac n B)\),打标记的复杂度最多为 \(O(B)\),总复杂度为 \(O(\dfrac n B + B)\),利用均值不等式容易得到最优复杂度为 \(O(\sqrt{n})\)

当然,每道题的操作不同,整块散块的复杂度也会不同,\(B\) 的最优复杂度也会不同,具体问题需要具体分析。
分块的优点是容易思考,比较暴力以及可以维护一些复杂的信息

2.2 其他分块

2.2.1 值域分块

类似于值域线段树,当值域较小时可用分块来维护值域。值域分块可以用来平衡复杂度。

在单点修改,区间查询中。
值域分块可以 \(O(1)\) 修改, \(O(\sqrt{n})\) 查询,即正常单点改,区间暴力查。
也可以 \(O(\sqrt{n})\) 修改,\(O(1)\) 查询,即维护块内前缀和,差分查询。

这样在某些题中可以取得更优的复杂度。详见莫队例 III

2.2.2 块状链表

可插入的序列。

设块长为 \(B\)
根号重构:当一个块内添加新元素后达到了 \(2B\) 时,将该块拆为两大小为 \(B\) 的块。

这样最多分裂 \(O(\dfrac n B)\) 次,且每块大小都在 \([B,2B]\) 间。

2.3 经典问题

这里以最经典的区间修改,区间查询为例。
首先考虑修改:

  • 对于整块,只需要一个加法标记即可。
  • 对于散块,暴力加即可。

然后看查询:

  • 对于整块,需要整块的 \(sum\) 和,以及累加上加法标记。
  • 对于散块,暴力加即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+10,M = 330;
const ll inf = 1e17;
int n,m;
ll a[N],sum[M],la[M];
int L[N],R[N],bl[N],B,t; 
void prework(){
    B = sqrt(n);t = (n-1)/B+1;
    for(int i = 1;i <= t;i++)L[i] = (i-1)*B+1,R[i] = min(i*B,n);
    for(int i = 1;i <= n;i++)bl[i] = (i-1)/B+1,sum[bl[i]] += a[i];
}
void modify(int l,int r,ll x){
    int p = bl[l],q = bl[r];
    if(p == q){
        for(int i = l;i <= r;i++)a[i] += x,sum[p] += x;
        return; 
    }
    for(int i = l;i <= R[p];i++)a[i] += x,sum[p] += x;
    for(int i = L[q];i <= r;i++)a[i] += x,sum[q] += x;
    for(int i = p+1;i <= q-1;i++)la[i] += x; 
}
ll ask(int l,int r){
    int p = bl[l],q = bl[r];
    if(p == q){
        ll ans = 0;
        for(int i = l;i <= r;i++)ans += a[i] + la[p];
        return ans;
    }
    ll ans = 0;
    for(int i = l;i <= R[p];i++)ans += a[i] + la[p];
    for(int i = L[q];i <= r;i++)ans += a[i] + la[q];
    for(int i = p+1;i <= q-1;i++)ans += la[i] * (R[i] - L[i] + 1) + sum[i];
    return ans;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)scanf("%lld",&a[i]);
    prework();
    for(int i = 1;i <= m;i++){
        int op,l,r;ll x;
        scanf("%d%d%d",&op,&l,&r);
        if(op == 1){
            scanf("%lld",&x);
            modify(l,r,x);
        }
        else printf("%lld\n",ask(l,r));
    }
    

	return 0;

}

2.4 例题

I P3372 【模板】线段树 1
II P2801 教主的魔法
区间加,区间询问 \(\geq x\) 的数的个数。

发现 \(c\) 的值很大,考虑分块,预处理时将每块内排序。
对于修改:

  • 整块中,只需加上懒标记。
  • 散块中,直接修改,暴力重构即可。

对于询问:

  • 整块中,我们可以在块内排序,块内二分查找即可。
  • 散块中,直接查询判断即可。

总复杂度 \(O(m\sqrt{nlogn})\)
代码
III P4168 [Violet] 蒲公英[cogs 3231]
区间查询众数,输出最小众数,强制在线。

\(a\) 的值域很大,先离散化下。
考虑预处理,设 \(s[i][j]\) 为块 \(i\) 到块 \(j\) 的众数,设 \(cnt[i][j]\) 为数 \(i\)\(j\) 块出现的次数,这样可以差分。
都可 \(O(nB)\) 求出。
考虑询问:

  • 对于整块,直接提出 \(s\) 数组内众数即可。
  • 对于散块,在桶内暴力统计,暴力比较整块与散块中个数和。

时间复杂度 \(O(n\sqrt{n})\),空间复杂度 \(O(n\sqrt{n})\)
代码

但是仅仅这样还是不毒瘤,lxl卡了卡内存,变成了
IV P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III
只能要 \(O(n)\) 的内存。首先把 \(s\) 变为区间众数的次数。
发现是 \(cnt\) 的问题,考虑优化。可以建立一个 \(vector\) 存每个数出现的下标,这样求区间 \([l,r]\)\(i\) 出现的次数只需要两次二分即可。
这样空间复杂度降到了 \(O(n)\),不过时间复杂度变为了 \(O(n\sqrt{nlogn})\)

这样就行了吗?不行!
本题必须严格根号复杂度才可过,继续考虑优化。
在询问中设当前整块的答案为 \(ans = s[p+1][q-1]\),若左部分散块的元素 \(x\) 在范围内的个数大于 \(ans\),则在 \(vector\) 中一定有 \(ve[x][d_i+ans]\) 的值小于等于 \(r\)(这里 \(d_i\) 指 每个元素在相应vector里的下标),直接暴力 \(ans++\)。右部分同理。
\(d\) 数组可以预处理,又因为散块中的数最多有 \(2\sqrt{n}\) 个,所以复杂度合理。
时间复杂度为严格 \(O(n\sqrt{n})\),空间复杂度为 \(O(n)\)。(不太卡常)
代码

V P4117 [Ynoi2018] 五彩斑斓的世界
分块不能没有 \(Ynoi\)。这里放一道不咋卡常的大分块。
修改:使区间 \([l,r]\) 内大于 \(x\) 的数减去 \(x\)。这和区间减少 \(x\),再将小于 \(x\) 的数加 \(x\) 是等价的。
查询:查区间 \([l,r]\) 内等于 \(x\) 的个数。
常有数据结构都无法维护,考虑分块。

首先看修改,对于散块,暴力重构即可。
而对于整块,一个显然的做法是若块内最大值 \(mx\) 小于 \(x\),则不修改,否则暴力修改,最坏情况下会爆。
我们观察数据范围,可知 \(x\) 的值域是与 \(n\) 同阶的,可以考虑一种关于值域的算法。

  • \(mx < 2x\),则暴力块内将 \([x,mx]\) 中的值减去 \(x\),复杂度 \(O(mx-x)\),块内 \(mx\) 减少了 \(mx-x\)
  • \(mx \geq 2x\),则将块内 \([1,x]\) 中的值加上 \(x\),再打上区间减标记,这样复杂度 \(O(x)\),块内 \(mx\) 减少了 \(x\)

这就相当于用 \(O(X)\) 的时间复杂度,使 \(mx\) 减少了 \(X\),而又因为 \(mx\) 不会增加,所以每块的均摊复杂度为 \(O(n)\)

再来考虑如何维护,直接加减显然是不行的,因为询问的是值为 \(x\) 的个数,无法做到。
考虑形象的就是将值域 \([x,mx] \Rightarrow [0,mx-x]\)\([1,x] \Rightarrow [x+1,2x]\)
这种平移可以用并查集维护,复杂度 \(O(\alpha(n))\),但如果直接将每个数直接维护值域并查集的话显然会爆。
我们可以维护下标并查集,并在每个块内维护值域,这样时空复杂度都为 \(O(nB)\)

最后考虑询问,每个块内维护值域时同时维护值域为 \(x\)\(size\) 即可,是简单的。
整块直接询问并查集每块内的\(size\)散块暴力查询。复杂度 \(O(\dfrac {n^2} B)\)

总最优时间复杂度 \(O(n\sqrt{n})\),空间 \(O(n\sqrt{n})\)
代码

这样就完成了 CF896E Welcome home, Chtholly
但是 lxl 又又加强了,并卡了卡常,变为了这道题。
区别就是数据范围更大了,且内存限制变为了恐怖的64MB,就是说只能有 \(O(n)\) 的空间。

这个 \(trick\) 不太难,但比较智慧
观察一下每块的操作,可以发现每块内的操作都是独立的,与其他块互不相关,所以我们可以离线!!,每次只需单独处理其中一块的操作。
每块共用一个值域,空间复杂度 \(O(n)\)

代码

3. 莫队

莫队————优雅的暴力

3.1 算法简介

莫队是一种 离线 算法,资瓷修改。

主要思想:对询问分块(相当于对 \(l\) 分块),将询问在以 \(l\) 所在块为第一关键字排序,再以 \(r\) 为第二关键字升序。同时维护两个指针 \(l\)\(r\),根据询问伸缩区间,这样时间复杂度就为 \(O(n\sqrt{n} \times W)\)\(W\) 表示指针移动的复杂度。

证明:因为在一块内 \(r\) 为升序,最坏复杂度为 \(O(n)\)\(l\) 只在块内,最坏复杂度为 \(O(B^2)\),总 \(O(\dfrac {n^2} B + nB)\),取 \(B = \sqrt{n}\) 复杂度最优。得证。

  • 奇偶性排序:当 \(l\) 在奇数块内 \(r\) 以降序排序,否则内 \(r\) 以升序排序,会使 \(r\) 呈类似折线形,可有效减少常数。

3.2 带修莫队

因为是离线,莫队一般不资瓷修改,但我们可以暴力增加一个 时间维,同 \(l,r\) 指针一起伸缩。

询问变成了三维,排序也要多以个关键字,类似普通莫队,我们以 \(O(n^{\frac 2 3})\)为块长。
我们分成了 \(n^{\frac 1 3}\) 块,第一关键字 \(l\) 所在块排序,第二关键字以 \(r\) 所在块排序,第三关键字是时间。
这样复杂度是 \(O(n^{\frac 3 5})\)
证明:设 \(n,m,q\) 同阶。

  • \(l,r\) 所在块不变,\(t\) 是单增的,复杂度为 \(O(n)\),总 \(O(\dfrac {n^3} {B^2})\)
  • 对于 \(l\),在块内移动最多 \(O(B)\) 次,总 \(O(nB)\)
  • 对于 \(r\)\(l\) 所在块不变,复杂度为 \(O(B)\)\(l\) 所在块变,最坏移动 \(O(n)\) 次,复杂度为 \(O(\frac {n^2} B)\)
  • 总复杂度 \(O(\dfrac {n^3} {B^2} + nB + \dfrac {n^2} B)\),大约取到 \(B = n^{\frac 2 3}\) 时最小,为 \(O(n^{\frac 5 3})\)

3.3 回滚莫队

当维护的信息在区间增加与区间减小其中之一无法实现的情况下(如区间最大值),此时我们就需要 回滚莫队
虽然不能够缩短或伸长,但增加或撤销操作也都是可以实现的。
具体操作(以不能缩短为例):

  • 首先我们需要对序列 分块,然后以 \(l\) 所在块为第一关键字排序,以 \(r\) 为第二关键字排序。
  • 如果 \(l,r\)同一块内,则暴力查询(类似分块)。
  • 如果 \(l,r\) 不在同一块,设 \(l\) 所在块为 \(T\),则将 \(l\) 伸缩为 \(R[t]+1\),将 \(r\) 伸缩为 \(R[t]\)
  • \(r\) 点在块内只增加,而 \(l\) 有可能会是乱序的,则我们需要在完成该次询问后撤销回去,使 \(l\) 重回 \(R[t]+1\),形象的:可以再加一个变量 \(l_1\) 代替 \(l\) 进行该询问的增加,而 \(l\) 本身不变,即完成了 "撤销"

类似的,若信息不能增加,则可以使 \(r\) 为第二关键字降序,在不同块内将 \(l,r\) 设为 \(L[t],n\)大区间即可。

3.4 树上莫队

莫队只能在序列里使用,我们考虑把树的路径转化为序列。

欧拉序:在 \(dfs\) 每次到达某个点时,在序列中加入该点。离开某个点时,再加入一次,得到长为 \(2n\) 的序列。
我们称 \(in[x]\)\(x\) 在该序列第一次的位置, \(out[x]\)\(x\) 在该序列第二次的位置。
\(u \rightarrow v\) 的路径就是:

  • \(u\)\(v\) 的祖先,则是 \(in[u]\)\(in[v]\)
  • \(u,v\) 互不为祖先,则是 \(out[u]\)\(in[v]\) 再加上 \(lca(u,v)\)

这样可以把询问也改到序列上。

则我们只需要在转化后的序列跑普通莫队即可,当然带修也可以。
注意长度为 \(2n\)

3.5 莫队二次离线

咕咕咕

3.6 例题

I P1494 [国家集训队] 小 Z 的袜子 [cogs 1775]
给定序列,求在 \([l,r]\) 内随机抽取两个数相等的概率。

显然总概率为 \(\dbinom {r-l+1} 2\)。若当前数为 \(x\),考虑增点,则需要一个桶 \(c\) 记录 ,增加的概率就是 \(c[x]\)。考虑减点,则减少的概率就是 \(c[x]-1\)
只需按 \(l\) 所在块为第一关键字升序排序,\(r\) 为第二关键字升序排序即可。

在区间伸缩时,必须先 伸长缩短

代码

II P1903 [国家集训队] 数颜色 / 维护队列 [cogs 1901]
给定序列,求 \([l,r]\) 内不同数字的个数,带修改。

数据伸缩是简单的。考虑修改,维护时间轴 \(t\) 一起伸缩,可以先删除原有数据,再添加修改数据即可。
排序时以 \(l\) 所在块为第一关键字升序排序,\(r\) 所在块为第二关键字升序排序,以 \(t\) 为第三关键字升序排序。

代码

III P4396 [AHOI2013] 作业[cogs 1822]
区间 \([l,r]\) 内在 \([a,b]\) 范围内的个数以及在 \([a,b]\)不同的数的个数。

可以离线,考虑莫队,先排序询问。
然后考虑指针伸缩,可以想到维护一个值域 \(BIT\),前缀和查询。复杂度 \(O(n\sqrt{n}log(n))\)
\(BIT\) 其实是 \(log(n)\) 的修改与查询,总应该是 \(O(n\sqrt{n}log(n)+mlog(n))\)

可以考虑平衡复杂度,利用值域分块的 \(O(1) - O(\sqrt{n})\)
分析复杂度:

  • 莫队 \(O(mB + \dfrac {n^2} B)\)\(B = \dfrac n {\sqrt{m}}\) 时最优,为 \(O(n\sqrt{m})\)
  • 值域分块 \(O(m\sqrt{M})\)

总复杂度 \(O(n\sqrt{m} + m\sqrt{M})\)

代码

IV Machine Learning
注意该题是求每个数出现次数\(mex\)
我们考虑如何求 \(mex\),根据 \(mex\) 定义可知,若结果为 \(x\) 则至少有 \(1+2+...+x\) 个数,得 \(n\) 个数的最大 \(mex\) 大约为 \(\sqrt{n}\),可暴力求解。

需要两个桶分别记录数组 \(a\) 的次数与记录数组 \(a\) 的桶各数出现的次数,信息维护是简单的。
套用上题,维护 \(t\),先删除原有数据,再添加修改后数据。
复杂度 \(O(q\sqrt{n})\)

代码

V 歴史の研究
\([l,r]\) 内最大 \(x \times c[x]\),其中 \(c[x]\) 表示 \([l,r]\)\(x\) 出现的次数。

可以发现,该信息增加是容易的,但是删除时却很困难(不知道次大值),需要用到 回滚莫队
首先我们把序列分块,然后根据 \(l\) 所在块为第一关键字升序排序,以 \(r\) 为第二关键字升序排序。

考虑询问,若 \(l,r\) 在同一块内则直接暴力查询(注意是独立查询),否则从 \(l\) 所在块 \(T\) 开始,将 \(l,r\) 定为 \(R[T]+1,R[t]\)回滚 \(l\)\(r\) 单增即可。

代码

VI COT2 - Count on a tree II
只是把区间操作改到了树上路径。

利用上述方法,将树上路径改为序列区间,将区间询问改为序列上区间(加上 \(lca\))。
区间伸缩与例 II 相同。
然后在序列上跑普通莫队即可。

代码

VII P4074 [WC2013] 糖果公园
树上带修。

首先考虑将树上路径改为序列区间,拍成欧拉序,求一下 \(lca\) 即可。

考虑区间伸缩,若增加一个点 \(x\),则 \(sum\) 增加 \(w[cnt[c[x]]+1] * v[c[x]]\),若删除点,则 \(sum\) 减少 \(w[cnt[c[x]]] * v[c[x]]\),是 \(O(1)\) 的。
然后可以直接在序列上跑 带修莫队 即可。

代码

参考文章:
「分块」数列分块入门1 – 9 by hzwer
分块相关杂谈
『回滚莫队及其简单运用』(强烈推荐)

posted @ 2024-04-12 18:16  Hao_Xu  阅读(85)  评论(2编辑  收藏  举报