来学点好玩的。


引入

我们也许学过,\(FFT\) 可以解决一类卷积:

\[C_i=\sum^{k+j=i} A_iB_j
\]

现在我们稍微变一下式子:

\[C_i=\sum^{i=k \And j} A_kB_j
\]
\[C_i=\sum^{i =k\mid j} A_kB_j
\]
\[C_i=\sum^{i=k \oplus j} A_kB_j
\]
上面那个圆圆的东西是异或

怎么求?

\(FWT\) 或

也就是这个式子:

\[C_i=\sum^{i =k\mid j} A_kB_j
\]

\(FWT\) 的定义

模仿 \(FFT\),对于 \(A\) 我们想要在可接受时间内得到一个 \(FWT(A)\),使得

\[FWT(C)_i=FWT(A)_i\times FWT(B)_i
\]

\(i\) 代表第 \(i\) 个位置的数。

这样我们就可以 \(O(n)\) 暴力乘起来了。

因此我们构造 \(FWT(A)_i=\sum^{}_{j|i=i}A_j\)。

现在我们来证明这个构造的正确性。

\[FWT(A)_i\times FWT(B)_i
\]
\[=\sum^{}_{j|i=i}A_j\times \sum^{}_{k|i=i}B_k
\]
\[=\sum^{}_{j|i=i}\sum^{}_{k|i=i}A_j B_k
\]

因为可以从 \(j|i=i\) 与 \(k|i=i\) 中得出 \((j|k)|i=i\),所以我们还可以消去另一个式子

\[=\sum^{}_{(j|k)|i=i}A_j B_k
\]

根据 \(FWT\) 的定义,这个式子就是 \(FWT(C)_i\)。证毕。

另一种理解

涉及到了或运算,我们不妨把数全都变成二进制。于是我们想到一种经典转换:将二进制中的 \(0\) 与 \(1\) 转化为集合中一个数选还是不选。那么或操作代表什么呢?

bingo!两个集合的并集!

于是我们可以把上面的式子改写一个形式:

\[C_i=\sum^{i=k \mid j} A_kB_j
\]

变成

\[C_i=\sum^{i =k\bigcup j} A_kB_j
\]

注意看,这时 \(i,j,k\) 都是集合,只不过我们将其用二进制表示.

这样我们可以改写 \(FWT\) 的定义。

重新来一遍,\(FWT(A)_i=\sum^{}_{k \subseteq i} A_k\)。

因此我们为 \(FWT\) 找到了新的定义,他代表着集合 \(i\) 的子集之和。我们有了更自然的推导:

\[\sum^{}_{j \subseteq i}A_j\times \sum^{}_{k \subseteq i}B_k
\]
\[=\sum^{}_{j,k \subseteq i}A_j\times B_k
\]
\[=\sum^{}_{x \subseteq i}\sum^{}_{j\bigcup k = x }A_j\times B_k
\]
\[=\sum^{}_{x \subseteq i}C_x
\]
\[=FWT(c)_x
\]

换句话说,我们对子集做了个前缀和操作(发现了吗?子集的前缀和进行或卷积与普通前缀和进行加法卷积具有相似性),并用前缀和相乘代替了原来的 \(O(n^2)\) 相乘。

如何变化

我们把原序列 \(A\) 按下标最高位是 \(0\) 还是 \(1\) 分成两部分 \(A_0\) 与 \(A_1\) 分治求解。显然,前半部分(最高位为 \(0\) 的部分)就是 \(FWT(A_0)\),所以我们考虑后半部分的答案。

后半部分最高位为 \(1\),因此此时“子集”这一概念不仅包含分治处理的他子集,还包括把最大值变为 \(0\) 后的,序列 \(A0\) 中同一位置的子集。要将 \(A_0\) 中的同一位置加到当前答案上。

写成数学形式就是:

\[FWT(A) = merge(FWT(A_0),FWT(A_0)+FWT(A_1))
\]

上面的 \(merge\) 代表拼接,就是字面意思。

于是我们就能写出分治递归代码了!但为了常数着想,我们试着把递归这一步骤去掉。

去掉的部分并不难写,我们按照层数从小到大递归,不难发现第 \(i\) 层(从 \(0\) 开始编号,最底层为 \(0\))就是枚举第 \(i\) 位是 \(0\) 还是 \(1\),并且乱填其他数进行转移。

代码也简单:

void OR(mint *f){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)// k 为当前的层,o 仅用于穷举左边进行转移
for(int i = 0; i < n; i += o)// 穷举左边
for(int j = 0; j < k; j++){ // 穷举右边
f[i + j + k] = f[i + j + k] + f[i + j];
}
}

如何转回来

再看一眼转移的式子

\[FWT(A) = merge(FWT(A_0),FWT(A_0)+FWT(A_1))
\]

思考只有两个数的情况。此时 \(1\) 位置是不会变的,\(2\) 位置加上了 \(1\) 位置的贡献,要减去。

我们发现更大的情况也是一样的,只要依次把前面的贡献减去就好。

void IOR(mint *f){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)// k 为当前的层,o 仅用于穷举左边进行转移
for(int i = 0; i < n; i += o)// 穷举左边
for(int j = 0; j < k; j++){ // 穷举右边
f[i + j + k] = f[i + j + k] - f[i + j];
}
}

这两份代码显然是可以合并的。因此我们得到了 \(FWT\) 或 的全过程。

void OR(mint *f, int type){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j++){
f[i + j + k] = f[i + j + k] + (f[i + j] * mint(type));
}
}

\(FWT\) 与

和 或 差不多,只是要从 \(1\) 转移到 \(0\)。

可以发现,实际上我们用子集后缀和优化了运算。

void AND(mint *f, int type){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j++)
f[i + j] = f[i + j] + (f[i + j + k] * mint(type));
}

\(FWT\) 异或

\[C_i=\sum^{i=k \oplus j} A_kB_j
\]

很遗憾,我并没有发现这个东西的集合意义,如果有大佬知道可以告诉我。。。

正着转化

思考 \(FWT\) 的作用,我们想要把 \(A_kB_j\) 变成 \(A_iB_i\) 的形式,以此来简化运算。

我们考虑这样 \(n\) 个 \(n\) 维向量 \(b\),\(b(i)\) 只有下标 \(i\) 处是 \(1\),其他位置都是 \(0\)。

现在我们把 \(FWT\) 后的 \(A,B\) 看作系数,此时显然 \(A_1b(1),A_2b(2),...,A_nb(n)=A_1,A_2,A_3,A_4...,A_n\)

显然,异或卷积对于乘法有分配律。

设异或卷积为 \(\ast\),则

\[(\sum^{n}_{i=1} A_ib(i)) \ast (\sum^{n}_{i=1} B_jb(j))
\]
\[=(\sum^{n}_{i=1}\sum^{n}_{j=1} A_iB_j (b(i)\ast b(j))
\]

发现后面的东西可以简单表示,即 \(b_i \ast b_i = b_i,b_i \ast b_j = 0(i \neq j)\)。

那么整个式子就是我们寻找的形式:

\[\sum^{n}_{i=1} A_iB_i
\]

而我们要做的事情无非是求出 \(FWT\) 之前的 \(b_i\)。

太长不看版:异或 \(FWT\) 与原序列线性相关

既然这样,我们设 \(FWT(A)_x=\sum^{n}_{i=1} g(x,i)A_i\)。

那么因为 \(FWT(C)_x=FWT(A)_x\times FWT(b)_x\)

所以 \(\sum^{n}_{k=1} g(x,k)C_k=\sum^{n}_{i=1} g(x,i)A_i \times \sum^{n}_{j=1} g(x,j)B_j\)。

整理一下可以得出:

\[\sum^{n}_{k=1} g(x,k)C_k=\sum^{n}_{i=1}\sum^{n}_{j=1} g(x,i)g(x,j)\times A_iB_j
\]

把 \(C_k\) 用 \(A,B\) 表示可得:

\[\sum^{n}_{k=1} g(x,k)\sum^{k=i \oplus j} A_iB_j=\sum^{n}_{i=1}\sum^{n}_{j=1} g(x,i)g(x,j)\times A_iB_j
\]

更改求和顺序,我们枚举 \(i,j\) 可得:

\[\sum^{n}_{i=1}\sum^{n}_{j=1} g(x,i \oplus j) A_iB_j=\sum^{n}_{i=1}\sum^{n}_{j=1} g(x,i)g(x,j)\times A_iB_j
\]

于是我们发现了 \(g\) 的关系:

\[g(x,i \oplus j) = g(x,i)g(x,j)
\]

现在问题来了,与 \(i,j\) 相关的什么东西,使异或之后的值等于原来两值的乘积?

于是我们可以想到有人托梦给我奇偶性。

具体的,我们发现异或前后 \(1\) 的个数奇偶性不变。原因如下:

按每一位依次考虑。如果第 \(i\) 位异或后为 \(1\),那么原来必定有且仅有一个 \(1\)。个数不变

如果为 \(0\),要么是两个 \(0\),此时 \(1\) 的个数不变,要么是两个 \(1\),此时 \(1\) 的个数减 \(2\),奇偶性仍不变。

所以我们定义 \(g(x,i)=(-1)^{|i \bigcap x|}\)。那么上式就等价于:

\[(-1)^{|(i \oplus j) \bigcap x|} = (-1)^{|i \bigcap x|}(-1)^{|j \bigcap x|}
\]

根据上面的推论,左右两边奇偶性不变,与 后无非是减去两个相同的数,奇偶性还是不变。

于是我们得出 \(FWT\) 的转移式:

\[FWT(A)_x=\sum^{n}_{i=1} (-1)^{|i \bigcap x|}A_i
\]

如何求解

考虑模仿前两个 \(FWT\) 的形式,讨论最高位 \(i\) 为 \(0\) 和为 \(1\) 两种情况。

原来最高位为 \(0\),\(FWT\) 后的前 \(2^{i-1}\) 个数最高位还是 \(0\)。由于 \(1 \And 0=0\),所以后 \(2^{i-1}\) 个数的贡献为正。前半部分答案为 \(FWT(A_0)+FWT(A_1)\)。

\(FWT\) 后的后 \(2^{i-1}\) 个数最高位变成了 \(1\),此时 \(A_0\) 的贡献还是正(因为 \(1 \And 0=0\))。但是此时后半部分加了 \(1\),于是贡献要取反。后半部分答案为 \(FWT(A_0)-FWT(A_1)\)。

所以我们得出:

\[FWT(A) = merge(FWT(A_0)+FWT(A_1),FWT(A_0)-FWT(A_1))
\]

当然,参照 或 FWT,我们可以写出不依赖递归的程序:

void XOR(mint *f){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)//具体意义参考 FWT 或
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j ++){
mint x = f[i + j], y = f[i + j + k];
f[i + j] = x + y;
f[i + j + k] = x - y;
}
}

求逆变换

实际上就是把贡献减去

\[IFWT(A) = merge(\frac{IFWT(A_0)+IFWT(A_1)}{2},\frac{IFWT(A_0)-IFWT(A_1))}{2}
\]

显然这两个东西是可以合并的。于是我们可以得出模板的完整代码:

#include<bits/stdc++.h>
using namespace std; #define forp(i, a, b) for(int i = (a);i <= (b);i ++)
#define forc(i, a, b) for(int i = (a);i >= (b);i --) const int maxn = 6e5 + 5;
const int mod = 998244353; int read(){
int u;cin >> u;return u;
} class mint{
private : int v;
public:
mint(){}
int operator()(void)const{
return v;
}
mint (const int &u){
v = u % mod;
}
mint operator+(const mint &a) const{
int x = a.v + v;
if(x >= mod) return mint(x - mod);
if(x < 0) return mint(x + mod);
return x;
}
mint operator-(const mint& a)const{
return v < a.v ? v - a.v + mod : v - a.v;
}
mint operator*(const mint &a) const{
return mint((1ll * a.v * v) % mod);
}
}; mint qpow(mint u, int v){
mint ans = mint(1);
while(v){
if(v & 1) ans = ans * u;
u = u * u;
v >>= 1;
}
return ans;
}
mint inv2 = qpow(2, mod - 2); int n;
mint A[maxn], B[maxn], C[maxn];
mint g[maxn]; void OR(mint *f, int type){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j++){
f[i + j + k] = f[i + j + k] + (f[i + j] * mint(type));
}
} void AND(mint *f, int type){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j++)
f[i + j] = f[i + j] + (f[i + j + k] * mint(type));
} void XOR(mint *f, int type){
for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
for(int i = 0; i < n; i += o)
for(int j = 0; j < k; j ++){
mint x = f[i + j], y = f[i + j + k];
f[i + j] = x + y;
f[i + j + k] = x - y;
if(type == -1){
f[i + j] = f[i + j] * inv2;
f[i + j + k] = f[i + j + k] * inv2;
}
}
} signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0); n = (1 << read());
forp(i, 0, n - 1) A[i] = mint(read());
forp(i, 0, n - 1) B[i] = mint(read()); OR(A, 1);OR(B, 1);
forp(i, 0, n - 1) C[i] = (A[i] * B[i]);
OR(C, -1);
forp(i, 0, n - 1) cout << C[i]() << ' ';
cout << endl;
OR(A, -1);OR(B, -1); AND(A, 1);AND(B, 1);
forp(i, 0, n - 1) C[i] = (A[i] * B[i]);
AND(C, -1);
forp(i, 0, n - 1) cout << C[i]() << ' ';
cout << endl;
AND(A, -1);AND(B, -1); XOR(A, 1);XOR(B, 1);
forp(i, 0, n - 1) C[i] = (A[i] * B[i]);
XOR(C, -1);
forp(i, 0, n - 1) cout << C[i]() << ' ';
cout << endl;
XOR(A, -1);XOR(B, -1);
return 0;
}

大概就是这样。。。应用也许会另外开坑吧。


后记

感谢 xht 的博客,从这篇博客里我学到了 FWT 的基础知识。

感谢同校大佬 yllcm 为本人解释符号与定义。

感谢万能的U群群友 Untitled_unrevised 解释 FWT 的目的。

感谢万能的U群群友 rqy 学姐与 樱初音斗橡皮 解释为什么异或 FWT 是线性变换顺便发现了我在线性代数方面的巨大缺口

FWT/快速沃尔什变换 入门指南的更多相关文章

  1. FWT快速沃尔什变换学习笔记

    FWT快速沃尔什变换学习笔记 1.FWT用来干啥啊 回忆一下多项式的卷积\(C_k=\sum_{i+j=k}A_i*B_j\) 我们可以用\(FFT\)来做. 甚至在一些特殊情况下,我们\(C_k=\ ...

  2. [学习笔记]FWT——快速沃尔什变换

    解决涉及子集配凑的卷积问题 一.介绍 1.基本用法 FWT快速沃尔什变换学习笔记 就是解决一类问题: $f[k]=\sum_{i\oplus j=k}a[i]*b[j]$ 基本思想和FFT类似. 首先 ...

  3. 浅谈算法——FWT(快速沃尔什变换)

    其实FWT我啥都不会,反正就是记一波结论,记住就好-- 具体证明的话,推荐博客:FWT快速沃尔什变换学习笔记 现有一些卷积,形如 \(C_k=\sum\limits_{i\lor j=k}A_i*B_ ...

  4. 知识点简单总结——FWT(快速沃尔什变换),FST(快速子集变换)

    知识点简单总结--FWT(快速沃尔什变换),FST(快速子集变换) 闲话 博客园的markdown也太傻逼了吧. 快速沃尔什变换 位运算卷积 形如 $ f[ i ] = \sum\limits_{ j ...

  5. 初学FWT(快速沃尔什变换) 一点心得

    FWT能解决什么 有的时候我们会遇到要求一类卷积,如下: Ci=∑j⊕k=iAj∗Bk\large C_i=\sum_{j⊕k=i}A_j*B_kCi​=j⊕k=i∑​Aj​∗Bk​此处乘号为普通乘法 ...

  6. FWT快速沃尔什变换例题

    模板题 传送门 #include<bits/stdc++.h> #define ll long long #define max(a,b) ((a)>(b)?(a):(b)) #de ...

  7. FWT快速沃尔什变换——基于朴素数学原理的卷积算法

    这是我的第一篇学习笔记,如有差错,请海涵... 目录 引子 卷积形式 算法流程 OR卷积 AND卷积 XOR卷积 模板 引子 首先,考虑这是兔子 数一数,会发现你有一只兔子,现在,我再给你一只兔子 再 ...

  8. FWT快速沃尔什变换

    前言 学多项式怎么能错过\(FWT\)呢,然而这真是个毒瘤的东西,蒟蒻就只会背公式了\(\%>\_<\%\) 或卷积 \[\begin{aligned}\\ tf(A) = (tf(A_0 ...

  9. STM32F767ZI NUCLEO144 基于CubeIDE快速开发入门指南

    刚入手的NUCLEO-F767ZI:整合官网资源,理清思路,便于快速进行快发: 文章目录 1 NUCLEO 系列 2 NUCLEO-F767ZI 3 环境搭建 3.1 Keil/IAR安装 3.2 C ...

  10. AngularJS快速入门指南20:快速参考

    thead>tr>th, table.reference>tbody>tr>th, table.reference>tfoot>tr>th, table ...

随机推荐

  1. calibredrv命令

    flattencell: set L1 [layout create *.gds -dt_expand] $L1 flatten cell TOP_CELL_NAME $L1 gdsout ./*_f ...

  2. 记一次p标签内容不换行记录

    p标签内容l里面如果全部是英文,那么默认是不会换行的. 需要添加word-wrap: break-word; 属性 这样就会自动换行了

  3. 在Unity3D中开发的Hologram Shader

    SwordMaster Hologram Shader 特点 此全息投影风格的Shader是顶点片元Shader,由本人手动编写完成 此全息投影风格的Shader已经在移动设备真机上进行过测试,可以直 ...

  4. C#textbox控件区分扫码枪输入和键盘输入

    前言: 一般我们在某个UI界面输入内容的时候又不想人为的去键盘输入,这个时候就需要区分键盘输入和扫码枪的输入,从而禁止人为键盘输入内容,只能使用扫码枪扫码输入.就目前来说这种直插式扫码枪输入和键盘输入 ...

  5. Python pdb模块的使用

    野路子出生,写Python也有段时间了,一般的调试都用的print, PyCharm的debug功能也用的比较少,主要一般也用不到,第二是自己也不怎么会用. 服务器开发,本地根本没有运行的环境,前面学 ...

  6. Leetcode——二分法bisect_left,bisect_right

    !前提--列表有序 case 1 如果列表中没有元素x,那么bisect_left(ls, x)和bisec_right(ls, x)返回相同的值,该值是x在ls中"合适的插入点索引,使得数 ...

  7. jmeter dubbo测试

    一.环境准备 1.安装jmeter 2.安装dubbo插件,下载地址jmeter-plugins-dubbo, 将jar包放入${JMETER_HOME}\lib\ext路径下,重启即可 二.添加一个 ...

  8. Jmeter八、关联

    关联的方式:1.正则2.Xpath 后置 处理器→正则表达式提取器 正则表达式:(.*) 模板$1$ 匹配数字:0代表随机,-1代表所有 缺省值为空即可

  9. 微信小程序ECharts通过调用api接口实现图表的数据可视化

    小程序ECharts使用接口调入数据 首先附上js文件链接:axios.js 提取码:AxIo 将此放到小程序目录下的utils文件夹下 在已经完成图表的js文件中完成以下修改: ①引用axios.j ...

  10. Vue组件template中html代码自动补齐设置

    1.vscode设置==>扩展==>JSON==>在settings.json中编辑 2.在最后 } 前添加如下代码保存文件即可 // 自动补全模板字符串 "emmet.t ...