update on 2020.4.20: 修改了评论大佬指出的一个细节错误(orz),以及调整了一下文章。
正题
二次离线莫队是一个新的科技,可以处理这样的问题:一个序列,m个询问,每次询问l~r间有多少对(x,y)(l<=x<y<=r)满足某条件
。
而在这道题中,某条件
也就是x^y的二进制表示下有k个1
。
莫队嘛,肯定先要按莫队的套路来,先进行第一次离线,然后问题就出现了,排完序求解的时候,发现左右端点进行一次移动,很难在 O ( 1 ) O(1) O(1) 或是 O ( l o g n ) O(logn) O(logn) 的时间内更新答案,于是就需要用二次离线了。
(为了方便讲解,我们设输入的那个序列为 a [ 1 ] a[1] a[1] ~ a [ n ] a[n] a[n])
以右端点
r
i
g
h
t
right
right 向右移一位为例,这一次移动的贡献是a[right+1] ^ a[left]~a[right] 所能得到的二进制下有k个1的数的个数
,发现这个是可以差分的,也就是
a
[
r
i
g
h
t
+
1
]
a[right+1]
a[right+1] 对
a
[
1
]
a[1]
a[1] ~
a
[
r
i
g
h
t
]
a[right]
a[right] 贡献减去
a
[
r
i
g
h
t
+
1
]
a[right+1]
a[right+1] 对
a
[
1
]
a[1]
a[1] ~
a
[
l
e
f
t
−
1
]
a[left-1]
a[left−1]的贡献。
于是我们将莫队的 n n n \sqrt n nn 次查询贡献变成了 2 n n 2n\sqrt n 2nn次查询贡献。
好像更加难做了……
但是,查询变成一个个对前缀的查询后,就可以使用扫描线算法来计算了。这就是算法的核心,因为扫描线是离线算法,所以,称之为二次离线。
扫描线具体做法
现在的询问变成了:问 1 1 1 ~ x x x 这个前缀对 y y y 的贡献。于是我们可以开 n n n 个 vector,记录每个前缀对 对应的所有 y y y 的贡献。具体就是每有一个询问,就在第 x x x 个 vector 处 pushback 一个 y y y。
根据异或的一个性质( a a a ^ b = c , b b=c,b b=c,b ^ c = a c=a c=a),我们可以开个桶, t o n g [ i ] tong[i] tong[i] 表示当前前缀中有多少个数异或 y y y 得到的数在二进制下有 k k k 个 1 1 1。
然后从前缀 1 1 1 ~ i i i 转移到 1 1 1 ~ i + 1 i+1 i+1 时,只需要将所有 t o n g [ a [ i + 1 ] tong[a[i+1] tong[a[i+1] ^ p ] p] p]加一即可。其中 p p p 是指二进制下有 k k k 个 1 1 1 的那些数。
发现 p p p 的个数最多只有 C 14 7 = 3432 C_{14}^7 =3432 C147=3432 个,所以扫描线部分的时间复杂度只有 O ( n ∗ 3432 ) O(n*3432) O(n∗3432),完全可以接受。
看起来所有问题都已经解决了呢,但是
千辛万苦造出来以后发现毒瘤lxl卡了空间
——引自Scarlet
上面的做法有 2 n n 2n \sqrt n 2nn 个询问,但是如果全部存下来的话,算上一些杂七杂八的空间花费,空间复杂度瞬间爆炸。
但是我为了用实践来验证一下真理 (其实只是脑残) ,我去试了一下,然后发现Scarlet大佬所言无半句假话。
证据:MLE记录
于是我们还需要进行对空间的优化……
Here we go!
不妨先把莫队的所有移动的贡献列出来:
- 右端点右移:需要 a [ r i g h t + 1 ] a[right+1] a[right+1] 对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right]和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left−1] 的贡献
- 右端点左移:需要 a [ r i g h t ] a[right] a[right] 对 a [ 1 ] a[1] a[1] ~ a [ r i g h t − 1 ] a[right-1] a[right−1] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left−1] 的贡献
- 左端点右移:需要 a [ l e f t ] a[left] a[left] 对 a [ 1 ] a[1] a[1] ~ a [ l e f t ] a[left] a[left] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
- 左端点左移:需要 a [ l e f t − 1 ] a[left-1] a[left−1] 对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left−1] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
但是自己不可能对自己产生贡献,于是我们将上面这个可以稍作改动(荧光部分):
- 右端点右移:需要 a [ r i g h t + 1 ] a[right+1] a[right+1] 对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left−1] 的贡献
- 右端点左移:需要 a [ r i g h t ] a[right] a[right] 对 a [ 1 ] a[1] a[1] ~ a [ r i g h t − 1 ] a[right-1] a[right−1] 和对 a [ 1 ] a[1] a[1] ~ a [ l e f t − 1 ] a[left-1] a[left−1] 的贡献
- 左端点右移:需要 a [ l e f t ] a[left] a[left] 对 a [ 1 ] a[1] a[1] ~ a [ a[ a[ l e f t − 1 left-1 left−1 ] ] ] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
- 左端点左移:需要 a [ l e f t − 1 ] a[left-1] a[left−1] 对 a [ 1 ] a[1] a[1] ~ a [ a[ a[ l e f t − 2 left-2 left−2 ] ] ] 和对 a [ 1 ] a[1] a[1] ~ a [ r i g h t ] a[right] a[right] 的贡献
仔细观察,发现需要计算的贡献有两大类:
- a [ x ] a[x] a[x] 对 a [ 1 ] a[1] a[1] ~ a [ x − 1 ] a[x-1] a[x−1] 的贡献
- a [ i ] a[i] a[i] 对 a [ 1 ] a[1] a[1] ~ a [ y ] a[y] a[y] 的贡献
对于第一类贡献
第一类贡献只需要一个大小为 n n n 的数组即可存下,并且发现可以用前缀和优化。
对于第二类贡献
莫队每一次是的移动所产生的询问是连续的,比如将 [ l , r ] [l,r] [l,r] 变成 [ l , r + x ] [l,r+x] [l,r+x] 的过程中,产生的两类贡献询问是这个样子的:
询问
a
[
r
+
1
]
a[r+1]
a[r+1] 对
a
[
1
]
a[1]
a[1] ~
a
[
l
−
1
]
a[l-1]
a[l−1] 的贡献;
询问
a
[
r
+
2
]
a[r+2]
a[r+2] 对
a
[
1
]
a[1]
a[1] ~
a
[
l
−
1
]
a[l-1]
a[l−1] 的贡献;
询问
a
[
r
+
3
]
a[r+3]
a[r+3] 对
a
[
1
]
a[1]
a[1] ~
a
[
l
−
1
]
a[l-1]
a[l−1] 的贡献;
……
询问
a
[
r
+
x
]
a[r+x]
a[r+x] 对
a
[
1
]
a[1]
a[1] ~
a
[
l
−
1
]
a[l-1]
a[l−1] 的贡献;
也就是说,我们的询问可以合并为:询问 a [ r + 1 ] a[r+1] a[r+1] ~ a [ r + x ] a[r+x] a[r+x] 对 a [ 1 ] a[1] a[1] ~ a [ l − 1 ] a[l-1] a[l−1] 的贡献。
这好办,只需要在做扫描线的时候将 t o n g [ a [ r + 1 ] ] tong[a[r+1]] tong[a[r+1]] ~ t o n g [ a [ r + x ] ] tong[a[r+x]] tong[a[r+x]]累加起来即可。
也就是说,对于莫队每次端点的移动(例如 r r r 移动到 r + x r+x r+x)所产生的二类贡献只需要一个变量就可以存下来了。
总结上文
于是,空间复杂度从 O ( n n ∗ O(n \sqrt n * O(nn∗玄学常数 ) ) )降到了 O ( m ) O(m) O(m)。
perfect~
最后一步,上代码!
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
#define maxn 100010
#define ll long long
int n,m,k;
int a[maxn];
struct node{int l,r,past;ll answer;};
node ask[maxn];
struct vec{
vector <node>ve;
int t;
vec():t(-1){}
ll get(){return ve[++t].answer;}
}s[maxn];//上面提到的n个vector
int belong[maxn];
void work1()
{
int sqrtn=sqrt(n),tot=0,tt=1;
for(int i=1;i<=n;i++)
{
tot++;
if(tot>sqrtn)tot=1,tt++;
belong[i]=tt;
}
}
bool cmp1(node x,node y){return belong[x.l]==belong[y.l]?x.r<y.r:belong[x.l]<belong[y.l];}
ll me[maxn];//a[1]~a[x-1] 对a[x]的贡献
//from和to表示从from转移到to,x表示询问的那个前缀,from+del1和to+del2是询问的那个区间
void move(int &from,int to,int x,int del1,int del2)
{
s[x].ve.push_back((node){from+del1,to+del2,0,0});
from=to;
}
int kk[maxn],kt=0;
void work2(int x,int y,int z)//求出二进制下有k个1的数,将他们存在kk中
{
if(x==14)
{
if(z==0)kk[++kt]=y;
return;
}
else
{
if(z>0)work2(x+1,y|(1<<x),z-1);
work2(x+1,y,z);
}
}
ll tong[maxn];
void SMX()//扫描线
{
work2(0,0,k);
for(int i=1;i<=n;i++)
{
me[i]=tong[a[i]];//得出一类贡献
for(int j=1;j<=kt;j++)
tong[a[i]^kk[j]]++;//更新
for(int j=0;j<s[i].ve.size();j++)//计算二类贡献
{
ll tot=0;
if(s[i].ve[j].l<s[i].ve[j].r)//注意l是可能大于r的
for(int pp=s[i].ve[j].l;pp<=s[i].ve[j].r;pp++)
tot+=(ll)tong[a[pp]]-(pp<=i?k==0:0);
//这里要特判一下当k=0的时候自己是会对自己产生贡献的,要减掉
//因为这个我WA掉4个点调了半天555……
else
for(int pp=s[i].ve[j].l;pp>=s[i].ve[j].r;pp--)
tot+=(ll)tong[a[pp]]-(pp<=i?k==0:0);
s[i].ve[j].answer=tot;
}
}
}
ll sum[maxn];
bool cmp2(node x,node y){return x.past<y.past;};
int main()
{
scanf("%d %d %d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
scanf("%d %d",&ask[i].l,&ask[i].r),ask[i].past=i;
work1();//预处理一下分块,后面莫队排序用
sort(ask+1,ask+m+1,cmp1);//莫队排序
int left=1,right=0;//先跑一次莫队,把需要所有对贡献的询问存下来
for(int i=1;i<=m;i++)
{
//move是在记录二类贡献,建议这里move的参数要理解透,不然看下面第二次跑莫队计算答案时会很难受
if(right<ask[i].r)move(right,ask[i].r,left-1,1 ,0);
if(right>ask[i].r)move(right,ask[i].r,left-1,0 ,1);
if(left<ask[i].l) move(left ,ask[i].l,right ,0,-1);
if(left>ask[i].l) move(left ,ask[i].l,right ,-1,0);
}
SMX();
left=1;right=0;
ll ans=0;
for(int i=1;i<=n;i++)
sum[i]=sum[i-1]+me[i];
for(int i=1;i<=m;i++)//计算答案
{
if(right<ask[i].r)
{
ans+=sum[ask[i].r]-sum[right];
ans-=s[left-1].get();
}
if(right>ask[i].r)
{
ans-=sum[right]-sum[ask[i].r];
ans+=s[left-1].get();
}
right=ask[i].r;
if(left<ask[i].l)
{
ans+=sum[ask[i].l-1]-sum[left-1];
ans-=s[right].get();
}
if(left>ask[i].l)
{
ans-=sum[left-1]-sum[ask[i].l-1];
ans+=s[right].get();
}
left=ask[i].l;
if(ask[i].l==ask[i].r)ans=0;
ask[i].answer=ans;
}
sort(ask+1,ask+m+1,cmp2);
for(int i=1;i<=m;i++)
printf("%lld\n",ask[i].answer);
}