FWT 学习笔记
前言
怎么都会 FWT。
感觉写的好多,好繁杂,把一个不难的东西讲的很恶心的样子。另外,公式恐惧症患者需要克服一下。
本文在经过修改之后可能还出现了不少疏漏,请审核员或者是广大同学们不吝指出。
定义
FWT 是一种用来求位运算卷积的算法。
先讲一下什么叫做位运算卷积。位运算卷积就是指两个数组
其中 and、or 和 xor 等。
序列
::::info[题外话]{open}
注意到当
这正是我们 FFT/NTT 处理的
方法讲述
FFT/NTT 的处理方式虽然和 FWT 不同,但是其思想和 FWT 是相同的。都是先进行一个正变换,然后逐位相乘,最后经过逆变换得到答案。
那到底怎么变换呢?由于 FWT 所涉及的位运算大多都是 and、or 和 xor 中间的一个,而这三种位运算的处理方式也是不太相同的。我将会分别讲述。
or 卷积
不妨使用 | 来代表 or 运算,因此我们求的是:
正变换 FWT
不难注意到存在一个显然的性质:若
然后我们的 FWT 是怎么操作的呢?它构造了一个数组:
就是指每一个位置的子集对应的位置上的数之和。
这样就是 FWT 的正变换,以后都将对于数组
那么,我们要对这个
前两个等号都是基本操作,第三个等号运用了之前的结论,第四个等号是基本操作,第五个等号运用了
那么根据这个式子你发现什么了?没错,直接把
但是我们还有一个问题,就是如何快速求出
回忆 FFT/NTT 处理卷积问题的过程,不难想到 FFT/NTT 是先将
对于一个长度为
考虑这样定义有什么用处。
结合分治思想,我们可以列出以下式子:
- 如果
a>0 ,则一定可以分出A_0 和A_1 。而我们分治求出F(A_0) 和F(A_1) 之后,则F(A)=\text{merge}(F(A_0),F(A_1)+F(A_0)) 。其中+ 表示逐位相加,\text{merge} 是指将两个数组首位拼接起来。在文章的后面会一直沿用这两个符号来表示。 - 如果
a=0 ,则显然有F(A)=A 。
简单的复杂度分析可以发现这样的复杂度是
逆变换 IFWT
不要忘记先逐位相乘。
设对
给出做法,只有一句话:把正变换的符号换一下就行了,其余都和正变换一模一样。
可能有人会不太懂,描述成式子就是,原来的
感性理解就像是正变换是前缀和,逆变换是差分。证明看文献,不想写了/tuu。
and 卷积
不妨使用 & 来代表 and 运算,因此我们求的是:
正变换 FWT
注意到 and 和 or 似乎在很多方面上都相反。所以我们考虑把子集和反过来,也就是超集和,得出
而根据 or 卷积的经验,我们需要对
注意到显然当
因此我们仍然可以直接在正变换后直接逐位相乘。现在的问题还是如何快速求出
仍然使用分治做法。先把
分治的方法也就是平凡的了。
- 如果
a>0 ,则一定可以分出A_0 和A_1 。而我们分治求出F(A_0) 和F(A_1) 之后,则F(A)=\text{merge}(F(A_0)+F(A_1),F(A_1)) 。 - 如果
a=0 ,则显然有F(A)=A 。
逆变换 IFWT
和 or 卷积差不多,依旧把正变换的符号换一下,然后再做一遍正变换就行了。
感性上的理解仍然是正变换是前缀和,逆变换是差分。
xor 卷积
下文使用 xor。
正变换 FWT
这时候就比较困难了。xor 和 and 或 or 的联系都不大。
仿照 and 和 or 的做法,考虑仍然构造一个
我们的目标就是,构造
直接构造很困难。回想,我们是怎么证明
我们第一步就是先直接展开。所以不难得出,我们的
因为有这个
代数变换得到以下式子:
然后我们注意到,欸这个
那么我们该怎么构造呢?(下面要有些注意力了说白了就是笔者不知道是怎么想到的)
首先有一个结论:
注意到,
而注意到
那么也就一定有
那么依旧考虑分治。把
- 如果
a=0 ,则F(A)=A 。 - 如果
a>0 ,则将前2^{a-1} 个数称为A_0 ,后2^{a-1} 个数称为A_1 。先递归计算F(A_0),F(A_1) 。然后计算F(A) 就相当于加入了二进制的一位,然后考虑F(A) 是怎样的两个数组拼接起来:- 对于
F(A) 的前2^{a-1} 个数,它和A_0 中的位置\cap 起来在2^a 这一位是0 的,对于f 这个系数肯定不会改变,所以首先有F(A_0) 。而它和A_1 中的位置\cap 起来在2^a 这一位也是0 ,也不会改变一丝一毫的f ,所以还要逐位加上F(A_1) 。 - 对于
F(A) 的后2^{a-1} 个数,它和A_0 中的位置\cap 起来在2^a 这一位还是0 ,对于f 这个系数不会改变,所以有F(A_0) 。而它和A_1 中的位置\cap 起来在2^a 这一位终于是1 了,会改变所有f 的正负性,所以还要逐位减去F(A_1) 。 - 于是有
F(A)=\text{merge}(F(A_0)+F(A_1),F(A_0)-F(A_1)) 。
- 对于
这样就可以在
逆变换 IFWT
就是把
感性一点理解的话,就是直接移项然后和差状物。
代码实现
虽然我前面给出的做法可以直接递归模拟,但是递归的常数比较大。所以为了避免被卡常,所以要使用循环。
给出循环的代码,可以自己理解一下代码的含义是什么。另外,建议打这些什么什么变换的都使用函数封装。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000010, mod = 998244353;
int a[N], b[N];
int x[N], y[N];
int n;
void mul() {
for (int i = 1; i <= n; i++)
x[i] = x[i] * y[i] % mod;
}
void mem() {
for (int i = 1; i <= n; i++)
x[i] = a[i], y[i] = b[i];
}
void outp() {
for (int i = 1; i <= n; i++)
cout << x[i] << " ";
cout << endl;
}
void OR(int *a, int op) {
for (int i = 1; i < n; i *= 2) {
int siz = i * 2;
for (int j = 1; j <= n; j += siz)
for (int k = 0; k < i; k++)
a[i + j + k] = (a[i + j + k] + a[j + k] * op % mod + mod) % mod;
}
}
void AND(int *a, int op) {
for (int i = 1; i < n; i *= 2) {
int siz = i * 2;
for (int j = 1; j <= n; j += siz)
for (int k = 0; k < i; k++)
a[j + k] = (a[j + k] + a[i + j + k] * op % mod + mod) % mod;
}
}
void XOR(int *a, int op) {
for (int i = 1; i < n; i *= 2) {
int siz = i * 2;
for (int j = 1; j <= n; j += siz)
for (int k = 0; k < i; k++) {
int xx = a[j + k], yy = a[i + j + k];
a[j + k] = (xx + yy) % mod * op % mod, a[i + j + k] = (xx - yy + mod) % mod * op % mod;
}
}
}
signed main() {
cin >> n;
n = (1 << n);
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
mem(), OR(x, 1), OR(y, 1), mul(), OR(x, -1), outp();
mem(), AND(x, 1), AND(y, 1), mul(), AND(x, -1), outp();
mem(), XOR(x, 1), XOR(y, 1), mul(), XOR(x, (mod + 1) / 2), outp();
return 0;
}
参考文献
- P4717 题解。尤其是这篇。