洛谷P4135 作诗
题意:N个数,M组询问,每次问[l,r]中有多少个数出现正偶数次。(强制在线)
强制在线,不能用莫队、树状数组等优雅算法,考虑暴力分块。
设分块后,f[i] [j] 表示第i块到第j块的答案。
于是,对于每次询问,设p为l所在块的编号,q为r所在块的编号,先查询l到r之间整块的答案,即f[p] [q]。
之后对于两边的不完整的块,暴力扫每个数,记录出现次数,与这个数在第p块和第q块之间出现的次数比较
- 若这个数在两边的不完整的块和中间的整块中出现次数均为奇数,++ans
- 若这个数在两边的不完整的块出现偶数次,在中间的整块中出现次数为0,++ans
- 若这个数在两边的不完整的块出现奇数次,在中间的整块中出现次数为偶数且不为0,--ans
于是,问题关键在于如何快速求出某个数在任意两块间的出现次数。
记分块的数量为T,每块的长度len = n / T。序列长度n、询问次数m同阶。
解法一:
与 洛谷P4168 [Violet]蒲公英 类似,对每种数开一个vector,存下这个数在原序列中出现的位置,查询某个数在[L,R]出现次数,只需二分查找<=R的最大下标和>=L的最小下标作差+1.
如 1 2 2 1 2 3 2 这组数,查询2在[3,6]之间出现的次数。vector[2]={2,3,5,7},即2在原序列中出现的位置。二分查找<=6的最大数为5,在vector中下标为2(vector下标从0开始),之后再二分查找>=3的最小数为3,在vector中的下标为1,两个下标作差+1 = 2,即查询结果为2。
预处理时,从每个块的起点到n进行统计,复杂度\(O(T*n)\) 。
查询时,对两边不完整块的每个数需要二分查找,每次查找为\(logn\) ,共\(n/T\) 个数。
总时间复杂度\(O(nT+m(n/T*logn))\) ,空间为\(T^2\) ,取\(T=\sqrt{n*logn}\) ,此时总复杂度\(O(n\sqrt{n*logn})\) 。
注意:
- 这里T如果取\(\sqrt n\) 会导致效率低下,\(\sqrt{n*logn}\) 约为\(\sqrt n\) 的三到四倍。
- vector常数很大,再加上二分用STL中的lower_bound和upper_bound,常数上天。如果分块数量正确,开了O2之后勉强能过。
- 可以手写二分,用数组代替vector,不开O2就能过。如果再加上一些卡常技巧(快读、inline、register等)跑的很快。
Code
#pragma GCC optimize(2)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cctype>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5, T = 1280;
vector<int> pos[N];
int n, c, m, len, tim, Stack[N], v[N], cnt[N], vis[N], block[N], bl[T], br[T], f[T][T];
inline void read(int &tmp) {
register char c = getchar();
for(tmp = 0; !isdigit(c); c = getchar());
for(; isdigit(c); tmp = tmp * 10 + c - 48, c = getchar());
}
void init() {
read(n);
read(c);
read(m);
len = ceil(n * 1.0 / (sqrt(n * log(n))));
for(int i = 1; i <= n; ++i) {
read(v[i]);
pos[v[i]].push_back(i);
block[i] = (i - 1) / len + 1;
}
for(int i = 1; i <= block[n]; ++i) {
bl[i] = (i - 1) * len + 1;
br[i] = i * len;
}
br[block[n]] = n;
for (int i = 1, res; i <= block[n]; ++i) {
res = 0;
for(int j = bl[i]; j <= n; ++j) cnt[v[j]] = 0;
for (int j = bl[i]; j <= n; ++j) {
++cnt[v[j]];
if ((cnt[v[j]] & 1) && (cnt[v[j]] > 1)) --res;
else if (!(cnt[v[j]] & 1)) ++res;
f[i][block[j]] = res;
}
}
}
inline int get(int x, int L, int R) {
return upper_bound(pos[x].begin(), pos[x].end(), R) - lower_bound(pos[x].begin(), pos[x].end(), L);
}
int query(int L, int R) {
int p = block[L], q = block[R], tp = 0, res = 0;
if(q - p < 2) {//同块或相邻块
for(int i = L; i <= R; ++i) {//暴力扫
if(vis[v[i]] != tim) {//第一次扫到的标记
vis[v[i]] = tim;
cnt[v[i]] = 1;
} else {
++cnt[v[i]];
if(cnt[v[i]] & 1) --res;
else ++res;
}
}
return res;
}
res = f[p + 1][q - 1];//两块间的答案
for(int i = L; i <= br[p]; ++i) {//左边角块
if(vis[v[i]] != tim) {
vis[v[i]] = tim;
cnt[v[i]] = 1;
Stack[++tp] = v[i];
} else ++cnt[v[i]];
}
for(int i = bl[q]; i <= R; ++i) {//右边角块
if(vis[v[i]] != tim) {
vis[v[i]] = tim;
cnt[v[i]] = 1;
Stack[++tp] = v[i];
} else ++cnt[v[i]];
}
while(tp) {
int t = Stack[tp--], k = get(t, bl[p + 1], br[q - 1]);
if(!k) {
if(!(cnt[t] & 1)) ++res;
} else if((k & 1) && (cnt[t] & 1)) ++res;
else if(!(k & 1) && (cnt[t] & 1)) --res;
}
return res;
}
int main() {
init();
int res = 0;
for(int i = 1, l, r; i <= m; ++i) {
++tim;
read(l);
read(r);
l = (l + res) % n + 1;
r = (r + res) % n + 1;
if(l > r) swap(l, r);
printf("%d\n", res = query(l, r));
}
return 0;
}
解法二:
利用前缀和思想,设g[i] [j]表示从序列开始到第i块中,数字j的出现次数。则查询数字x在第p+1块到第q-1块的出现次数,为g[q-1] [x] - g[p] [x]。
实际写的时候,可以把g数组定义为第i块到序列末尾中数字j的出现次数,即后缀和,方便在同一个循环中处理。查询时类似于前缀和。
这样每次查询为O(1),显然,总复杂度为\(O(n\sqrt n)\) ,空间为\((Tn)\) 。
理论复杂度比解法一快,实际上,当T取\(\sqrt n\) 时,不开O2有两个点会TLE(可能是我写的太丑了QAQ)。然鹅,T取较大值时,耗时更短,但空间会炸(要开T*n的二维数组...)
Code
#pragma GCC optimize(2)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cctype>
using namespace std;
const int N = 1e5 + 5, T = 350;
int n, c, m, len, Stack[N], v[N], cnt[N], block[N], bl[T], br[T], f[T][T], g[T][N];
inline void read(int &tmp) {
register char c = getchar();
for(tmp = 0; !isdigit(c); c = getchar());
for(; isdigit(c); tmp = tmp * 10 + c - 48, c = getchar());
}
void init() {
read(n);
read(c);
read(m);
len = sqrt(n);
for(register int i = 1; i <= n; ++i) {
read(v[i]);
block[i] = (i - 1) / len + 1;
}
for(int i = 1; i <= block[n]; ++i) {
bl[i] = (i - 1) * len + 1;
br[i] = i * len;
}
br[block[n]] = n;
for (int i = 1, res; i <= block[n]; ++i) {
res = 0;
for (int j = bl[i]; j <= n; ++j) {
++g[i][v[j]];
if ((g[i][v[j]] & 1) && (g[i][v[j]] > 1)) --res;
else if (!(g[i][v[j]] & 1)) ++res;
f[i][block[j]] = res;
}
}
}
int query(int L, int R) {
int p = block[L], q = block[R];
register int res = 0, tp = 0, c, num;
if(q - p < 2) {
for(int i = L; i <= R; ++i) ++cnt[Stack[++tp] = v[i]];
while(tp) {
c = Stack[tp];
if(cnt[c]) res += (cnt[c] & 1) ^ 1;
cnt[Stack[tp--]] = 0;
}
return res;
}
res = f[p + 1][q - 1];
for(int i = L; i <= br[p]; ++i) ++cnt[Stack[++tp] = v[i]];
for(int i = bl[q]; i <= R; ++i) ++cnt[Stack[++tp] = v[i]];
while(tp) {
c = Stack[tp];
if(cnt[c]) {
num = g[p + 1][c] - g[q][c];
if((num & 1) && (cnt[c] & 1)) ++res;
else if(!(num & 1) && num && (cnt[c] & 1)) --res;
else if(!num && !(cnt[c] & 1)) ++res;
}
cnt[Stack[tp--]] = 0;
}
return res;
}
int main() {
init();
int res = 0;
for(register int i = 1, l, r; i <= m; ++i) {
read(l);
read(r);
l = (l + res) % n + 1;
r = (r + res) % n + 1;
if(l > r) swap(l, r);
printf("%d\n", res = query(l, r));
}
return 0;
}