标签:query 思路 有用 for 添加 前缀和 ons cal 复杂
400 AC 祭
题面 : 【Link】
长度为 \(n\) 的序列,序列中的值为 1 或 -1。
有 \(m\) 个询问,询问在 \([L,R]\) 中区间和为 0 的区间的最大长度。
\(1 \leq n,m \leq 5 \times 10 ^ 4\),\(1 \leq l \leq r \leq n\)。
一般来说看到这种在区间里的的一些操作,并多次询问,如果看到题目中的序列长度不超过 \(10 ^ 6\),且不强制在线,就可以用莫队来解决。
首先想到求前缀和,这样我们就可以 \(O(1)\) 查询这个区间内部的总和是多少,如果求 \([L,R]\) 内的总和,直接就算 \(sum_R - sum_{L - 1}\) 就可以了。
如果用莫队做,我们发现想要加入一个数很容易,我们可以直接加入,用一个桶来记录,但是我们发现想要删除一个点的话很难,因为我们每次删除的时候总会发现我们不知道要更新哪个值,只能暴力枚举,复杂度直接上天。
这样我们可以用回滚莫队。
回滚莫队是一种只添加但是并不删除的莫队,我们用分块的方式把整个序列分为 \(\sqrt{n}\) 个块。(也可以不是 \(\sqrt n\),根据题目来看)。
接下根据分块对询问进行排序,我们要保证以左区间所在的块为第一关键字,如果左区间所在块相同,那么就按照右端点递增排序。(这里千万不要排错,会 WA )。
bool CMP(Query a,Query b)
{
return block[a.l_] ^ block[b.l_] ? block[a.l_] < block[b.l_] : a.r_ < b.r_;
}
最后就是我们的询问操作了 :
我们枚举每一个块,我们设这个块的右端点为 \(BLO\),那么这一次的指针为 \(l = BLO + 1\),右端点为 \(r = BLO\)。
分为两种情况 :
统计答案后操作 :
因为我们移动了左指针,但是我们并没有按照左端点排序,我们的左端点是没有任何递增的特性的,这样的答案肯定会对后面的查询造成干扰,所以我们要不断增加 \(l\),使其回到初始位置 \(BLO\)。
但是这时我们的答案还没有更新,我们前面记录的只移动 \(r\) 后的答案就有用了,我们的右端点单调递增,不会造成干扰,所以就把前面记录的值取回来就行了。
这就是整个回滚莫队的流程。
回到这个题目,很显然我们可以和回滚莫队模板题有同样的思路。
我们开两个数组 \(st[i]\) 和 \(ed[i]\) 记录前缀和为 \(i\) 的元素在区间内部的第一次出现的位置和最后一次出现的位置,查询时不断取 \(\max\) 就行了。和模板题一模一样。
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int Maxk = 5e5 + 100;
int n,m,len,T;
int sum[Maxk],a[Maxk];
int L[Maxk],R[Maxk],block[Maxk];
int ans[Maxk];
int st[Maxk],ma[Maxk];
struct Query{
int l_;
int r_;
int pos_;
}s[Maxk];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == ‘-‘, ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int Max(int x,int y) {return x > y ? x : y;}
void BLOCK()
{
len = sqrt(n);
T = n / len;
for(int i = 1;i <= T;i ++) {
L[i] = (i - 1) * len + 1;
R[i] = i * len;
}
if(R[T] < n) T ++,L[T] = R[T - 1] + 1,R[T] = n;
for(int i = 1;i <= T;i ++)
for(int j = L[i];j <= R[i];j ++)
block[j] = i;
return;
}
bool CMP(Query a,Query b)
{
return block[a.l_] ^ block[b.l_] ? block[a.l_] < block[b.l_] : a.r_ < b.r_;
}
int calc(int l,int r)
{
int cnt[Maxk];
int maxn = -1;
for(int i = l - 1;i <= r;i ++) cnt[sum[i]] = 0;
for(int i = l - 1;i <= r;i ++) {
if(!cnt[sum[i]]) cnt[sum[i]] = i;
else maxn = Max(maxn,i - cnt[sum[i]]);
}
return maxn;
}
void solve()
{
sort(s + 1,s + m + 1,CMP);
for(int i = 1,j = 1;j <= T;j ++) {
int br = min(n,j * len),l = br + 1,r = l - 1,Ans = 0;
memset(ma,0,sizeof ma);
memset(st,0,sizeof st);
for(;block[s[i].l_] == j;i ++) {
if(block[s[i].r_] == j) {
ans[s[i].pos_] = calc(s[i].l_,s[i].r_);
continue;
}
while(r < s[i].r_) {
r ++;
ma[sum[r]] = r;
if(!st[sum[r]]) st[sum[r]] = r;
Ans = max(Ans,r - st[sum[r]]);
}
int tmp = Ans;//先保存一下,因为右区间的贡献不会被刷新,但左区间的会
while(l >= s[i].l_) {
l --;
if(ma[sum[l]]) Ans = max(Ans,ma[sum[l]] - l);
else ma[sum[l]] = l;//最后出现的位置可能在左区间中
}
ans[s[i].pos_] = Ans;
while(l <= br) {
if(ma[sum[l]] == l) ma[sum[l]] = 0;
l ++;
}
Ans = tmp;
}
}
return;
}
signed main()
{
// freopen("498.in","r",stdin);
// freopen("409.out","w",stdout);
n = read(),m = read();
sum[0] = 50011;
for(int i = 1;i <= n;i ++) sum[i] = read(),sum[i] += sum[i - 1];;
for(int i = 1;i <= m;i ++) {
s[i].l_ = read();
s[i].r_ = read();
s[i].pos_ = i;
}
BLOCK();
solve();
for(int i = 1;i <= m;i ++) {
if(ans[i] < 0) cout << 0 << endl;
else cout << ans[i] << endl;
}
return 0;
}
标签:query 思路 有用 for 添加 前缀和 ons cal 复杂
原文地址:https://www.cnblogs.com/Ti-despair/p/14532277.html