根号算法学习笔记

发布时间 2023-05-16 21:46:07作者: Xy_top

最近整理并学习了一些根号算法,总共分为三个。

$1.$ 莫队

$2.$ 分块

$3.$ 根号分治

$1.$ 莫队

$1_.$ 序列莫队

这是一个离线算法(当然有在线的, 但是 CCF 不会卡吧)。

它可以在 $q\sqrt{n}+n\sqrt{n}$ 的时间内解决数列上多组询问的问题,问题大多给一个区间 $l$ $r$,让你输出 $[l,r]$ 的某个信息,比如区间和。

莫队的思想就是维护一个当前拥有信息的指针 $L$ $R$,通过不断地移动 $L$ $R$ 的位置添加或删除数字来得到 $l$ $r$ 的答案。

假如要求 $[2,5]$ 的区间和,现在手头上有 $[3,4]$ 的区间和,加上 $a[2]$ 和 $a[5]$ 的值就可以了。

现在归纳一下,假如有的信息是 $[L,R]$,要求 $[l,r]$,就可以通过 $|L-l| + |R-r|$ 次移动得到答案了。

所以我们要把所有的询问排一下序,使得 $\sum\limits_{i=2}^n |a[i].l - a[i - 1].l|+|a[i].r - a[i -1].r|$ 最小,这个有人证明过最小是 值域乘以 $\log$ 值域的,但怎么排列是一个 NP Hard,莫队算法的优势就是简洁好写,如果和 NP hard 弄在一起...

所以现在要做的就是找到一种容易求得的排列方式,代价又小。

如果仅仅按照右端点来排序,左端点一次在很前面,一次在很后面的交替,就卡死了。

如果按照左端点排序,右端点也可以移来移去还是不行。

我们需要找到一种兼顾左端点和右端点的排序方法,如果按照左端点所在的块排序,块相同的按照右端点排序就会很快。

设块长为 $b$,右端点在每个块内都会移动 $n$ 次,一共 $\frac{n}{b}$ 个块,所以 $\frac{n^2}{b}$。

左端点在每个块内每次都会移动 $b$,跨越块的移动 $b$ 次,每次 $\frac{n}{b}$,所以需要 $nb+n$

现在找到 $b$ 使得这两个加起来最小,显然 $b=\sqrt{n}$,这就是莫队算法。

$0xFF$ 浅谈在线莫队

在线莫队的思想其实很简单,先预处理出一些区间的答案,询问时找到离询问参数 $l$ $r$ 最近的预处理过的 $L$ $R$ 然后转移过去。

怎样预处理呢?我们每隔 $\sqrt{n}$ 个数就留下一个特征点,这样就有 $\sqrt{n}$ 个特征点,每两个特征点搭配组成特征区间,所以大约有 $\frac{n}{2}$ 个特征区间。

每次询问给定参数 $l$ $r$,对于部分莫队题目,加操作(即 l 向左,r 向右)能做,减操作无法处理,就要找到被 $l$ $r$ 包含的最大的特征区间,这样在线莫队代替回滚莫队?!!

对于加操作减操作都能处理的,那就找最近的特征区间啦!

莫队核心代码::

while (r < a[i].r) add (++ r);
while (l > a[i].l) add (-- l);
while (r > a[i].r) rem (r --);
while (l < a[i].l) rem (l ++);

$0xFE$ 莫队总结

讲完这个,我们就来看几道例题。

能用莫队解决的题目,大概具有什么特点呢?

询问可支持离线(在线莫队不管),数据范围不卡 $n\sqrt{n}$(废话),没有修改操作(只是对于简单的莫队而言)

可以以较低的复杂度移动左右指针(为什么是较低,因为后面有一道题 $\log n$ 移动然后卡过了)。

$0x00$ 例题

P2709 小 B 的询问

当然不像我们说的那么简单,维护区间和,这题要维护一个桶。

加入一个数 $x$ 之前其出现次数为 $b[x]$,那么加完后区间的答案 $ans$ 就变成 $ans - b[x]^2+(b[x] + 1)^2$。

当然可以化简成 $ans+2\times b[x]+1$,减去一个数同理啦,略。

#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, k;
int l, r, sum = 1, block;
int c[50010], ans[50010], cnt[50010];
struct Sec{int l, r, id;}a[50010];
void add (int x)
{
    sum += 2 * cnt[c[x]] + 1;
    cnt[c[x]] ++;
}
void rem (int x)
{
    sum -= 2 * cnt[c[x]] - 1;
    cnt[c[x]] --;
}
bool cmp (Sec s1, Sec s2){return s1.l / block < s2.l / block || s1.l / block == s2.l / block && s1.r < s2.r;}
int main()
{
    scanf("%d%d%d", &n, &m, &k);
    block = sqrt(n);
    for (int i = 1; i <= n; i ++) scanf("%d", &c[i]);
    for (int i = 1; i <= m; i ++)
    {
        scanf("%d%d", &a[i].l, &a[i].r);
        a[i].id = i;
    }
    sort (a + 1, a + m + 1, cmp);
    l = r = a[1].l;
    cnt[c[l]] ++;
    for (int i = 1; i <= m; i ++)
    {
        while (r < a[i].r) add (++ r);
        while (l > a[i].l) add (-- l);
        while (r > a[i].r) rem (r --);
        while (l < a[i].l) rem (l ++);
        ans[a[i].id] = sum;
    }
    for (int i = 1; i <= m; i ++) cout << ans[i] << "\n";
    return 0;
}

 

P4462 异或序列

属于是半思维半莫队啦!

首先你需要知道异或具有自反性,即 $a\oplus a=0$。

令 $o_i=a_1\oplus a_2\cdots \oplus a_i$,那么 $a_x\oplus a_{x+1}\cdots \oplus a_y$ 就等于 $o_y \oplus o_{x-1}$。

询问有多少子区间异或和为 $k$ 其实就相当于询问有多少个点 $x$ $y$,其中 $l-1 \leq x\leq r-1$,$l\leq y\leq r$,使得 $o_x \oplus o_y = k$。

$k$ 是已知的,假如我们要假如第 $R$ 个位置,那就需要统计在合法的范围内,$o_L \oplus o_R = k$,$L$ 的数量喽,可以先算出 $o_L$ 的值 $=k\oplus o_R$。

那么我们用一个桶维护,$b[x]$ 维护的就是 $o_L=x$ 的数量,减也同理就不说了。(注意这里我没有加减函数都写,而是用一个参数使一个函数替代这两个的作用了,码风更加清新)

#include <iostream>
#include <algorithm>
using namespace std;
int n, m, k;
int l = 1, r, res;
int a[100005], b[100005], ans[100005];
struct Node {int l, r, id;}q[100005];
bool cmp (Node n1, Node n2) {return n1.l / 300 < n2.l / 300 || n1.l / 300 == n2.l / 300 && n1.r < n2.r;}
void fun (int x, int s) {
    res += s * b[x ^ k];
    b[x] += s;
}
int main () {
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        a[i] ^= a[i - 1];
    }
    for (int i = 1; i <= m; i ++) {
        cin >> q[i].l >> q[i].r;
        -- q[i].l;
        q[i].id = i;
    }
    sort (q + 1, q + m + 1, cmp);
    for (int i = 1; i <= m; i ++) {
        while (r < q[i].r) fun (a[++ r], 1);
        while (l > q[i].l) fun (a[-- l], 1);
        while (l < q[i].l) fun (a[l ++], -1);
        while (r > q[i].r) fun (a[r --], -1);
        ans[q[i].id] = res;
    }
    for (int i = 1; i <= m; i ++) cout << ans[i] << "\n";
    return 0;
}

其他题目

关于莫队的题目数不胜数,给个题单,https://www.luogu.com.cn/training/38213#problems。

带修莫队

带修莫队,顾名思义就是带修改的莫队,其实也很简单。

我们每个询问有三个参数了:$l$ $r$ $t$,$t$ 是前面经过了多少次的修改操作。

询问的排序大家可以先不看,自己口胡一下。

 

 

 

 

按照左端点所在的块排序,相同的按照右端点所在的块排序,再相同按照 $t$ 排序。

时间复杂度如何呢?我们设块长为 $b$,那么左端点每次会移动 $b$ 次,一共 $n$ 次,跨越区间移动 $n/b$ 次,每次 $b$,总共 $nb$。

右端点,它每次会移动 $b$ 次。跨越块的移动忽略不计。每次左端点换到下一个块了右端点移动 $n$ 次,一共切换 $n/b$ 次,时间复杂度为 $n^2/b+nb$。

$t$ 最好算了,左端点和右端点每在一个块内时,也就是 $(n/b)^2$ 种,每次右从最前到最后移动 $n$ 次,所以 $n^4/b^2$。

总时间复杂度为 $n^2/b+n^4/b^2+nb$,当 $b$ 取 $n^{\frac{2}{3}}$ 时有最优复杂度 $n^{\frac{3}{5}}$,足以过 $1e5$。

例题:[P1903 数颜色](https://www.luogu.com.cn/problem/P1903)。写带修莫队时注意,看我发的警示贴:https://www.luogu.com.cn/discuss/525358

暂且更到这里。