莫队总结

莫队

莫队是一种基于分块思想的,离线处理的数据结构,其代码量通常比较少,速度也不比其他数据结构慢多少,是一个十分方便的数据结构。

普通莫队

引例

给出一个长度为 n n n 的数列 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an ,有 q q q 个询问,每个询问给出数对 ( i , j ) (i,j) (i,j) ,需要你给出 a i , a i + 1 . . . a j a_i,a_{i + 1}...a_j ai,ai+1...aj 这一段中有多少不同的数字。

其实用线段树就可以很好的维护这样一个问题,用主席树维护即可,但是写起来并不简洁,我们很容易想到最暴力的方法,每次都整个扫描询问的区间,用一个 c n t cnt cnt 来记录某个数是否出现过,如果没有则 a n s + 1 ans+1 ans+1 ,这样做时间复杂度上限为 O ( n 2 ) O(n^2) O(n2) ,显然不是很好,那时间到底浪费到了哪里,其实,多个区间内总有重复的部分,这些部分我们没有利用上,而是每次询问完一个区间就丢掉了,那我们想到如果每次都是在上一个区间的基础上扩展或者收缩,会不会更快,显然这样的想法没错,但如果数据是 ( 1 , 1 ) , ( n , n ) , ( 2 , 2 ) , ( n − 1 , n − 1 ) (1,1),(n,n),(2,2),(n-1,n-1) (1,1),(n,n),(2,2),(n1,n1) ,那又回到了 n 2 n^2 n2 ,因此我们就需要对询问的区间进行一个合理的排序,让每次两个端点的移动尽可能的少,于是乎按照分块的思想,我们对于所有询问的左端点所在块排序,块一样的按照右端点排序,这样就能完美解决这一问题,于是这样一个问题就用一种新的思路解决了。

下面来看看具体实现。(有些题目可能需要离散化)

向当前维护的序列中加入或删除一个数,我们都只需要 O ( 1 ) O(1) O(1) 的时间即可完成。

void add(int x) {
    if(!cnt[a[x]]++) ans++;
}
void del(int x) {
    if(!--cnt[a[x]]) ans--;
}

我们要将所有的询问存下来,然后离线处理。

struct query {
    int l, r, i;
    bool operator<(const query& x) const {
        return bel[l] ^ bel[x.l] ? bel[l] < bel[x.l] : bel[l] & 1 ? r < x.r : r < x.r;
    }
}

在处理每个询问的时候我们只需要移动区间端点即可。

for (int i = 1; i <= m; i++) {
    int ql = q[i].l, qr = q[i].r;
    while(l < ql) del(l++); //删掉一个存在的数,要先删再移动
    while(l > ql) add(--l); //加进来一个不存在的数,要先移动再加
    while(r < qr) add(++r); //与上面同理
    while(r > qr) del(r--); //与上面同理
}

例题

P1494 [国家集训队] 小 Z 的袜子

我们只需要对转移稍作修改即可,记录一个 s u m sum sum ,代表选到两个颜色相同的袜子的方案数,方案的总数可以直接用 KaTeX parse error: Undefined control sequence: \C at position 1: \̲C̲_{len}^2 来计算,我们考虑 s u m sum sum 如何维护,我们只需在每次加(或减)的时候,加上(或减去)操作这件物品之前的和它颜色一样的物品数即可,这样我们只需对两个函数稍作修改即可。

void add(int x) {
    sum += cnt[a[x]]++;
}
void del(int x) {
    sum -= --cnt[a[x]];
}

CF617E XOR and Favorite Number

刚看到时可能没有思路,我们可以一点一点考虑,首先对于一段数 a i . . . a j a_i...a_j ai...aj 的异或和,我们显然可以表示为 ( a 1 . . . a i − 1 ) (a_1...a_{i-1}) (a1...ai1) ^ ( a 1 . . . a j ) (a_1...a_j) (a1...aj) ,这样我们就能想到记录一个前缀异或和数组 s s s ,我们每次询问就是在询问满足 s i s_i si ^ s j = k s_j=k sj=k 的数对 ( i , j ) (i,j) (i,j) 有几个,同时我们又有 x x x ^ y = z y=z y=z x x x ^ z = y z=y z=y ,所以每次加入时,我们只需要查询值为 s x s_x sx ^ k k k 的数有几个即可,同时将 s x s_x sx 记录下来即可,其他地方都不变。

void add(int x) {
    ans += cnt[s[x] ^ k];
    cnt[s[x]]++;
}
void del(int x) {
    cnt[s[x]]--;
    ans -= cnt[s[x] ^ k];
}

P3709 大爷的字符串题

简化一下题意,每次取出一个严格上升序列,最少取几次能取完,也就是要求区间众数出现的次数,我们需要维护两个数组 c n t cnt cnt n c n t ncnt ncnt 分别代表某个数的出现次数以及出现次数为某个数的数有多少个(挺绕的),之后每次加入或删除时通过这两个数组即可正确的维护。

p . s . p.s. p.s. 这题用回滚莫队也可以很好的维护

void add(int x) {
    ncnt[cnt[a[x]++]--;
    ncnt[cnt[a[x]]]++;
    ans = max(ans, cnt[a[x]]);
}
void del(int x) {
    ncnt[cnt[a[x]]]--;
    if(cnt[a[x]] == ans && !ncnt[cnt[a[x]]]) ans--;
    ncnt[--cnt[a[x]]]++;
}

P3245[HNOI2016]大数

首先我们定义 a i a_i ai 为第 i i i ~ n n n 位构成的数字,那么我们有:如果 ,则 ( t l − t r + 1 ) / 1 0 r − l + 1 = 0 ( m o d   p ) (t_l-t_{r+1})/10^{r-l+1}=0(mod\ p) (tltr+1)/10rl+1=0(mod p) ,同时又因为 gcd ⁡ ( 10 , p ) = 1 \gcd(10,p)=1 gcd(10,p)=1 ,因此 1 0 r − l + 1 ≠ 0 ( m o d   p ) 10^{r-l+1}\neq0(mod\ p) 10rl+1=0(mod p) ,所以 [ l , r ] [l,r] [l,r] 构成的数字是 p 的倍数的充要条件为 t l − t r + 1 = 0 ( m o d   p ) t_l-t_{r+1}=0(mod\ p) tltr+1=0(mod p) ,在记录 t t t 时,我们令 t i % = p t_i\%=p ti%=p ,这样题意就变成了求区间内相同数的个数,我们只需维护一个 c n t cnt cnt 即可。

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

从这些普通莫队的题中可以发现,对于题意的转化是一个很重要的方面,最后基本都是只改了 a d d add add d e l del del 两个函数。

带修莫队

引例

给出一个长度为 n n n 的数列 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an ,有 q q q 个询问,每个询问给出数对 ( i , j ) (i,j) (i,j) ,需要你给出 a i , a i + 1 . . . a j a_i,a_{i + 1}...a_j ai,ai+1...aj 这一段中有多少不同的数字,或者是给出 x , k x,k x,k ,修改 a i a_i ai 的值为 k k k

与普通莫队不同的是,这里多了一个修改操作,我们同样考虑将数据离线是否可做,答案是可行,我们只需要在之前的基础上加一维时间轴,这样在每次询问区间时,我们只需要多看一下时间轴是否正确,否则移动即可,移动时跟加减函数差不多,只需将旧值的贡献减掉,新值的贡献加上即可,于是我们新加一个函数。

struct change {
    int id, col;
}C[maxn];
void Modify(int l, int r, int x) {
    int id = C[x].id, & col = C[x].col;
    if(l <= id && r >= id) {
        ans -= !--cnt[a[id]];
        ans += !cnt[col]++;
    }
    swap(a[id], col)
}
for (int i = 1; i <= m; i++) {
    int ql = q[i].l, qr = q[i].r, qt = q[i].t;
    while(l < ql) del(l++); //删掉一个存在的数,要先删再移动
    while(l > ql) add(--l); //加进来一个不存在的数,要先移动再加
    while(r < qr) add(++r); //与上面同理
    while(r > qr) del(r--); //与上面同理
    while(t < qt) Modify(ql, qr, ++t);
    while(t > qt) Modify(ql, qr, t--);
}

例题

CF940F Machine Learning

关于题意的一点解释,所谓 m e x mex mex 是数字出现次数的 m e x mex mex ,比如有 1个3 ,2个1,4个2,出现的是 1,2,4,最小没有出现的是3。

首先发现 1 < = a i < = 1 e 9 1 <= a_i <=1e9 1<=ai<=1e9 ,而 1 < = n < = 1 e 5 1 <= n <= 1e5 1<=n<=1e5 因此对于这个题来说,我们需要对数据进行离散化,对于答案,我们可以暴力枚举,也就是我们记录一个 c n t cnt cnt 数组和 n c n t ncnt ncnt 数组,代表一个数出现的次数和,我们只需要从 1 开始枚举答案,只要 n c n t a n s i ncnt_{ans_i} ncntansi 不是 0 ,那我们就可以继续向上加,由于答案是 O ( n ) O(n) O(n) 级别的,总复杂度是 O ( n n ) O(n\sqrt{n}) O(nn ) 级别的,所以暴力枚举答案并不影响总的时间复杂度。

同时这个题目也是一个待修改的莫队,因此我们需要再写一个 m o f i d y mofidy mofidy 函数。

回滚莫队

对于有些问题来说,普通莫队里的加操作很好维护,但是减的操作十分繁琐,对于这样一个问题,我们发明出一个回滚莫队,也就是不删除莫队,我们只添加,不删除,具体的来说,每次询问我们将 l , r l,r l,r 放到当前询问区间左端点所在块的又端点,由于我们是按照 r r r 递增来排序的,区间的右端点只增不减,对于每个询问,开始时都将区间左端点拉到所在快的右端点,之后开始增加直到当前询问的左端点,这样就能完美解决删除难的问题。

引例

AT1219 歴史の研究

首先是离散化的处理,与其他离散化不同的一点,这里还要记录一下离散化后的数所对应的原数是多少,因为在统计答案时需要用到,为此我们可以记录一个 l s ls ls 数组,其中 l s i ls_i lsi 代表离散后 i i i 这个数字所对应的原来的数字,在每次更换块时,我们需要清空 c n t cnt cnt 数组,由于块有 n \sqrt{n} n 个,所以总的时间复杂度为 O ( n n ) O(n\sqrt{n}) O(nn ) ,还需记录的一个是历史答案,也就是 l l l 在同一个块内时,每次重新移动 i i i 之前的答案,更换块时清零,当 l , r l,r l,r 在同一个块内移动时,我们对于 r r r 只操作 c n t a r − − cnt_{a_r}-- cntar ,但并不统计答案,在 l l l 往左走的时候,会将减掉的都加回来,即可正确统计出答案。

离散化

sort(N + 1, N + n + 1, [](node x, node y) {
	return x.x < y.x;
});
for (int i = 1; i <= n; i++) {
	if (N[i].x != ls[c])
		ls[++c] = N[i].x;
	N[i].x = c;
}
sort(N + 1, N + n + 1, [](node x, node y) {
	return x.i < y.i;
});

主要部分

void add(int x) {
	cnt[N[x].x]++;
	k = max(k, cnt[N[x].x] * ls[N[x].x]);
}
void del(int x) { //这里的del形同虚设,并没有麻烦的去统计答案,因此还是算作不删除莫队
	cnt[N[x].x]--;
}
for (int i = 1; i <= m; i++) {
	int ql = Q[i].l, qr = Q[i].r;
	l = siz * bel[ql];
	if (bel[ql] > bel[Q[i - 1].l]) {
		for (int j = 1; j <= c; j++)
			cnt[j] = 0;
		r = l - 1;
		k = last = 0;
	}
	k = last; //去掉上一次移动左端点的贡献
	while (r < qr) add(++r);
	while (r > qr) del(r--);
	last = k; //记录区间又端点移动后的贡献,因为这个贡献不会被刷新
	while (l > ql) add(--l);
	ans[Q[i].i] = k;
	for (int j = siz * bel[ql] - 1; j >= l;)
		del(j--);
}

例题

P5906 【模板】回滚莫队&不删除莫队

我们设 l , r l,r l,r 为当前询问区间, m i d mid mid l l l 所在块的右端点,则答案可能有三种情况:

  • 完全出现在 [ l , m i d ] [l,mid] [l,mid]
  • 完全出现在 [ m i d , r ] [mid,r] [mid,r]
  • 一部分在 [ l , m i d ] [l,mid] [l,mid] 另一部分在 [ m i d , r ] [mid,r] [mid,r]

那么我们可以记录一个数最早出现和最晚出现的位置,在更新左右端点的时候维护这两个值,同时为了节省时间,我们记录那些被修改了,之后在重置的时候就可以少枚举一些无用的。

对于在同一个块内的询问,我们直接暴力计算即可:

int calc(int l, int r) {
	int ret = 0;
	for (int i = l; i <= r; i++)
		las[a[i]] = 0;
	for (int i = l; i <= r; i++)
		if (!las[a[i]]) las[a[i]] = i;
		else ret = max(ret, i - las[a[i]]);
	return ret;
}
int l = 0, r = 0, last = 0;
for (int i = 1; i <= m; i++) {
	int ql = Q[i].l, qr = Q[i].r;
	l = min(n, siz * bel[ql]); //当前块的右端点
	if (bel[ql] > bel[Q[i - 1].l]) { //一定要先判断是否进入下一个块,再判断是否左右端点在同一个块内
		for (int j = 1; j <= cc; j++)
			fi[clear[j]] = la[clear[j]] = 0;
		r = l - 1;
		k = last = 0;
		cc = 0;
	}
	if (bel[ql] == bel[qr]) { //左右端点在同一个块内,暴力计算
		ans[Q[i].i] = calc(ql, qr);
		continue;
	}
	k = last; //去掉上一次移动左端点的贡献
	while (r < qr) {
		r++;
		la[a[r]] = r; //记录最后出现的位置
		if (!fi[a[r]]) fi[a[r]] = r, clear[++cc] = a[r]; //如果最开始出现的位置没有被记录,则记录下来,并将其计入clear数组,因为更换块时需要去掉这些贡献
		k = max(k, r - fi[a[r]]); //计算贡献
	}
	last = k; //记录下来移动r后的贡献
	while (l > ql) { //这里不用维护最开始出现的位置,因为l每次都在变小,当前出现的一定是最开始出现的位置
		l--;
		if (la[a[l]]) k = max(k, la[a[l]] - l); //如果有最后出现的位置,计算贡献
		else la[a[l]] = l; //记录最后出现的位置
	}
	ans[Q[i].i] = k;
	for (int j = min(n, siz * bel[ql]) - 1; j >= l; j--) //去掉修改l的贡献
		if (la[a[j]] == j) la[a[j]] = 0;
}

树上莫队

引例

SP10707 COT2 - Count on a tree II

  • 给定 n n n 个结点的树,每个结点有一种颜色。
  • m m m 次询问,每次询问给出 u u u v v v ,回答 u u u v v v 之间的路径上的结点的不同颜色数。

与普通莫队不同的地方是,这次的询问放在了树上,这样一来节点间路径的编号就不一定连续了,那么我们就需要转化成一个序列,我们可能首先会想到 d f s dfs dfs 序,但在这里 d f s dfs dfs 序行不通了,例如有这样一棵树

它的 d f s dfs dfs 序是 1 , 2 , 4 , 6 , 7 , 5 , 3 1,2,4,6,7,5,3 1,2,4,6,7,5,3 ,现在我们要询问 4 4 4 ~ 3 3 3 ,查找到的 d f s dfs dfs 序为 4 , 6 , 7 , 5 , 3 4,6,7,5,3 4,6,7,5,3 ,再加上 l c a lca lca 1 1 1 ,显然不是连续的,因此这里我们引入一个叫做欧拉序的东西,他的求法为,访问这个节点时将其加入一次,完全访问结束后再将其加入一次,例如这棵树的欧拉序为 1 , 2 , 4 , 5 , 5 , 7 , 7 , 6 , 6 , 4 , 2 , 3 , 3 , 1 1,2,4,5,5,7,7,6,6,4,2,3,3,1 1,2,4,5,5,7,7,6,6,4,2,3,3,1 ,现在我们要询问 6 6 6 ~ 2 2 2 6 6 6 2 2 2 的子树里,所以我们统计 s t 2 st_2 st2 ~ s t 6 st_6 st6 这段区间,也就是 2 , 4 , 5 , 5 , 7 , 7 , 6 2,4,5,5,7,7,6 2,4,5,5,7,7,6 ,其中出现两次的我们不用统计,所以我们实际上只统计 2 , 4 , 6 2,4,6 2,4,6 ,是正确的,再例如我们询问 4 4 4 ~ 3 3 3 ,这时两个节点不在同一个子树内,我们统计 e d 4 ed_4 ed4 ~ s t 3 st_3 st3 ,也就是 4 , 2 , 3 4,2,3 4,2,3 ,然后再加上 l c a lca lca 即可。( p . s . p.s. p.s. s t st st 表示某个数开始时对应的欧拉序, e d ed ed 表示结束时对应的欧拉序,实现的时候我们让 t i m tim tim 一直递增,同时记录 n d f s t i m = u ndfs_{tim}=u ndfstim=u ,也就是让欧拉序里面的数与节点对应即可)。

关于如何处理重复出现的节点,我们只需要记录一个 r e m rem rem 数组,当其为 0 0 0 时我们添加,为 1 1 1 时我们删除即可,每次操作完让其 ^ = = = 1 1 1

树链剖分和求 l c a lca lca 部分:

inline void dfs1(int u, int f) {
	fa[u] = f; dep[u] = dep[f] + 1;
	st[u] = ++tim; ndfn[tim] = u;
	siz[u]++;
	for (int i = head[u]; i; i = E[i].next) {
		int v = E[i].v;
		if (v == f) continue;
		dfs1(v, u);
		siz[u] += siz[v];
		if (siz[v] > siz[son[u]]) son[u] = v;
	}
	ed[u] = ++tim; ndfn[tim] = u;
}
inline void dfs2(int u, int t) {
	top[u] = t;
	if (son[u]) dfs2(son[u], t);
	for (int i = head[u]; i; i = E[i].next) {
		int v = E[i].v;
		if (v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
}
inline int lca(int x, int y) {
	while (top[x] != top[y]) {
		if (dep[top[x]] < dep[top[y]])
			swap(x, y);
		x = fa[top[x]];
	}
	return dep[x] < dep[y] ? x : y;
}

修改函数:

inline void add(int x) {
	k += !cnt[a[x]]++;
}
inline void del(int x) {
	k -= !--cnt[a[x]];
}
inline void insert(int x) {
	rem[x] ? del(x) : add(x);
	rem[x] ^= 1;
}

对询问的处理及离线:

for (int i = 1; i <= m; i++) {
	int x = read(), y = read();
	if (st[x] > st[y]) swap(x, y);
	Q[i].i = i;
	Q[i].lca = lca(x, y);
	if (Q[i].lca == x) {
		Q[i].l = st[x];
		Q[i].r = st[y];
		Q[i].lca = 0;
	}
	else {
		Q[i].l = ed[x];
		Q[i].r = st[y];
	}
}

莫队二次离线

引例

一个序列 a a a ,每次查询给一个区间 [ l , r ] [l,r] [l,r] ,查询 l ≤ i < j ≤ r l \leq i < j \leq r li<jr,且 a i ⊕ a j a_i \oplus a_j aiaj 的二进制表示下有 k k k 1 1 1 的二元组 ( i , j ) (i,j) (i,j) 的个数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值