暴力算法 --- 莫队
创始人
2024-03-15 11:57:31
0

文章目录

      • 莫队
        • 基础莫队
        • 带修改莫队
        • 树上莫队
        • 回滚莫队

莫队

什么是莫队?

答:优雅的暴力!!!

基础莫队

重复的数

题目描述:给出一个长度为NNN的序列,有若干查询,每次查询区间[li,ri][l_i, r_i][li​,ri​]出现kik_iki​次的数有多少个?

解题思路:最直观的做法就是拿到区间[l,r][l, r][l,r]后,暴力计算,用cnt[x]cnt[x]cnt[x]保存xxx出现了几个,pos[t]pos[t]pos[t]出现ttt次的数有几个。此时,时间复杂度最坏为:O(n2)\mathcal O(n^2)O(n2)
可以尝试来优化一下这个操作:使用两个指针LLL和RRR(初始化:L=1,R=0L = 1, R = 0L=1,R=0),对于每次查询,依次的把这两个指针移动到目标区间[l,r][l, r][l,r]的同时,更新cntcntcnt和pospospos数组。假设现在目标区间为:[1,5][1, 5][1,5],L=2,R=5L = 2, R = 5L=2,R=5;移动LLL指针左移L--,并更新cntcntcnt和pospospos数组:

L --;
pos[cnt[a[L]]] --;
cnt[a[L]] ++;
pos[cnt[a[L]]] ++;

移完以后,同理移动RRR指针:

R ++;
pos[cnt[a[R]]] --;
cnt[a[R]] ++;
pos[cnt[a[R]]] ++;

可以发现上述都是把不在指针区间[L,R][L, R][L,R]的数加进来了,因此可以封装成一个函数:

void add(int x) { // 把一个数x加入当指针区间内pos[cnt[x]] --;cnt[x] ++;pos[cnt[x]] ++;
}

有增加就有删除,删除也是一样的:

void del(int x) { // 把一个数从指针区间删除pos[cnt[x]] ++;cnt[x] --;pos[cnt[x]] --;
}

为了减少[L,R][L, R][L,R]这两个指针的移动次数,可以先将序列分成n\sqrt nn​块,并从1∼n1 \sim \sqrt n1∼n​编号,对于所有的查询按照块编号,从小到大排序,对于同一块,按照区间右端点升序排序。排完序后,我们再按照上述的操作去移动L,RL, RL,R指针,这样的时间复杂度为:O(nn)\mathcal O(n\sqrt n)O(nn​)

简单证明一下:

  1. 区间排序,sort一遍,O(nlogn)\mathcal O(n log n)O(nlogn)
  2. LLL指针的移动:对于每块最坏跨越整个块n\sqrt nn​,最坏有nnn块需要移动,总的:O(nn)\mathcal O(n\sqrt n)O(nn​)
  3. RRR指针的移动,因为对于每块,区间是按照右端点升序排序,所以在块内最多也移动n\sqrt nn​,最坏有nnn块需要移动,总的O(nn)\mathcal O(n \sqrt n)O(nn​)

综上述:莫队的总时间复杂度为: O(nlogn)+O(nn)+O(nn)=O(nn)\mathcal O(nlogn) + \mathcal O (n \sqrt n) + \mathcal O(n \sqrt n) = \mathcal O(n \sqrt n)O(nlogn)+O(nn​)+O(nn​)=O(nn​)

AC_Code:

#include 
using namespace std;
constexpr int N = 1e5 + 10;
int n, len, m, a[N], cnt[N], pos[N];
int bel(int x) {return x / len;
}
struct query {int l, r, k, id;bool operator < (const query &b){return bel(l) == bel(b.l) ? r < b.r : bel(l) < bel(b.l);}
};void add(int x) {pos[cnt[x]] --; cnt[x] ++;pos[cnt[x]] ++;
}void del(int x) {pos[cnt[x]] --;cnt[x] --;pos[cnt[x]] ++;
}int main() {scanf("%d", &n); len = sqrt(n);for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);scanf("%d", &m);vector q(m);vector ans(m);for (int i = 0; i < m; i ++) {q[i].id = i;cin >> q[i].l >> q[i].r >> q[i].k;}sort(q.begin(), q.end());for (int i = 0, l = 1, r = 0; i < m; i ++) {int ql = q[i].l, qr = q[i].r;while (l > ql) add(a[-- l]);while (r < qr) add(a[++ r]);while (l < ql) del(a[l ++]);while (r > qr) del(a[r --]);ans[q[i].id] = pos[q[i].k];}for (int i = 0; i < m; i ++) cout << ans[i] << "\n";
}

带修改莫队

P1903 [国家集训队] 数颜色 / 维护队列

题目描述:有长度为NNN的序列,现在有两个操作:

  1. QLRQ \ L \ RQ L R 区间[L,R][L, R][L,R]有多少个不同的数
  2. RPCR \ P \ CR P C 把PPP位置的数改成CCC

解题思路:查询操作可以用莫队轻松解决,可是这个修改要怎么办呢?我们可以加上一维时间,使之变为待修改莫队!具体操作如下:

增加一维时间t,在把区间移动到目标区间后,检查当前时间是不是目标时间,不是的话,跟修改L,RL, RL,R指针一样去修改时间t。

这里要注意的是,比如你现在把某个位置的数xxx改成了ccc,此时的时间为t1t_1t1​,在把xxx修改为ccc后,这个时候你要把原来的数xxx记下来,如果以后询问小于t1t_1t1​的时候,这个位置的数是xxx而不是ccc,这里有个简化操作,可以直接把xxx和ccc互换即可,下次操作直接更改就行(具体细节看代码

AC_Code:

#include 
using namespace std;constexpr int N = 1e6 + 10;
int n, m, len, a[N], cnt[N];int bel(int x) {return x / len;
}struct query {int l, r, t, id;bool operator < (const query & b) {return (bel(l) ^ bel(b.l)) ? bel(l) < bel(b.l) : (bel(r) ^ bel(b.r)) ? bel(r) < bel(b.r) : t < b.t; }
};
struct M {int pos, color;
};M modify[N];
int now;
void add(int x) {if(!cnt[x]) ++ now;cnt[x] ++;
}
void del(int x) {cnt[x] --;if(!cnt[x]) -- now;
}
int main() {scanf("%d%d", &n, &m); //len = sqrt(n); 此时退化为了O(n^2)len = pow(n, 2.0 / 3);for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);vector q; int cntc = 0;for (int i = 0; i < m; i ++) {char opt[2]; int l, r;scanf("%s%d%d", opt, &l, &r);if(opt[0] == 'Q') {q.push_back({l, r, cntc, (int)q.size()});} else {modify[++cntc] = {l, r};}}sort(q.begin(), q.end());vector ans(q.size());for (int i = 0, l = 1, r = 0, t = 0; i < (int)q.size(); i ++) {int ql = q[i].l, qr = q[i].r, qt = q[i].t;while (l > ql) add(a[-- l]);while (r < qr) add(a[++ r]);while (l < ql) del(a[l ++]);while (r > qr) del(a[r --]);while (t < qt) {++ t;if(ql <= modify[t].pos && qr >= modify[t].pos) {del(a[modify[t].pos]);add(modify[t].color);}swap(a[modify[t].pos], modify[t].color);}while (t > qt) {if(ql <= modify[t].pos && qr >= modify[t].pos) {del(a[modify[t].pos]);add(modify[t].color);}swap(a[modify[t].pos], modify[t].color);-- t;}ans[q[i].id] = now;}for (int &x : ans) cout << x << "\n";
}

树上莫队

COT2 - Count on a tree II

题目描述:给定NNN个节点的树,每个节点有一个颜色。现在有mmm次询问,每次询问给出两个节点u,vu,vu,v,输出u,vu,vu,v路径上的节点颜色个数。颜色为≤2×109\le 2 × 10^9≤2×109的非负整数

解题思路

先得到树的欧拉序。

欧拉序特点:

每个节点出现两次,且他们中间的节点为这个节点子树上的点

欧拉序的求法:

在dfs的时候,得到一个点加入序列,退出的时候也加入序列,这样就得到了欧拉序

得到欧拉序有什么用呢,可以尝试将两个节点映射成欧拉序上的一段区间,这样问题就可以转换为莫队求区间不同数了
在这里插入图片描述

看上图,1->3很容易得到(就是图中红线)而且之间出现两次的数刚好不会算在其中。但是4->3怎么算?用第一个4指定不行,只能用第二个4,但是我们发现少了1,但是1是什么呢,是4和3的lca!所以,可以得到下面两个结论!(保证first[x]

  1. 如果LCA(x,y)==xLCA(x, y) == xLCA(x,y)==x,区间为[first[x],first[y][first[x], first[y][first[x],first[y] 对应上图1->3情况
  2. 如果LCA(x,y)!=xLCA(x, y) \ != xLCA(x,y) !=x,区间为[second[x],first[y]][second[x], first[y]][second[x],first[y]],同时标记lca为LCA(x,y)LCA(x, y)LCA(x,y),在计算的时候加上lca

AC_Code:

#include 
using namespace std;
template < typename T>
inline void read( T &x)
{x = 0; bool f = 0;char ch = getchar();while(!isdigit(ch)) { f ^= !(ch ^ 45), ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();x = f ? -x : x;
}
template < typename T>
inline void write(T x)
{if(x < 0) putchar('-'), x = -x;if(x > 9) write(x / 10);putchar(x % 10 + 48);
}
template 
inline void write(T x, char ch) {write(x);putchar(ch);
}
constexpr int N = 2e5 + 10;
int n, m, len, a[N], cnt[N], ord[N], first[N], last[N], ncnt, depth[N], fa[N][30];
vector G[N];int bel(int x) {return x / len;
}struct query {int l, r, lca, id;bool operator < (const query & b) {return (bel(l) ^ bel(b.l)) ? bel(l) < bel(b.l) : r < b.r;}
};void dfs(int u) {ord[++ ncnt] = u;first[u] = ncnt;for (int &x : G[u]) {if(x == fa[u][0]) continue;depth[x] = depth[u] + 1;fa[x][0] = u;for (int i = 1; (1 << i) <= depth[x]; i ++)fa[x][i] = fa[fa[x][i - 1]][i - 1];dfs(x);}ord[++ ncnt] = u;last[u] = ncnt;
}int LCA(int u, int v) {if(depth[u] < depth[v]) swap(u, v);for (int i = 20; i + 1;i --)if(depth[u] - (1 << i) >= depth[v]) u = fa[u][i];if(u == v) return u;for (int i = 20; i + 1; i --) if(fa[u][i] != fa[v][i])u = fa[u][i], v = fa[v][i];return fa[u][0];
}int vis[N], now;
vector all;void solve(int pos) {if(vis[pos]) {cnt[a[pos]] --;if(!cnt[a[pos]]) now --;} else {if(!cnt[a[pos]]) now ++;cnt[a[pos]] ++;}vis[pos] ^= 1;
}int main() {read(n); read(m);for (int i = 1; i <= n; i ++)read(a[i]), all.push_back(a[i]);sort(all.begin(), all.end());all.erase(unique(all.begin(), all.end()), all.end());for (int i = 1; i <= n; i ++)a[i] = lower_bound(all.begin(), all.end(), a[i]) - all.begin() + 1;for (int i = 1; i < n; i ++) {int u, v;read(u); read(v);G[u].push_back(v); G[v].push_back(u);}depth[1] = 1;dfs(1);len = sqrt(ncnt);vector q(m);for (int i = 0; i < m; i ++) {int l, r;read(l); read(r);int lca = LCA(l, r);if(first[l] > first[r]) swap(l, r);if(l == lca) {q[i].l = first[l];q[i].r = first[r];} else {q[i].l = last[l];q[i].r = first[r];q[i].lca = lca;}q[i].id = i;}sort(q.begin(), q.end());vectorans(m);for (int i = 0, l = 1, r = 0; i < m; i ++) {int ql = q[i].l, qr = q[i].r, lca = q[i].lca;while (l > ql) solve(ord[-- l]);while (r < qr) solve(ord[++ r]);while (l < ql) solve(ord[l ++]);while (r > qr) solve(ord[r --]);if(lca) solve(lca);ans[q[i].id] = now;if(lca) solve(lca);}for (int &x : ans) write(x, '\n');
}

回滚莫队

// TODO 待补

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...