莫队
曼波~
莫队有一种暴力的美感。
普通莫队
做法
莫队的核心思想:
有一些题目的询问可以用极小的复杂度,比如 \(O(1)\) 或 \(O(\log n)\),由当前区间的答案扩展出相邻区间的答案,那么我们可以思考能不能对于每一个询问,直接暴力从上一个询问一步一步移动到当前询问来求出答案。
可以发现这样子复杂度是 \(O(n^2)\) 的,因为最坏情况下我们可能需要每次把区间从最左边移动到最右边,又从最右边移动到最左边。
这时我们就很容易想到把询问离线下来,然后通过某种排序把离得近的查询放在一起回答。问题是怎么排序呢?这就要提到莫队的精髓了:分块。
具体的来说,在 \(n\) 和 \(m\) 同阶的情况下,我们以 \(\sqrt{n}\) 为块长分块。然后对于区间 \((l,r)\), 以 \(l\) 所属块的编号为第一关键字,以 \(r\) 为第二关键字排序。
另外还有几个小细节:
- 移动区间时,要先扩大区间,再缩小区间。
- 奇偶化排序:对于属于奇数块的询问,r 按从小到大排序,对于属于偶数块的排序,r 从大到小排序。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 5e4 + 5;
long long ans, out[N], n, m, k, a[N], b[N], tong[N];
void add(int x){
ans += 2ll * tong[x] + 1;
tong[x]++;
}
void sub(int x){
ans -= 2ll * tong[x] - 1;
tong[x]--;
}
struct cx{
int l, r, bh;
}c[N];
bool cmp(cx x, cx y){
if(b[x.l] == b[y.l]) return x.r < y.r;
else return b[x.l] < b[y.l];
}
int main(){
cin >> n >> m >> k;
int g = sqrt(n);
for(int i = 1; i <= n; i++){
cin >> a[i];
b[i] = i / g;
}
for(int i = 1; i <= m; i++){
cin >> c[i].l >> c[i].r;
c[i].bh = i;
}
sort(c + 1, c + m + 1, cmp);
int nwl, nwr, ltl = 1, ltr = 0;
for(int i = 1; i <= m; i++){
nwl = c[i].l; nwr = c[i].r;
while(ltl > nwl) ltl--, add(a[ltl]);
while(ltr < nwr) ltr++, add(a[ltr]);
while(ltl < nwl) sub(a[ltl]), ltl++;
while(ltr > nwr) sub(a[ltr]), ltr--;
out[c[i].bh] = ans;
}
for(int i = 1; i <= m; i++){
cout << out[i] << endl;
}
return 0;
}
带修莫队
做法
在普通莫队上增加一个时间轴,移动的时候加上在时间轴移动即可。
如果两个查询之间没有修改操作,显然我们可以认为它们是同一时间发生的,所以每一个查询的时间就是它之前的修改操作的个数。
考虑在时间轴上移动的时候如何维护答案。假设当前移动到的这个时间上的修改操作修改的点是 \(p\),如果 \(p\) 在当前区间外,那么不用修改;否则调用 add 和 sub 函数分别加上修改值,减去原有值即可。
如果是从后往前移动,就相当于撤销多的操作,那就加上原有值,减去修改值即可。
这里有一个小技巧:因为第一次经过一个时间后一定从前往后,所以下一次经过这个时间就一定是从后往前。所以修改和撤销不需要写两个函数,在函数最后 swap 修改值和原有值即可。
分块的时候以 \(l\) 所属快为第一关键字, \(r\) 所属快为第二关键字,\(t\) 为第三关键字从小到大排序即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, m, a[N], tong[N], out[N], ans;
int cnt1, cnt2;
struct query{
int l, r, t, id, bl1, bl2;
}q[N];
struct modify{
int p, c;
}mo[N];
void add(int x){
if(!tong[x]) ans++;
tong[x]++;
}
void sub(int x){
tong[x]--;
if(!tong[x]) ans--;
}
void update(int x, int t){
if(q[x].l <= mo[t].p && q[x].r >= mo[t].p){
add(mo[t].c);
sub(a[mo[t].p]);
}
swap(a[mo[t].p], mo[t].c);
}
bool cmp(query a, query b){
if(a.bl1 == b.bl1 && a.bl2 == b.bl2) return a.t < b.t;
if(a.bl1 == b.bl1) return a.bl2 < b.bl2;
return a.bl1 < b.bl1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
int g = pow(n, 0.666);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++){
char op;
cin >> op;
if(op == 'Q'){
cnt1++;
cin >> q[cnt1].l >> q[cnt1].r;
q[cnt1].id = cnt1; q[cnt1].t = cnt2; q[cnt1].bl1 = q[cnt1].l / g; q[cnt1].bl2 = q[cnt1].r / g;
}
else {
cnt2++;
cin >> mo[cnt2].p >> mo[cnt2].c;
}
}
sort(q + 1, q + m + 1, cmp);
int nwl, nwr, nwt, ltl = 1, ltr = 0, ltt = 0;
for(int i = 1; i <= m; i++){
nwl = q[i].l; nwr = q[i].r; nwt = q[i].t;
while(nwl < ltl) add(a[--ltl]);
while(nwr > ltr) add(a[++ltr]);
while(nwl > ltl) sub(a[ltl++]);
while(nwr < ltr) sub(a[ltr--]);
while(nwt > ltt) update(i, ++ltt);
while(nwt < ltt) update(i, ltt--);
out[q[i].id] = ans;
}
for(int i = 1; i <= cnt1; i++) cout << out[i] << '\n';
return 0;
}
回滚莫队
个人认为这个题比板子题更板,也是非常经典的回滚莫队的题。
可以发现如果扩大区间,也就是加入一个数是很容易更新答案的,只需要 \(tong_x + 1\), 然后把 \(tong_x \cdot x\) 和 \(ans\) 取 \(\max\) 即可。
但是如果我们要缩小区间就会发现维护答案很麻烦,这时我们就可以用到回滚莫队来解决这个问题。
做法
回滚莫队的核心思想是:既然删除难以处理,那么我们就使删除操作尽量少就可以了:
-
首先,对于每一个块我们都单独处理,既如果当前询问和上一个询问不属于一个块,我们就重置莫队的指针,而不是在块之间移动。这样对块内的询问按 \(r\) 从小到大排序后,就会发现我们莫队的 \(R\) 指针就只会扩大而不会缩小了。
-
重置指针时,我们把 \(L\) 重置为当前块的末尾加一,把 \(R\) 重置为块的末尾,这样就能保证当前询问的 \(l\) 一定在莫队的 \(L\) 指针左边,所以也就只会进行扩展操作。
-
读到这想必读者会发现有两个问题:
-
如果当前询问的 \(l\) 和 \(r\) 在同一个块内,那么就会发现 \(r\) 在莫队的 \(R\) 指针右边,就会出现 \(R\) 的缩小操作。为了避免这种情况,我们对于 \(l\) 和 \(r\) 在同一个块内的询问直接暴力求解即可。
-
虽然第一个询问的 \(L\) 一定是向左扩展的,但是一旦移动过了 \(L\) 指针,下一个询问的 \(l\) 就有可能在 \(L\) 的右边。对于这种情况,就要用到回滚莫队的核心——也就是回滚来解决。听着很高深,其实就是做完 \(L\) 的扩展操作求得答案之后,又撤销所有 \(L\) 的扩展操作,让 \(L\) 指针重新滚回块的末尾。
-
回滚莫队看起来可能会感觉非常暴力,接下来就给一个复杂度证明:
时间复杂度证明(这里假设分的块长是 \(\sqrt{n}\),询问数和 \(n\) 同阶):
-
对于 \(l\) 和 \(r\) 在同一个块内的询问,单个询问是 \(\sqrt{n}\) 的,所以总复杂度不超过 \(O(n\sqrt{n})\)。
-
对于其他询问,处理每个询问时莫队的 \(L\) 指针移动次数是 \(O(\sqrt{n})\) 的,所以总移动次数是 \(O(n\sqrt{n})\) 级别的;对于莫队的 \(R\) 指针,由于同一个块内的 \(r\) 是递增的,所以对于每个块 \(R\) 指针移动次数是 \(O(n)\) 的,又因为一共有 \(\sqrt{n}\) 个块,所以 \(R\) 指针总移动次数也是 \(O(n\sqrt{n})\) 的。
代码实现上有一个我认为初次写容易感觉到困惑的小细节,就是在暴力处理 \(l\) 和 \(r\) 在同一个块的询问时:
if(block[q[i].l] == block[q[i].r]){
for(int j = q[i].l; j <= q[i].r; j++) tong[a[j]]++;
for(int j = q[i].l; j <= q[i].r; j++) out[q[i].id] = max(out[q[i].id], tong[a[j]] * da[a[j]]);
for(int j = q[i].l; j <= q[i].r; j++) tong[a[j]]--;
}
有人就要问了:暴力求解的时候用全局的桶算答案不会出问题吗?怎么保证这时的桶一定是空的?
其实仔细想这个问题其实是不难理解的:因为我们按 \(r\) 递增排了序,所以如果出现 \(l\) 和 \(r\) 在同一个块的情况,要么这是这个块的第一个询问,那么前面一步重置莫队指针时已经清空了桶;要么这个块前面的所有询问也一定都是暴力求解的情况,那么这时的桶也一定是空的。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, ans, a[N], da[N], ed[N], out[N], tong[N], block[N];
struct query{
int l, r, id;
}q[N];
void prework(){
sort(da + 1, da + n + 1);
int len = unique(da + 1, da + n + 1) - da - 1;
for(int i = 1; i <= n; i++) a[i] = lower_bound(da + 1, da + len + 1, a[i]) - da;
int g = sqrt(n), tot = 0;
block[0] = -1;
for(int i = 1; i <= n; i++){
block[i] = i / g + 1;
if(block[i] != block[i - 1]){
ed[tot] = i - 1;
tot++;
}
}
if(!ed[tot]) ed[tot] = n;
}
bool cmp(query a, query b){
if(block[a.l] != block[b.l]) return block[a.l] < block[b.l];
return a.r < b.r;
}
void add(int x){
tong[x]++;
ans = max(ans, tong[x] * da[x]);
}
void sub(int x){
tong[x]--;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
da[i] = a[i];
}
prework();
for(int i = 1; i <= m; i++){
cin >> q[i].l >> q[i].r;
q[i].id = i;
}
sort(q + 1, q + m + 1, cmp);
int nwl, nwr, ltl = 1, ltr = 0;
for(int i = 1; i <= m; i++){
nwl = q[i].l; nwr = q[i].r;
if(block[q[i].l] != block[q[i - 1].l]){
for(int j = ltl; j <= ltr; j++) sub(a[j]);
ans = 0;
ltl = ed[block[q[i].l]] + 1;
ltr = ltl - 1;
}
if(block[q[i].l] == block[q[i].r]){
for(int j = q[i].l; j <= q[i].r; j++) tong[a[j]]++;
for(int j = q[i].l; j <= q[i].r; j++) out[q[i].id] = max(out[q[i].id], tong[a[j]] * da[a[j]]);
for(int j = q[i].l; j <= q[i].r; j++) tong[a[j]]--;
}
else {
while(nwr > ltr) add(a[++ltr]);
int tmp = ans, now = ltl;
while(nwl < ltl) add(a[--ltl]);
out[q[i].id] = ans;
while(ltl < now) sub(a[ltl++]);
ans = tmp;
}
}
for(int i = 1; i <= m; i++) cout << out[i] << '\n';
return 0;
}

浙公网安备 33010602011771号