FWT 快速沃尔什变换
参考文献
前置知识
-
\(\text{FFT}\) 快速傅里叶变换
-
二进制位运算
问题引入
给出 \(n\) 和两个长为 \(2^n\) 的序列 \(a_{0...2^n-1},b_{0...2^n-1}\),求一个序列 \(c\),对于 \(i(1\le i<2^n)\):
其中符号 \(\oplus\) 是三个位运算中的一个,即 \(\text{or,and,xor}\)。数据范围:\(1\le n\le20\)。
显然,这是一个卷积的问题,但符号不一样,可能是 \(\text{or,and,xor}\) 三种,我们称之为位运算卷积。
思路转化
考虑 \(\text{FFT}\) 是如何快速计算两个序列的加卷积的,我们把系数序列转成点值的形式,然后每一位分别相乘,最后插值回来。
我们尝试利用这种思想,设 \(fwt_a[i]\) 表示 \(a\) 序列经过变换后的第 \(i\) 位的值。
为了满足条件,我们构造的 \(fwt\) 序列需要满足:
其中 \(*\) 表示卷积,\(\cdot\) 表示点积。
\(\text{or}\) 卷积
求 \(c_i=\sum_{j\text{or} k}\)。
不妨设 \(fwt_a[i]=\sum\limits_{j|i=i}a_j\)。若 \(fwt_c[i]=fwt_a[i]\cdot fwt_b[i]\),尝试验证是否满足卷积式子。
发现满足,我们只需要求出 \(a,b\) 序列的 \(fwt_a,fwt_b\) 就能求出 \(fwt_c\)。
考虑分治,按最高位分治。
设 \(a_0\) 为 \(a\) 中下标最高位为 \(0\) 的数组成的序列(换句话说,就是 \(a\) 的前半部分),\(a_1\) 为 \(a\) 中下标最高位为 \(1\) 的数组成的序列(就是 \(a\) 的后半部分)。如果我们求出了 \(fwt_{a_0},fwt_{a_1}\),我们有
其中 \(+\) 表示相应位置相加,\(\text{merge}\) 表示两个序列拼接。
这个式子的意思就是把下标最高位为 \(0\) 的数贡献到为 \(1\) 的数。具体实现时,我们采用递推的方式,从地位往高位递推。
void fwt_or(ll *a,ll n)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[i+j+k]+=a[j+k];
}
}
}
}
现在我们求出了 \(fwt_a,fwt_b\),进而求出了 \(fwt_c(fwt_c=fwt_a\cdot fwt_b)\),考虑如何进行 \(\text{IFWT}\),把 \(fwt_c\) 转成 \(c\)。
考虑从高位到地位递推,把式子反过来得:\(fwt_{a_0}=fwt_0,\space fwt_{a_1}=fwt_1-fwt_0\)。其中 \(fwt_0\) 表示 \(fwt_a\) 的前半部分,\(fwt_1\) 同理。
但这样做要求我们从大到小递推,发现其实我们递推的顺序没有很大关系,反正变换都是在子集上做的,从高到低和从低到高没有区别。
void ifwt_or(ll *a,ll n)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[i+j+k]-=a[j+k];
}
}
}
}
把两个函数结合一下,可得
void fwt_or(ll *a,ll n,ll v)
{
// v=1或-1
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[i+j+k]+=a[j+k]*v;
}
}
}
}
\(\text{and}\) 卷积
类似于 \(\text{or}\) 卷积,我们可以得出
void fwt_and(ll *a,ll n,ll v)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[j+k]+=a[i+j+k]*v;
}
}
}
}
\(\text{FWT}\) 线性变换矩阵的本质
考虑从线性变换矩阵入手,设矩阵为 \(w(i,j)\),即 \(fwt_a[i]=\sum\limits_jw(i,j)a_j\)
因为
所以
又知 \(fwt_c[i]=fwt_a[i]\cdot fwt_b[i]\),进而得到
比较上下两个式子,得到 \(w(i,x)w(i,y)=w(i,x\oplus y)\),只要变换矩阵满足这个式子即可。
由于位运算每一位都是独立的,我们只需要考虑 \(w(0/1,0/1)\) 就好。
对于 IDWT,我们其实就是乘个矩阵逆,所以我们构造出来的矩阵必须满足可逆。
\(\text{xor}\) 卷积
对于 \(i,x,y\),根据 \(\text{FWT}\) 变换矩阵的性质,有 \(w(i,x)w(i,y)=w(i,x \space\text{xor} \space y)\)。
对于 \(w(0,0)\),\(w(0,0)w(i,j)=w(i,j)\),所以 \(w(0,0)=1\)。
对于 \(w(1,1)\),\(w(1,1)w(1,1)=w(1,0)\)。如果 \(w(1,1)=w(1,0)=0\) 那么一行全是 \(0\),没有逆,所以 \(w(1,1)=\pm 1\),而 \(w(1,0)=1\)。
对于 \(w(0,1)\),\(w(0,1)w(0,1)=w(0,0)=1\),所以 \(w(0,1)=\pm 1\)。
不妨令 \(w(1,1)=-1,w(0,1)=1\)。
不难发现,\((i,j)\) 的变换系数就是 \((-1)^{\text{popcount}(i\&j)}\)
尝试分治构造 \(fwt_a\),仍然是按最高位 \(0,1\) 构造,分成 \(a_0,a_1\),假设已经构造出了 \(fwt_{a_0},fwt_{a_1}\)。
对于 \(i(i<2^{n-1})\)(即 \(i\) 的最高位为 \(0\)),以及 \(j(j\ge 2^{n-1})\),由于 \(i\operatorname{and}j\) 的最高位是 \(0\),因此 \(j\) 的最高位对 \(i\) 没有贡献。所以我们对于下标最高位为 \(0\) 的 \(fwt_a[i]\),把 \(fwt_{a_0},fwt_{a_1}\) 视为同类,即 \(fwt_a\) 的左半边为 \(fwt_{a_0}+fwt_{a_1}\)。
对于 \(i(i\ge 2^{n-1})\),若 \(j\) 最高位为 \(1\) 就有贡献,\(0\) 则没贡献,那么 \(fwt_a\) 的右半边为 \(fwt_{a_0}-fwt_{a_1}\)。
所以
void fwt_xor(ll *a,ll n)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[j+k]+=a[i+j+k];
a[i+j+k]=a[j+k]-2*a[i+j+k];
}
}
}
}
考虑如何进行 \(\text{IFWT}\),式子可以从上面的推导。
直接反过来
同理,从高到低和从低到高没有区别,可以从低到高递推。
void ifwt_xor(ll *a,ll n)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[j+k]+=a[i+j+k];
a[i+j+k]=a[j+k]-2*a[i+j+k];
a[j+k]>>=1; a[i+j+k]>>=1;
}
}
}
}
合并成一个函数
void fwt_xor(ll *a,ll n,ll flag)
{
for(ll i=1;i<n;i<<=1)
{
for(ll j=0;j<n;j+=(i<<1))
{
for(ll k=0;k<i;k++)
{
a[j+k]+=a[i+j+k];
a[i+j+k]=a[j+k]-2*a[i+j+k];
if(flag) a[j+k]>>=1, a[i+j+k]>>=1;
}
}
}
}
\(k\) 进制 \(\text{FWT}\)
例题
发现 \(n\) 很小,考虑直接枚举操作哪些行。
枚举操作的行的集合为 \(S\)。对于每一列,记 \(msk[i]\) 表示第 \(i\) 列 \(n\) 个数的 \(0/1\) 状态,那么操作之后的状态为 \(S\operatorname{xor} msk[i]\)。此时,我们可以选择操作第 \(i\) 列或不操作,这取决于 \(msk[i]\) 在二进制下 \(1\) 的个数 \(\text{popcount}(msk[i])\):
-
当 \(\text{popcount}(msk[i])\le \frac {n}2\) 时,不操作。
-
当 \(\text{popcount}(msk[i])>\frac n2\) 时,操作。
所以我们的任务既是统计 \(\text{popcount}(msk[i])\le\frac n2\) 的 \(i\) 的个数。
枚举的 \(S\) 是不断改变的,我们不能动态得知 \(\text{popcount}\) 的计数。设 \(msk'[i]\) 为操作后的 \(msk[i]\),因为 \(msk[i]\operatorname{xor}S=msk'[i]\),可得 \(S=msk[i]\operatorname{xor}msk'[i]\)
设 \(cnt[j]\) 表示有多少个 \(i\) 满足 \(msk[i]=j\),设 \(h[k,S]\) 表示有多少个 \(i\) 满足操作后 \(\text{popcount}(msk'[i])=k\)。
那么
这是一个异或卷积的形式,直接 \(\text{FWT}\) 即可。
我们可以预处理出所有集合 \(S\),判断其是否合法。
不难想到状压 \(\text{DP}\):设 \(f[S]\) 表示集合 \(S\) 的答案。
枚举 \(T\in S,\space T\not=S\),表示划分的第一个州区。转移:
其中 \(sum[S]\) 表示一个州区的人数和。
注意到子集卷积有个很经典的东西:
- 设 \(T_0\) 表示 \(S-T\)。把 \(T\in S\) 的条件转化成枚举 \(T,T_0\),满足 \(T|T_0=S,\space T\&T_0=0\),即
考虑把 \(T\&T_0\) 这个限制去掉,不难发现
- 当 \(\text{popcount}(T)+\text{popcount}(T_0)=\text{popcount}(T|T_0)\) 时,\(T\&T_0=0\)。
设 \(val[i,S]=[\text{popcount}(S)=i]\cdot sum[S]\cdot [S\text{合法}]\),\(F[i,S]=[\text{popcount}(S)=i]\cdot f[S]\)。
可得
直接 \(\text{FWT}\) 即可,时间复杂度 \(O(n^22^n)\)。
观察到 \(n\) 特殊的数据范围,考虑 \(\text{meet in middle}\)。
先转化,“选若干个点,两端至少有一个点被选的边的条数为偶数”相当于选择这些点的补集,满足导出子图边数与 \(m\) 奇偶性相同。
设 \(c=m\mod 2\)。把 \(n\) 个点分成前 \(k\) 个和后 \(n-k\) 个,那么选的点的导出子图包含的边有以下几种:
-
一类边:两端都属于前 \(k\) 个点
-
二类边:两端都属于后 \(n-k\) 个点。
-
三类边:一端属于前 \(k\) 个点,一端属于后 \(n-k\) 个点。
设 \(d_1[S]\) 表示选了前 \(k\) 个点的集合 \(S\),导出子图的一类边数量,\(d_2[S]\) 则为后 \(n-k\) 个点的集合 \(S\),导出子图的二类边数量,设 \(a[S]\) 表示选了前 \(k\) 个点的集合 \(S\),与后 \(n-k\) 个点的贡献集合。
不难写出答案式子:
里面有个 \(\text{popcount}\) 很烦,考虑枚举 \(\text{popcount}\) 里面的东西:
其中 \(cnt[k,S_0]\) 表示 \(d_1[S_1]\bmod 2=k\) 的且 \(a[S_1]=S_0\) 的 \(S_1\) 的个数。然后又发现 \((d_2[S_2]\bmod 2)\) 很烦,拆出来:
惊奇地发现这是个与卷积的形式,我们只需要分 \(\text{popcount}(T)\bmod 2\) 的取值分开卷积即可,\(\text{FWT}\) 即可做到 \(O((\frac n2)2^{\frac n2})\)。