1 条题解
-
0
自动搬运
来自洛谷,原作者为

JohnVictor
云OIer搬运于
2025-08-24 22:21:56,当前版本为作者最后更新于2020-05-21 21:03:47,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
放上官方题解。
令序列长 。
首先对于前 的数据可以直接暴力。复杂度 。
暴力枚举子集的方法是(这里是
c++代码)int f=x^y; for(int t=x^y;t>=0;t=(t-1)&f)当然还有什么都不发生的情况,也就是不存在满足要求的 。这个可以使用
a and b=a进行特判。继续开始思考后面的部分分。之后都假设所有的修改和询问都是有效的,也就是
a and b=a一直成立。这个修改、询问的形式类似一个区间,那么需要知道一个前置知识 高维前缀和。如果不知道的话可以学一下,比较简单。
那么类比不带修直接求区间的和,我们考虑用几个高维前缀和相加减去表示一个询问的答案。
这里可以从简单的情况开始考虑。如果 ,那么就直接是一个高维前缀和的答案;那么继续考虑,令 为 的高维前缀和,如果 ,那么询问 的答案是一些 的和,其中 是奇数并且
c and b=b。那么这个答案比 小了 是偶数,并且c and b=b的位数之和。后面的那个恰好可以用 表示。思路逐渐明朗起来,如果 是 与 相同且二进制位
1的一位的值,那么我们可以得到这个式子可以位运算进行优化,
x=a&-a,a-x可以用a^x表示。这个看似还没啥用——如果 相同那么这个复杂度还是 级别的。但是仔细分析:
如果 二进制不同的位有 位,相同的有 位,其中 ,那么直接暴力计算就可以用 次计算完成;如果用上面那种方法,它的复杂度是 。
进行特判并且选择可以做到 ,也就是 单次询问 。这个算法可以通过第二档部分分。
这里有一个很小的优化,没什么用:
可以快速预处理出 到 中每一个数有多少个二进制
1,这样查询二进制1的个数就从 变成了 。具体实现和FFT中的那个rev数组类似:for(int i=0;i<(1<<n);i++)count[i]=count[i>>1]+(i&1),rev[i]=(1<<n)-i-1;其实这么进行修改也是 的,但是唯一的问题就是无法及时更新。如果询问都在修改后面,可以考虑懒标记,用数组
curr记录。如果curr[i]记录了值,那么就代表所有i的子集都要加上这个值。这里用代码更清楚一点:
void modify(int x,int y,uint z) { if(count[x^y]<=n/2) { int f=x^y; for(int t=x^y;;t=(t-1)&f){ a[t|x]+=z; if(t==0) break; //这一部分是对修改值不多的情况进行暴力 } return; } if(x==0) { curr[y]+=z;//如果x=0那么就可以直接打上懒标记 return; } int lbt=x&-x; modify(x^lbt,y,z); modify(x^lbt,y^lbt,-z);//与 query 部分类似的递归 }等到所有修改结束后可以下放懒标记,暴力下放的复杂度是 ,总时间复杂度 ,可以通过第三档部分分。
然而这里并不是没有优化的空间了。下放懒标记的过程完全可以看成又一次高维前缀和。
本来的高维前缀和上的值是它的所有子集的和;而这时候需要加的值,是所有以它为自己的懒标记的和。
举个栗子,如果一个数的二进制表示是
001,那么高维前缀和中就是000,001的和,在这种情况下要加上001,011,101,111这四个懒标记的和。所以这样只用反着来一遍高维前缀和就行了。为了加速,我预处理了
rev数组代表一个数二进制取反后的结果,因为这里不能直接用~。(这个rev不是FFT中的rev)话不多说上代码:
void update() { for(int i=0;i<n;i++) for(int j=0;j<(1<<n);j++) { if(j&(1<<i)) curr[rev[j]]+=curr[rev[j^(1<<i)]]; } //这里是反着的高维前缀和 for(int i=0;i<(1<<n);i++) a[i]+=curr[i]; for(int i=0;i<(1<<n);i++) pres[i]=a[i]; for(int i=0;i<n;i++) for(int j=0;j<(1<<n);j++) if(j&(1<<i)) pres[j]+=pres[j^(1<<i)]; }其中
pres是高维前缀和数组。这一次update的复杂度是高维前缀和的复杂度 。好了,各位大佬们看到这里,正解已经呼之欲出了:分块!
我们假设块长为 ,一共 块。
修改还是正常修改,查询还是正常查询。这里复杂度是 。
查询的时候,唯一的问题就是计算块内贡献。这个可以位运算 计算每个贡献,复杂度是 ;每一块的
update的复杂度是 ,那么我们可以得到总的复杂度可以达到 ,在块长为 时最优。块内的贡献还是有细节的。大概是这两种情况:
(1)一个修改的数和询问的数没有相同的。
(2)有一部分相同。
这个位运算有很多种实现方法,直接上代码:
for(int i=1;i<=cnt;i++) { num=(s[i].a|a)&(s[i].b&b); if(num==int(s[i].a|a)) { num=(s[i].a|a)^(s[i].b&b); ans+=(1<<count[num])*s[i].k; } } cout<<ans<<endl;std的长度2235byte,作为数据结构还是比较良心的。如果写挂了可以找我要代码,前两天发现有人疑似抄我放的代码,所以不准备直接放。
- 1
信息
- ID
- 5152
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 6
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者