用途
在写序列莫队的时候,有时候我们会遇到这类问题:
为了统计答案,我们需要维护额外的结构或信息,导致时间复杂度从 (O(nsqrt{n})) 变成了 (O(nksqrt{n})) 。
(这里我们假设序列长度 (n) 和 (m) 同阶,否则需要重新考虑块的大小)
如果这里的信息具有可差分性,我们就可以尝试使用莫队二次离线解决这个问题。
做法
考虑我们现在已经统计的区间是 ([L,R]) 。每次我们可以将一个新的位置 (x) 加入到 ([L,R]) 里面来。
设 (f([L,R],x)) 为区间 ([L,R]) 加入了 (x) 的新增贡献。由于我们知道贡献可差分,于是就有:
平时我们是直接在线解决这个贡献;现在我们考虑离线。
然后你发现:
- 我们有 (n) 个前缀,因此 (f) 的第一个参数只有 (n) 种。
- 我们会移动 (O(nsqrt{n})) 次。
假如我们顺序扫描 ([1,1],[1,2],...,[1,n]) 这些前缀,并且维护它们的信息。于是我们就相当于有 (O(n)) 次修改操作和 (O(nsqrt{n})) 次查询。
注意到修改和查询很不平均,因此,如果我们适当地放大修改的时间,而缩小查询的时间,我们就可以减小时间复杂度!
另附一些优化:
- 莫队通常是修改 (L-1) 或者修改 (R+1) 。以修改 (R+1) 为例。此时 (f([1,R],R+1)) 只需要用一维状态描述,可以直接预处理。
- 莫队移动的时候,区间的边界移动了一段连续的区间。比如 ([L,R]) 变成 ([L,R+k]) ,那么 ((R,R+k]) 这些东西都会被加入到区间中。于是我们可以用 新增区间的范围 + (f) 第一维 来描述这样的查询。可以发现,这样做的话空间就优化到了 (O(n)) 。
例题
第十四次分块(前体)
先思考暴力莫队该怎么做。
首先发现,当 (k) 确定的时候,可用的数其实不多(存在上界 (P=inom{14}{7} = 3432) )。
因此我们可以先把这些符合要求的数暴力找出来。
然后,由于异或满足:(aoplus b=cLeftrightarrow aoplus c=b) ,因此我们可以用桶存下来每个数的出现次数。对于 (x) 查询的时候,我们就枚举合法异或值,并倒推出对应的数是哪一个。
此时的时间是 (O(Pnsqrt{n})) 。同时我们的维护的信息是 (O(1)) 修改、 (O(P)) 查询。
现在我们利用二次离线的技巧。此时我们需要降低查询时间。
显然我们可以直接倒过来做:我们加入 (x) 的时候,枚举合法异或值 (k) ;这就说明 (xoplus k) 又多了一个可配对的数,因此我们要增加 (xoplus k) 的答案。查询直接 (O(1)) 查。此时我们就做到了 (O(P)) 修改, (O(1)) 查询。
结合二次离线,时间就变成了 (O(np+nsqrt{n})) 。
代码:
#include <cmath>
#include <cstdio>
#include <algorithm>
typedef long long LL;
const int MAXN = 1e5 + 5, MAXS = 4e5 + 5, MAXK = 17005;
template<typename _T>
void read( _T &x )
{
x = 0;char s = getchar();int f = 1;
while( s > '9' || s < '0' ){if( s == '-' ) f = -1; s = getchar();}
while( s >= '0' && s <= '9' ){x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar();}
x *= f;
}
template<typename _T>
void write( _T x )
{
if( x < 0 ){ putchar( '-' ); x = ( ~ x ) + 1; }
if( 9 < x ){ write( x / 10 ); }
putchar( x % 10 + '0' );
}
int T;
struct Query
{
LL ans;
int l, r, blk, id;
Query() { ans = l = r = blk = id = 0; }
Query( int L, int R, int ID ) { ans = 0, l = L, r = R, blk = l / T, id = ID; }
bool operator < ( const Query &b ) const { return blk == b.blk ? ( blk & 1 ? r < b.r : r > b.r ) : l < b.l; }
}Q[MAXN];
struct PSQuery
{
int l, r, f, id;
PSQuery() { l = r = 0, f = 1; }
PSQuery( int L, int R, int F, int ID ) { l = L, r = R, f = F, id = ID; }
}PSQ[MAXS];
int head[MAXN], nxt[MAXS];
int cnt;
int num[MAXN], tot;
LL pre[MAXN];
LL ans[MAXN];
int buc[MAXK];
int a[MAXN];
int N, M, K;
int Ask( const int x ) { return buc[x]; }
void Clr() { for( int i = 0 ; i < 16384 ; i ++ ) buc[i] = 0; }
void Insert( const int x ) { for( int i = 1 ; i <= tot ; i ++ ) buc[num[i] ^ x] ++; }
void Add( const int p, const int left, const int rig, const int f, const int id )
{
PSQ[++ cnt] = PSQuery( left, rig, f, id );
nxt[cnt] = head[p], head[p] = cnt;
}
int main()
{
read( N ), read( M ), read( K ), T = sqrt( N );
for( int i = 1 ; i <= N ; i ++ ) read( a[i] );
for( int i = 1, l, r ; i <= M ; i ++ ) read( l ), read( r ), Q[i] = Query( l, r, i );
std :: sort( Q + 1, Q + 1 + M );
for( int i = 0 ; i < 16384 ; i ++ )
if( __builtin_popcount( i ) == K )
num[++ tot] = i;
for( int i = 1 ; i <= N ; i ++ ) pre[i] = Ask( a[i] ) + pre[i - 1], Insert( a[i] );
int L = 1, R = 0, qL, qR;
for( int i = 1 ; i <= M ; i ++ )
{
qL = Q[i].l, qR = Q[i].r;
if( L < qL ) Add( R, L, qL - 1, -1, i ), Q[i].ans += pre[qL - 1] - pre[L - 1], L = qL;
if( qL < L ) Add( R, qL, L - 1, +1, i ), Q[i].ans -= pre[L - 1] - pre[qL - 1], L = qL;
if( R < qR ) Add( L - 1, R + 1, qR, -1, i ), Q[i].ans += pre[qR] - pre[R], R = qR;
if( qR < R ) Add( L - 1, qR + 1, R, +1, i ), Q[i].ans -= pre[R] - pre[qR], R = qR;
}
Clr();
for( int i = 1 ; i <= N ; i ++ )
{
Insert( a[i] );
for( int j = head[i] ; j ; j = nxt[j] )
for( int k = PSQ[j].l, val ; k <= PSQ[j].r ; k ++ )
{
val = Ask( a[k] );
if( k <= i && K == 0 ) val --;
Q[PSQ[j].id].ans += PSQ[j].f * val;
}
}
for( int i = 1 ; i <= M ; i ++ ) ans[Q[i].id] = ( Q[i].ans += Q[i - 1].ans );
for( int i = 1 ; i <= M ; i ++ ) write( ans[i] ), putchar( '
' );
return 0;
}