快速傅里叶变换、数论变换、快速莫比乌斯变换、快速沃尔什变换学习笔记(代码还没补,以及应用)

快速傅里叶变换、数论变换、快速莫比乌斯变换、快速沃尔什变换学习笔记

前置知识

阅读下文需要复数和任意角三角函数等高中数学知识,请先自行学习这些内容。

快速傅里叶变换(FFT)

概述

离散傅里叶变换(Discrete Fourier Transform,DFT),是傅里叶变换在时域和频域上都呈离散的形式,将信号的时域采样变换为其 DTFT 的频域采样。快速傅里叶变换(Fast Fourier Transform,FFT)是一种高效实现 DFT 的算法。

DFT 在算法竞赛中常用于加速多项式乘法,并进一步加速多项式其他各种运算来解题。

多项式的系数与点值表示法

系数表示法是用每一项系数来表示多项式。\(f(x)=\sum\limits_{i=0}^{n-1}a_ix^i\iff f(x)=\{a_0,a_1,\cdots,a_{n-1}\}\)

点值表示法是用 \(n-1\) 次多项式在 \(n\) 个点的取值表示多项式。容易证明 \(n\) 个点的取值可以唯一确定一个不超过 \(n-1\) 次的多项式。\(f(x)=\sum\limits_{i=0}^{n-1}a_ix^i\iff f(x)=\{(x_0,y_0),(x_1,y_1),\cdots,(x_{n-1},y_{n-1})\}\)

两个系数表示法的多项式相乘需要计算 \(\mathcal O(n^2)\) 次,而两个点值表示法的多项式相乘只需要在 \(\mathcal O(n)\) 的时间把对应点值相乘即可,于是只要可以在 \(\mathcal o(n^2)\) 的时间实现系数和点值的互转,就可以加速多项式乘法。

离散傅里叶变换(DFT)的作用是将多项式从系数表示法转化为点值表示法,离散傅里叶逆变换(IDFT)的作用是将多项式从点值表示法转化为系数表示法。

了解了 DFT 和 IDFT 的想法,我们发现点值显然不能随便代入,因为这样计算就是 \(\mathcal O(n^2)\) 的了,于是我们希望取一些特殊点的点值。

单位复根

我们想到 \(\pm 1\) 的幂都很好算,就不需要计算 \(x^i\)\(0\le i < n\))了。同理,我们发现 \(\pm\textrm{i}\) 的幂也很好算,或者说 \(\omega^k=1\) 中的 \(\omega\) 也可以。

我们定义方程 \(x^n=1\) 在复数意义下的解是 \(n\) 次复根,显然这样的解有 \(n\) 个。我们设 \(\omega_n=\textrm{e}^\frac{2\pi\textrm{i}}{n}=\cos\dfrac{2\pi}{n}+\textrm{i}\sin\dfrac{2\pi}{n}\),于是 \(x^n=1\) 的解集为 \(\{\omega_n^k\mid k\in[0,n)\cap\Z\}\)。我们称 \(\omega_n\)\(n\) 次单位复根,它是把单位元 \(n\) 等分的第一个角对应的向量。

单位复根有三个重要的性质:

  • \(\omega_n^0=\omega_n^n=1\)
  • \(\omega_n^k=\omega_{2n}^{2k}\)
  • \(\omega_{2n}^{k+n}=-\omega_{2n}^k\)

离散傅里叶变换(DFT)

DFT 的思想是分治地求 \(x=\omega_n^k\)\(f(x)\) 的值,分治的方法是分奇偶次项处理。

注意 DFT 时多项式的次数必须是 \(2^k-1\),即有 \(2^k\) 项,不足的需要补 \(0\)

例如对于七次八项式:

\[f(x)=a_0+a_1x+a_2x^2+a_3x^3+a_4x^4+a_5x^5+a_6x^6+a_7x^7 \]

进行如下变形:

\[\begin{aligned} f(x)&=(a_0+a_2x^2+a_4x^4+a_6x^6)+(a_1x+a_3x^3+a_5x^5+a_7x^7)\\ &=(a_0+a_2x^2+a_4x^4+a_6x^6)+x(a_1+a_3x^2+a_5x^4+a_7x^6) \end{aligned} \]

设函数 \(g,h\)

\[\begin{aligned} g(x)&=a_0+a_2x+a_4x^2+a_6x^3\\ h(x)&=a_1+a_3x+a_5x^2+a_7x^3 \end{aligned} \]

则:

\[f(x)=g(x^2)+xh(x^2) \]

代入单位复根:

\[\begin{aligned} f(\omega_n^k)&=g(\omega_n^{2k})+\omega_n^kh(\omega_n^{2k})\\ &=g\left(\omega_{\frac{n}{2}}^k\right)+\omega_n^kh\left(\omega_{\frac{n}{2}}^k\right)\\ f\left(\omega_n^{k+\frac{n}{2}}\right)&=g(\omega_n^{2k+n})+\omega_n^{k+\frac{n}{2}}h(\omega_n^{2k+n})\\ &=g(\omega_n^{2k})-\omega_n^kh(\omega_n^{2k})\\ &=g\left(\omega_{\frac{n}{2}}^k\right)-\omega_n^kh\left(\omega_{\frac{n}{2}}^k\right) \end{aligned} \]

因此如果我们知道 \(g\left(\omega_{\frac{n}{2}}^k\right)\)\(h\left(\omega_{\frac{n}{2}}^k\right)\),就可以求出 \(f(\omega_n^k)\)\(f\left(\omega_n^{k+\frac{n}{2}}\right)\),可以对 \(g,h\) 递归进行 DFT。

时间复杂度为 \(T(n)=2T\left(\dfrac{n}{2}\right)+\mathcal O(n)=\mathcal O(n\log n)\)

【参考代码先咕着】

位逆序置换

DFT 的分治是递归形式的,时间和空间的常数都较大,FFT 就是通过一些神奇的方式把递归改为递推,使得常数变为原来的一半。

我们对刚刚的七次八项式进行模拟递归:

  • \(\{a_0,a_1,a_2,a_3,a_4,a_5,a_6,a_7\}\)
  • \(\{a_0,a_2,a_4,a_6\},\{a_1,a_3,a_5,a_7\}\)
  • \(\{a_0,a_4\},\{a_2,a_6\},\{a_1,a_5\},\{a_3,a_7\}\)
  • \(\{a_0\},\{a_4\},\{a_2\},\{a_6\},\{a_1\},\{a_5\},\{a_3\},\{a_7\}\)

那么序列 \(0,4,2,6,1,5,3,7\) 有没有什么特点呢?是有的,把它们写为三位的二进制数,发现就是把二进制高低位翻转了。这一点可以很简单地通过每次考虑最低位的排列方式来证明。这种序列我们称之为位逆序置换,也称蝴蝶变换。

根据位逆序置换的定义,我们显然可以在 \(\mathcal O(n\log n)\) 的时间求出 \(2^k\) 的位逆序置换。

事实上位逆序置换也可以早 \(\mathcal O(n)\) 的时间求出。我们记 \(0\sim 2^k-1\) 的位逆序置换为 \(R(0\cdots 2^k-1)\),则可以发现:

\[R(i)=\left\lfloor\dfrac{R\left(\left\lfloor\frac{i}{2}\right\rfloor\right)}{2}\right\rfloor+(x\bmod 2)\times\dfrac{n}{2} \]

【参考代码先咕着】

快速傅里叶变换(FFT)

上面说了,FFT 就是 DFT 改成递推版本,我们先对原多项式进行位逆序置换,然后利用倍增合并的方式处理即可。

【参考代码先咕着】

快速傅里叶逆变换(IFFT)

离散傅里叶逆变换(IDFT)

IDFT 可以用 DFT 表示,有两种理解方式。

理解方式一:线性代数

DFT 的过程可以视为将系数序列视为一个列向量,再左乘一个矩阵得到点值列向量:

\[\begin{pmatrix} y_0 \\ y_1 \\ y_2 \\ y_3 \\ \vdots \\ y_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n-1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n-1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \omega_n^{3(n-1)} & \cdots & \omega_n^{(n-1)^n} \end{pmatrix} \begin{pmatrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n-1} \end{pmatrix} \]

我们有了点值向量(也就是左侧的列向量),要求系数向量(也就是右侧的列向量),只需要左乘中间矩阵的逆矩阵即可。

中间矩阵十分特殊,它满足:

\[\begin{pmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n-1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n-1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \omega_n^{3(n-1)} & \cdots & \omega_n^{(n-1)^2} \end{pmatrix}^{-1} =\dfrac{1}{n} \begin{pmatrix} 1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^{-1} & \omega_n^{-2} & \omega_n^{-3} & \cdots & \omega_n^{-(n-1)} \\ 1 & \omega_n^{-2} & \omega_n^{-4} & \omega_n^{-6} & \cdots & \omega_n^{-2(n-1)} \\ 1 & \omega_n^{-3} & \omega_n^{-6} & \omega_n^{-9} & \cdots & \omega_n^{-3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{-(n-1)} & \omega_n^{-2(n-1)} & \omega_n^{-3(n-1)} & \cdots & \omega_n^{-(n-1)^2} \end{pmatrix} \]

根据单位复根的性质及欧拉公式:

\[\omega_n^{-1}=\textrm{e}^{-\frac{2\pi\textrm{i}}{n}}=\cos\dfrac{2\pi}{n}+\textrm{i}\sin\left(-\dfrac{2\pi}{n}\right) \]

我们只需要在 DFT 的基础上把单位根 \(\omega_n\) 取成这个数,再把计算结果除以 \(n\) 即可。

理解方式二:单位复根周期性

构造法

原多项式为 \(f(x)=\sum\limits_{i=0}^{n-1}a_ix^i\),我们已知 \(y_i=f(\omega_n^i)\)\(0\le i < n\)),要求 \(a_0\cdots a_{n-1}\)

构造多项式如下:

\[A(x)=\sum\limits_{i=0}^{n-1}y_ix^i \]

接下来有两种推导方式,对应两种实现。

推导方式一

\(b_i=\omega_n^{-i}\),则多项式 \(A\)\(x=b_0\cdots b_{n-1}\) 处的点值为 \(A(b_0)\cdots A(b_{n-1})\)

其中,我们可以把 \(A(b_k)\) 表示为:

\[\begin{aligned} A(b_k)&=\sum\limits_{i=0}^{n-1}f(\omega_n^i)\omega_n^{-ik}\\ &=\sum\limits_{i=0}^{n-1}\omega_n^{-ik}\sum\limits_{j=0}^{n-1}a_j\omega_n^{ij}\\ &=\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{n-1}a_j\omega_n^{i(j-k)}\\ &=\sum\limits_{j=0}^{n-1}a_j\sum\limits_{i=0}^{n-1}(\omega_n^{j-k})^i \end{aligned} \]

我们记 \(S(\omega_n^a)=\sum\limits_{i=0}^{n-1}(\omega_n^a)^i\)

\(a\equiv 0\pmod{n}\) 时,\(S(\omega_n^a)=n\)

\(a\not\equiv 0\pmod{n}\) 时,进行错位相减:

\[\begin{aligned} S(\omega_n^a)&=\sum\limits_{i=0}^{n-1}(\omega_n^a)^i\\ \omega_n^aS(\omega_n^a)&=\sum\limits_{i=1}^n(\omega_n^a)^i\\ S(\omega_n^a)&=\dfrac{(\omega_n^a)^n-(\omega_n^a)^0}{\omega_n^a-1}\\ &=0 \end{aligned} \]

综上,我们知道:

\[S(\omega_n^a)= \begin{cases} n,&a\equiv 0\pmod{n}\\ 0,&a\not\equiv 0\pmod{n} \end{cases} \]

代回原式:

\[\begin{aligned} A(b_k)&=\sum\limits_{j=0}^{n-1}a_jS(\omega_n^{j-k})\\ &=na_k \end{aligned} \]

所以我们取单位根为其倒数跑 DFT,再除以 \(n\) 得到的就是原多项式的系数表示。

推导方式二

我们将 \(\omega_n^k\) 代入 \(A\),与推导方式一类似,最终得到 \(A(\omega_n^k)=\sum\limits_{j=0}^{n-1}a_jS(\omega_n^{j+k})\),所以 \(A(\omega_n^k)=a_{n-k}\cdot n\)

所以我们直接对点值做 DFT 后除以 \(n\),再反转后 \(n-1\) 个元素,得到的也是原多项式的系数表示。

FFT/IFFT 代码实现

代码实现有两种,如下:

【参考代码先咕着】

【参考代码先咕着】

例题

这个我还得多做点,先咕着。

数论变换(NTT)

概述

NTT 是 FFT 在数论基础上的实现,用来解决带模数的多项式乘法。NTT 的运算过程中没有复数和浮点数,常数较小且没有精度误差。

NTT 的详细讲解需要较大量的群论知识,在下文我尝试避开群论知识把 NTT 讲明白。

原根

对于质数 \(p\),若满足 \(p=q\cdot 2^m+1\),那么它的原根 \(g\) 满足 \(g^{q\cdot 2^m}\equiv 1\pmod{p}\)

我们记 \(g_i=g^\frac{p-1}{i}\),发现 \(g_i\)\(\omega_i\) 有相似的性质。对于正整数 \(n=2^m\) 和整数 \(k\),有:(下文 \(\implies\) 表示对应关系而非推出)

  • \(\omega_n^n=1\implies g_n^n\equiv 1\pmod{p}\)
  • \(\omega_n^k=\omega_{2n}^{2k}\implies g_n^k\equiv g_{2n}^{2k}\pmod{p}\)
  • \(\omega_{2n}^{k+n}=-\omega_{2n}^k\implies g_{2n}^{k+n}\equiv -g_{2n}^k\pmod{p}\)
  • \(\omega_{2n}^n=-1\implies g_{2n}^n\equiv -1\pmod{p}\)

NTT 模数

常见的 NTT 模数 \(p\) 及其一个原根 \(g\) 有:

  • \(p=998244353=7\times 17\times 2^{23}+1,g=3\)
  • \(p=1004535809=479\times 2^{21}+1,g=3\)
  • \(p=469762049=7\times 2^{26}+1,g=3\)

数论逆变换(INTT)

类似于 IDFT 的推导过程,不过似乎只能用第二种代码实现。

NTT/INTT 代码实现

【参考代码先咕着】

例题

这个我还得多做点,先咕着。

高维前缀和与快速莫比乌斯变换(FMT)

高维前缀和

我们从二维前缀和开始考虑。

二维前缀和有一种很常用的基于容斥的求法:

// 数组下标从 1 开始
for(int i = 1; i <= k; ++i)
    for(int j = 1; j <= k; ++j)
        a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + a[i][j];

但这种方法不好推广到更高维,原因是容斥的项数是 \(\mathcal O(2^n)\) 的(其中 \(n\) 为维数),总复杂度就是 \(\mathcal O(k^n2^n)\)

二维前缀和还有另一种求法,是一维一维求:

// 数组下标从 1 开始
for(int i = 1; i <= k; ++i)
    for(int j = 1; j <= k; ++j)
        a[i][j] += a[i - 1][j];
for(int i = 1; i <= k; ++i)
    for(int j = 1; j <= k; ++j)
        a[i][j] += a[i][j - 1];

这种方法是可以推广的。

具体地,对于一个长度为 \(k^n\) 的数组(即:\(n\) 维,每个维度长度为 \(k\)),求它的高维前缀和的方法也是一维一维求:

// 注意:为了方便,与前文不同的是,这里数组下标从 0 开始
// let len := pow(k, n)
for(int j = 0, cur = 1; j < n; ++j, cur *= k)
    for(int i = 0; i < len; ++i)
        if((i / cur) % k)
            a[i] += a[i - cur];

复杂度为 \(\mathcal O(k^nn)\),可以看到比基于容斥的方法要小很多。正确性的话,可以想象前缀和的求和关系为一张有向无环的网格图,我们在上面按照拓扑序进行 DP,因此是正确的。

一般情况下,每个维度的长度为 \(2\),高维前缀和也就是子集关系:

// 数组下标从 0 开始
for(int j = 0; j < n; ++j)
    for(int i = 0; i < (1 << n); ++i)
        if((i >> j) & 1)
            a[i] += a[i ^ (1 << j)];

同理,我们还可以写出高维后缀和,也就是超集关系:

// 数组下标从 0 开始
for(int j = 0; j < n; ++j)
    for(int i = 0; i < (1 << n); ++i)
        if(!((i >> j) & 1))
            a[i] += a[i ^ (1 << j)];

Dirichlet 前缀和

狄利克雷前缀和:\(s_k=\sum\limits_{i\mid k}a_i\)

一种简单的做法是枚举 \(i\),枚举 \(i\) 的倍数 \(k\),然后加上去统计答案,这是调和级数的 \(\mathcal O(n\log n)\)

\(k=\prod p_i^{\alpha_i}\),我们可以把每个质因数 \(p_i\) 视为一维,做高维前缀和,复杂度与埃拉托色尼筛一样为 \(\mathcal O(n\log\log n)\)

代码:(里面求高维前缀和的函数名叫 fmt,不确定这算不算但不重要)

// Problem: P5495 Dirichlet 前缀和
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5495
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

//By: OIer rui_er
#include <bits/stdc++.h>
#define rep(x,y,z) for(uint x=(y);x<=(z);x++)
#define per(x,y,z) for(uint x=(y);x>=(z);x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef unsigned int uint;
const uint N = 2e7+5;

uint n, seed, a[N], tab[N], p[N], pcnt, ans;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
uint getnext() {
    seed ^= seed << 13;
    seed ^= seed >> 17;
    seed ^= seed << 5;
    return seed;
}
void sieve(uint lim) {
    rep(i, 2, lim) {
        if(!tab[i]) p[++pcnt] = i;
        for(uint j=1;j<=pcnt&&i*p[j]<=lim;j++) {
            tab[i*p[j]] = 1;
            if(!(i % p[j])) break;
        }
    }
}
void fmt(uint* a, uint n) {
    sieve(n);
    rep(i, 1, pcnt) for(uint j=1;j*p[i]<=n;j++) a[j*p[i]] += a[j];
}

int main() {
    scanf("%u%u", &n, &seed);
    rep(i, 1, n) a[i] = getnext();
    fmt(a, n);
    rep(i, 1, n) ans ^= a[i];
    printf("%u\n", ans);
    return 0;
}

概述

FMT 常用于处理子集相关的卷积问题。因为我们可以对集合进行状态压缩,所以 FMT 可以处理按位与、按位或等的卷积。

也就是对于序列 \(a,b\),求序列 \(c\),满足:

\[c_k=\sum\limits_{i\otimes j=k}a_ib_j \]

其中 \(\otimes\)\(\cap\)(交集、按位与)、\(\cup\)(并集、按位或)等。

并集卷积(或卷积)

考虑 FFT 的过程,先将 \(a,b\) 转化为可以直接对应位置相乘的点值表示,再转化回我们想要的系数表示,其中转化的过程用时为 \(\mathcal O(n\log n)\) 低于暴力的 \(\mathcal O(n^2)\)

那么我们能不能将 \(a,b\) 快速转化为可以直接对应位置相乘的序列 \(\hat{a},\hat{b}\),求出 \(\hat{c}\) 再快速转化回去呢?

\(a\to\hat{a}\) 的过程被称为快速莫比乌斯变换(FMT),\(\hat{a}\to a\) 的过程被称为快速莫比乌斯逆变换(IFMT)或快速莫比乌斯反演(FMI)。

我们令 \(\hat{a}_i=\sum\limits_{j\subseteq i}a_j\),则根据 \(c_k=\sum\limits_{i\cup j=k}a_ib_j\),进行如下推导:

\[\begin{aligned} \hat{c}_k&=\sum\limits_{l\subseteq k}c_l \\ &=\sum\limits_{l\subseteq k}\sum\limits_{i\cup j=l}a_ib_j \\ &=\sum\limits_{i\cup j\subseteq k}a_ib_j \\ &=\left(\sum\limits_{i\subseteq k}a_i\right)\left(\sum\limits_{j\subseteq k}b_j\right) \\ &=\hat{a}_k\hat{b}_k \end{aligned} \]

因此上面的 \(\hat{a}\) 可以直接对应位置相乘,符合我们的要求。问题转化为如何 \(a\to\hat{a}\) 以及 \(\hat{a}\to a\)

先考虑 \(a\to\hat{a}\)

如果我们暴力枚举 \(\hat{a}\) 定义式 \(\hat{a}_i=\sum\limits_{j\subseteq i}a_j\) 中的 \(i\),再枚举 \(i\) 的子集 \(j\),全集大小为 \(\log_2n\),所以枚举全局的子集的子集的复杂度为 \(\mathcal O(3^{\log_2n})=\mathcal O(n^{\log_23})\approx\mathcal O(n^{1.585})\),显然不符合我们的要求。

这时候可能会想到,\(\hat{a}_i=\sum\limits_{j\subseteq i}a_j\) 不就是 \(a_j\) 的高维前缀和嘛!于是我们可以 \(\mathcal O(n\log n)\) 求解。

再考虑 \(\hat{a}\to a\)

我们知道子集反演公式:

\[G(K)=\sum\limits_{L\subseteq K}F(L)\iff F(K)=\sum\limits_{L\subseteq K}{(-1)}^{|K|-|L|}G(L) \]

于是:

\[\hat{a}_i=\sum\limits_{j\subseteq i}a_j\iff a_i=\sum\limits_{j\subseteq i}{(-1)}^{|i|-|j|}\hat{a}_j \]

其中 \(|i|\) 表示 \(i\) 集合的大小,状态压缩为二进制后也就是 \(\operatorname{popcnt}(i)\)

如果你对容斥系数比较敏感的话,会想到把高维前缀和改为“高维前缀差”(瞎起的名字),也就是每次累加贡献改为累加负贡献。

【参考代码先咕着】

交集卷积(与卷积)

类似上面的并集卷积(或卷积),我们构造 \(\hat{a}_i=\sum\limits_{i\subseteq j}a_j\) 即可,也就是把 \(i,j\) 谁是子集谁是超集调换了一下。

代码实现方法和证明方法均同理。

【参考代码先咕着】

\(\gcd\) 卷积和 \(\operatorname{lcm}\) 卷积

也就是 \(c_k=\sum\limits_{\gcd(i,j)=k}a_ib_j\)\(c_k=\sum\limits_{\operatorname{lcm}(i,j)=k}a_ib_j\)

可以分别构造 \(\hat{a}_i=\sum\limits_{i\mid j}a_j\)\(\hat{a}_i=\sum\limits_{j\mid i}a_j\),不再赘述。

【参考代码先咕着】

快速沃尔什变换(FWT)

概述

FWT 的思想与 FFT、FMT 相同,也是构造序列 \(a\) 的变换 \(\hat{a}\) 使得可以对应位置相乘。在算法竞赛中,FWT 常用于处理位运算卷积。

也就是对于序列 \(a,b\),求序列 \(c\),满足:

\[c_k=\sum\limits_{i\otimes j=k}a_ib_j \]

其中 \(\otimes\)\(\cap\)(交集、按位与)、\(\cup\)(并集、按位或)等。

并集卷积(或卷积)

我们令 \(\hat{a}_i=\sum\limits_{j\subseteq i}a_j\),可以用上文 FMT 中的方法证明可以直接对应位置相乘。

先考虑 \(a\to\hat{a}\)

\(a_0,a_1\)\(a\) 的前半段和后半段,也就是 \(a_0\)\(a\) 中下标最高位为 \(0\) 的,\(a_1\) 是下标最高位为 \(1\) 的。

则有:

\[\hat{a}=\operatorname{concat}(\hat{a_0},\hat{a_0}+\hat{a_1}) \]

其中 \(\operatorname{concat}\) 是拼接,\(+\) 是序列对应位置相加,例如 \(\operatorname{concat}([0,1],[2,3])=[0,1,2,3]\)\([0,1]+[2,3]=[2,4]\)

于是我们可以分治来解决,而且可以不用递归。

再考虑 \(\hat{a}\to a\)

由上式可得:

\[a=\operatorname{concat}(a_0,a_1-a_0) \]

同样分治解决,而且两者还可以合在一起写。

【参考代码先咕着】

交集卷积(与卷积)

类似上面的并集卷积(或卷积),相信大家可以自己推出来了!

【参考代码先咕着】

对称差卷积(异或卷积)

对称差的定义:\(A\Delta B=(A\setminus B)\cup(B\setminus A)=(A\cup B)\setminus(A\cap B)\)

这应该是算法竞赛中 FWT 最常考的应用(虽说我刚学还没咋见过)。

定义 \(a\odot b=|a\cap b|\bmod 2\),用状态压缩表示的话就是 \(a\odot b=\operatorname{popcnt}(a\&b)\bmod 2\)

构造 \(\hat{a}_i=\sum\limits_{i\odot j=0}a_j-\sum\limits_{i\odot j=1}a_j\),则有:

\[\begin{aligned} \hat{a}_i\hat{b}_i&=\left(\sum\limits_{i\odot j=0}a_j-\sum\limits_{i\odot j=1}a_j\right)\left(\sum\limits_{i\odot k=0}b_k-\sum\limits_{i\odot k=1}b_k\right) \\ &=\left(\sum\limits_{i\odot j=0}a_j\right)\left(\sum\limits_{i\odot k=0}b_k\right)-\left(\sum\limits_{i\odot j=0}a_j\right)\left(\sum\limits_{i\odot k=1}b_k\right)-\left(\sum\limits_{i\odot j=1}a_j\right)\left(\sum\limits_{i\odot k=0}b_k\right)+\left(\sum\limits_{i\odot j=1}a_j\right)\left(\sum\limits_{i\odot k=1}b_k\right) \\ &=\sum\limits_{i\odot (j\Delta k)=0}a_jb_k-\sum\limits_{i\odot (j\Delta k)=1}a_jb_k\\ &=\hat{c}_i \end{aligned} \]

因此 \(\hat{a}=\operatorname{concat}(\hat{a_0}+\hat{a_1},\hat{a_0}-\hat{a_1})\)\(a=\operatorname{concat}\left(\dfrac{a_0+a_1}{2},\dfrac{a_0-a_1}{2}\right)\)

【参考代码先咕着】

再记录一个 \(k\) 进制 FWT 的 博客

posted @ 2022-06-28 23:10  rui_er  阅读(234)  评论(0编辑  收藏  举报