P7906 [Ynoi2005] rpxleqxq 题解
P7906 [Ynoi2005] rpxleqxq 题解
题目大意
给定一个长度为 \(n\) 的序列 \(A\),和一个常数 \(k\)。
有 \(m\) 次询问,每次给定一个区间 \([l,r]\),询问有多少二元组 \((i,j)\),满足:
- \(1\leq i < j\leq n\);
- \((A_i\oplus A_j)\leq k\)。
Solve
前置知识:莫队二次离线。
对于普通莫队,端点移动时,例如右端点从 \(r-1\) 移动到 \(r\),对答案的贡献为 \([l,r)\) 内和 \(r\) 的异或和小于等于 \(k\) 的元素 \(A_i\) 的个数。对于这个的维护,一种想法是,枚举 \(A_r\oplus A_i\) 是在哪一位上和 \(k\) 决出来谁小,这样就确定了合法 \(A_i\) 二进制表示下的一个前缀,剩下的位随便填。可以使用 01 trie
维护前缀出现次数,做到插入和查询复杂度都是 \(O(\log_2V)\)。
然后考虑二离,维护 \(f(i)\) 为 \(i\) 左边和它异或和小于等于 \(k\) 的元素个数。可以沿用 01 trie
,复杂度是 \(O(n\sqrt V\log_2 V+n\sqrt m)\)。
理一下思路。二离之后,我们有 \(n\) 次插入和 \(n\sqrt m\) 次查询。使用 01 trie
的话,可以做到插入和查询复杂度都是 \(O(\log_2 V)\)。所以我们考虑平衡一下插入和查询的复杂度,使得查询做到 \(O(1)\)。
可以在插入 \(x\) 时还是枚举决胜位 \(i\),那么对于一个能对其产生贡献的 \(y\),它的 \(i\sim T\) 位都是确定的,其中 \(T=\log_2 V\)。而 \(0\sim i-1\) 位可以任意填,即从 \(y\in[0,2^{i-1}-1]\),是一个区间加操作。也就是说,我们需要在值域上维护一个数据结构,支持区间加和单点查,要求单点查复杂度是 \(O(1)\)。分块维护即可。这样的话,若块长 \(B=\sqrt V\),我们就得到了一个 \(O(nT\sqrt V+n\sqrt m)\) 的算法。理论约 \(2\times 10^9\)。
写完之后手搓极限数据,本地只跑了 \(1\) 秒。交上去发现获得了谷的最优解。
复杂度还是不够优秀,理论不是很对。调整一下块长。考虑我插入一个数时,在值域上修改的若干区间都是不交的,所以对于整块的修改复杂度最坏就是 \(V\over B\) 的,而对于散块,修改复杂度是 \(TB\);单次总复杂度是 \({V\over B}+TB\),当 \(B=\sqrt {V\over T}\) 时取得最小值,为 \(\sqrt{TV}\)。
这样,算法整体复杂度就来到了 \(O(n\sqrt {V\log_2 V}+n\sqrt m)\)。又交了一发,快了一点。
Code
const int N=2e5+10,B=200,T=105,V=2e5;
int w[N],sum[N],p[N],l[N],r[N];
inline void modify(const int &x,const int &y)//给 x ~ y 区间加一
{
if(x>y) return;
int u=p[x],v=p[y];
if(u==v)
{
for(int i=x;i<=y;i=-~i) w[i]=-~w[i];
return;
}
for(int i=x;i<=r[u];i=-~i) w[i]=-~w[i];
for(int i=-~u;i<v;i=-~i) sum[i]=-~sum[i];
for(int i=l[v];i<=y;i=-~i) w[i]=-~w[i];
}
inline void get_block()
{
int R=V/T+(V%T!=0);
for(int i=1;i<=R;i=-~i)
{
l[i]=-~r[i-1];r[i]=i==R?V:r[i-1]+T;
for(int j=l[i];j<=r[i];j=-~j) p[j]=i;
}
}
inline void insert(const int &x)
{
for(int i=17,p=0,a,b,now=0;~i;i=~-i)
{
a=x>>i&1;b=k>>i&1;
if(b) modify(now|(a<<i),min(now|(a<<i)|(1<<i)-1,V));
now|=(a^b)<<i;
}
if((x^k)<=V) w[x^k]=-~w[x^k];
}
inline int query(const int &x){return w[x]+sum[p[x]];}
剩下的就是莫二离板子。