luoguP4887 第十四分块(前体)
题目传送门
分析
掌握核心莫队科技。
如果说题目中询问的是某个区间中的两两元素,子区间这种没有办法在
O
(
1
)
O(1)
O(1)统计的答案,有一种黑科技叫做莫队二次离线。
考虑莫队的一次插入与删除。
[
l
,
r
]
−
>
[
l
,
r
+
1
]
[l,r]->[l,r+1]
[l,r]−>[l,r+1]
这个过程中,需要变更的信息是
[
l
,
r
]
⊕
a
[
r
+
1
]
[l,r]\oplus a[r+1]
[l,r]⊕a[r+1]。
考虑开桶,也需要
C
14
7
=
3432
C_{14}^7=3432
C147=3432的复杂度来完成更新。
这个时候发现,变更的信息是可差分的。
D
e
l
t
a
(
l
,
r
)
=
A
n
s
(
[
1
,
r
]
⊕
a
[
r
+
1
]
)
−
A
n
s
(
[
1
,
l
−
1
]
⊕
a
[
r
+
1
]
)
Delta(l,r)=Ans([1,r]\oplus a[r+1])-Ans([1,l-1]\oplus a[r+1])
Delta(l,r)=Ans([1,r]⊕a[r+1])−Ans([1,l−1]⊕a[r+1])
不难发现,如果说能够预处理出
[
1
,
x
]
[1,x]
[1,x]区间内的贡献的桶,我们可以在
O
(
1
)
O(1)
O(1)的复杂度内处理
A
n
s
(
[
1
,
x
]
⊕
a
k
)
Ans([1,x]\oplus a_k)
Ans([1,x]⊕ak),而从
[
1
,
x
]
−
>
[
1
,
x
+
1
]
[1,x]->[1,x+1]
[1,x]−>[1,x+1],仅仅需要把
a
[
x
+
1
]
⊕
v
i
a[x+1]\oplus v_i
a[x+1]⊕vi塞到桶里面,其中
v
i
v_i
vi是所有二进制下恰有
k
k
k个
1
1
1的数。
所以考虑把莫队的所有
O
(
n
m
)
O(n\sqrt m)
O(nm)个操作暴力记录。开一个桶扫描线即可做到
O
(
3432
n
+
n
m
)
O(3432n+n\sqrt m)
O(3432n+nm),这就是所谓二次离线
然而出题人卡了空间。
这个时候就要考虑莫队的性质。
对于一个固定的询问
[
s
t
,
e
d
]
[st,ed]
[st,ed],莫队仅仅会做两种操作:
- 固定 l l l,将 r r r移到 e d ed ed
- 固定 r r r,将 l l l移到 s t st st
假设当前的操作是
[
l
,
r
]
−
>
[
l
,
e
d
]
[l,r]->[l,ed]
[l,r]−>[l,ed]。
答案就是
∑
x
=
r
+
1
e
d
D
e
l
t
a
(
l
,
x
)
=
∑
x
=
r
+
1
e
d
A
n
s
(
[
1
,
x
−
1
]
⊕
a
[
x
]
)
−
A
n
s
(
[
1
,
l
−
1
]
⊕
a
[
x
]
)
\sum_{x=r+1}^{ed}Delta(l,x)=\sum_{x=r+1}^{ed}Ans([1,x-1]\oplus a[x])-Ans([1,l-1]\oplus a[x])
∑x=r+1edDelta(l,x)=∑x=r+1edAns([1,x−1]⊕a[x])−Ans([1,l−1]⊕a[x])
显然
A
n
s
(
[
1
,
x
]
⊕
a
[
x
+
1
]
)
Ans([1,x]\oplus a[x+1])
Ans([1,x]⊕a[x+1])仅仅和
x
x
x有关,可以预处理+前缀和。
而
A
n
s
(
[
1
,
l
−
1
]
⊕
a
[
x
]
)
Ans([1,l-1]\oplus a[x])
Ans([1,l−1]⊕a[x])的
l
l
l是固定不变的,扫描线的时候可以一起处理区间
[
1
,
l
−
1
]
[1,l-1]
[1,l−1]对
x
∈
[
r
+
1
,
e
d
]
x\in[r+1,ed]
x∈[r+1,ed]的贡献。所以开一个邻接表或者
v
e
c
t
o
r
vector
vector记录下要算的区间即可。具体地,在
l
−
1
l-1
l−1位置插入
[
r
+
1
,
e
d
]
[r+1,ed]
[r+1,ed]区间,表示要计算
[
r
+
1
,
e
d
]
[r+1,ed]
[r+1,ed]中每个数的贡献,并且标记这个贡献对应的位置。
而其他情况是类似的,讨论一下即可。
这样的话就可以在
O
(
m
)
O(m)
O(m)的空间内完成这道题。
代码
#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;
}
模型抽取
神仙发明的神仙解法,我在此班门弄斧地解说一波。
二次离线法的问题模型:
- 基于莫队算法。
- 莫队 [ l , r ] [l,r] [l,r]拓展时,会用到 [ l , r ] [l,r] [l,r]中若干数对扩展数的贡献,暂且称这个贡献为 A n s ( [ l , r ] − > x ) ( x = r + 1 , l − 1 , r − 1 , l + 1 ) Ans([l,r]->x)(x=r+1, l-1,r-1,l+1) Ans([l,r]−>x)(x=r+1,l−1,r−1,l+1)
- 这个贡献可差分,即 A n s ( [ l , r ] − > x ) = A n s ( [ 1 , r ] − > x ) − A n s ( [ 1 , l − 1 ] − > x ) Ans([l,r]->x)=Ans([1,r]->x)-Ans([1,l-1]->x) Ans([l,r]−>x)=Ans([1,r]−>x)−Ans([1,l−1]−>x)
假设采用了某种数据结构(桶,堆,树状数组,块)维护了当前区间
[
l
,
r
]
[l,r]
[l,r]的信息,单点插入的复杂度为
O
(
I
)
O(I)
O(I),单点查询的复杂度为
O
(
Q
)
O(Q)
O(Q)
如果按照普通莫队算法进行分析,复杂度就是
O
(
n
m
(
I
+
Q
)
)
O(n\sqrt m(I+Q))
O(nm(I+Q))
而二次离线法可以将其优化到
O
(
n
I
+
n
m
Q
)
O(nI+n\sqrt m Q)
O(nI+nmQ)
做法如下:
考虑一个当前区间
[
l
,
r
]
[l,r]
[l,r],莫队算法对每一次询问的每次操作等价于固定某一个端点,将另一个端点扩展至目标端点
e
d
ed
ed,并计算贡献。(下面均采用固定左端点,挪动右端点的情况,其余情况类似)
也就是
D
e
l
t
a
(
[
l
,
r
]
−
>
[
l
,
e
d
]
)
=
∑
x
=
r
e
d
−
1
A
n
s
(
[
l
,
x
]
−
>
x
+
1
)
Delta([l,r]->[l,ed])=\sum_{x=r}^{ed-1} Ans([l,x]->x+1)
Delta([l,r]−>[l,ed])=∑x=red−1Ans([l,x]−>x+1)
- 由差分的性质可以得到, D e l t a ( [ l , r ] − > [ l , e d ] ) = ∑ x = r e d − 1 A n s ( [ 1 , x ] − > x + 1 ) − A n s ( [ 1 , l − 1 ] − > x + 1 ) Delta([l,r]->[l,ed])=\sum_{x=r}^{ed-1}Ans([1,x]->x+1)-Ans([1,l-1]->x+1) Delta([l,r]−>[l,ed])=∑x=red−1Ans([1,x]−>x+1)−Ans([1,l−1]−>x+1)
- 因为 A n s ( [ 1 , x ] − > x + 1 ) Ans([1,x]->x+1) Ans([1,x]−>x+1)仅仅与 x x x有关,可以预处理+前缀和求出。
- 因为 A n s ( [ 1 , l − 1 ] − > x + 1 ) Ans([1,l-1]->x+1) Ans([1,l−1]−>x+1)的左端点不变,相当于固定 [ 1 , l − 1 ] [1,l-1] [1,l−1],每次采用 [ 1 , l − 1 ] [1,l-1] [1,l−1]的信息去计算 x ∈ [ r + 1 , e d ] x\in[r + 1,ed] x∈[r+1,ed]的所有 x x x的贡献,所以将区间 [ r + 1 , e d ] [r+1,ed] [r+1,ed]标记至 l − 1 l-1 l−1的位置。
- 采用扫描线,用数据结构维护出 [ 1 , x ] [1,x] [1,x]的信息,再计算被标记到 x x x上所有区间内的数的贡献。 4.采用扫描线,用数据结构维护出 [ 1 , x ] [1,x] [1,x]的信息,再计算被标记到 x x x上所有区间内的数的贡献。
由于采用了扫描线,插入的时间复杂度被缩短到了
O
(
n
I
)
O(nI)
O(nI),询问照常。考虑每一个莫队上的询问,最多产生两个要插入的区间,所以空间复杂度不变。
采用了这个模型解决这道题就很快了。每个数插入桶的时间是
O
(
3432
)
O(3432)
O(3432),询问
O
(
1
)
O(1)
O(1),可以差分,于是就可以采用这个神仙做法。
分块、莫队其实就是各种时间与空间的平衡,果然是一门大学问,学到了学到了。