跳到主要内容

学习笔记:快速沃尔什变换

参考文献

快速变换之Fast Walsh Hadamard Transform

前置知识

  1. FFT(快速傅里叶变换)
  2. 卷积定理:函数卷积的傅里叶变换是函数傅里叶变换的乘积
  3. 正交:是线性代数的概念,是垂直这一直观概念的推广。 作为一个形容词,只有在一个确定的内积空间中才有意义。 若内积空间中两向量的内积为0,则称它们是正交的。 如果能够定义向量间的夹角,则正交可以直观的理解为垂直。
  4. 二次剩余:在数论中,特别在同余理论里,一个整数 XX 对另一个整数 PP 的二次剩余(英语:Quadratic residue)指 XX 的平方 X2X^2 除以 PP 得到的余数。

引入

首先,我们知道多项式乘法可以通过 FFT 降低复杂度。

即通过 FFT,能够在 O(nlogn)O(n\log{n}) 的时间复杂度求下面的循环卷积:

ck=i+j=kaibjc_k=\sum_{i+j=k}a_ib_j

由卷积定理保证了:

DFT(c)=DFT(a)DFT(b)DFT(c)=DFT(a)·DFT(b)

因此如果要求 aba·b,可以先利用 FFT进行转换,再点积,进行逆转换:

c=IDFT(DFT(a)DFT(b))c=IDFT(DFT(a)·DFT(b))

但是,假如我们需要将卷积转化为与/或/异或卷积,也就是求解:

ck=ij=kaibjc_k=\sum_{i*j=k}a_ib_j

其中 * 是与/或/异或运算之一,是否存在一种线性变换满足如下相同的卷积定理呢?

A(c)=A(a)A(b)A(c)=A(a)·A(b)

并且,正如 FFT ,该变换也要有快速算法。答案是肯定的,即沃尔什变换。

每种运算对应一种线性变换,其中异或运算就对应 WHT,沃尔什-阿达玛变换。

简介

沃尔什-阿达玛变换(Walsh-Hadamard Transform)是一种广义傅里叶变换,是信号处理,集成电路和图像处理中频谱分析的一种变换方法,用于替换离散傅里叶变换。快速沃尔什-阿达玛变换(FWHT,或者通常在国内算法竞赛界被称为 FWT)是一种进行 WHT 的快速算法,和 FFT 类似。

傅里叶变换是浮点类型(还是复数)的,把信号在谐波下进行分解,而 Walsh 把信号在不同震荡频率方波下分解,因此所有的系数都是绝对值大小相同的整数,这使得不需要作浮点数的乘法运算,提高了运算速度。

这里谐波指的就是单位正弦波或者余弦波的倍数频率的波,比如 sin(kx)\sin(kx)。而方波就是矩形的波。

方波

如果用数学公式描述,就是使用 Hadamard 矩阵对信号向量进行变换,以下是四阶和八阶的 Hadamard 矩阵:

H4=[1111111111111111]H_4=\begin{bmatrix}1&1&1&1\\1&-1&1&-1\\1&1&-1&-1\\1&-1&-1&1\end{bmatrix} H8=[1111111111111111111111111111111111111111111111111111111111111111]H_8=\begin{bmatrix}1&1&1&1&1&1&1&1\\1&-1&1&-1&1&-1&1&-1\\1&1&-1&-1&1&1&-1&-1\\1&-1&-1&1&1&-1&-1&1\\1&1&1&1&-1&-1&-1&-1\\1&-1&1&-1&-1&1&-1&1\\1&1&-1&-1&-1&-1&1&1\\1&-1&-1&1&-1&1&1&-1\end{bmatrix}

Hadamard 矩阵

Hadamard 矩阵即一个只包含 111-1 的矩阵,满足每一行/列都相互正交。

1122 以及 4k4^k24k2*4^k 阶的 Hadamard 矩阵一定存在,可证明 Hadamard 矩阵的阶数除了 1122 外都是 44 的倍数。

因而有 Hadamard 猜想:4n4n 阶矩阵一定存在。

一般 4n4n 阶的 Hadarmard 矩阵存在性问题以及构造,依旧是个未解决难题。关于 Hadarmard 矩阵,可以看张贤达的《矩阵分析》一书以及冯荣权宋春伟的《组合数学》有一些结论和比较旧的相关文献。

西尔维斯特(Sylvester)给出了由一个 Hadarmard 矩阵构造一个新的 Hadarmard 矩阵的方法,若 HH 是一个 Hadarmard 矩阵,则容易证明下面矩阵也是 Hadarmard 矩阵

[HHHH]\begin{bmatrix}H&H\\H&-H\end{bmatrix}

Arasu 等人的 Hadamard and Conference Matrices 中提出了构造 Hadamard 的另一种方法。

Paley 在 1933 年利用二次剩余的理论发现了一种更有效的构造 Hadamard 矩阵。

沃尔什·阿达玛变换(Walsh-Hadamard Transform)

2k2^k 阶的 Walsh-Hadamard Transform 定义为

X=12kH2kx或者X=H2kxX=\frac{1}{2^k}H_{2^k}x\,\,\,\text{或者}\,\,\,X=H_{2^k}x

显然逆变换就是

x=H2kTX或者x=12kH2kTXx=H_{2^k}^{T}X\,\,\,\text{或者}\,\,\,x=\frac{1}{2^k}H_{2^k}^{T}X

此处使用后者的定义,H2kH_{2^k} 是通过西尔维斯特构造法得到的 Hadarmard 矩阵。

H2k=[H2k1H2k1H2k1H2k1]H_{2^k}=\begin{bmatrix}H_{2^{k-1}}&H_{2^{k-1}}\\H_{2^{k-1}}&-H_{2^{k-1}}\end{bmatrix}

容易发现 H2kH_{2^k} 是一个对称正交阵,所以 H2kT=H2kH_{2^k}^T=H_{2^k}

沃尔什变换可以展开成显式的求和表达式。注意到 H1=[1]H_1=\begin{bmatrix}1\end{bmatrix},则 H2kH_{2^k} 的 i 行 j 列为

H2k=(1)[i2k1andj2k1]H2k1(i%2k1,j%2k1)H_{2^k}=(-1)^{[i\geq2^{k-1}\text{and}j\geq2^{k-1}]}H_{2^{k-1}(i\%2^{k-1},j\%2^{k-1})}

pc(a)pc(a)aa 中二进制 11 的个数,[expression][expression]expressionexpression 为真时为 11,否则为 00。则可以得到

H2k=(1)pc(i&j)H_{2^k}=(-1)^{pc(i\&j)}

因此可以将 Walsh 变换写成

Hnx=((1)pc(i&0)xi,(1)pc(i&1)xi,,(1)pc(i&(n1))xi)H_nx=\left(\sum(-1)^{pc(i\&0)}x_i,\sum(-1)^{pc(i\&1)}x_i,\cdots,\sum(-1)^{pc(i\&(n-1))}x_i \right)

其中 n=2kn=2^k 的形式较为常见,或者简写成

WT[x]i=(H2kx)i=ij=0xjij=1WT[x]_i=(H_{2^k}x)_i=\sum_{i\oplus j=0}x_j-\sum_{i\oplus j=1}

WT[x]iWT[x]_i 表示 xx 经过 Walsh 变换后的第 ii 位。

快速沃尔什变换

我们可以将 xx 进行拆分,x=[x0,x1]x=[x_0,x_1]。因而

H2kx=[H2k1H2k1H2k1H2k1]=[H2k1x0+H2k1x1H2k1x0H2k1x1]H_{2^k}x=\begin{bmatrix}H_{2^{k-1}}&H_{2^{k-1}}\\H_{2^{k-1}}&-H_{2^{k-1}}\end{bmatrix}=\begin{bmatrix}H_{2^{k-1}}x_0+H_{2^{k-1}}x_1\\H_{2^{k-1}}x_0-H_{2^{k-1}}x_1\end{bmatrix}

故只需要计算 H2k1x0H_{2^{k-1}}x_0H2k1x1H_{2^{k-1}}x_1,按照这个过程继续分解就能得到快速沃尔什变换。

逆变换同理:

12kH2kTX=12k[H2k1TX0+H2k1TX1H2k1TX0H2k1TX1]=12[12k1H2k1TX0+12k1H2k1TX112k1H2k1TX012k1H2k1TX1]\frac{1}{2^k}H_{2^k}^TX=\frac{1}{2^k}\begin{bmatrix}H_{2^{k-1}}^TX_0+H_{2^{k-1}}^TX_1\\H_{2^{k-1}}^TX_0-H_{2^{k-1}}^TX_1\end{bmatrix}=\frac{1}{2}\begin{bmatrix}\frac{1}{2^{k-1}}H_{2^{k-1}}^TX_0+\frac{1}{2^{k-1}}H_{2^{k-1}}^TX_1\\\frac{1}{2^{k-1}}H_{2^{k-1}}^TX_0-\frac{1}{2^{k-1}}H_{2^{k-1}}^TX_1\end{bmatrix}

也可以写成如下形式

FWT(X)=merge(FWT(X0)+FWT(X1),FWT(X0)FWT(X1))IFWT(X)=merge(IFWT(X0)+IFWT(X1)2,IFWT(X0)IFWT(X1)2)FWT(X)=\text{merge}(FWT(X_0)+FWT(X_1),FWT(X_0)-FWT(X_1))\\ IFWT(X)=\text{merge}\left(\frac{IFWT(X_0)+IFWT(X_1)}{2},\frac{IFWT(X_0)-IFWT(X_1)}{2}\right)

其中 FWT , IFWT 是正逆 Walsh 变换,merge 是向量拼接。这种形式在网上比较常见。

Xor(dyadic) 卷积和卷积定理

对两个长度为 2k2^k 的向量 x,yx,y,定义 Xor/dyadic 卷积如下

z(n)=d=02k1x(d)y(nd)z(n)=\sum_{d=0}^{2^k-1}x(d)y(n\oplus d)

其中 n<2kn<2^k,或者等价形式

z(n)=ab=nx(a)y(b)z(n)=\sum_{a\oplus b=n}x(a)y(b)

其中\oplus为异或,x(a)x(a) 表示 xxaa 位。显然这个卷积满足交换律,乘法分配律,结合律。

按照上面定义的 Xor 卷积有如下的卷积定理

z=xyZ=XYz=x*y\Rightarrow Z=XY

对所有 2k2^k 阶的 WHT 变换都成立。其中 X,Y,ZX,Y,Zx,y,zx,y,z 的 Walsh-Hadarmard 变换。

证明:采用归纳法,当 k=0k=0

Z(0)=z(0)=x(0)y(0)=X(0)Y(0)Z(0)=z(0)=x(0)y(0)=X(0)Y(0)

假设当 n=kn=k 时成立,现在令 n=k+1n=k+1,首先我们将 x,yx,y 拆分成两半 Xi=H2kxi,Yi=H2kyiX_i=H_{2^k}x_i,Y_i=H_{2^k}y_i,有

Xi=H2kxi=[H2kH2kH2kH2k][x0x1]X_i=H_{2^k}x_i=\begin{bmatrix}H_{2^k}&H_{2^k}\\H_{2^k}&-H_{2^k}\end{bmatrix}\begin{bmatrix}x_0\\x_1\end{bmatrix} X=[X0+X1X0X1]andY=[Y0+Y1Y0Y1]X=\begin{bmatrix}X_0+X_1\\X_0-X_1\end{bmatrix}\text{and}\,Y=\begin{bmatrix}Y_0+Y_1\\Y_0-Y_1\end{bmatrix} z=xy=12[x0y0+x1y1x0y1+x1y0]z=x*y=\frac{1}{2}\begin{bmatrix}x_0*y_0+x_1*y_1\\x_0*y_1+x_1*y_0\end{bmatrix}

因此

Z=H2k+1z=[H2kH2kH2kH2k][x0y0+x1y1x0y1+x1y0]=[X0Y0+X1Y1+X0Y1+X1Y0X0Y0+X1Y1X0Y1X1Y0]=[(X0+X1)(Y0+Y1)(X0X1)(Y0Y1)]=XY\begin{align} Z &= H_{2^{k+1}}z\\ &= \begin{bmatrix}H_{2^k}&H_{2^k}\\H_{2^k}&-H_{2^k}\end{bmatrix}\begin{bmatrix}x_0*y_0+x_1*y_1\\x_0*y_1+x_1*y_0\end{bmatrix}\\ &= \begin{bmatrix}X_0Y_0+X_1Y_1+X_0Y_1+X_1Y_0\\X_0Y_0+X_1Y_1-X_0Y_1-X_1Y_0\end{bmatrix}\\ &= \begin{bmatrix}(X_0+X_1)(Y_0+Y_1)\\(X_0-X_1)(Y_0-Y_1)\end{bmatrix}\\ &= XY \end{align}

证毕。

下面证明 Walsh-Hadarmard 变换是满足这个卷积定理的唯一可逆线性变换(在忽略行顺序的意义下,因为行之间的交换不会影响这个性质)。

z(n)=ab=nx(a)y(b)z(n)=\sum_{a\oplus b=n}x(a)y(b)

当这个变换是一阶时,显然,变换为单位变换。故下面设阶大于1。

引理1:

A:,ij=A:,iA:,jA_{:,i\oplus j}=A_{:,i}·A_{:,j}

其中 A:,iA_{:,i} 表示 AA 的第 ii 列。

证明:首先,令 x,yx,y 为第 i,ji,j 位为 11,其他为 00,可以得到

AzAzAAiji\oplus j列。而 AxAxAA 的第 ii 列,AyAyAA 的第 jj 列。因此

A:,ij=A:,iA:,jA_{:,i\oplus j}=A_{:,i}·A_{:,j}

推论2:由引理1,令 i=0i=0 知道第一列全为 11,其他列都只有 1-111,对一个固定的 tt,任意 iii2ti\oplus 2^t 两列的按位乘都相等。并且 AA 可逆所以所有列都不一样。

引理3(用处不大): AA 的某一行全 11,其他行 1-111 各占一半

证明:令 yy11 向量。则 z=x1=11Tz=x*1=11^T

Az=AxA1=1TxA1Az=Ax·A1=1^TxA1

因此除了某一行全为 11,其他行的和都为 00。这是因为,至多有一行为全 11,其他为 00,否则 AA 不可逆。另外,必然有一行为全 11,因为 11 向量的补空间为 2k12^k-1 维,如果没有一行全 11 ,则

A1=01=0A1=0\Rightarrow1=0

定理4:Walsh-Hadarmard 变换是满足这个卷积定理的唯一可逆线性变换(忽略行序的情况)

证明:由推论 1,要确定一行,只需确定其中的 kk 个位置的值,这 kk 个位置为 20,21,,2k12^0,2^1,\cdots,2^{k-1}。由于推论 2,每个位置只能取 ±1\pm1。因此,可能有 2k2^k 种情况。

AA 共有 2k2^k 行且可逆,因此每一种情况对应 AA 中的一行。故 AA 必然就是 Hadarmard 矩阵。(一开始想用归纳证明,把自己绕进去好久)

或卷积和与卷积

对于或以及与,沃尔什阿达玛变换就不满足条件了。然而有了上面证明唯一性的经验,我们就可以照猫画虎,来找满足条件的线性变换。

仿照引理 1,推论 2,可以得到,对于或卷积,这个变换 AA 必须满足 $$

A:,ij=A:,iA:,jA_{:,i|j}=A_{:,i}·A_{:,j}

并且第一列必须为 11。并且元素只能为 0011。令 j=n1=2k1j=n-1=2^k-1,得

A:,j=A:,iA:,j,iA_{:,j}=A_{:,i}·A_{:,j},\forall i

所以最后一列,如果某个位置为 11,则该行为 11。因此,最后一列有且只有一个 11。其中 2k=n2^k=n。同理 20,21,,2k12^0,2^1,\cdots,2^{k-1} 这些列决定了整个矩阵。

同样 AA 只能有 0/10/1 。所以所有行包含了 2k2^k 种情况。我们可以让从下往上让第 ii 行这 kk 个位对应二进制 ii。通过分析知道 AA 具有如下形式:

A=[HHH0]A=\begin{bmatrix}H&H\\H&0\end{bmatrix}

其中 HH 必须满足 2k12^{k-1} 阶的卷积定理。因此满足或卷积定理的线性变可以通过如下方法构造(H1=[1]H_1=[1]

H2k=[H2k1H2k1H2k10]H_{2^k}=\begin{bmatrix}H_{2^{k-1}}&H_{2^{k-1}}\\H_{2^{k-1}}&0\end{bmatrix}

或者(随便改变行序都行,不过为了变换方便,我们采用这两种方式之一)

H2k=[H2k10H2k1H2k1]H_{2^k}=\begin{bmatrix}H_{2^{k-1}}&0\\H_{2^{k-1}}&H_{2^{k-1}}\end{bmatrix}

同理可以得到满足与卷积定理的线性变换为可以通过下面方法构造

H2k=[H2k1H2k10H2k1]H_{2^k}=\begin{bmatrix}H_{2^{k-1}}&H_{2^{k-1}}\\0&H_{2^{k-1}}\end{bmatrix}

可以得到或形式快速变换可以写成

FWTor(X)=merge(FWTor(X0),FWTor(X0)+FWTor(X1))FWT_{or}(X)=\text{merge}(FWT_{or}(X_0),FWT_{or}(X_0)+FWT_{or}(X_1)) IFWTor(X)=merge(IFWTor(X0),IFWTor(X0)IFWTor(X1))IFWT_{or}(X)=\text{merge}(IFWT_{or}(X_0),IFWT_{or}(X_0)-IFWT_{or}(X_1))

与形式的变换写成

FWTand(X)=merge(FWTand(X0)+FWTand(X1),FWTand(X1))FWT_{and}(X)=\text{merge}(FWT_{and}(X_0)+FWT_{and}(X_1),FWT_{and}(X_1)) IFWTand(X)=merge(IFWTand(X0)IFWTand(X1),IFWTand(X1))IFWT_{and}(X)=\text{merge}(IFWT_{and}(X_0)-IFWT_{and}(X_1),IFWT_{and}(X_1))

C++实现

重新回顾一下三种快速变换

  • OR
FWTor(X)=merge(FWTor(X0),FWTor(X0)+FWTor(X1))FWT_{or}(X)=\text{merge}(FWT_{or}(X_0),FWT_{or}(X_0)+FWT_{or}(X_1)) IFWTor(X)=merge(IFWTor(X0),IFWTor(X0)IFWTor(X1))IFWT_{or}(X)=\text{merge}(IFWT_{or}(X_0),IFWT_{or}(X_0)-IFWT_{or}(X_1))
  • AND
FWTand(X)=merge(FWTand(X0)+FWTand(X1),FWTand(X1))FWT_{and}(X)=\text{merge}(FWT_{and}(X_0)+FWT_{and}(X_1),FWT_{and}(X_1)) IFWTand(X)=merge(IFWTand(X0)IFWTand(X1),IFWTand(X1))IFWT_{and}(X)=\text{merge}(IFWT_{and}(X_0)-IFWT_{and}(X_1),IFWT_{and}(X_1))
  • XOR
FWT(X)=merge(FWT(X0)+FWT(X1),FWT(X0)FWT(X1))IFWT(X)=merge(IFWT(X0)+IFWT(X1)2,IFWT(X0)IFWT(X1)2)FWT(X)=\text{merge}(FWT(X_0)+FWT(X_1),FWT(X_0)-FWT(X_1))\\ IFWT(X)=\text{merge}\left(\frac{IFWT(X_0)+IFWT(X_1)}{2},\frac{IFWT(X_0)-IFWT(X_1)}{2}\right)

实现起来比 FFT 简单得多,从底至上循环递推一下即可。可以把正负变换一起实现,因为只有系数不一样。

此处除了 Fwt 类,还实现了两个类,整数取模类ModInt, 多项式类Poly。为了复用性,牺牲了一点性能,一般不至于把时间卡那么死。这个多项式类和ModInt在其地方也都用到。还有个快速幂qpow也写成模板的形式。fwt部分的代码也就30行。

#include <bits/stdc++.h>
using namespace std;
constexpr int P = 998244353;

template<class T>
T qpow(T n, int k) {
T res = 1;
for (; k; k >>= 1, n *= n) {
if (k & 1) {
res *= n;
}
}
return res;
}

template<int P = P>
struct ModInt {
int x;
ModInt(int x_ = 0) : x(mod(x_)) {}
ModInt(const ModInt& that) : x(that.x) {}
int val() const {
return x;
}

// assume -P <= x < 2P
int mod(int x) {
if (x < 0) {
x += P;
}
if (x >= P) {
x -= P;
}
return x;
}
ModInt operator-() const {
return ModInt(P - x);
}
ModInt inv() const {
// assert(x != 0);
return qpow(*this, P - 2);
}
ModInt& operator*=(const ModInt& rhs) {
x = int64_t(x) * rhs.x % P;
return *this;
}
ModInt& operator+=(const ModInt& rhs) {
x = mod(x + rhs.x);
return *this;
}
ModInt& operator-=(const ModInt& rhs) {
x = mod(x - rhs.x);
return *this;
}
ModInt& operator/=(const ModInt& rhs) {
return *this *= rhs.inv();
}
friend ModInt operator*(const ModInt& lhs, const ModInt& rhs) {
ModInt res = lhs;
res *= rhs;
return res;
}
friend ModInt operator+(const ModInt& lhs, const ModInt& rhs) {
ModInt res = lhs;
res += rhs;
return res;
}
friend ModInt operator-(const ModInt& lhs, const ModInt& rhs) {
ModInt res = lhs;
res -= rhs;
return res;
}
friend ModInt operator/(const ModInt& lhs, const ModInt& rhs) {
ModInt res = lhs;
res /= rhs;
return res;
}
};

template<typename T>
struct Poly {
vector<T> v;
Poly(int n = 0) {
v.resize(n);
}

Poly(const Poly& p) {
v.resize(p.size());
for (int i = 0; i < size(); i++) {
v[i] = p[i];
}
}

size_t size() const {
return v.size();
}

T& operator[](int i) {
return v[i];
}

T operator[](int i) const {
return v[i];
}

Poly operator-() const {
Poly p = v;
for (int i = 0; i < p.size(); i++) {
p[i] = -p[i];
}
return p;
}

Poly& operator*=(Poly& rhs) {
for (int i = 0; i < size(); i++) {
v[i] *= rhs[i];
}
return *this;
}
Poly& operator+=(const Poly& rhs) {
for (int i = 0; i < size(); i++) {
v[i] += rhs[i];
}
return *this;
}
Poly& operator-=(const Poly& rhs) {
for (int i = 0; i < size(); i++) {
v[i] -= rhs[i];
}
return *this;
}
Poly& operator/=(const Poly& rhs) {
for (int i = 0; i < size(); i++) {
v[i] /= rhs[i];
}
return *this;
}
friend Poly operator*(Poly lhs, Poly rhs) {
return lhs *= rhs;
}
friend Poly operator+(Poly lhs, Poly rhs) {
return lhs += rhs;
}
friend Poly operator-(Poly lhs, const Poly rhs) {
return lhs -= rhs;
}
friend Poly operator/(Poly lhs, Poly rhs) {
return lhs /= rhs;
}
};

struct Fwt {
template<typename T>
static Poly<T> OR(Poly<T> f, bool inv = false) {
T x = inv ? -1 : 1;
int n = f.size();
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] * x;
return f;
}
template<typename T>
static Poly<T> AND(Poly<T> f, bool inv = false) {
T x = inv ? -1 : 1;
int n = f.size();
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 + k] * x;
return f;
}

template<typename T>
static Poly<T> XOR(Poly<T> f, bool inv = false) {
T x = inv ? T(1) / 2 : 1;
int n = f.size();
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 + k],
f[i + j + k] = f[i + j] - f[i + j + k] - f[i + j + k],
f[i + j] *= x, f[i + j + k] *= x;
return f;
}

template<typename T>
static Poly<T> IOR(Poly<T> f) {
return Fwt::OR(f, true);
}
template<typename T>
static Poly<T> IAND(Poly<T> f) {
return Fwt::AND(f, true);
}
template<typename T>
static Poly<T> IXOR(Poly<T> f) {
return Fwt::XOR(f, true);
}

};
template<int P>
istream& operator>>(istream& ins,
ModInt<P>& v) {
ins >> v.x;
return ins;
}

template<typename T>
istream& operator>>(istream& ins,
vector<T>& v) {
for (int i = 0; i < int(v.size()); i++) ins >> v[i];
return ins;
}

template <typename T>
istream& operator>>(istream& ins,
Poly<T>& p) {
ins >> p.v;
return ins;
}

template<int P>
ostream& operator<<(ostream& outs,
const ModInt<P>& v) {
outs << v.val();
return outs;
}

template<typename T>
ostream& operator<<(ostream& outs,
const vector<T>& v) {
for (int i = 0; i < int(v.size()); i++) outs << v[i] << " ";
return outs;
}

template <typename T>
ostream& operator<<(ostream& outs,
Poly<T>& p) {
outs << p.v;
return outs;
}

int main() {
int n, m;
cin >> m;
n = 1 << m;

Poly<ModInt<>> a(n), b(n);
cin >> a >> b;
Poly<ModInt<>> c = Fwt::IOR(Fwt::OR(a) * Fwt::OR(b));
Poly<ModInt<>> d = Fwt::IAND(Fwt::AND(a) * Fwt::AND(b));
Poly<ModInt<>> e = Fwt::IXOR(Fwt::XOR(a) * Fwt::XOR(b));

cout << c << "\n" << d << "\n" << e << "\n";

return 0;
}