莫隊二次離線
用途
在寫序列莫隊的時候,有時候我們會遇到這類問題:
為了統計答案,我們需要維護額外的結構或資訊,導致時間複雜度從 \(O(n\sqrt{n})\) 變成了 \(O(nk\sqrt{n})\) 。
(這裡我們假設序列長度 \(n\) 和 \(m\) 同階,否則需要重新考慮塊的大小)
如果這裡的資訊具有可差分性,我們就可以嘗試使用莫隊二次離線解決這個問題。
做法
考慮我們現在已經統計的區間是 \([L,R]\) 。每次我們可以將一個新的位置 \(x\) 加入到 \([L,R]\) 裡面來。
設 \(f([L,R],x)\) 為區間 \([L,R]\) 加入了 \(x\) 的新增貢獻。由於我們知道貢獻可差分,於是就有:
\[f([L,R],x)=f([1,R],x)-f([1,L],x) \]
平時我們是直接線上解決這個貢獻;現在我們考慮離線。
然後你發現:
- 我們有 \(n\) 個字首,因此 \(f\) 的第一個引數只有 \(n\) 種。
- 我們會移動 \(O(n\sqrt{n})\) 次。
假如我們順序掃描 \([1,1],[1,2],...,[1,n]\) 這些字首,並且維護它們的資訊。於是我們就相當於有 \(O(n)\) 次修改操作和 \(O(n\sqrt{n})\) 次查詢。
注意到修改和查詢很不平均,因此,如果我們適當地放大修改的時間,而縮小查詢的時間,我們就可以減小時間複雜度!
另附一些優化:
- 莫隊通常是修改 \(L-1\)
- 莫隊移動的時候,區間的邊界移動了一段連續的區間。比如 \([L,R]\) 變成 \([L,R+k]\) ,那麼 \((R,R+k]\) 這些東西都會被加入到區間中。於是我們可以用 新增區間的範圍 + \(f\) 第一維 來描述這樣的查詢。可以發現,這樣做的話空間就優化到了 \(O(n)\) 。
例題
第十四次分塊(前體)
先思考暴力莫隊該怎麼做。
首先發現,當 \(k\) 確定的時候,可用的數其實不多(存在上界 \(P=\binom{14}{7} = 3432\)
因此我們可以先把這些符合要求的數暴力找出來。
然後,由於異或滿足:\(a\oplus b=c\Leftrightarrow a\oplus c=b\) ,因此我們可以用桶存下來每個數的出現次數。對於 \(x\) 查詢的時候,我們就列舉合法異或值,並倒推出對應的數是哪一個。
此時的時間是 \(O(Pn\sqrt{n})\) 。同時我們的維護的資訊是 \(O(1)\) 修改、 \(O(P)\) 查詢。
現在我們利用二次離線的技巧。此時我們需要降低查詢時間。
顯然我們可以直接倒過來做:我們加入 \(x\) 的時候,列舉合法異或值 \(k\) ;這就說明 \(x\oplus k\) 又多了一個可配對的數,因此我們要增加 \(x\oplus k\) 的答案。查詢直接 \(O(1)\) 查。此時我們就做到了 \(O(P)\) 修改, \(O(1)\) 查詢。
結合二次離線,時間就變成了 \(O(np+n\sqrt{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( '\n' );
return 0;
}