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

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

前置知识

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

快速傅里叶变换(FFT)

概述

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

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

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

系数表示法是用每一项系数来表示多项式。f(x)=i=0n1aixif(x)={a0,a1,,an1}

点值表示法是用 n1 次多项式在 n 个点的取值表示多项式。容易证明 n 个点的取值可以唯一确定一个不超过 n1 次的多项式。f(x)=i=0n1aixif(x)={(x0,y0),(x1,y1),,(xn1,yn1)}

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

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

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

单位复根

我们想到 ±1 的幂都很好算,就不需要计算 xi0i<n)了。同理,我们发现 ±i 的幂也很好算,或者说 ωk=1 中的 ω 也可以。

我们定义方程 xn=1 在复数意义下的解是 n 次复根,显然这样的解有 n 个。我们设 ωn=e2πin=cos2πn+isin2πn,于是 xn=1 的解集为 {ωnkk[0,n)Z}。我们称 ωnn 次单位复根,它是把单位元 n 等分的第一个角对应的向量。

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

  • ωn0=ωnn=1
  • ωnk=ω2n2k
  • ω2nk+n=ω2nk

离散傅里叶变换(DFT)

DFT 的思想是分治地求 x=ωnkf(x) 的值,分治的方法是分奇偶次项处理。

注意 DFT 时多项式的次数必须是 2k1,即有 2k 项,不足的需要补 0

例如对于七次八项式:

f(x)=a0+a1x+a2x2+a3x3+a4x4+a5x5+a6x6+a7x7

进行如下变形:

f(x)=(a0+a2x2+a4x4+a6x6)+(a1x+a3x3+a5x5+a7x7)=(a0+a2x2+a4x4+a6x6)+x(a1+a3x2+a5x4+a7x6)

设函数 g,h

g(x)=a0+a2x+a4x2+a6x3h(x)=a1+a3x+a5x2+a7x3

则:

f(x)=g(x2)+xh(x2)

代入单位复根:

f(ωnk)=g(ωn2k)+ωnkh(ωn2k)=g(ωn2k)+ωnkh(ωn2k)f(ωnk+n2)=g(ωn2k+n)+ωnk+n2h(ωn2k+n)=g(ωn2k)ωnkh(ωn2k)=g(ωn2k)ωnkh(ωn2k)

因此如果我们知道 g(ωn2k)h(ωn2k),就可以求出 f(ωnk)f(ωnk+n2),可以对 g,h 递归进行 DFT。

时间复杂度为 T(n)=2T(n2)+O(n)=O(nlogn)

【参考代码先咕着】

位逆序置换

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

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

  • {a0,a1,a2,a3,a4,a5,a6,a7}
  • {a0,a2,a4,a6},{a1,a3,a5,a7}
  • {a0,a4},{a2,a6},{a1,a5},{a3,a7}
  • {a0},{a4},{a2},{a6},{a1},{a5},{a3},{a7}

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

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

事实上位逆序置换也可以早 O(n) 的时间求出。我们记 02k1 的位逆序置换为 R(02k1),则可以发现:

R(i)=R(i2)2+(xmod2)×n2

【参考代码先咕着】

快速傅里叶变换(FFT)

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

【参考代码先咕着】

快速傅里叶逆变换(IFFT)

离散傅里叶逆变换(IDFT)

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

理解方式一:线性代数

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

(y0y1y2y3yn1)=(111111ωn1ωn2ωn3ωnn11ωn2ωn4ωn6ωn2(n1)1ωn3ωn6ωn9ωn3(n1)1ωnn1ωn2(n1)ωn3(n1)ωn(n1)n)(a0a1a2a3an1)

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

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

(111111ωn1ωn2ωn3ωnn11ωn2ωn4ωn6ωn2(n1)1ωn3ωn6ωn9ωn3(n1)1ωnn1ωn2(n1)ωn3(n1)ωn(n1)2)1=1n(111111ωn1ωn2ωn3ωn(n1)1ωn2ωn4ωn6ωn2(n1)1ωn3ωn6ωn9ωn3(n1)1ωn(n1)ωn2(n1)ωn3(n1)ωn(n1)2)

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

ωn1=e2πin=cos2πn+isin(2πn)

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

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

构造法

原多项式为 f(x)=i=0n1aixi,我们已知 yi=f(ωni)0i<n),要求 a0an1

构造多项式如下:

A(x)=i=0n1yixi

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

推导方式一

bi=ωni,则多项式 Ax=b0bn1 处的点值为 A(b0)A(bn1)

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

A(bk)=i=0n1f(ωni)ωnik=i=0n1ωnikj=0n1ajωnij=i=0n1j=0n1ajωni(jk)=j=0n1aji=0n1(ωnjk)i

我们记 S(ωna)=i=0n1(ωna)i

a0(modn) 时,S(ωna)=n

a0(modn) 时,进行错位相减:

S(ωna)=i=0n1(ωna)iωnaS(ωna)=i=1n(ωna)iS(ωna)=(ωna)n(ωna)0ωna1=0

综上,我们知道:

S(ωna)={n,a0(modn)0,a0(modn)

代回原式:

A(bk)=j=0n1ajS(ωnjk)=nak

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

推导方式二

我们将 ωnk 代入 A,与推导方式一类似,最终得到 A(ωnk)=j=0n1ajS(ωnj+k),所以 A(ωnk)=ankn

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

FFT/IFFT 代码实现

代码实现有两种,如下:

【参考代码先咕着】

【参考代码先咕着】

例题

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

数论变换(NTT)

概述

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

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

原根

对于质数 p,若满足 p=q2m+1,那么它的原根 g 满足 gq2m1(modp)

我们记 gi=gp1i,发现 giωi 有相似的性质。对于正整数 n=2m 和整数 k,有:(下文 表示对应关系而非推出)

  • ωnn=1gnn1(modp)
  • ωnk=ω2n2kgnkg2n2k(modp)
  • ω2nk+n=ω2nkg2nk+ng2nk(modp)
  • ω2nn=1g2nn1(modp)

NTT 模数

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

  • p=998244353=7×17×223+1,g=3
  • p=1004535809=479×221+1,g=3
  • p=469762049=7×226+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];

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

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

// 数组下标从 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];

这种方法是可以推广的。

具体地,对于一个长度为 kn 的数组(即: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];

复杂度为 O(knn),可以看到比基于容斥的方法要小很多。正确性的话,可以想象前缀和的求和关系为一张有向无环的网格图,我们在上面按照拓扑序进行 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 前缀和

狄利克雷前缀和:sk=ikai

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

k=piαi,我们可以把每个质因数 pi 视为一维,做高维前缀和,复杂度与埃拉托色尼筛一样为 O(nloglogn)

代码:(里面求高维前缀和的函数名叫 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,满足:

ck=ij=kaibj

其中 (交集、按位与)、(并集、按位或)等。

并集卷积(或卷积)

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

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

aa^ 的过程被称为快速莫比乌斯变换(FMT),a^a 的过程被称为快速莫比乌斯逆变换(IFMT)或快速莫比乌斯反演(FMI)。

我们令 a^i=jiaj,则根据 ck=ij=kaibj,进行如下推导:

c^k=lkcl=lkij=laibj=ijkaibj=(ikai)(jkbj)=a^kb^k

因此上面的 a^ 可以直接对应位置相乘,符合我们的要求。问题转化为如何 aa^ 以及 a^a

先考虑 aa^

如果我们暴力枚举 a^ 定义式 a^i=jiaj 中的 i,再枚举 i 的子集 j,全集大小为 log2n,所以枚举全局的子集的子集的复杂度为 O(3log2n)=O(nlog23)O(n1.585),显然不符合我们的要求。

这时候可能会想到,a^i=jiaj 不就是 aj 的高维前缀和嘛!于是我们可以 O(nlogn) 求解。

再考虑 a^a

我们知道子集反演公式:

G(K)=LKF(L)F(K)=LK(1)|K||L|G(L)

于是:

a^i=jiajai=ji(1)|i||j|a^j

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

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

【参考代码先咕着】

交集卷积(与卷积)

类似上面的并集卷积(或卷积),我们构造 a^i=ijaj 即可,也就是把 i,j 谁是子集谁是超集调换了一下。

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

【参考代码先咕着】

gcd 卷积和 lcm 卷积

也就是 ck=gcd(i,j)=kaibjck=lcm(i,j)=kaibj

可以分别构造 a^i=ijaja^i=jiaj,不再赘述。

【参考代码先咕着】

快速沃尔什变换(FWT)

概述

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

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

ck=ij=kaibj

其中 (交集、按位与)、(并集、按位或)等。

并集卷积(或卷积)

我们令 a^i=jiaj,可以用上文 FMT 中的方法证明可以直接对应位置相乘。

先考虑 aa^

a0,a1a 的前半段和后半段,也就是 a0a 中下标最高位为 0 的,a1 是下标最高位为 1 的。

则有:

a^=concat(a0^,a0^+a1^)

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

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

再考虑 a^a

由上式可得:

a=concat(a0,a1a0)

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

【参考代码先咕着】

交集卷积(与卷积)

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

【参考代码先咕着】

对称差卷积(异或卷积)

对称差的定义:AΔB=(AB)(BA)=(AB)(AB)

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

定义 ab=|ab|mod2,用状态压缩表示的话就是 ab=popcnt(a&b)mod2

构造 a^i=ij=0ajij=1aj,则有:

a^ib^i=(ij=0ajij=1aj)(ik=0bkik=1bk)=(ij=0aj)(ik=0bk)(ij=0aj)(ik=1bk)(ij=1aj)(ik=0bk)+(ij=1aj)(ik=1bk)=i(jΔk)=0ajbki(jΔk)=1ajbk=c^i

因此 a^=concat(a0^+a1^,a0^a1^)a=concat(a0+a12,a0a12)

【参考代码先咕着】

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

posted @   rui_er  阅读(278)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示