文章目录
- 莫队
- 基础莫队
- 带修改莫队
- 树上莫队
- 回滚莫队
莫队
什么是莫队?
答:优雅的暴力!!!
基础莫队
重复的数
题目描述:给出一个长度为 N N N的序列,有若干查询,每次查询区间 [ l i , r i ] [l_i, r_i] [li,ri]出现 k i k_i ki次的数有多少个?
解题思路:最直观的做法就是拿到区间
[
l
,
r
]
[l, r]
[l,r]后,暴力计算,用
c
n
t
[
x
]
cnt[x]
cnt[x]保存
x
x
x出现了几个,
p
o
s
[
t
]
pos[t]
pos[t]出现
t
t
t次的数有几个。此时,时间复杂度最坏为:
O
(
n
2
)
\mathcal O(n^2)
O(n2)
可以尝试来优化一下这个操作:使用两个指针
L
L
L和
R
R
R(初始化:
L
=
1
,
R
=
0
L = 1, R = 0
L=1,R=0),对于每次查询,依次的把这两个指针移动到目标区间
[
l
,
r
]
[l, r]
[l,r]的同时,更新
c
n
t
cnt
cnt和
p
o
s
pos
pos数组。假设现在目标区间为:
[
1
,
5
]
[1, 5]
[1,5],
L
=
2
,
R
=
5
L = 2, R = 5
L=2,R=5;移动
L
L
L指针左移L--
,并更新
c
n
t
cnt
cnt和
p
o
s
pos
pos数组:
L --;
pos[cnt[a[L]]] --;
cnt[a[L]] ++;
pos[cnt[a[L]]] ++;
移完以后,同理移动 R R R指针:
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 n n块,并从 1 ∼ n 1 \sim \sqrt n 1∼n编号,对于所有的查询按照块编号,从小到大排序,对于同一块,按照区间右端点升序排序。排完序后,我们再按照上述的操作去移动 L , R L, R L,R指针,这样的时间复杂度为: O ( n n ) \mathcal O(n\sqrt n) O(nn)
简单证明一下:
- 区间排序,
sort
一遍, O ( n l o g n ) \mathcal O(n log n) O(nlogn) - L L L指针的移动:对于每块最坏跨越整个块 n \sqrt n n,最坏有 n n n块需要移动,总的: O ( n n ) \mathcal O(n\sqrt n) O(nn)
- R R R指针的移动,因为对于每块,区间是按照右端点升序排序,所以在块内最多也移动 n \sqrt n n,最坏有 n n n块需要移动,总的 O ( n n ) \mathcal O(n \sqrt n) O(nn)
综上述:莫队的总时间复杂度为: O ( n l o g n ) + O ( n n ) + O ( n n ) = O ( n n ) \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 <bits/stdc++.h>
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<query> q(m);
vector<int> 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 [国家集训队] 数颜色 / 维护队列
题目描述:有长度为 N N N的序列,现在有两个操作:
- Q L R Q \ L \ R Q L R 区间 [ L , R ] [L, R] [L,R]有多少个不同的数
- R P C R \ P \ C R P C 把 P P P位置的数改成 C C C
解题思路:查询操作可以用莫队轻松解决,可是这个修改要怎么办呢?我们可以加上一维时间,使之变为待修改莫队!具体操作如下:
增加一维时间t,在把区间移动到目标区间后,检查当前时间是不是目标时间,不是的话,跟修改 L , R L, R L,R指针一样去修改时间t。
这里要注意的是,比如你现在把某个位置的数 x x x改成了 c c c,此时的时间为 t 1 t_1 t1,在把 x x x修改为 c c c后,这个时候你要把原来的数 x x x记下来,如果以后询问小于 t 1 t_1 t1的时候,这个位置的数是 x x x而不是 c c c,这里有个简化操作,可以直接把 x x x和 c c c互换即可,下次操作直接更改就行(具体细节看代码
AC_Code:
#include <bits/stdc++.h>
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<query> 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<int> 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
题目描述:给定 N N N个节点的树,每个节点有一个颜色。现在有 m m m次询问,每次询问给出两个节点 u , v u,v u,v,输出 u , v u,v u,v路径上的节点颜色个数。颜色为 ≤ 2 × 1 0 9 \le 2 × 10^9 ≤2×109的非负整数
解题思路:
先得到树的欧拉序。
欧拉序特点:
每个节点出现两次,且他们中间的节点为这个节点子树上的点
欧拉序的求法:
在dfs的时候,得到一个点加入序列,退出的时候也加入序列,这样就得到了欧拉序
得到欧拉序有什么用呢,可以尝试将两个节点映射成欧拉序上的一段区间,这样问题就可以转换为莫队求区间不同数了。
看上图,1->3很容易得到(就是图中红线)而且之间出现两次的数刚好不会算在其中。但是4->3怎么算?用第一个4指定不行,只能用第二个4,但是我们发现少了1,但是1是什么呢,是4和3的lca!所以,可以得到下面两个结论!(保证 f i r s t [ x ] < f i r s t [ y ] first[x] < first[y] first[x]<first[y],不满足就交换一下)
- 如果 L C A ( x , y ) = = x LCA(x, y) == x LCA(x,y)==x,区间为 [ f i r s t [ x ] , f i r s t [ y ] [first[x], first[y] [first[x],first[y] 对应上图1->3情况
- 如果 L C A ( x , y ) ! = x LCA(x, y) \ != x LCA(x,y) !=x,区间为 [ s e c o n d [ x ] , f i r s t [ y ] ] [second[x], first[y]] [second[x],first[y]],同时标记lca为 L C A ( x , y ) LCA(x, y) LCA(x,y),在计算的时候加上lca
AC_Code:
#include <bits/stdc++.h>
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 <typename T>
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<int> 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<int> 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<query> 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());
vector<int>ans(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 待补