1004.I love counting
补题地址
题目大意:
给出n个数,q次询问,每次询问l-r区间有多少个不同的数异或a小于b
1
≤
n
,
q
≤
1
0
5
,
1
≤
l
≤
r
≤
n
,
0
≤
a
,
b
≤
n
+
1
1 \leq n,q\leq10^5,1 \leq l \leq r\leq n,0 \leq a,b\leq n+1
1≤n,q≤105,1≤l≤r≤n,0≤a,b≤n+1
题解:
先考虑这样一个子问题:
n个数中有多少个数异或a小于b?(先不考虑l,r与“不同”这两个条件)
子问题解:
先将n个数按照值进行统计,然后从高位到低位分别讨论a,b的情况,进行计数。
举例说明:(以下以二进制表示数,最低位为第0位)
a=11001,b=01100
第4位b为0,a为1,所以第4位我们只能填1才能继续填下去,填0的话异或上a就肯定大于b了
第3位b为1,a为1,该位填1的话异或上a肯定小于b,贡献为值在11000-11111的数的个数,填0的话接着统计
第2为b为1,a为0, 该位填0的话异或上a肯定小于b,贡献为值在10000-10011的数的个数,填1的话继续统计
第1位b为0,a为0,只好填0
第0位b为0,a为1只好填1
所以总的贡献就是值在[11000,11111] ⋃ \bigcup ⋃ [10000,10011] ⋃ \bigcup ⋃ {10101(a^b)} 中数的个数。
回到原问题:
我们可以考虑将[l,r]中的数放到某种数据结构S中。
解决了该子问题之后,我们可以使用莫队+哈希数组(记录每个数出现的次数)的办法来保证S中的数都在[l,r]内,并且都是无重的。
对于每一个询问讨论a,b的每个位进行区间查询即可。
那么我们就需要一个单点修改区间查询的数据结构S。
我们知道修改的次数是 n ∗ n n*\sqrt n n∗n次(不妨设q=n)
而查询的次数最大是n*log次。
重要的是通过上面的分析我们可知查询具有极强的特点,每次查询的区间大小都是2的幂次,对于每个数的查询区间都是由大到小
我们可以选取分块来充当S,这样的话修改的复杂度是O(1),并且由于询问区间的性质导致查询平摊的复杂度很低。
对于单个询问复杂度的粗略证明:
对于每次询问只考虑左端点,左端点距离所处块的右端点的距离的期望显然是 b l o c k 2 \frac{block}{2} 2block(block为块的大小,通常设置成 n \sqrt n n ,右端点距离所处块的左端点的距离的期望也是 b l o c k 2 \frac{block}{2} 2block ,其查询的长度为2的次幂,如果左右端点在一个块内遍历即可, i ≤ 8 i\leq 8 i≤8时在同一个块内。
那么对于单次查询复杂度为:
∑
i
=
9
17
(
2
i
b
l
o
c
k
+
b
l
o
c
k
)
+
∑
i
=
1
8
2
i
\sum_{i=9}^{17} (\frac{2^i}{block}+block) + \sum_{i=1}^8 2^i
i=9∑17(block2i+block)+i=1∑82i
当
n
=
1
0
5
n=10^5
n=105时其值为3400左右,如果考虑每次询问分块的复杂度都是分块的上限即block,那么单次询问的复杂度为 5700左右,由理论值即可看出询问的性质对分块的复杂度影响很大。
总复杂度为O( n ∗ n + n ∗ ( ∑ i = 9 17 ( 2 i b l o c k + b l o c k ) + ∑ i = 1 8 2 i ) n*\sqrt n+n*(\sum_{i=9}^{17} (\frac{2^i}{block}+block) + \sum_{i=1}^8 2^i) n∗n+n∗(∑i=917(block2i+block)+∑i=182i))
当 n = 1 0 5 n=10^5 n=105时,其值为 3 ∗ 1 0 8 3*10^8 3∗108
该题时间限制是2s.
以上的计算只是理论值,由于实际上有很多情况是不需要询问的,实际测试时单次询问平均复杂度仅在1300左右,峰值在3000左右(和理论值很接近),并且询问分块和里面涉及的都是加法,跑的比较快。
我试图构造全是峰值的数据卡掉自己的程序,没有成功,O2优化下仅用了不到1s就跑出了结果(虽然复杂度计数器真的跑到了3e8,但是真的跑到很快,这和分块查询中的计算几乎全是加法关系很大)。
如果数据结构使用线段树或者树状数组以及字典树,那么修改的复杂度是logn的,一定会超时。
这启发我们在修改次数很多而求和相对比较少时使用分块算法。
AC代码如下:
#include <bits/stdc++.h>
#define debug(x) cout<<#x"=" <<x<<'\n';
using ll=long long;
using namespace std;
template<typename T> void read(T &x){
x = 0;char ch = getchar();ll f = 1;
while(!isdigit(ch)){if(ch == '-')f*=-1;ch=getchar();}
while(isdigit(ch)){x = x*10+ch-48;ch=getchar();}x*=f;
}
const int M=1e5+5;
int block;///块的大小,记得计算
struct node{
int l,r,id,a,b;
bool operator < (const node &cmp) const{
if(l/block == cmp.l/block) return r < cmp.r;
return l/block < cmp.l/block;
}
}q[M];
struct BLOCK
{
int sum[M],a[M<<1];
void add(const int &x,const int &u) ///在x的位置加上y
{
//debug(x);
//debug(u);
sum[x/block]+=u;
a[x]+=u;
}
int ask(const int &l,const int &r)///询问[l,r]的和
{
//debug(l);
//debug(r);
int ans=0;
if(r-l<=block)
{
for(int i=l;i<=r;i++) ans+=a[i];
//debug(ans);
return ans;
}
int bl=(l/block+1)*block;
int br=r/block*block;
for(int i=l;i<bl;i++) ans+=a[i];
for(int i=br;i<=r;i++) ans+=a[i];
int blockl=l/block+1;
int blockr=r/block-1;
for(int i=blockl;i<=blockr;i++) ans+=sum[i];
return ans;
}
}B;
int a[M];
int num[M];
int hs[M];
inline void add(int x)
{
x=a[x];
if(hs[x]==0)
B.add(x,1);
hs[x]++;
}
inline void del(int x)
{
x=a[x];
if(hs[x]==1)
B.add(x,-1);
hs[x]--;
}
inline int ask(const int &a,const int &b)
{
int ans=0;
int now=0;
for(int i=17;i>=0;i--)
{
if(b>>i &1)
{
if(a>>i & 1)
{
ans+=B.ask(now+(1<<i),now+(1<<(i+1))-1);
}
else ans+=B.ask(now,now+(1<<i)-1);
}
if((a>>i &1) ^ (b>>i &1)) now|=1<<i;
}
ans+=B.ask(a^b,a^b);
return ans;
}
int main() {
int n;
read(n);
block=sqrt(n);
for(int i=1;i<=n;i++) read(a[i]);
int m;
read(m);
for(int i=1;i<=m;i++)
{
read(q[i].l),read(q[i].r),read(q[i].a),read(q[i].b);
q[i].id=i;
}
sort(q+1,q+m+1);
int l = 1,r = 0;
for(int i = 1;i <= m;i ++){
while(r < q[i].r) add(++r);
while(r > q[i].r) del(r--);
while(l < q[i].l) del(l++);
while(l > q[i].l) add(--l);
num[q[i].id]=ask(q[i].a,q[i].b);
}
for(int i = 1;i <= m;i ++) printf("%d\n",num[i]);
return 0;
}
/*
5
1 2 2 4 5
1
1 3 1 3
*/
参考资料:
某大佬博客: 2021杭电多校2 (asukakyle.top)
思维历程
比赛时一直在想要到字典树上去做询问,而没有进一步分析"多少个数异或a小于b"这个条件,没有将其转化为单点修改,区间求和问题。