位运算卷积

回顾 FFT,它要求的其实是:

Ck=i+j=kAiBjC_k=\sum_{i+j=k}A_iB_j

将加号换成位运算符就是 FWT 了。

或卷积

首先看较为简单的或,我们这里把二进制数看作集合:

Ck=ij=kAiBjC_k=\sum_{i\cup j=k}A_iB_j

将多项式看作向量,我们需要找到一个线性映射 FWT(X)\text{FWT}(X) 满足:

FWT(C)k=FWT(A)kFWT(B)k\text{FWT}(C)_k=\text{FWT}(A)_k\text{FWT}(B)_k

并且这个线性映射需要做到可逆,这里直接给出式子了:

FWT(A)k=ikAi\text{FWT}(A)_k=\sum_{i\subset k}A_i

因此有:

FWT(A)kFWT(B)k=ikAijkBj=(ij)kAiBj=xk(ij)=xAiBj=FWT(C)k\begin{aligned} \text{FWT}(A)_k\text{FWT}(B)_k&=\sum_{i\subset k}A_i\sum_{j\subset k}B_j\\ &=\sum_{(i\cup j)\subset k}A_iB_j\\ &=\sum_{x\subset k}\sum_{(i\cup j)=x} A_iB_j\\ &=\text{FWT}(C)_k \end{aligned}

我们考虑如何进行 FWT\text{FWT} 变换,假设多项式共有有 2n2^n 项,前半部分用 A0A_0 表示,后边部分用 A1A_1 表示,考虑分治:

FWT(A)=merge(FWT(A0),FWT(A0)+FWT(A1))\text{FWT}(A)=\text{merge}(\text{FWT}(A_0),\text{FWT}(A_0)+\text{FWT}(A_1))

由于后半部分每个对应位置都相当于前半部分对应位置的二进制位最前面添上一个 11,所以后半部分就可以直接向量相加,补充上缺失的部分。merge\text{merge} 就是把两个向量首尾相接拼上。

因为有这个式子,所以我们进行逆变换的时候,直接这样:

IFWT(A)=merge(IFWT(A0),IFWT(A1)IFWT(A0))\text{IFWT}(A)=\text{merge}(\text{IFWT}(A_0),\text{IFWT}(A_1)-\text{IFWT}(A_0))

这个是因为 FWT\text{FWT} 是线性变换,于是 FWT(A0)+FWT(A1)=FWT(A0+A1)\text{FWT}(A_0)+\text{FWT}(A_1)=\text{FWT}(A_0+A_1),所以在逆变换的时候可以直接减掉。

和卷积

类似地,定义一下卷积:

Ck=ij=kAiBjC_k=\sum_{i\cap j=k}A_iB_j

然后定义一下 FWT\text{FWT} 这个变换:

FWT(A)k=ikAi\text{FWT}(A)_k=\sum_{i\supset k}A_i

因此有:

FWT(A)kFWT(B)k=ikAijkBj=(ij)kAiBj=xkij=xAiBj=FWT(C)k\begin{aligned} \text{FWT}(A)_k\text{FWT}(B)_k &=\sum_{i\supset k} A_i\sum_{j\supset k}B_j\\ &=\sum_{(i\cap j)\supset k}A_iB_j\\ &=\sum_{x\supset k}\sum_{i\cap j=x}A_iB_j\\ &=\text{FWT}(C)_k \end{aligned}

然后看如何分治去求,下标 kk 如果包含在前半部分的某个下 ii 里面,它也一定包含在后半部分的里边;反之就不成立了,所以可以这样写:

FWT(A)=merge(FWT(A0)+FWT(A1),FWT(A1))IFWT(A)=merge(IFWT(A0)IFWT(A1),IFWT(A1))\begin{aligned} \text{FWT}(A)&=\text{merge}(\text{FWT}(A_0)+\text{FWT}(A_1),\text{FWT}(A_1))\\ \text{IFWT}(A)&=\text{merge}(\text{IFWT}(A_0)-\text{IFWT}(A_1),\text{IFWT}(A_1)) \end{aligned}

异或卷积

处理起来不太一样,主要用到了 popcount\text{popcount} 的性质。

popcount(i&k)+popcount(j&k)popcount((ij)&k)(mod2)\text{popcount}(i\& k)+ \text{popcount}(j\& k)\equiv \text{popcount}((i\oplus j)\&k)\pmod 2

分每一位去看:

  • kk 的这一位为 00,那么最终是不产生贡献的。
  • kk 的这一位为 11,并且 i,ji,j 这一位相同,左边的贡献为 22,右边的贡献为 00,是同余的。
  • kk 的这一位为 11,并且 i,ji,j 这一位不同,左边的贡献为 11,右边的贡献为 11,是同余的。

看到同余于 22,想到一定会有:

(1)LHS=(1)RHS(-1)^{\text{LHS}}=(-1)^{\text{RHS}}

所以定义线性变换:

FWT(A)k=i(1)popcount(i&k)Ai\text{FWT}(A)_k=\sum_{i}(-1)^{\text{popcount}(i\&k)} A_i

因此有:

FWT(A)kFWT(B)k=i,j(1)popcount(i&k)Ai(1)popcount(j&k)Bj=i,j(1)popcount((ij)&k)AiBj=FWT(C)k\begin{aligned} \text{FWT}(A)_k\text{FWT}(B)_k&=\sum_{i,j}(-1)^{\text{popcount}(i\&k)}A_i(-1)^{\text{popcount}(j\& k)}B_j\\ &=\sum_{i,j}(-1)^{\text{popcount}((i\oplus j)\&k)}A_iB_j\\ &=\text{FWT}(C)_k \end{aligned}\\

这和前面两个不同的地方在于每一项都是对整个向量所有值进行加权求和,所以最后根据加法交换律就不用在乎这一项到底是啥,只要前面的系数对了就行。

同样可以进行分治去求,由于前半部分的 kk 的最高位为 00,所以 ii 是没有贡献到的,后半部分贡献到了:

FWT(A)=merge(FWT(A0)+FWT(A1),FWT(A0)FWT(A1))IFWT(A)=merge(IFWT(A0)+IFWT(A1)2,IFWT(A0)IFWT(A1)2)\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}(\frac{\text{IFWT}(A_0)+\text{IFWT}(A_1)}{2},\frac{\text{IFWT}(A_0)-\text{IFWT}(A_1)}{2})

接下来就是代码实现了。

代码

来自 P4717 的模板题,如果写个自动取模的 mint 可能会好看一点,我这里就不写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <bits/stdc++.h>
using namespace std;

const int N = 18, INV2 = 499122177, P = 998244353;
int n, a[1<<N], b[1<<N], A[1<<N], B[1<<N], C[1<<N];

void OR(int A[], int inv) {
for (int len = 2, half = 1; len <= 1 << n; len <<= 1, half <<= 1) {
for (int i = 0; i < 1 << n; i += len) {
for (int j = 0; j < half; j++) {
// i: block start, j: block pointer
A[i+j+half] = (A[i+j+half] + A[i+j] * inv) % P;
if (A[i+j+half] < 0) A[i+j+half] += P;
}
}
}
}

void AND(int A[], int inv) {
for (int len = 2, half = 1; len <= 1 << n; len <<= 1, half <<= 1) {
for (int i = 0; i < 1 << n; i += len) {
for (int j = 0; j < half; j++) {
// i: block start, j: block pointer
A[i+j] = (A[i+j] + A[i+j+half] * inv) % P;
if (A[i+j] < 0) A[i+j] += P;
}
}
}
}

void XOR(int A[]) {
for (int len = 2, half = 1; len <= 1 << n; len <<= 1, half <<= 1)
for (int i = 0; i < 1 << n; i += len)
for (int j = 0; j < half; j++) {
int A0 = A[i+j], A1 = A[i+j+half];
A[i+j] = (A0 + A1) % P;
A[i+j+half] = (A0 - A1) % P;
if (A[i+j+half] < 0) A[i+j+half] += P;
}
}

void IXOR(int A[]) {
for (int len = 2, half = 1; len <= 1 << n; len <<= 1, half <<= 1)
for (int i = 0; i < 1 << n; i += len)
for (int j = 0; j < half; j++) {
int A0 = A[i+j], A1 = A[i+j+half];
A[i+j] = 1LL * (A0 + A1) * INV2 % P;
A[i+j+half] = 1LL * (A0 - A1) * INV2 % P;
if (A[i+j+half] < 0) A[i+j+half] += P;
}
}

void cpy() {
for (int i = 0; i < 1 << n; i++) A[i] = a[i];
for (int i = 0; i < 1 << n; i++) B[i] = b[i];
}

void mul() {
for (int i = 0; i < 1 << n; i++) C[i] = 1LL * A[i] * B[i] % P;
}

void out() {
for (int i = 0; i < 1 << n; i++) printf("%d ", C[i]);
puts("");
}

int main() {
scanf("%d", &n);
for (int i = 0; i < 1 << n; i++) scanf("%d", &a[i]);
for (int i = 0; i < 1 << n; i++) scanf("%d", &b[i]);

cpy();
OR(A, 1), OR(B, 1);
mul();
OR(C, -1);
out();

cpy();
AND(A, 1), AND(B, 1);
mul();
AND(C, -1);
out();

cpy();
XOR(A), XOR(B);
mul();
IXOR(C);
out();

return 0;
}