莫队(普通莫队,带修莫队,树上莫队)

article/2025/10/13 6:47:36
  1. 听说 莫队算法是一种“优雅的暴力”(小声bb)。

普通莫队

1/ 引入

problem:给你一个长度为n的数组,有m次查询,每次查询询问一个区间[L, R]内有多少个不同的数。

  1. 首先想想暴力怎么做。
    开一个数组用来记数,然后扫一遍[L,R],如果这个数是第一次出现,那么对答案贡献+1。暴力出来的时间复杂度是O(n*m),如果n、m较大,那暴力肯定是不行的。
  2. 再想想进一步优化。
    开两个指针 lr,将之前的每次扫区间[L,R]改成移动指针l、r,使得指针与区间边界对齐。在移动指针的时候,对应地去删除或添加对答案的贡献即可。但问题来了,如果每次询问的区间[L,R]都需要移动很长的距离,就比如第一次询问[1,2],第二次询问[n-1,n],这么一来时间复杂度依旧是O(n*m)。优化了个寂寞

2/ 莫队算法的实现

  • 回到刚才的第二种思路,我们可不可以在原来的基础上,对左区间进行排序以保证指针l不会移动到曾经被左指针扫过的地方,思想是好的,但是不能确保指针r的移动不重复,所以还是不行。

这个时候就需要莫队算法了。它的核心是分块和排序:将长度为n的数组分为若干个块,每个块内有sqrt(n)个元素,再将每个块按顺序编号,然后排序(对于区间左端点来说,如果在同一个块内,就按右端点排序,否则按左端点排序)。时间复杂度是O(n^1.5),优化了很大一部分。详细的时间复杂度证明可自行百度

另外多数莫队题的输入输出量是比较大的,用cin大概率会tle,老老实实用scanf或者快读吧

如果足够细心应该会注意到,对要询问的区间进行排序,那将会打乱原有的询问顺序。换而言之,必须要先输入所有询问,再一次性输出所有对应的答案,也就是我们通常所说的离线操作


Code

数据说明:

  • e[ ].l :询问的左端点。 e[ ].r :询问的右端点。 e[ ].id :询问的次序。
  • size :每个区间的大小,即 sqrt(n)。
  • cnt[i] :记 i 出现的次数。
  • belong[i] :记 i 属于的块的编号,即 i/size。

  1. 结构体排序(未优化):
bool cmp(node x, node y) {if (belong[x.l] == belong[y.l]) return x.r < y.r;return x.l < y.l;
}
  1. .结构体排序(优化后):
bool cmp(node x, node y) {return belong[x.l]^belong[y.l] ? x.l < y.l : (belong[x.l]&1)? x.r < y.r : x.r > y.r;
}

首先用位运算代替了if,这样会快些。另外对于左端点在同一奇数块的区间,右端点按升序排列,反之降序(不论奇数升序偶数降序、还是偶数升序奇数降序 其实都是不影响优化后的复杂度的),时间大概能优化一半吧,至于奇偶排序具体原理嘛。。。简单来说, emm还是画个图吧(灵魂画手show time)
在这里插入图片描述
这里R指针的移动是上下上下上,这是按照R升序排列的,那按照奇偶排列的话,那就是这样的:
在这里插入图片描述
R指针只需要上下上就完成了,就相当于把两个波峰合并成了一个波峰,那么扫过的路径就会减少大致一半,从而优化了近一半的时间。

完整代码:

const int maxn = 1e6+5;struct node {int l, r, id;
}e[maxn];
int n, m, size, l, r;
ll a[maxn], ans[maxn], cnt[maxn], sum;
int belong[maxn];// 优化 
bool cmp(node x, node y) {return belong[x.l]^belong[y.l] ? x.l < y.l : (belong[x.l]&1)? x.r < y.r : x.r > y.r;
}
/* 
bool cmp(node x, node y) {if (belong[x.l] == belong[y.l]) return x.r < y.r;return x.l < y.l;
}
*/ void add(int pos) {if (!cnt[a[pos]]) sum++;cnt[a[pos]]++;
}
void del(int pos) {cnt[a[pos]]--;if (!cnt[a[pos]]) sum--;
}
int main() {read(n);size = sqrt(n);for (int i = 1; i <= n; i++) {read(a[i]);belong[i] = (i-1)/size;} read(m);for (int i = 1; i <= m; i++) {read(e[i].l), read(e[i].r);e[i].id = i;}sort(e+1, e+1+m, cmp);l = 1, r = 0, sum = 0;for (int i = 1; i <= m; i++) {while (l < e[i].l) del(l++);while (l > e[i].l) add(--l);while (r < e[i].r) add(++r);while (r > e[i].r) del(r--);ans[e[i].id] = sum;}for (int i = 1; i <= m; i++) {cout << ans[i] << endl;}return 0;
}

带修莫队

1/ 引入

  • 前面说过莫队算法是离线算法,按理来说,输入的数据是不能修改的,但是对于一些允许离线的需要修改区间的查询来说,带修莫队也是可以解决的。

2/ 带修莫队的实现

其实和普通莫队差不多,之前不是用到了指针L,R嘛,带修莫队的区别就在于加了一个维度------时间
如果不修改的话,就沿用之前的时间戳;修改的话时间戳++并记录修改的信息。进行查询操作时,通过移动time、L和R指针使得当前区间与所查询区间的左右端点、时间戳均相同,所得即答案。
说白了就是在之前[l,r+1]、[l,r-1]、[l+1,r]、[l-1,r] (时间戳不变)的基础上,再加两个[l,r,t+1]、[l,r,t-1]就行了
至于排序嘛,升序排列,优先级是L,R,time

  • 当修改后,有可能要改回来,所以还要记录好原来的值。但其实也可以不存,只要在修改后把修改的值和原值交换一下,那么改回来时也只要交换一下,两次交换后不就相当于还原了嘛。

还有一点就是关于块的大小,经过证明,当块的大小为(n^4*t)的立方根时复杂度最优。块的大小可以取pow(n,2.0/3.0),那么时间复杂度为pow(n,5.0/3.0)这个时间还能接受吧。

Code

const int maxn = 2e6+5;
int n, m, siz, cntq, cntc, now, t, l, r;
int a[maxn], belong[maxn], ans[maxn], cnt[maxn]; struct node1 {int l, r, id, tim;
}e[maxn];
struct node2 {int pos, val;
}c[maxn];bool cmp(node1 x, node1 y) {return (belong[x.l]^belong[y.l]) ? x.l<y.l : (belong[x.r]^belong[y.r]) ? x.r < y.r : x.tim < y.tim;
}void add(int x) {if (!cnt[a[x]]) now++;cnt[a[x]]++;
}
void del(int x) {cnt[a[x]]--;if (!cnt[a[x]]) now--;
}int main() {read(n), read(m);siz = pow(n, 2.0/3);for (int i = 1; i <= n; i++) {read(a[i]);belong[i] = (i-1)/siz;}cntq = cntc = now = 0;for (int i = 1; i <= m; i++) {char op[50];scanf("%s", op);if (op[0] == 'Q') {read(e[++cntq].l);read(e[cntq].r);e[cntq].id = cntq;e[cntq].tim = cntc;} else {read(c[++cntc].pos);read(c[cntc].val);}}sort(e+1, e+1+cntq, cmp);l = 1, r = 0, t = 0;for (int i = 1; i <= cntq; i++) {while (l < e[i].l) del(l++);while (l > e[i].l) add(--l);while (r < e[i].r) add(++r);while (r > e[i].r) del(r--);while (t < e[i].tim) {t++;if (e[i].l <= c[t].pos && c[t].pos <= e[i].r) {del(c[t].pos);if (!cnt[c[t].val]) now++;cnt[c[t].val]++;}swap(a[c[t].pos], c[t].val);}while (t > e[i].tim) {if (e[i].l <= c[t].pos && c[t].pos <= e[i].r) {del(c[t].pos);if (!cnt[c[t].val]) now++;cnt[c[t].val]++;				}swap(a[c[t].pos], c[t].val);t--;}ans[e[i].id] = now;}for (int i = 1; i <= cntq; i++) {cout << ans[i] << endl;}return 0;
}

树上莫队

1/ 引入

  • 适用通常是:给你一棵树,求点u到v之间有多少个不同的数

2/ 树上莫队的实现

首先介绍欧拉序

在这里插入图片描述
对于这棵树,对应的欧拉序是:1 2 4 7 7 4 5 5 2 3 6 8 9 9 10 10 11 11 8 6 3 1
对照一下图和序列,应该不难知道欧拉序的定义。

经过仔细的观察,不难发现 (直接上结论吧还是)

树的欧拉序上两个相同编号(设为x)之间的所有编号都出现两次,且都位于x子树上

(先假设你会求LCA,具体怎么求后面会讲)
那么假设设每个点(编号为a)首次出现的位置first[a],最后出现的位置为last[a],那么对于路径u→v,不妨默认first[u]<=first[v](不满足的话交换就好了,这个操作的意义在于,如果u、v在一条链上,则u一定是v的祖先或等于v),相当于免去了分类讨论嘛。如果lca(u,v)=u,则直接查询[first[u],first[v]];否则查询[last[u],first[v]]区间,但这个区间是不包含u和v的LCA的,只需要在查询的时候额外加上即可,这样一来就把树上的问题转换成了区间问题,接下来就成了普通莫队啦。

Code

const int maxn = 1e5+5;
int n, m, id, cntn, now;
int a[maxn], head[maxn], val[maxn], tmp[maxn], fir[maxn], las[maxn], ord[maxn], vis[maxn], f[maxn][30], dep[maxn], belong[maxn], cnt[maxn], ans[maxn];
// ord[]---欧拉序struct node {int to, nex;
}e[maxn];
void addedge(int u, int v) { e[++id].to = v;e[id].nex = head[u];head[u] = id;
}void dfs(int x) {ord[++cntn] = x;fir[x] = cntn;for (int i = head[x]; i != -1; i = e[i].nex) {int to = e[i].to;if (to != f[x][0]) {dep[to] = dep[x] + 1;f[to][0] = x;for (int j = 1; (1 << j) <= dep[to]; j++) f[to][j] = f[f[to][j-1]][j-1];dfs(to);}}ord[++cntn] = x;las[x] = cntn;
}int LCA(int u, int v) { // LCA后面会有讲解if (dep[u] < dep[v]) swap(u, v);for (int i = 20; i >= 0; i--)if (dep[f[u][i]] >= dep[v])u = f[u][i];if (u == v) return u;for (int i = 20; i >= 0; i--) {if (f[u][i] != f[v][i]) {u = f[u][i];v = f[v][i];}}return f[u][0];
}struct node2 {int l, r, id, lca;
}q[maxn];
int cmp(node2 x, node2 y) {return (belong[x.l]^belong[y.l]) ? x.l < y.l : (belong[x.l]&1) ? x.r < y.r : x.r > y.r;  
}void work(int pos) { //通过忽略走过两遍的点(欧拉序的特性)vis[pos] ? now -= !(--cnt[val[pos]]) : now += !(cnt[val[pos]]++);vis[pos] ^= 1;
}int main() {read(n), read(m);mem(head, -1);for (int i = 1; i <= n; i++) read(val[i]), tmp[i] = val[i];sort(tmp+1, tmp+1+n);// 数据范围大,需要离散化int tot = unique(tmp+1, tmp+1+n) - tmp-1;for (int i = 1; i <= n; i++) {val[i] = lower_bound(tmp+1, tmp+tot+1, val[i]) - tmp;}for (int k = 1; k < n; k++) {int u, v;read(u), read(v);addedge(u, v);addedge(v, u);}dep[1] = 1;dfs(1);int siz = sqrt(cntn);for (int i = 1; i <= cntn; i++) {belong[i] = (i-1)/siz;}for (int i = 1; i <= m; i++) {int l, r, lca;read(l);read(r);lca = LCA(l, r);if (fir[l] > fir[r]) swap(l, r);if (l == lca) {q[i].l = fir[l];q[i].r = fir[r];} else {q[i].l = las[l];q[i].r = fir[r];q[i].lca = lca;}q[i].id = i;}sort(q+1, q+1+m, cmp);int l = 1, r = 0;for (int i = 1; i <= m; i++) {int ql = q[i].l, qr = q[i].r, qlca = q[i].lca;while (l < ql) work(ord[l++]);while (l > ql) work(ord[--l]);while (r < qr) work(ord[++r]);while (r > qr) work(ord[r--]);if (qlca) work(qlca); //lca不在序列内ans[q[i].id] = now;if (qlca) work(qlca); //还原}for (int i = 1;i <= m; i++) {cout << ans[i] << endl;}return 0;
}

http://chatgpt.dhexx.cn/article/Em3UbZ6u.shtml

相关文章

回滚莫队解析

&#xff08;南昌理工ACM集训队&#xff09; 泻药&#xff0c;不匿&#xff08;bushi&#xff09; 浅谈莫队回滚莫队样例小结 浅谈莫队 莫队是要摸清回滚莫队必须掌握的前置技能(๑ŐдŐ)b 前两天打多校有幸接触了莫队算法&#xff0c;花了几天时间搞懂了这些东西&#xff0…

【莫队/树上莫队/回滚莫队】原理详解及例题:小B的询问(普通莫队),Count on a tree II(树上莫队),kangaroos(回滚莫队)

文章目录 问题引入介绍莫队算法及其实现过程时间复杂度莫队算法适用范围莫队奇偶优化普通莫队&#xff1a;小B的询问树上莫队&#xff1a;SP10707 COT2 - Count on a tree II回滚莫队&#xff1a;[PA2011]Kangaroos upd&#xff1a;2021-08-11&#xff1a;重新对博客进行了外观…

超全的莫队算法一遍过

前置知识&#xff1a;双指针、分块 简单概括而言&#xff0c;莫队是一个离线算法&#xff0c;核心思想是将所有的询问按照分块进行排序后&#xff0c;对于每个询问可以通过双指针增删数据来达到总体的复杂度。 莫队通常用来解决求一段区间内不同数的个数相关的问题。 目录 1、…

莫队(普通操作)C++

由于&R闲着无聊&#xff0c;今天上午学习的莫队也并不是完全掌握&#xff0c;老师才发的PPT也没有保存&#xff0c;于是决定乱写一篇靠着自己的理解写一篇关于莫队用法的博客......如果有错的麻烦之处&#xff0c;纯属&R自己理解有问题哈。 一.莫队的含义 莫队是在一…

【算法笔记】莫队算法(基础莫队,带修莫队,回滚莫队,树上莫队,二次离线莫队)

整理的算法模板合集&#xff1a; ACM模板 目录 1. 基础莫队2. 带修莫队3. 回滚莫队4. 树上莫队5. 二次离线莫队6. 在线莫队 来这里学习莫队以及神奇的证明&#xff1a;莫队算法 --算法竞赛专题解析&#xff08;26&#xff09; 我们首先考虑双指针的暴力法&#xff0c;发现很容…

linux(vscode)配置coderunner for python3

1.Python3 安装路径查看 which python3 2.vscode扩展中安装coderunner 3.配置coderunner 管理->设置->扩展->coderunner,找到下图所示内容&#xff0c;选择在settings.json中编辑。 在文件中输入code-runner.executorMap&#xff0c;会自动补全代码&#xff0c;如…

问题解决:VSCode Code Runner输出中文乱码

用vscode code runner插件经常会出现输出中文乱码的问题 那么如何解决这个问题呢&#xff1f; 解决方法&#xff1a; 通过设置&#xff0c;可以把代码放到 VS Code 内置的 Terminal 来运行&#xff0c;那么问题就能迎刃而解了。 选择 文件 -> 首选项 -> 设置&#xff0c…

VSCode——使用CodeRunner开发python输出中文出现乱码的解决方法

目录 方法一&#xff1a;每次都要加 方法二&#xff1a;一劳永逸 方法一 在程序中加入如下代码强行控制输出格式为utf-8即可 # -*- coding: utf-8 -*- import sys import io sys.stdoutio.TextIOWrapper(sys.stdout.buffer,encodingutf8)#改变默认输出的标准编码 编码改变…

修改CodeRunner快捷键

默认运行是 alt ctrl N, 改为alt ctrl r. run改更合理。 停止改为alt ctrl s stop也更合理&#x1f43b;

vscode用CodeRunner运行C++文件,提示系统找不到路径

问题描述 如图&#xff0c;直接点运行出现如下提示&#xff1a; 但是分开执行还是正常的&#xff1a; 原因 cmd执行多条语句要使用" && “作为连接符&#xff0c;对应的powerShell要用” ; "。 解决 在用户设置中修改code-runner.executorMap中对应的值…

vscode_Code Runner(codeRunner)配置(自定义输入映射内容)/指定.c/.cpp编译选型/编译运行含有中文名的文件示例(最佳实践)

文章目录 声明:文件(目录)中包含中文的问题关于配置调试功能(推荐) 找到配置入口点击在setting.json中编辑可以指定各种语言的命令行映射,这里以c语言指定以c99编译修改为例.可见到,届时编译.c文件的时候就会是以C99的标准指定编译. 运行效果 声明:文件(目录)中包含中文的问题 …

用VSCode 编写C++ 引入自定义头文件容易犯的错 和CodeRunner配置

问题描述 undefined reference to log(char const*) collect2.exe: error: ld returned 1 exit status 原因分析&#xff1a; 一般是由于c编译时&#xff0c;.cpp源文件未找到引起的 解决方案&#xff1a; 1.正确操作 一.例如你一个文件下有一个.h的文件和一个.cpp的文件 在v…

VSCode用Code Runner编译运行c/c++

首先vscode需要在文件夹下打开&#xff0c;以下所有的文件都需要放到你编写程序文件夹下的.vscode文件夹里 平时可以右击文件夹&#xff0c;选择open with code&#xff0c;结构应该是这样的 配置CodeRunner 下载 左下角齿轮找到设置 在扩展中找到Run Code configuration,点击…

【Mac】VScode配置Python开发环境详细教程(报错解决Import Error No module named ) CodeRunner插件

文章目录 在VScode中安装python插件解决报错SyntaxError: Non-ASCII character \xef in file解决报错"No module named xxx "VScode上使用Jupyter Notebook的方法20.02.26 最新方法&#xff1a;Code Runner插件一键安装&#xff08;python、java、C&#xff09;终端目…

CodeRunner插件:自定义编译参数

在设置里找到Coderunner选项&#xff0c;然后找到executorMap 点击 在 settings.json 中编辑 &#xff0c;你将看到对不同语言的编译指令。 以下为本人设置&#xff0c;为Mac系统&#xff0c;指令有所不同&#xff0c;不过操作流程是一样的。 "code-runner.executorMap…

java扩展包_CodeRunner 的 Java 扩展 Jar 包支持

CodeRunner 介绍 CodeRunner 是 Mac 上一款功能强大但使用简单代码工具&#xff0c;官方介绍支持几乎所有语言(20种语言)&#xff0c;同时支持语法高亮、代码提示和多种界面主题&#xff0c;在学习新的语言或编写简单测试代码时非常实用。 我常常用它来管理一些代码片段和测试不…

vscode 中 coderunner无法运行dart程序

如果你的vscode使用了CodeRunner&#xff0c;运行dart文件出错找不到dart的情况下: /bin/sh: dart: command not found可以直接修改coderunner扩展配置信息, 将其路径改为你希望的位置

vscode使用codeRunner将c/c++程序运行到外部控制台窗口

需要先关闭codeRunner的Run in Terminal 只需要在codeRunner的setting中&#xff0c;在从c/c的设置命令中添加 start cmd /c //或者/k /** 如果添加的是/c&#xff0c;则需要在程序的末尾添加system("pause"),不然会一闪而过&#xff0c; /k则不需要&#xff0c;不…

CodeRunner破解

CodeRunner2是Mac系统下的一款编程软件&#xff0c;支持23种语言和.txt文档制作&#xff0c;比Xcode都强大&#xff0c;Xcode只支持4种语言&#xff0c;原来的破解补丁有联网验证的问题&#xff0c;现在我做了程序防止联网验证。 破解方法&#xff1a;先下载CodeRunner2&#x…

windows系统VS code coderunner 运行shell脚本

无需设置环境变量&#xff0c;即可运行。 shell脚本第一行不要指定shell&#xff0c;什么“#!/bin/bash"不要写&#xff0c;这个会覆盖掉coderunner的设置。 在vs code设置中搜索“Code-runner: Executor Map” 点击“在setting.json中编辑” 修改shellscript后面引号里…