本文是自己对傅里叶变换的学习记录。
正交
参考[1],我们可以将定义域在上的函数表达为一个空间的向量:
当时,我们可以得到。
假如我们在空间中有一组正交向量基,,则可以表达为:
那么系数。
而三角函数,则是两两正交的一组基底,其中
傅里叶变换
从上面的正交中我们可以看到,假如我们可以找到一组基底部,那么前面的系数就是我们该基底向量在整个组合中的权重(频率)。这也就对应了常说的,傅里叶变换是时域和频域的变换。
参考[1],根据上述找到的三角函数基底,我们可以通过欧拉公式,三角函数性质等等的变换,得到有限定义域,无穷情况下的傅里叶变换公式。
离散情况下
同样也可以将频域转换为时域,即逆傅里叶变换。
傅里叶变换与卷积
给定两个函数,其卷积
等价于这两个函数在频域的乘积
证明参见[2]。
快速傅里叶变换
快速傅里叶变换是加快离散形式的傅里叶变换的方法。
当我们把看成其在是多项式在幂的系数时,求得的恰好是多项式代入的结果。
快速傅里叶变换是利用奇偶性,通过递归的方式求解傅里叶变换值,参考[4]。
例如,对于如下多项式
按照奇偶性,写成如下形式
同时根据欧拉公式,当,得到,同时利用和均为偶函数,可以得到如下关系:
这样我们知道和即可。同时根据和函数的定义,它们也是一个多项式(只不过幂数均减为一半),因此也可以调用快速傅里叶求的,形成递归。
python的代码来源为[3],
def FFT(x):
"""A recursive implementation of the 1D Cooley-Tukey FFT"""
x = np.asarray(x, dtype=float)
N = x.shape[0]
if N % 2 > 0:
raise ValueError("size of x must be a power of 2")
elif N <= 32: # this cutoff should be optimized
return DFT_slow(x)
else:
X_even = FFT(x[::2])
X_odd = FFT(x[1::2])
factor = np.exp(-2j * np.pi * np.arange(N) / N)
return np.concatenate([X_even + factor[:N // 2] * X_odd,
X_even + factor[N // 2:] * X_odd])
堆叠成矩阵形式
def FFT_vectorized(x):
"""A vectorized, non-recursive version of the Cooley-Tukey FFT"""
x = np.asarray(x, dtype=float)
N = x.shape[0]
if np.log2(N) % 1 > 0:
raise ValueError("size of x must be a power of 2")
# N_min here is equivalent to the stopping condition above,
# and should be a power of 2
N_min = min(N, 32)
# Perform an O[N^2] DFT on all length-N_min sub-problems at once
n = np.arange(N_min)
k = n[:, None]
M = np.exp(-2j * np.pi * n * k / N_min)
X = np.dot(M, x.reshape((N_min, -1)))
# build-up each level of the recursive calculation all at once
while X.shape[0] < N:
X_even = X[:, :X.shape[1] // 2]
X_odd = X[:, X.shape[1] // 2:]
factor = np.exp(-1j * np.pi * np.arange(X.shape[0])
/ X.shape[0])[:, None]
X = np.vstack([X_even + factor * X_odd,
X_even - factor * X_odd])
return X.ravel()
应用
见例题替换字符后匹配,
class Solution:
def matchReplacement(self, s: str, sub: str, mappings: List[List[str]]) -> bool:
n, m = len(s), len(sub)
ok = set((x, y) for x, y in mappings)
mp1 = defaultdict(lambda: [0] * n) # 用于记录每种字符出现的索引
mp2 = defaultdict(lambda: [1] * m) # 用于记录哪些索引处的字符不能变成key
for i, c in enumerate(s):
mp1[c][i] = 1
for j, c1 in enumerate(sub):
for c2 in mp1:
if c1 == c2 or (c1, c2) in ok:
mp2[c2][j] = 0
# 卷积求出所有不合法的起始位置 i-j
# 将第二个数组 reverse 之后 i+j 变成 i+(m-j-1)
bad = [False] * (n - m + 1)
for key in mp1:
nums1, nums2 = mp1[key], mp2[key][::-1]
conv = convolve(nums1, nums2)
for i in range(m - 1, n):
bad[i - m + 1] |= conv[i] != 0
return not all(bad)
import numpy as np
def convolve(nums1: Any, nums2: Any) -> np.ndarray:
"""fft求卷积"""
fftLen = 1
while 2 * fftLen < len(nums1) + len(nums2) - 1:
fftLen *= 2
fftLen *= 2
Fa = np.fft.rfft(nums1, fftLen)
Fb = np.fft.rfft(nums2, fftLen)
Fc = Fa * Fb
res = np.fft.irfft(Fc, fftLen)
res = np.rint(res).astype(np.int64)
return res[: len(nums1) + len(nums2) - 1]
参考
- [2] DBinary:卷积与傅里叶变换
- [4] Oi wiki:快速傅里叶变换