曇天も払う光なら凄惨な旅路だって往け|

AzusidNya

园龄:1年7个月粉丝:4关注:4

回滚莫队 学习笔记

板子题交 998244353 遍一直 UKE 我哭死。

update on 2023/8/28 终于不 UKE 了。

回滚莫队

有些题看起来像个莫队,想着想着发现 add 操作很容易实现,而 del 操作怎么都想不出来,或者是 del 操作时间复杂度不是 O(1) 时间复杂度爆炸,那么回滚莫队就能派上用场。这种莫队不带删因此也叫做不带删莫队。


AT_joisc2014_c 歴史の研究

给你一个长度为 n 的数组 Am 个询问 (1n,m105),每次询问一个区间 [L,R] 内重要度最大的数字,要求输出其重要度。一个数字 i 重要度的定义为 i 乘上 i 在区间内出现的次数。

sol:

从莫队的角度出发,首先我们尝试实现 add 和 del 函数。

add 函数:在当前区间内增加一个数并更新答案。加入数 x 后令 cntxcntx+1 然后更新答案即可。

del 函数:?好像很难 O(1) 实现。因为删除最大值后好像没法快速找回次小值。

那怎么办?不要删除操作就行了。

首先还是很正常的莫队分块+排序。

然后按顺序处理询问:

  1. 如果询问的左端点跳到了新的一块,那么将左右指针扔到当前块的右边形成空集。

  2. 如果询问的左右端点在同一块内,直接暴力即可。

  3. 往右跳右指针到右端点。

  4. 然后往左跳左指针到左端点。

  5. 到位后回答询问。

  6. 撤销之前的修改,把指针挪回去。

注意这里的撤销并不是删除,撤销的方法很多,例如这题就是直接记录原位的答案,然后再把 cnt 一路滚回去。

这样我们就做到了不带删除完成莫队的内容。

时间复杂度方面,不是很会证明。设询问数量是 q 且与 n 同阶,我的感性理解就是左端点一直只在一块内扫,变化量在块长 T 以内,变化次数与块的个数和询问个数有关当块长取根号级别的而右端点往右扫所以是个 O(n) 。块长取 nq 是最优的,时间复杂度 nq,当然如果 nq 不同阶那还需要再仔细调整下块长。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#include<cstring>
#define int long long
using namespace std;
int n, Q, N, T, bl, tmp;
int a[100005], b[100005];
int L[100005], R[100005];
int belong[100005];
int ans[100005];
struct Query{
int l, r, id;
}q[100005];
bool cmp(Query u, Query v){
return belong[u.l] == belong[v.l] ? u.r < v.r : u.l < v.l;
}
int cn[100005], cnt[100005];
int solve(int l, int r){
int ans = 0;
for(int i = l; i <= r; i ++)
cn[a[i]] ++;
for(int i = l; i <= r; i ++)
ans = max(ans, cn[a[i]] * b[a[i]]);
for(int i = l; i <= r; i ++)
cn[a[i]] --;
return ans;
}
int add(int x){
cnt[a[x]] ++;
tmp = max(tmp, cnt[a[x]] * b[a[x]]);
return 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i ++)
cin >> a[i], b[i] = a[i];
for(int i = 1; i <= Q; i ++)
cin >> q[i].l >> q[i].r, q[i].id = i;
sort(b + 1, b + n + 1);
N = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1; i <= n; i ++)
a[i] = lower_bound(b + 1, b + N + 1, a[i]) - b;
T = sqrt(n), bl = n / T;
for(int i = 1; i <= n; i ++)
L[i] = R[i - 1] + 1, R[i] = L[i] + T - 1;
if(R[bl] < n) ++ bl, L[bl] = R[bl - 1] + 1, R[bl] = n;
for(int i = 1; i <= n; i ++)
belong[i] = (i - 1) / T + 1;
sort(q + 1, q + Q + 1, cmp);
for(int i = 1, r = 0, l = 0, nw = 1; i <= bl; i ++){
memset(cnt, 0, sizeof(cnt));
r = R[i];
tmp = 0;
while(belong[q[nw].l] == i){
l = R[i] + 1;
if(q[nw].r - q[nw].l <= T){
ans[q[nw].id] = solve(q[nw].l, q[nw].r);
++ nw;
continue;
}
while(q[nw].r > r) add(++ r);
int ret = tmp;
while(l > q[nw].l)
add(-- l);
ans[q[nw].id] = tmp;
tmp = ret;
while(l <= R[i])
cnt[a[l ++]] --;
++ nw;
}
}
for(int i = 1; i <= Q; i ++)
cout << ans[i] << "\n";
return 0;
}

另外一直在 UKE 我也不知道对不对反正样例是过了。

update on 2023/8/28: Accept.


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

给定一个序列,多次询问一段区间 [l,r],求区间中相同的数的最远间隔距离

序列中两个元素的间隔距离指的是两个元素下标差的绝对值

sol:

这个至少能交。

思路一样,只需要处理下 add 和 solve 函数,然后写一下 rollback 还原就行了。

  • add:我的做法常数比较大。直接记录每个数在当前区间的最左边的出现位置和最右边的出现位置即可,然后相减更新答案。

  • solve:暴力而已,按照上面的思路暴力下即可。

  • rollback:在向左扩展左指针的时候开一个栈记录上一个修改的位置,然后在 rollback 的时候依次将元素还原、退栈即可。

然后就是一个回滚莫队板子了。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int inf = 0x3f3f3f3f;
int n, Q, N, T, bl, tmp;
int a[200005], b[200005];
int L[200005], R[200005];
int belong[200005];
int ans[200005];
struct Query{
int l, r, id;
}q[200005];
bool cmp(Query u, Query v){
return belong[u.l] == belong[v.l] ? u.r < v.r : u.l < v.l;
}
int cn1[200005], cn2[200005], cnt1[200005], cnt2[200005], tp;
pair<int, pair<int, int> > st[200005];
int solve(int l, int r){
int ans = 0;
for(int i = l; i <= r; i ++)
cn1[a[i]] = min(cn1[a[i]], i), cn2[a[i]] = max(cn2[a[i]], i);
for(int i = l; i <= r; i ++)
ans = max(ans, cn2[a[i]] - cn1[a[i]]);
for(int i = l; i <= r; i ++)
cn1[a[i]] = inf, cn2[a[i]] = 0;
return ans;
}
int add(int x){
cnt1[a[x]] = min(cnt1[a[x]], x);
cnt2[a[x]] = max(cnt2[a[x]], x);
tmp = max(tmp, cnt2[a[x]] - cnt1[a[x]]);
return 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
memset(cn1, 0x3f, sizeof(cn1));
for(int i = 1; i <= n; i ++)
cin >> a[i], b[i] = a[i];
cin >> Q;
for(int i = 1; i <= Q; i ++)
cin >> q[i].l >> q[i].r, q[i].id = i;
sort(b + 1, b + n + 1);
N = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1; i <= n; i ++)
a[i] = lower_bound(b + 1, b + N + 1, a[i]) - b;
T = sqrt(n), bl = n / T;
for(int i = 1; i <= n; i ++)
L[i] = R[i - 1] + 1, R[i] = L[i] + T - 1;
if(R[bl] < n) ++ bl, L[bl] = R[bl - 1] + 1, R[bl] = n;
for(int i = 1; i <= n; i ++)
belong[i] = (i - 1) / T + 1;
sort(q + 1, q + Q + 1, cmp);
for(int i = 1, r = 0, l = 0, nw = 1; i <= bl; i ++){
memset(cnt1, 0x3f, sizeof(cnt1));
memset(cnt2, 0, sizeof(cnt2));
r = R[i];
tmp = 0;
while(belong[q[nw].l] == i){
l = R[i] + 1;
if(q[nw].r - q[nw].l <= T){
ans[q[nw].id] = solve(q[nw].l, q[nw].r);
++ nw;
continue;
}
while(q[nw].r > r) add(++ r);
int ret = tmp;
while(l > q[nw].l)
st[++ tp].first = a[-- l], st[tp].second.first = cnt1[a[l]], st[tp].second.second = cnt2[a[l]], add(l);
ans[q[nw].id] = tmp;
tmp = ret;
l = R[i] + 1;
while(tp)
cnt1[st[tp].first] = st[tp].second.first, cnt2[st[tp].first] = st[tp].second.second, tp --;
++ nw;
}
}
for(int i = 1; i <= Q; i ++)
cout << ans[i] << "\n";
return 0;
}

p.s. Rollback 的时候要注意你 roll 了个什么东西。直接把 l 压到栈里还原的都不知道是什么东西(


Rmq Problem / mex

有一个长度为 n 的数组 {a1,a2,,an}

m 次询问,每次询问一个区间内最小没有出现过的自然数。

sol:

不扩大区间的回滚莫队。(或者叫只删莫队(自己取的)

好像可以值域分块但是我摆。

依然是考虑 add 和 del 函数。

  • del:记录一个桶,删除一个数时,如果这一次操作将这个数删完了,那就更新当前 mex

  • add:思考好一会,发现很难实现快速增加一个数。

那可以同样的套路回滚。如果不能扩大区间,那就只用删除解决所有问题。

首先排序时,对于同一块的点,右端点从大到小排序。然后回滚莫队。

具体而言:

  1. 先暴力求出满集的答案,把桶更新好。

  2. 如果询问的左端点跳到了新的一块,那么右指针 rollback 到最右边形成满集,左指针 del 到当前块首。

  3. 如果询问的左右端点在同一块内,直接暴力即可。

  4. 往左跳右指针到右端点。

  5. 然后往右跳左指针到左端点。

  6. 到位后回答询问。

  7. 撤销之前的修改,把指针挪回去。

其实没什么不同。但是 rollback 的时候需要稍微注意些细节。在这题因为桶不会清空所以不能用 memset,而自然右指针移动到最右端只能用 rollback 的方式。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
using namespace std;
int n, m;
int a[200005];
int L[200005], R[200005];
int T, bl;
int belong[200005];
struct Query{
int l, r, id;
}q[200005];
int ans[200005];
bool cmp(Query u, Query v){
return belong[u.l] == belong[v.l] ? u.r > v.r : u.l < v.l;
}
int cn[200005];
int solve(int l, int r){
int ret = 0;
for(int i = l; i <= r; i ++){
cn[a[i]] ++;
if(a[i] == ret) while(cn[++ ret]);
}
for(int i = l; i <= r; i ++)
cn[a[i]] --;
return ret;
}
int cnt[200005];
int tmp;
int del(int u){
cnt[a[u]] --;
if(!cnt[a[u]]) tmp = min(tmp, a[u]);
return 0;
}
int 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], cnt[a[i]] ++;
for(int i = 1; i <= m; i ++){
cin >> q[i].l >> q[i].r, q[i].id = i;
}
T = sqrt(n), bl = n / T;
for(int i = 1; i <= bl; i ++)
L[i] = R[i - 1] + 1, R[i] = L[i] + T - 1;
if(R[bl] < n)
bl ++, L[bl] = R[bl - 1] + 1, R[bl] = n;
for(int i = 1; i <= bl; i ++)
for(int j = L[i]; j <= R[i]; j ++)
belong[j] = i;
sort(q + 1, q + m + 1, cmp);
tmp = solve(1, n);
int t1 = tmp;
for(int i = 1, l = 1, r = 0, nw = 1; i <= bl; i ++){
r = n;
tmp = t1;
while(l < L[i]) del(l ++);
t1 = tmp;
while(belong[q[nw].l] == i){
if(q[nw].r - q[nw].l <= T){
ans[q[nw].id] = solve(q[nw].l, q[nw].r);
nw ++;
continue;
}
while(q[nw].r < r)
del(r --);
int ret = tmp;
while(l < q[nw].l)
del(l ++);
ans[q[nw].id] = tmp;
tmp = ret;
while(l > L[i])
cnt[a[-- l]] ++;
nw ++;
}
while(r < n)
cnt[a[++ r]] ++;
}
for(int i = 1; i <= m; i ++)
cout << ans[i] << "\n";
return 0;
}

本文作者:AzusidNya

本文链接:https://www.cnblogs.com/AzusidNya/p/17652889.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   AzusidNya  阅读(20)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起