题解 P6562 【[SBCOI2020] 归家之路】
JohnVictor · · 题解
放上官方题解。
令序列长
首先对于前
暴力枚举子集的方法是(这里是 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,作为数据结构还是比较良心的。
如果写挂了可以找我要代码,前两天发现有人疑似抄我放的代码,所以不准备直接放。