暴力算法 --- 莫队

莫队

什么是莫队?

答:优雅的暴力!!!

基础莫队

重复的数

题目描述:给出一个长度为 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 1n 编号,对于所有的查询按照块编号,从小到大排序,对于同一块,按照区间右端点升序排序。排完序后,我们再按照上述的操作去移动 L , R L, R L,R指针,这样的时间复杂度为: O ( n n ) \mathcal O(n\sqrt n) O(nn )

简单证明一下:

  1. 区间排序,sort一遍, O ( n l o g n ) \mathcal O(n log n) O(nlogn)
  2. L L L指针的移动:对于每块最坏跨越整个块 n \sqrt n n ,最坏有 n n n块需要移动,总的: O ( n n ) \mathcal O(n\sqrt n) O(nn )
  3. 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的序列,现在有两个操作:

  1. Q   L   R Q \ L \ R Q L R 区间 [ L , R ] [L, R] [L,R]有多少个不同的数
  2. 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],不满足就交换一下)

  1. 如果 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情况
  2. 如果 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 待补

例题:

ABC293_G

题目链接:点我
题意:有一个长度为 N N N的序列,现在有 m m m次询问,每次询问一段区间内 ( i < j < k ) (i \lt j\lt k) (i<j<k)并且 a i = a j = a k a_i = a_j = a_k ai=aj=ak的对数
sol:利用莫队,往区间加数 X X X的时候,对答案增加 c n t [ x ] ∗ ( c n t [ x ] − 1 ) / 2 cnt[x] * (cnt[x] - 1) / 2 cnt[x](cnt[x]1)/2,当前这一个和已有的匹配,删除同理

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> pii;
template < typename T> inline void Max(T &a, T b) { if(a < b) a = b; }
template < typename T> inline void Min(T &a, T b) { if(a > b) a = b; }
 
const int N = 2e5 + 10, B = 450;

struct Q {
	int l, r, id;
	bool operator < (const Q &b) const {
		if(l / B == b.l / B) return r < b.r;
		return l < b.l;
	}
}q[N];

int n, m;
int a[N];
LL c, ans[N], cnt[N];

void add(int x) {
	c += (cnt[a[x]] * (cnt[a[x]] - 1)) / 2;
	cnt[a[x]] ++;
}

void del(int x) {
	cnt[a[x]] --;
	c -= (cnt[a[x]] * (cnt[a[x]] - 1)) / 2;
}

int main() {
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin >> n >> m;

	for (int i = 1; i <= n;i ++) {
		cin >> a[i];
	}

	for (int i = 1; i <= m; i ++) {
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}

	sort(q + 1, q + 1 + m);

	for (int i = 1, l = 1, r = 0; i <= m; i ++) {
		while (l > q[i].l) add(-- l);
		while (r < q[i].r) add(++ r);
		while (l < q[i].l) del(l ++);
		while (r > q[i].r) del(r --);
		ans[q[i].id] = c;
	}

	for (int i = 1;i <= m; i ++)
		cout << ans[i] << "\n";

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

W⁡angduoyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值