根号算法学习笔记
最近整理并学习了一些根号算法,总共分为三个。
莫队
分块
根号分治
莫队
序列莫队
这是一个离线算法(当然有在线的, 但是 CCF 不会卡吧)。
它可以在
莫队的思想就是维护一个当前拥有信息的指针
假如要求
现在归纳一下,假如有的信息是
所以我们要把所有的询问排一下序,使得
所以现在要做的就是找到一种容易求得的排列方式,代价又小。
如果仅仅按照右端点来排序,左端点一次在很前面,一次在很后面的交替,就卡死了。
如果按照左端点排序,右端点也可以移来移去还是不行。
我们需要找到一种兼顾左端点和右端点的排序方法,如果按照左端点所在的块排序,块相同的按照右端点排序就会很快。
设块长为
左端点在每个块内每次都会移动
现在找到
浅谈在线莫队
在线莫队的思想其实很简单,先预处理出一些区间的答案,询问时找到离询问参数
怎样预处理呢?我们每隔
每次询问给定参数
对于加操作减操作都能处理的,那就找最近的特征区间啦!
莫队核心代码::
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 ++);
莫队总结
讲完这个,我们就来看几道例题。
能用莫队解决的题目,大概具有什么特点呢?
询问可支持离线(在线莫队不管),数据范围不卡
可以以较低的复杂度移动左右指针(为什么是较低,因为后面有一道题
例题
P2709 小 B 的询问
当然不像我们说的那么简单,维护区间和,这题要维护一个桶。
加入一个数
当然可以化简成
#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 异或序列
属于是半思维半莫队啦!
首先你需要知道异或具有自反性,即
令
询问有多少子区间异或和为
那么我们用一个桶维护,
#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; }
P3834 【模板】可持久化线段树
也是一道莫队的题目,我们可以采用莫队套线段树,每次加一个数就在权值线段树上加,查询就线段树上二分,时间复杂度为
当然,如果你不会线段树,也不要紧,这仅仅是一个跳板,我们发现每加一个数时间复杂度
这导致每次移动完左右区间,查询的复杂度加起来仅为
代码大家可以参考扶咕咕的题解。
其他题目
关于莫队的题目数不胜数,给个题单,https://www.luogu.com.cn/training/38213#problems。
带修莫队
带修莫队,顾名思义就是带修改的莫队,其实也很简单。
我们每个询问有三个参数了:
即我们把时间轴也当成一个参数来移动了,如果看不懂就看代码。
询问的排序大家可以先不看,自己口胡一下。
这里我要说一下区间的转移,首先还是
这样就可以做修改操作时不影响答案,举个例子:
其中
大家大概明白了吧,但是这个我不好用准确的语言描述。
排序方法就是按照左端点所在的块排序,相同的按照右端点所在的块排序,再相同按照
时间复杂度如何呢?我们设块长为
右端点,它每次会移动
总时间复杂度为
P1903 数颜色
写带修莫队时注意,看我发的警示贴
这题是一个最基本的带修莫队,主要是还要开一个结构体存修改操作,警示贴大家看过了我就不多说了,代码虽然很长,只要看带修莫队移动指针的部分就行了:
#include <cmath> #include <iostream> #include <algorithm> using namespace std; char op; int n, m, x, y; int su, sq, block;//su:sum of update sq:sum of query int l, r, tim, sum = 1;//time:time 但是不让用这个变量 int t[140000], ans[140000], cnt[1000010], color[140000]; struct Question{int l, r, t, id;}q[140000]; void add(int x) { if (! cnt[x]) ++ sum; ++ cnt[x]; } void rem(int x) { -- cnt[x]; if (! cnt[x]) -- sum; } struct Upd{int id, pre, val;}u[140000]; bool cmp (Question q1, Question q2) { if (q1.l / block < q2.l / block) return true; if (q1.l / block == q2.l / block && q1.r / block < q2.r / block) return true; if (q1.l / block == q2.l / block && q1.r / block == q2.r / block && q1.t < q2.t) return true; return false; } int main () { scanf("%d%d", &n, &m); block = pow (n, 2.0 / 3); for (int i = 1; i <= n; ++ i) { scanf("%d", &color[i]); t[i] = color[i]; } for (int i = 1; i <= m; ++ i) { cin >> op >> x >> y; if (op == 'Q') q[++ sq] = Question{x, y, su, sq}; else { u[++ su] = {x, t[x], y}; t[x] = y; } } sort (q + 1, q + sq + 1, cmp); l = r = q[1].l; cnt [color[l] ] ++; for (int i = 1; i <= sq; i ++) { while (r < q[i].r) add(color[++ r]); while (l > q[i].l) add(color[-- l]); while (r > q[i].r) rem(color[r --]); while (l < q[i].l) rem(color[l ++]); while (tim < q[i].t) { tim ++; if (l <= u[tim].id && r >= u[tim].id) { rem(u[tim].pre); add(u[tim].val); } color[u[tim].id] = u[tim].val; } while (tim > q[i].t) { if (l <= u[tim].id && r >= u[tim].id) { rem (u[tim].val); add (u[tim].pre); } color[u[tim].id] = u[tim].pre; tim --; } ans[q[i].id] = sum; } for (int i = 1; i <= sq; i ++) cout << ans[i] << "\n"; return 0; }
P2464 郁闷的小 J
和上一题一样,只不过要离散化,我这里用了一个垃圾回收站来处理,实在不行用 map 亦可。
代码略(好像是 ctj AC 的???)
莫队的优化
最简单的一个就是奇偶排序了,就从最基础的序列莫队看,当一个块处理完后,右端点最坏在最后一个,而在下一个块开始时,右端点又在第一个了。
每过一个块,右端点就会多移动
大家有没有想过带修莫队,左端点总共只移动
其他莫队
其他的感觉大家看看题解啊能自己口胡出来的,那我就不说了,在线莫队都讲过了
分块
序列分块
遇到一道毒瘤题时,大家的选择是什么?就打数据结构认怂吗?不!我们还有对付毒瘤的专用武器,那就是分块。
分块常数小,甚至在数据 CPU 配置相同的情况下比单 log 的平衡树还要快(
我们来介绍一下分块算法吧,如果你学过线段树,那就会很轻松。
分块算法的思路也是打懒标记,一个长度为
每个大块都会维护它的信息诸如和,懒标记等一些东西。
询问的时候,设左右端点为
可以发现,最多找到
大块旁边的元素个数是小于
所以一次询问的时间复杂度为
例题
P3372 【模板】线段树 1
多么好的一道数据结构模板题啊,可是我就要用分块。
刚刚的内容已经讲的很清晰了,大家试着自己写下代码,我贴个 AC 代码:
#include <cmath> #include <iostream> using namespace std; long long n, m, sz; long long op, x, y, k; long long a[100010], sum[316], add[316]; int sec (int x) { if (x > sz * sz) return sz; return x / sz + (x % sz != 0); } int len (int x) { int l = sz * (x - 1) + 1, r = sz * x; if (x == sz) r = n; return r - l + 1; } int main () { scanf("%lld%lld", &n, &m); sz = pow(n, 0.5); for (int i = 1; i <= n; i ++) { scanf("%lld", &a[i]); sum[sec(i)] += a[i]; } while (m --) { scanf("%lld%lld%lld", &op, &x, &y); int secl = sec(x), secr = sec(y); if (op == 1) { scanf("%lld", &k); if (secl == secr) { for (int i = x; i <= y; i ++) a[i] += k; sum[secl] += (y - x + 1) * k; } else { for (int i = x; i <= sz * secl; i ++) a[i] += k; sum[secl] += k * (sz * secl - x + 1); secl ++; for (int i = sz * (secr - 1) + 1; i <= y; i ++) a[i] += k; sum[secr] += k * (y - sz * (secr - 1) ); secr --; for (int i = secl; i <= secr; i ++) add[i] += k; } } else { long long ans = 0; if (secl == secr) { for (int i = x; i <= y; i ++) ans += a[i]; printf("%lld\n", ans + add[secl] * (y - x + 1) ); } else { for (int i = x; i <= sz * secl; i ++) ans += a[i] + add[secl]; secl ++; for (int i = sz * (secr - 1) + 1; i <= y; i ++) ans += a[i] + add[secr]; secr --; for (int i = secl; i <= secr; i ++) ans += sum[i] + add[i] * len(i); printf("%lld\n", ans); } } } return 0; }
虽然在这个时候线段树的代码比分块短些,但是在有多个修改操作查询操作,(比如区间乘上
P5356 [Ynoi2017] 由乃打扑克
毒瘤题,不过掌握分块了还是很好写的,把块内排序然后二分就行了。
但排序了会打乱顺序,所以要一个备用数组。
先来考虑如何朴素的求出第
设块长为
当
当
二分还要乘上
再来看修改,显然:大块打标记,小块暴力搞。
时间复杂度为
总复杂度
其他算法几乎过不掉,不过分块的常数小,过了(
值域分块
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异