【精选】前缀和の超级进化——SOS-DP & FMT/FWT
SMT0x400
·
·
个人记录
前缀和の超级进化——SOS-DP & FMT
作者 SMT0x400,版权本人所有,允许转载。
0.说句闲话:研究本文的最好方式是
浅谈前缀和的非常好写法 - DengDuck の Blog - 洛谷博客 (luogu.com.cn)
SOSdp - 果冻的博客 - 洛谷博客 (luogu.com.cn)
FWT 学习笔记 - _Famiglistimo - 博客园 (cnblogs.com)
或多或少地参考了以上三位大佬的博客。感谢 @Jelly_prx 和 @Dengduck !
0.1 本文定位
属于一篇有一定门槛的科普文。需要学会高维前缀和的写法。
对于 提高组 选手而言,只要会一点点集合符号和容斥原理即可学会 SOS-DP。
对于 省选/NOI 选手而言,这篇文章可以扩展到 FWT/FMT,是一个常用的位运算卷积的技能点。
0.2 规定
数组下标为了方便写式子,统一使用方括号而不是下标。
1.引入:高维前缀和
1.1 普通前缀和 / 差分
普及组知识:我们一般利用前缀和来解决一些 O(n) 预处理 O(1) 查询的题目,比如查询区间和为多少,不带修改。
另外,使用差分打标记+前缀和最后计算,来解决 O(1) 修改,O(n) 查询的题目。
我们考虑把它丢到二维平面上,若是我要计算 (i,j) 左上角所有数的和,那么公式是
这个式子不长,很短很好写。
---
但是如果我把它扩展到三维的情况呢?此时就需要一个个容斥展开了。
我直接再写个三重循环做容斥系数不就行了吗。
进一步:要是我的维度是 $k$ 维呢?
我再写个 $k$ 重循环容斥不就行了吗。时间复杂度是 $O(2^k n)$(其中 $n$ 为点数)。
但是,这里介绍一种好写又好想的做法,只需要 $O(kn)$ 的时间复杂度。
----
不要局限于眼前的二维和三维,我们要**泛化维度**思想。
考虑外层一个循环,循环 $k$ 次,每一次枚举一个低维,从这个低维转移过来。
那么代码很简单,有待补充。
我们的“维度”需要保证相互独立,不能交叉,否则会出现问题。
## 2.SOS-DP 的引入
在此先卖个关子,不直接讲 SOS-DP,但是先来做一道题:
### 狄利克雷前缀和
给定 $a$ 数组,要求求 $b[i]=\sum _{d|i}a[d]$,算法时间复杂度要求线性。
天然地想到:枚举每个**质数**,作为前缀和转移的“维度”。因为是**质数**作为维度,所以它们之间**相互独立**。
所以无需直接枚举因子,枚举质数 $i$,然后每次 $f[S\div i]\to f[S]$。
于是很快就能写出这样的代码。
```cpp
I sd;inline I g(){return sd^=sd<<13,sd^=sd>>17,sd^=sd<<5,sd;}
const I N=2e7+10;
I n,a[N],ans,pr[N/10],m;
bool p[N];
signed main(){
n=in();sd=in();
fo(i,1u,n)a[i]=g();
fo(i,2u,n){
if(!p[i])pr[++m]=i;
for(I j=1;j<=m&&1ll*pr[j]*i<=1ll*n&&(p[i*pr[j]]=1,i%pr[j]!=0);++j);}
fo(i,1u,m)for(I j=pr[i],k=1;j<=n;j+=pr[i],++k)a[j]+=a[k];
fo(i,1u,n)ans^=a[i];
return printf("%u",ans),0;}
```
这和我们今天所探讨的子集求和、子集卷积没有半毛钱关系,但是“泛化维度”思想对我们而言,又是一把处理位运算的利剑。
好了,回归正题,讲讲 SOS-DP 能解决啥问题。
子集求和:给定 $g$,需要求这样的一个数组 $f$,$f[S]=\sum_{T\subseteq S}g[T]$。
我们可以 $O(3^n)$ 直接暴力枚举子集算贡献,代码很简单就不放了。
在 $n\le 16$ 的情况下都可以这么做。
但是,它不够快,也不够优美。很多贡献,都是重复的,等待着我们去优化。
这里的“求和”其实**不是严格意义上的求和**,取 $\min,\max$ 甚至更多奇怪的操作,也是可以的。
## 3.SOS-DP 的流程
回顾一下我们的算法目标:$f[S]=\sum_{T\subseteq S}g[T]$;(子集卷积是 $\sum_{T\subseteq S} g[T]h[\complement_S T]$,不一样的)
那么我们不妨利用 $S$ 的子集去处理。
则 ${f[S]=g[S]+\sum _{i\in S}f[\complement_S \{i\}]}$。
会不会有重复呢?可能会有。所以我们需要钦定外层的枚举顺序,先枚举 $i$,再枚举 $S$。
这样每个 $f[S]$ 其实就是对于 $i$ 维的一个子集之和,每次新来一个 $i$ 都把不相关的两个集合合并了,它是高效且正确的。
代码很好写:(这里的 `mrg` 函数就是“求和”函数,前面说了,不仅仅可以是加法)
```cpp
fo(i,0,n-1)fo(j,0,ns)
if(j&(1<<i))mrg(f[j],f[j^(1<<i)]);
```
---
既然子集求和有了,再来讲超集求和。理论上超集卷积原理和子集卷积一样,详见 FWT(但是我并不会)。
算法目标:$f[S]=\sum_{T\supseteq S} g[T]$。
那么 ${f[S]=g[S]+\sum _{i\notin S}f[\complement_S \{i\}]}$。
---
OK,这两个类似,接下来看多点题目。
## 4.SOS-DP 的例题
### ARC100E Or Plus Max
题意:对于每个 $k$ 求 $\max(A_i+A_j)[i<j,i\operatorname{or} j\le k]$。已经有下标的形式了,就不需要转下标了。
我们观察一下 $i\operatorname{or} j\le k$ 是啥。直接做十分不好弄。
于是考虑一个这样的事情:令 $i,j\subseteq k$ 那么 $i\cup j\subseteq k$。显然,若 $i\subseteq k$,则 $i\le k$。
所以这题就变成了对于每个 $k$ 求其子集中某两种不同编号子集的最大值。再理清楚一点,就是维护子集中前两大的 $a$ 值。
化成子集求和的形式,$a[S]\to f[T]$,其中 $S\subseteq T$。然后直接套用子集求和的 SOS-DP,这题就结束了。
```cpp
#include<bits/stdc++.h>
#define fo(i,a,b) for(auto i(a),_ei(b);i<=_ei;++i)
#define gch(w) for(;w(CH);CH=getchar())
using I=int;using V=void;using LL=long long;I FL,CH;LL in(){LL a(0);FL=1;gch(!isdigit)FL=(CH=='-')?-1:1;gch(isdigit)a=a*10+CH-'0';return a*FL; }using namespace std;
const I NS=262144;
#define pii array<I,2>
pii f[NS];I n,ns,mx;
V mrg(pii&a,pii b){
array<I,4>c={a[0],a[1],b[0],b[1]};
sort(&c,&c+4);a={c[3],c[2]};}
I main(){n=in();ns=(1<<n)-1;
fo(i,0,ns)f[i][0]=in();
fo(i,0,n-1)fo(j,0,ns)if(j&(1<<i))mrg(f[j],f[j^(1<<i)]);
fo(i,1,ns)printf("%d\n",mx=max(mx,f[i][0]+f[i][1]));
return 0;
}
```
### CF1208F [Bits And Pieces](https://www.luogu.com.cn/problem/CF1208F)
题意:给定 $n$ 个数的数组 $d$,找到 $i\lt j\lt k $ 的 $i,j,k$,使得 $d_i|(d_j \& d_k)$ 最大。
我们观察实际上更能满足条件的是进制位,于是考虑令 $a_{d_i}=i$ 然后转移。
---
再来理清一下问题。现在我可以枚举 $k$,然后判断一个 $x$,其中 $x$ 是等于某两个元素组成的,要求组成 $f[x]$ 的那两个元素编号尽可能大。
所有进制都有一个套路,考虑从高到低位构造 $k|x$ 这个值,就能让这个值最大化。
设 $k[i]$ 为 $k$ 二进制下从低到高第 $i$ 位。
若 $k[i]=1$ 的时候,不用管它,直接跳过;(这里**不需要强制填数**);
若 $k[i]=0$ 的时候,判断有没有一个 $x$ 能够使得**填数的位**都能做到 $1$。如果能就最好,不能就算了。
具体而言,设当前填的答案 $ans$ 只算到了第 $i$ 位,令低位的先不填,当前的填上,然后算 $ans$ 的超集即可。
如果它的超集的次大坐标大于 $i$,则可以保留;否则不可以保留,直接把这位变成 $0$。
为什么要算超集?很显然:因为超集保证一定满足该填的都填了,不该填的也可能填。
---
问题就被我们转化成了:计算 $f[x]$,对于 $x$ 超集“求和”,依此更新答案。于是维护最大坐标和次大坐标就行了。有些小细节,需要注意。
```cpp
#include<bits/stdc++.h>
#define fo(i,a,b) for(auto i(a),_ei(b);i<=_ei;++i)
using I=int;const I N=1<<21;using namespace std;
I n,mxa,lgn,a[N],Mans,pn;
#define I2 array<I,2>
void mrg(I2&a,I2 b){
array<I,4>c={a[0],a[1],b[0],b[1]};
sort(c.begin(),c.end());a={c[3],c[2]};}I2 f[N];
I main(){ios::sync_with_stdio(0);cin.tie(0);
cin>>n;pn=n;
fo(i,1,n)cin>>a[i],mrg(f[a[i]],{i,0}),mxa=max(mxa,a[i]);
for(n=1;n<=mxa;n<<=1,++lgn);
fo(i,0,lgn-1)for(I j=n-1;j>=0;--j)
if(!(j&(1<<i)))mrg(f[j],f[j^(1<<i)]);
fo(i,1,pn-2){I ans(0);
for(I k=lgn-1;~k;--k){
if((a[i]>>k)&1)continue;
ans|=1<<k;
if(f[ans][1]<=i)ans^=1<<k;}
Mans=max(Mans,ans|a[i]);}
return printf("%d\n",Mans),0;
}
```
这个题目给我们的思考规律就是:
1. 值和下标互相转化。
2. 考虑一个复杂的条件,转为子集(主要是 $\operatorname{or}$)或者超集(主要是 $\operatorname{and}$)。
3. 直接 SOS-DP。
### [CF383E Vowels](https://www.luogu.com.cn/problem/CF383E)
题意:给出 $n$ 个长度为 $3$ 的由 $\texttt{a}\sim\texttt{x}$ 组成的单词,一个单词是正确的当且仅当**其包含至少一个元音字母**。
这里的**元音字母**是 $\texttt{a}\sim\texttt{x}$ 的一个**子集**。对于**所有**元音字母**集合**,求这 $n$ 个单词中**正确单词的数量平方**的 **异或和**。
---
按照套路,首先先丢进桶里面统计个数。观察 $n$ 很小,字母数量很大。
---
考虑对于 $S$ 集合统计答案。
**至少一个?那么就相当于取一个没有的补集!**求对于 $S$ 集合中元音字母一个没有的方案数。(直接用 $n$ 减去它即为答案)。
再次转化,因为我丢的桶不好搞,那么就求对于 $\complement_U S$ 的所有子集方案数。
然后直接做子集求和即可。
----
### CF449D Jzzhu and Numbers
题意:有 $n$ 个二进制数字,每个数字 $m$ 位,求选择一些数 and 起来,恰好等于 $0$ 的方案数,取模 $10^9+7$。
先用 SOS-DP 解决问题。
设 $a[i]$ 为数字选 $i$ 有多少种方案,维护 $b[i]$ 为数字选 $i$ 的**超集**有多少种方案。
很明显,若目标集合为 $S$,则**选择 $S$ 的超集的**方案数应该为 $g[S]=2^{b[S]}-1$ 种(一定要选,所以要减 $1$)。
于是再差分回去,即为答案。差分只需要变动符号。
---
观察到和 FWT 的 and 很像,确实是这样的。这就是 FWT 的另一种实现。
```cpp
#include<bits/stdc++.h>
#define fo(i,a,b) for(auto i(a),_ed(b);i<=_ed;++i)
#define gch(k) for(;(k);CH=getchar())
using namespace std;using I=int;using LL=long long;using V=void;I FL,CH;LL in(){LL a=0;FL=1;gch(!isdigit(CH))FL=(CH=='-')?-1:1;gch(isdigit(CH))a=a*10+CH-'0';return a*FL;}
const I mod=1e9+7,NS=1<<20;
struct mo{I x;mo(I a=0){x=a;if(x<0)x+=mod;if(x>=mod)x-=mod;}
mo operator +(mo a){return x+a.x;}
V operator +=(mo a){x+=a.x;if(x<0)x+=mod;if(x>=mod)x-=mod;}
mo operator *(mo a){return 1ll*x*a.x%mod;}
}f[NS];
I n,ns;
mo ksm(mo x,I k){mo a(1);
for(;k;k>>=1,x=x*x)if(k&1)a=a*x;
return a;}
V fwt(mo*a,I op){
fo(i,0,n-1)for(I j=ns;~j;--j)if(!(j&(1<<i)))f[j]+=mo(op*f[j^(1<<i)].x);}
I main(){fo(i,1,(n=in()))++f[in()].x;
ns=NS-1;n=20;fwt(f,1);
fo(i,0,ns)f[i]=ksm(2,f[i].x)+mo(mod-1);
fwt(f,-1);printf("%d\n",f[0].x);
return 0;}
```
### [P6442 [COCI] KOŠARE](https://www.luogu.com.cn/problem/P6442)
题意:有 $n$ 个二进制数字,每个数字 $m$ 位,求选择一些数或起来,恰好等于 $2^{m}-1$ 的方案数,取模 $10^9+7$。
---
按照套路,一个个转移是没必要而且无所谓的,每个数字的位置也是没有影响的,所以丢进桶里面,给次数加上 $1$。
直接转为补集,然后就成了 CF449D。
---
这里考虑直接算。设 $a[S]$ 为数字集合为 $S$ 的方案数(原始的那个桶)。
设 $b[S]$ 为数字集合**通过位与操作**做到集合为 $S$ 的**子集**的方案数。
这里的 $b[S]$ 不好直接算,但是我们可以先给 $a[S]$ 做一遍子集求和,仍然在 $a$ 数组上。
按照套路,我们可以对 $b[S]$ 本身直接容斥最后算答案,也可以对 $b[S]$ 差分。差分具体就见 CF449D 了,就是改个符号的事情。
## 5.FWT/FMT 的引入
FMT 是“**快速莫比乌斯变换**”,思想基于容斥+DP,它的长相你等会就知道了。
FWT 是“**快速沃尔什变换**”,它的运行结果和 FMT 一模一样。
FWT 思想可以支持异或卷积,这里暂时不讲。因为本文主要在于 SOS-DP 的缘故,所以主要讲 FMT。FWT 打法类似,只是长相更像 FFT,这里不讲。
### 5.1 它能解决什么问题
首先定义一个东西,叫做“集合幂级数”。它可不是什么形如 $x^2+2x+1$ 的“形式幂级数”多项式,但是也是和多项式紧密相关的。
既然是多项式,那么就有“卷积”。它的“卷积”操作无非就是 ${c[i]=\sum_{j \operatorname{op} k=i} a[i]b[j]}$。
这里的 $\operatorname{op}$ 指一类集合运算(计算机里面,通常是位运算)。比如**且**、**或**和**异或**。
我们就是需要通过 FWT/FMT 高效的时间效率,来优化一系列“卷积”的问题。
### 5.2 和 FFT 有什么关系
普通多项式卷积的式子是 ${c[i]=\sum_{j+k=i}a[j]b[k]}$。
考虑它一般拿来做卷积的流程。
1. 使用 DFT,转化为点值序列;时间 $O(n\log_2 n)$。
2. 在点值序列上做乘法操作;时间 $O(n)$(取决于具体你做了什么操作)。
3. 使用 IDFT,将点值序列,转化回普通形式幂级数形式,就是那个形如 $x^2+2x+1$ 的那个形式。
这里就是我们拿 FFT 优化一系列多项式毒瘤题目的过程。
考虑 FWT/FMT 的过程,若我们把这个序列,通过一种类似于 DFT 的变换,变为“点值”的形式,就可以做到比较快速的乘法。
我们就需要**找到一种**对于原序列的**变换**,使得 $\operatorname{FWT}(A)\times \operatorname{FWT}(B)=\operatorname{FWT}(C)$。
## 6.FMT \& IFMT 算法
以下过程,设 $2^n$ 为序列长度,序列从 $0$ 开始。其最大下标位数为 $2^{n}-1$。
前面做了那么多 SOS-DP 的题目,FMT 大家应该就更加容易做了吧。
### 6.1 或卷积
别急,一个个来。
首先设 $a'=\operatorname{FWT}(a)$ 为 $a$ 序列经过 FWT 变换之后的序列是啥。
这里的序列变换,则是变为序列 $a'[i]=\sum_{j\subseteq i}a[i]$(注意符号)。
咦?这么那么眼熟?
发现就是我们的老朋友——**子集求和**!
于是我们只需要做子集求和即可。
我们要求还原,怎么办?老规矩,前缀和过来就给它**差分**回去!
这里**子集差分**的大致流程请看 CF449D,就是改个符号的事情。实在不懂就去做 ABC322E,可回撤的背包方案数问题,包你满意。
于是我们就搞定了 `or` 运算的 FMT 和 IFMT 流程。
我们再来回顾以下求的是什么,求的是 $c[i]=\sum_{j\cup k=i} a[j]b[k]$。
考虑 $j\cup k=i$ 的必要条件,必然有 $j,k\subseteq i$。
那么根据乘法原理,直接让 $c[i]=(\sum_{j\subseteq i}a[j])(\sum_{j\subseteq i}b[j])=a'[i]b'[i]$ 就是对的吗?
实际上不是。因为 $c[i]$ 实际上还算上了它子集的答案。
那么怎么办呢?就把 $c$ 数组容斥一下,剔除不该有的东西就好了。
咦?这不就是我们子集差分的流程吗,它也叫做 IFWT。
所以,我们就用 SOS-DP 优雅地解决了问题,并且从 SOS-DP 的角度思考,证明了这个 FWT 变换的正确性。
代码也很好写。等会统一放。
### 6.2 且卷积
按照 SOS-DP 的套路,也容易思考,这里的 FWT 变换是**超集求和**。
同理,这里的 IFMT 变换无非就是**超集差分**。下面来证明它的正确性。
假设我要让 $c[i]$ 满足 $c[i]=\sum_{j\cap k=i}a[j]b[k]$。
老规矩,先考虑必要条件:$j,k\supseteq i$。然后超集求和一波,设 $c'[i]=a'[i]b'[i]$。
发现 $c'$ 算多了!于是用超集差分搞掉就行了!代码也很短!
于是我们又用 SOS-DP 优雅地搞掉了这一问!
### 6.3 子集卷积
先明确问题:
$${c[i]=\sum_{j\subseteq i}a[j]b[\complement_i j]}$$
显然可以 $O(3^n)$ 做,但是太慢了,我要更快!
考虑把子集卷积的形式拆开:若 $j$ 包含于 $k$,且 $i$ 为补集,则 $i\cup j=k$ 且 $ i\cap j=0$。
考虑先来解决第一个问题。第一个问题可以 FMT/FWT 的子集求和解决。
但是第二个问题我们发现只用 FMT 还会算重,那么怎么办呢?
于是我们就不直接加到最终的集合,考虑这样一件事情,$|i|+|j|=|i+j|$,于是按照集合大小,再开一维,维护即可。
这样就不会算重了。于是问题就结束了。
```cpp
#include<bits/stdc++.h>
#define fo(i,a,b) for(auto i(a),_ed(b);i<=_ed;++i)
#define gch(k) for(;(k);CH=getchar())
using namespace std;using I=int;using LL=long long;using V=void;I FL,CH;LL in(){LL a=0;FL=1;gch(!isdigit(CH))FL=(CH=='-')?-1:1;gch(isdigit(CH))a=a*10+CH-'0';return a*FL;}
const I mod=1e9+9;
struct mo{I x;mo(I a=0){x=a;if(x<0)x+=mod;if(x>=mod)x-=mod;}
V operator +=(const mo&a){x+=a.x;if(x>=mod)x-=mod;}
mo operator *(const mo&a){return 1ll*x*a.x%mod;}};
using VI=vector<mo>;
#define pc __builtin_popcount
VI convo(I n,VI&A,VI&B){
auto fmt=[&](I n,VI&f,mo id){
fo(i,0,n)fo(j,0,(1<<n)-1)if((j>>i)&1)f[j]+=id*f[j^(1<<i)];};
VI a[n+1],b[n+1],c[n+1];fo(i,0,n)a[i].resize(1<<n),b[i].resize(1<<n),c[i].resize(1<<n);
fo(i,0,(1<<n)-1)a[pc(i)][i]=A[i],b[pc(i)][i]=B[i];
fo(i,0,n)fmt(n,a[i],1),fmt(n,b[i],1);
fo(i,0,n)fo(j,0,i)fo(k,0,(1<<n)-1)c[i][k]+=a[j][k]*b[i-j][k];
fo(i,0,n)fmt(n,c[i],mo(-1));
VI C(1<<n,0);fo(i,0,(1<<n)-1)C[i]=c[pc(i)][i];
return C;}
I main(){I n=in();VI a(1<<n),b(1<<n);
fo(i,0,(1<<n)-1)a[i]=in();
fo(i,0,(1<<n)-1)b[i]=in();
VI c=convo(n,a,b);
for(mo i:c)printf("%d ",i.x);
return 0;}
```
### 6.4 异或卷积以及其他
这些算法是 FWT 的内容了,由于作者水平所限,此文不涉及。
## 7.致谢
谢谢大家的观看。