FWT & FMT(位运算卷积)学习笔记

它们两个的全名叫 快速沃尔什变换(FWT) 和 快速莫比乌斯变换(FMT),用来在 \(O(n\log n)\) 时间复杂度内求位运算卷积。
因为 FMT 能解决的问题是 FWT 的子集,所以这里不讲 FMT,把它拎出来是想说它们两个的区别。

参考资料:偶耶XJX-浅谈快速沃尔什变换(FWT)&快速莫比乌斯变换(FMT)zcxxn-多项式学习笔记

位运算卷积

位运算卷积,就是求下面这个问题:
给定长度为 \(n\) 的序列 \(A,B\),求它们的卷积 \(C_i=\sum\limits_{j\oplus k=i} A_j\times B_k\),其中 \(\oplus\)\(\text{and,or,xor}\) 中的一种。这里如果把 \(\oplus\) 换成加号就是多项式乘法。

Or

我们要求 \(C_i=\sum\limits_{j|k=i} A_j\times B_k\)。这个柿子长得和多项式乘法差不多,所以试图仿照 FFT 的思路对序列变换进行构造。
\(\text{fwt}_i(A)=\sum\limits_{j|i=i} A_j\)。考虑这样构造有什么性质:

\[\begin{aligned} \text{fwt}_i(A)\text{fwt}_i(B) &=(\sum_{j|i=i} A_j)(\sum_{k|i=i} B_k)\\ &=\sum_{j|i=i}\sum_{k|i=k} A_j B_k\\ &=\sum_{(j|k)|i=i} A_j B_k \end{aligned} \]

继续推 \(\text{fwt}(C)\)

\[\begin{aligned} \text{fwt}_i(C)&=\sum_{j|i=i} C_j\\ &=\sum_{j|i=i}\sum_{x|y=j} A_x B_y\\ &=\sum_{(x|y)|i=i} A_x B_y\\ \end{aligned} \]

那么有:

\[\text{fwt}_i(C)=\text{fwt}_i(A)\text{fwt}_i(B) \]

这很好,只需要知道怎么转化 \(A\)\(\text{fwt}(A)\) 就解决问题了。

\(A\to \text{fwt}(A)\)

肯定不能按定义式求,这样搞是 \(n^2\) 的。

考虑根据下标二进制下最高位的值是 \(0/1\)\(A\) 序列分治。我们定义最高位为 \(0\) 的序列叫 \(A_0\),另一半叫 \(A_1\)

那么,\(\text{fwt}(A)\) 的前半部分,\(j\)\(k\) 的最高位也都只能取 \(0\),即 \(\text{fwt}(A_0)\);后半部分,\(j\)\(k\) 的最高位没有限制,所以即 \(\text{fwt}(A_0)+\text{fwt}(A_1)\)

\(\text{merge}(a,b)\) 表示把 \(a\)\(b\) 序列前后拼接在一起,则有:

\[\text{fwt}(A)=\text{merge}(\text{fwt}(A_0),\text{fwt}(A_0)+\text{fwt}(A_1))\]

递归即可。当然更常见的写法是直接循环迭代。

\(\text{fwt}(A)\to A\)

把变换反过来,思路是差不多的:

我们这次把 \(\text{fwt}(A)\) 序列按二进制最高位分为 \(\text{fwt}(A_0)\)\(\text{fwt}(A_1)\)

同理地,\(A\) 的前半部分只由 \(\text{fwt}(A_0)\) 得到,而后半部分的 \(\text{fwt}(A_1)\) 里面两种都有,去掉 \(0\) 开头的就是 \(\text{fwt}(A_1)-\text{fwt}(A_0)\)

那么有反演式(这里似乎也有的博客叫 \(\text{ufwt}\)):

\[\text{Ifwt}(A)=\text{merge}(\text{Ifwt}(A_0),\text{Ifwt}(A_1)-\text{Ifwt}(A_0)) \]

代码实现时可以合并这两个过程。

void Or(int *a,int tp)
{
    for(int len=1;len<(1<<n);len<<=1)
        for(int i=0;i<(1<<n);i+=(len<<1))
            for(int j=0;j<len;j++) add(a[i+j+len],a[i+j]*tp);
}

And

和 Or 卷积本质相同,因此这里直接给出结论,读者可以自行证明。

\[\text{fwt}(A)=\text{merge}(\text{fwt}(A_0)+\text{fwt}(A_1),\text{fwt}(A_0)) \]

\[\text{Ifwt}(A)=\text{merge}(\text{Ifwt}(A_1)-\text{Ifwt}(A_0),\text{Ifwt}(A_0)) \]

void And(int *a,int tp)
{
    for(int len=1;len<(1<<n);len<<=1)
        for(int i=0;i<(1<<n);i+=(len<<1))
            for(int j=0;j<len;j++) add(a[i+j],a[i+j+len]*tp);
}

FMT

插播一条 FMT!

不少博客说做 And 和 Or 卷积的 FWT 就是 FMT,这句话的正确性有待考据。

实际上,FMT 和 FWT 在前面构造的部分都是一样的,区别在于转化 \(A\to \text{fwt}(A)\) 的方式不同。已经知道,FWT 是基于分治加速运算,而 FMT 则是基于 DP 对这一步进行的加速。代码难度上也没有太大差别,具体是怎么加速的这里不讲了 qaq。

Xor

Xor 卷积就只能用 FWT 做了,而且也是最难理解的部分。

\(d(x)\) 表示 \(x\) 在二进制下 \(1\) 的个数。对 FWT 函数做如下定义:

\[\text{fwt}_i(A)=\sum_j (-1)^{d(i\&j)} A_j \]

换句话讲,改变 \(d\) 的定义,令 \(d(x)\) 表示 \(1\) 的个数的奇偶性,偶为 \(0\),奇为 \(1\)。那么这个式子可以写成:

\[\text{fwt}_i(A)=\sum_{d(i\&j)=0} A_j-\sum_{d(i\&j)=1} A_j \]

所以根据 \((i\text{ and }k)\text{ xor }(j\text{ and }k)=(i\text{ xor }j)\text{ and }k\),有:

\[\begin{aligned} \text{fwt}_i(A)\text{fwt}_i(B)&=(\sum_{d(i\&j)=0} A_j-\sum_{d(i\&j)=1} A_j)(\sum_{d(i\&k)=0} B_k-\sum_{d(i\&k)=1} B_k)\\ &=\sum_{d(i\&j)\text{ xor }d(i\&k)=0}A_j B_k-\sum_{d(i\&j)\text{ xor }d(i\&k)=0}A_j B_k\\ &=\sum_{d(i\&(j\text{ xor }k))=0}A_j B_k-\sum_{d(i\&(j\text{ xor }k))=1}A_j B_k\\ &=\text{fwt}_i(C) \end{aligned} \]

同样基于分治思想构造 \(A\to \text{fwt}(A)\) 的转移。这个东西实在过于抽象,感性理解,理解不了就记下来(

\[\text{fwt}(A)=\text{merge}(\text{fwt}(A_0)+\text{fwt}(A_1),\text{fwt}(A_0)-\text{fwt}(A_1)) \]

\[\text{Ifwt}(A)=\text{merge}\left(\frac{\text{Ifwt}(A_0)+\text{Ifwt}(A_1)}{2},\frac{\text{Ifwt}(A_0)-\text{Ifwt}(A_1)}{2}\right) \]

代码长得有点像 FFT。

void Xor(int *a,int tp)
{
    for(int len=1;len<(1<<n);len<<=1)
        for(int i=0;i<(1<<n);i+=(len<<1))
            for(int j=0;j<len;j++)
            {
                int x=a[i+j],y=a[i+j+len];
                a[i+j]=(x+y)*tp%mod,a[i+j+len]=(x-y+mod)%mod*tp%mod;
            }
}

一些神秘优化

Fan facts:len 可以倒过来枚举,不影响答案。
据说会变快,但试了下好像并没有什么效果,不知道是不是我的问题。


于是这篇昨天晚上就说要写的博客咕到了现在。我是摆怪猫猫!

posted @ 2023-09-08 16:19  樱雪喵  阅读(124)  评论(0编辑  收藏  举报