luoguP4887 第十四分块(前体)
题目传送门
分析
掌握核心莫队科技。
如果说题目中询问的是某个区间中的两两元素,子区间这种没有办法在
统计的答案,有一种黑科技叫做莫队二次离线。
考虑莫队的一次插入与删除。
这个过程中,需要变更的信息是
。
考虑开桶,也需要
的复杂度来完成更新。
这个时候发现,变更的信息是可差分的。
不难发现,如果说能够预处理出
区间内的贡献的桶,我们可以在
的复杂度内处理
,而从
,仅仅需要把
塞到桶里面,其中
是所有二进制下恰有
个
的数。
所以考虑把莫队的所有
个操作暴力记录。开一个桶扫描线即可做到
,这就是所谓二次离线
然而出题人卡了空间。
这个时候就要考虑莫队的性质。
对于一个固定的询问
,莫队仅仅会做两种操作:
- 固定 ,将 移到
- 固定 ,将 移到
假设当前的操作是
。
答案就是
显然
仅仅和
有关,可以预处理+前缀和。
而
的
是固定不变的,扫描线的时候可以一起处理区间
对
的贡献。所以开一个邻接表或者
记录下要算的区间即可。具体地,在
位置插入
区间,表示要计算
中每个数的贡献,并且标记这个贡献对应的位置。
而其他情况是类似的,讨论一下即可。
这样的话就可以在
的空间内完成这道题。
代码
#include<bits/stdc++.h>
const int N = 1e5 + 10;
int ri() {
char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int v[N], a[N], pr[N], cnt[N], nx[N << 1], C, tp, n, m, k, B;
long long Ans[N], cur, A[N << 1], s1[N], s2[N];
struct Q {int l, r, id; void in(int i) {l = ri(), r = ri(); id = i;}}q[N], to[N << 1];
void Add(int u, Q a) {to[++C] = a; nx[C] = pr[u]; pr[u] = C;}
bool cmp(Q a, Q b) {
int x = a.l / B, y = b.l / B;
return x == y ? (x & 1 ? a.r > b.r : a.r < b.r) : x < y;
}
int main() {
n = ri(); m = ri(); k = ri(); B = sqrt(n);
for(int i = 0, x, c;i < 16384; ++i) {
for(x = i, c = 0; x; x -= x&-x) ++c;
if(c == k) v[++tp] = i;
}
for(int i = 1;i <= n; ++i) a[i] = ri();
for(int i = 1;i <= m; ++i) q[i].in(i);
std::sort(q + 1, q + m + 1, cmp);
for(int i = 1, l = q[1].r + 1, r = q[1].r;i <= m; ++i) {
int st = q[i].l, ed = q[i].r, x = q[i].id;
if(l < st) Add(r, (Q) {l, st - 1, x << 1});
else if(l > st) Add(r, (Q) {st, l - 1, x << 1});
l = st;
if(r < ed) Add(l - 1, (Q) {r + 1, ed, x << 1 | 1});
else if(r > ed) Add(l - 1, (Q) {ed + 1, r, x << 1 | 1});
r = ed;
}
for(int i = 1;i <= n; ++i) {
s1[i] = s1[i - 1] + cnt[a[i]];
for(int j = 1;j <= tp; ++j) ++cnt[a[i] ^ v[j]];
s2[i] = s2[i - 1] + cnt[a[i]];
for(int j = pr[i]; j; j = nx[j])
for(int k = to[j].l;k <= to[j].r; ++k)
A[to[j].id] += cnt[a[k]];
}
for(int i = 1, l = q[1].r + 1, r = q[1].r;i <= m; ++i) {
int st = q[i].l, ed = q[i].r, x = q[i].id;
if(l < st) cur += s2[st - 1] - s2[l - 1] - A[x << 1];
else if(l > st) cur += A[x << 1] - s2[l - 1] + s2[st - 1];
l = st;
if(r < ed) cur += s1[ed] - s1[r] - A[x << 1 | 1];
else if(r > ed) cur += A[x << 1 | 1] - s1[r] + s1[ed];
r = ed; Ans[x] = cur;
}
for(int i = 1;i <= m; ++i) printf("%lld\n", Ans[i]);
return 0;
}
模型抽取
神仙发明的神仙解法,我在此班门弄斧地解说一波。
二次离线法的问题模型:
- 基于莫队算法。
- 莫队 拓展时,会用到 中若干数对扩展数的贡献,暂且称这个贡献为
- 这个贡献可差分,即
假设采用了某种数据结构(桶,堆,树状数组,块)维护了当前区间
的信息,单点插入的复杂度为
,单点查询的复杂度为
如果按照普通莫队算法进行分析,复杂度就是
而二次离线法可以将其优化到
做法如下:
考虑一个当前区间
,莫队算法对每一次询问的每次操作等价于固定某一个端点,将另一个端点扩展至目标端点
,并计算贡献。(下面均采用固定左端点,挪动右端点的情况,其余情况类似)
也就是
- 由差分的性质可以得到,
- 因为 仅仅与 有关,可以预处理+前缀和求出。
- 因为 的左端点不变,相当于固定 ,每次采用 的信息去计算 的所有 的贡献,所以将区间 标记至 的位置。
- 采用扫描线,用数据结构维护出 的信息,再计算被标记到 上所有区间内的数的贡献。 4.采用扫描线,用数据结构维护出 的信息,再计算被标记到 上所有区间内的数的贡献。
由于采用了扫描线,插入的时间复杂度被缩短到了
,询问照常。考虑每一个莫队上的询问,最多产生两个要插入的区间,所以空间复杂度不变。
采用了这个模型解决这道题就很快了。每个数插入桶的时间是
,询问
,可以差分,于是就可以采用这个神仙做法。
分块、莫队其实就是各种时间与空间的平衡,果然是一门大学问,学到了学到了。