题解 洛谷P6562 [SBCOI2020] 归家之路
题目大意
给定\(n\)以及\(a_0,a_1,\dots,a_{2^n-1}\)的初始值。对于整数\(x,y\),定义\(x\subseteq y\)为真当且仅当\(x\operatorname{and}y=x\)。\(q\)次操作,操作分两种:
- 修改操作:\(1\ x\ y\ k\),对于所有\(x\subseteq t\subseteq y\),把\(a_t\)加上\(k\)。
- 询问操作:\(2\ x\ y\),对于所有\(x\subseteq t\subseteq y\),求出\(a_t\)的和。对\(2^{32}\)取模。
数据范围:\(1\leq n\leq 16\), \(1\leq q\leq 2\times10^5\)。
本题题解
对于所有修改和询问,我们首先判断它是否满足\(x\subseteq y\)。如果不满足,则可以直接忽略(如果是询问则答案等于\(0\))。以下只考虑\(x\subseteq y\)的情况。
修改和询问,方法是类似的。这里我们以修改为例。暴力的做法是枚举所有\(t\in[0,2^n)\),判断是否满足\(x\subseteq t\subseteq y\)。容易想到一种更优雅的暴力:设\(z=y\setminus x\),我们枚举\(t'\subseteq z\),则所有合法的\(t\),都可以表示为\(t'\cup x\)。我们很容易实现不重不漏地枚举子集。那么,如果设\(y\)比\(x\)多的二进制位有\(d\)个,我们现在就可以\(O(2^d)\)实现一次修改或查询。
//不重不漏地枚举子集
int z=x^y;
for(int i=z;;i=(i-1)&z){
// do something ...
if(i==0)break;
}
不过显而易见的是,\(d\)可能达到\(O(n)\)级别,所以仅有上述的暴力还是不够的。
对于这种子集询问,可以将其看做\(n\)个维度,于是想到做高维前缀和。设\(a\)的高维前缀和数列为\(s\),即\(s_{y}=\sum_{x\subseteq y}a_x\)。记询问\((x,y)\)对应的答案为\(f(x,y)\)。显然,当\(x=0\)时,\(f(0,y)=s_y\)。
考虑\(x\neq 0\)时,取出任意一个\(x\)里是\(1\)的二进制位,例如,取出\(p=\operatorname{lowbit}(x)\)。因为\(x\subseteq y\),所以\(y\)的这一位也必为\(1\)。那么:
这很好理解:\(f(x-p,y)\)的意思是,\(p\)这一位既可以选也可以不选。但是在符合\(f(x,y)\)要求的数里,\(p\)这一位必须为\(1\),所以减去\(p\)这一位为\(0\)的情况\(f(x-p,y-p)\)即可。
我们可以按这个式子递归下去,每次\(x\)一定会减少一个二进制位,递归的边界就是\(x=0\)。设\(x\)里有\(c\)个\(1\),则递归的复杂度是\(O(2^c)\)的。
这个复杂度看上去依然高达\(O(2^n)\),不过我们可以和上一个暴力结合一下。回顾:\(d\)是\(y\)比\(x\)多的二进制位数,那么\(c+d\)就是\(y\)的二进制位数,显然\(c+d\leq n\)。因此:\(\min(c,d)\leq\frac{n}{2}\)。所以结合这两种做法,单次修改或查询的复杂度可以降至\(O(2^{\frac{n}{2}})\)。
不过别高兴得太早。这种做法,需要预先知道高维前缀和数组\(s\)。这在静态的问题(subtask3)中是好办的:修改时,维护一个标记数组\(m\),\(m_y\)表示对\(y\)的所有子集都要加上这个数。所有修改完成后,做一遍高维后缀和(fwt and),就能还原出每个位置\(a_i\)的增量。然后对新的\(a\),做一遍高维前缀和(fwt or),就得到\(s\)了。两次fwt的复杂度都是\(O(2^nn)\)。
但是在非静态的问题里,就不能这样处理\(s\)。
我们考虑对操作分块,每\(B\)个操作分为一块。
对于修改操作,还是和上面一样进行,仍然维护一个标记数组\(m\),不过这个\(m\)只记录当前块的信息,跳到下一个块时要清空。
对于查询操作,分别考虑【本块内的修改】和【前面块的修改】对答案的贡献。
- 【本块内的修改】的贡献:枚举所有本块内的修改,设修改的范围是\((x_1,y_1)\),当前询问的范围是\((x_2,y_2)\)。则这个修改对本次询问,产生贡献的范围就是\((x_1\operatorname{or} x_2,y_1\operatorname{and}y_2)\)。这个范围里的数的数量,就是\(2^{\text{bitcnt}((y_1\operatorname{and}y_2)\setminus(x_1\operatorname{or} x_2))}\)。
- 【前面块的修改】的贡献:可以把前面块修改后的高维前缀和数组\(s\)预处理好。然后根据\(c\), \(d\)的大小,枚举子集或递归查询即可(这和静态问题是一样的。所以说分块的好处就是,不同的块之间,相当于是静态问题)。
每处理完一个块,就更新高维前缀和数组\(s\)。
时间复杂度\(O(2^nn\cdot \frac{q}{B}+(B+2^{\frac{n}{2}})q)\)。\(B\)取\(2000\)左右可以AC。
参考代码:
//problem:P6562
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
template<typename T>inline void ckmax(T& x,T y){x=(y>x?y:x);}
template<typename T>inline void ckmin(T& x,T y){x=(y<x?y:x);}
const int MAXN=16;
const int MAXM=1<<MAXN;
const int MAXQ=2e5;
const int B=2000;
//const int BCNT=MAXQ/B+10;
int n,q,m,half_n;
int bitcnt[MAXM+5];
uint v[MAXM+5],s[MAXM+5],mdf_v[MAXM+5],mdf_s[MAXM+5];
template<typename T>
void fwt_or(T* a,int n,T flag){
for(int i=1;i<n;i<<=1){
for(int j=0;j<n;j+=(i<<1)){
for(int k=0;k<i;++k){
a[j+k+i]+=a[j+k]*flag;
}
}
}
}
template<typename T>
void fwt_and(T* a,int n,T flag){
for(int i=1;i<n;i<<=1){
for(int j=0;j<n;j+=(i<<1)){
for(int k=0;k<i;++k){
a[j+k]+=a[i+j+k]*flag;
}
}
}
}
struct ModifyInfo{
int a,b;uint k;
ModifyInfo(){}
ModifyInfo(int _a,int _b,uint _k){
a=_a;b=_b;k=_k;
}
}sta[B+5];
int top;
void modify_work(int a,int b,uint k){
if(!a){
mdf_s[b]+=k;
return;
}
int lb=(a&(-a));
modify_work(a-lb,b,k);
modify_work(a-lb,b-lb,-k);
}
void modify(int a,int b,uint k){
if((a&b)!=a)return;
sta[++top]=ModifyInfo(a,b,k);
if(bitcnt[a^b] <= half_n){
int dif=a^b;
for(int i=dif;;i=(i-1)&dif){
mdf_v[a^i]+=k;
if(i==0)break;
}
return;
}
modify_work(a,b,k);
}
uint query_work(int a,int b){
if(!a){
return s[b];
}
int lb=(a&(-a));
return query_work(a-lb,b)-query_work(a-lb,b-lb);
}
uint query(int a,int b){
if((a&b)!=a)return 0;
uint res=0;
// 本块内的修改对本次询问的贡献
for(int i=1;i<=top;++i){
int na=(a|sta[i].a);
int nb=(b&sta[i].b);
if((na&nb)!=na)continue;
res+=(1u<<bitcnt[na^nb])*sta[i].k;
}
if(bitcnt[a^b] <= half_n){
int dif=a^b;
for(int i=dif;;i=(i-1)&dif){
res+=v[a^i];
if(i==0)break;
}
return res;
}
//之前的块的贡献
res+=query_work(a,b);
return res;
}
void rebuild(){
fwt_and(mdf_s,m,1u);//懒标记下放
for(int i=0;i<m;++i){
v[i] += mdf_v[i] + mdf_s[i];
s[i]=v[i];
mdf_v[i]=mdf_s[i]=0;
}
fwt_or(s,m,1u);
top=0;
}
int main() {
cin>>n>>q;
m=1<<n;
half_n=n>>1;
for(int i=0;i<m;++i){
cin>>v[i];
s[i]=v[i];
}
fwt_or(s,m,1u);//高维前缀和
for(int i=1;i<m;++i)
bitcnt[i]=bitcnt[i>>1]+(i&1);//预处理bitcnt
for(int tq=1;tq<=q;++tq){
int op;cin>>op;
if(op==1){
int a,b;uint k;
cin>>a>>b>>k;
modify(a,b,k);
}
else{
int a,b;
cin>>a>>b;
cout<<query(a,b)<<endl;
}
if(tq%B==0)rebuild();
}
return 0;
}