快速傅里叶变换:应用
FFT 的所有应用都围绕着它的本质功能:计算两个多项式的乘积。
大整数乘法(高精度乘法) #
假设有两个整数 $a=537,b=721$。$a,b$ 的每一位可以视为多项式 $A(x),B(x)$ 的系数,就像这样
$$A(x)=5x^2+3x+7, B(x)=7x^2+2x+1$$
计算 $C(x)=A(x)\cdot B(x)$:
$$C(x)=35x^4+31x^3+60x^2+17x+7$$
提取 $C(x)$ 的系数:
$$\{35,31,60,17,7\}$$
从右往左,逢 $10$ 进 $1$:
$$\{3,8,7,1,7,7\}$$
这样就能得到 $537\times721$ 的结果是 $387177$。
中间的多项式乘法由 $\text{FFT}$ 进行。如果 $a$ 和 $b$ 是 $n$ 位整数,则总时间复杂度是 $O(n\log{n})$。
整数乘法和多项式乘法具有高度的相似性。
一个整数可以分解为它的位值的和。例如,整数 $123$ 可以分解为 $1\times10^2+2\times10+3$。
设 $a=\overline{a_2a_1a_0}$,$b=\overline{b_2b_1b_0}$,则
$$\begin{aligned} &a \times b \\ = \ & (a_2 \cdot 10^2 + a_1 \cdot 10 + a_0) \cdot (b_2 \cdot 10^2 + b_1 \cdot 10 + b_0) \\ = \ & a_2b_2 \cdot 10^4 + (a_2b_1 + a_1b_2) \cdot 10^3 + (a_2b_0 + a_1b_1 + a_0b_2) \cdot 10^2 + (a_1b_0 + a_0b_1) \cdot 10 + a_0b_0 \end{aligned}$$
设 $A(x)=a_2x^2+a_1x+a_0$,$B(x)=b_2x^2+b_1x+b_0$,则
$$\begin{aligned} &A(x) \times B(x)\\ = \ & (a_2x^2 + a_1x + a_0) \cdot (b_2x^2 + b_1x + b_0) \\ = \ & a_2b_2\cdot x^4 + (a_2b_1 + a_1b_2)\cdot x^3 + (a_2b_0 + a_1b_1 + a_0b_2)\cdot x^2 + (a_1b_0 + a_0b_1)\cdot x + a_0b_0 \end{aligned}$$
可以看出整数乘法和多项式乘法基本是同构的。整数乘法在最后一步需要额外处理进位。
模板 #
// put FFT algorithm template here
vector<Comp> string_to_vector(const string& s) {
vector<Comp> result;
for (auto rit = s.rbegin(); rit != s.rend(); ++rit) {
result.push_back(*rit - '0');
}
return result;
}
string vector_to_string(const vector<Comp>& v) {
string result;
for (const Comp& c : v) {
result += to_string(int(c.real())) + " ";
}
return result;
}
string upgrade(vector<Comp>& a) {
string result;
int carry = 0;
for (auto i : a) {
int num = int(real(i) + 0.5) + carry;
carry = num / 10;
result += (num % 10) + '0';
}
while (!result.empty() && result.back() == '0') {
result.pop_back();
}
reverse(result.begin(), result.end());
return result.empty() ? "0" : result;
}
string multiply_big_integers(const string& s1, const string& s2) {
vector<Comp> a = string_to_vector(s1);
vector<Comp> b = string_to_vector(s2);
vector<Comp> c = multiply(a, b);
return vector_to_string(c);
}
int main() {
string num1, num2;
cin >> num1 >> num2;
string product = multiply_big_integers(num1, num2);
cout << product << endl;
return 0;
}
字符串匹配 #
朴素字符串匹配 #
设有两个字符串 $\text{text}$,$\text{pattern}$ 的长度分别为 $n,m(n\ge m)$,并只包含小写字母 $a\sim z$。问 $\text{pattern}$ 在 $\text{text}$ 中出现了几次,以及所有的出现位置。
该问题可使用 $\text{FFT}$ 在 $O(n\log{n})$ 的复杂度下解决。
tip
用 $\text{FFT}$ 做字符串匹配是一个差想法。$\text{KMP}$ 可以在 $O(n+m)$ 的线性复杂度下解决字符串匹配问题,并且支持通配符。
约定 $\text{str}\Big|_a^b$ 表示从 $\text{str}[a]$ 到 $\text{str}[b]$ 的子串。
首先将 $\text{text}$ 和 $\text{pattern}$ 的所有字母都映射成数字($a\rightarrow 1,b\rightarrow 2,\cdots$),得到两个整型数组 $A$ 和 $B$。
$$\begin{aligned} \text{text}&\rightarrow A:\{A_0,A_1,\cdots,A_{n-1}\}\\ \text{pattern}&\rightarrow B:\{B_0,B_1,\cdots,B_{m-1}\} \end{aligned}$$
容易发现
$$\left(A_i-B_j\right)^2=\begin{cases} 0,&A_i=B_j\\ \\ >0,&A_i\not=B_j \end{cases}$$
进而构造
$$\begin{aligned} P_k&=(A_k-B_0)^2+(A_{k+1}-B_1)^2+\cdots+(A_{k+m-1}-B_{m-1})^2\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\\ &=\begin{cases} 0,&\text{text}\Big|_{k}^{k+m-1}=\text{pattern}\\ \\ >0,&\text{text}\Big|_{k}^{k+m-1}\not=\text{pattern} \end{cases} \end{aligned}$$
因此可以利用 $P_k$ 判断 $\text{pattern}$ 是否出现在 $\text{text}$ 的第 $k$ 位。
展开 $P_k$:
$$\begin{aligned} P_k&=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}^2+B_{i}^2-2A_{k+i}B_{i}\right)\\ &=\sum_{i=0}^{m-1}A_{k+i}^2+\sum_{i=0}^{m-1}B_{i}^2-2\sum_{i=0}^{m-1}A_{k+i}B_{i} \end{aligned}$$
将数组 $B$ 反转得到 $\widetilde{B}$,有 $B_i=\widetilde{B}_{m-i-1}$。代入得
$$P_k=\sum_{i=0}^{m-1}A_{k+i}^2+\sum_{i=0}^{m-1}B_{i}^2-2\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}$$
分析该式的三项:
- $\sum_{i=0}^{m-1}A_{k+i}^2$ 可以通过前缀和技巧实现 $O(n)$ 预处理,$O(1)$ 查询;
- $\sum_{i=0}^{m-1}B_{i}^2$ 是常数,可以直接 $O(n)$ 预处理;
- $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}$ 是卷积式,可以通过 $\text{FFT}$ 实现 $O(n\log{n})$ 预处理,$O(1)$ 查询。
对于第 3 项,更具体地说:
构造多项式
$$A(x)=A_0+A_1x+A_2x^2+\cdots+A_{n-1}x^{n-1}\\ \widetilde{B}(x)=\widetilde{B}_0+\widetilde{B}_1x+\widetilde{B}_2x^2+\cdots+\widetilde{B}_{m-1}x^{m-1}$$
利用 $\text{FFT}$ 可以 $O(n\log{n})$ 得出多项式 $C(x)=A(x)\cdot \widetilde{B}(x)$ 的系数向量。易知 $C(x)$ 中 $x^{k+m-1}$ 前的系数为 $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}$,这正是我们想要的第 3 项。
依次计算 $P_k(k=0,1,2,\cdots,n-m)$,当得到 $0$ 时说明 $\text{pattern}$ 出现在 $\text{text}$ 的第 $k$ 位。
模板 #
// put FFT algorithm template here
vector<int> stringMatchingFFT(const string& text, const string& pattern) {
int n = text.size(), m = pattern.size();
vector<Comp> A(n), B(m);
long long sum_AA[n], sum_BB = 0;
for (int i = 0; i < n; i ++) {
int value = text[i] - 'a' + 1;
A[i] = value;
sum_AA[i] = value * value;
if (i) {
sum_AA[i] += sum_AA[i - 1];
}
}
for (int i = 0; i < m; i ++) {
int value = pattern[i] - 'a' + 1;
B[m - i - 1] = value;
sum_BB += value * value;
}
vector<Comp> C = multiply(A, B);
vector<int> matches;
for (int i = 0; i <= n - m; i ++) {
long long
part1 = sum_AA[i + m - 1] - (i ? sum_AA[i - 1] : 0),
part2 = sum_BB,
part3 = C[i + m - 1].real();
if (part1 + part2 - 2 * part3 == 0)
matches.push_back(i);
}
return matches;
}
int main() {
string text, pattern;
cin >> text >> pattern;
vector<int> matches = stringMatchingFFT(text, pattern);
cout << matches.size() << "\n";
for (int i : matches) cout << i << " ";
cout << "\n";
return 0;
}
带通配符的字符串匹配 #
设有两个字符串 $\text{text}$,$\text{pattern}$ 的长度分别为 $n,m(n\ge m)$,并只包含小写字母 $a\sim z$ 和通配符 $*$(可与任意字符匹配)。问 $\text{pattern}$ 在 $\text{text}$ 中的匹配次数和所有匹配位置。
令通配符 $*$ 映射到 $0$,则
$$\left(A_i-B_j\right)^2\cdot A_i\cdot B_j=\begin{cases} 0,&A_i=B_j\\ \\ >0,&A_i\not=B_j \end{cases}$$
$$\begin{aligned} P_k&=\sum_{i=0}^{m-1}\left(A_{k+i}-B_i\right)^2\cdot A_{k+i}\cdot B_i\\ &=\sum_{i=0}^{m-1}\left(A_{k+i}^2+B_{i}^2-2A_{k+i}B_{i}\right)\cdot A_{k+i}\cdot B_i\\ &=\sum_{i=0}^{m-1}A_{k+i}^3B_i+\sum_{i=0}^{m-1}A_{k+i}B_{i}^3-2\sum_{i=0}^{m-1}A_{k+i}^2B_{i}^2\\ &=\sum_{i=0}^{m-1}A_{k+i}^3\widetilde{B}_{m-i-1}+\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}^3-2\sum_{i=0}^{m-1}A_{k+i}^2\widetilde{B}_{m-i-1}^2 \end{aligned}$$
构造多项式
$$A_k(x)=A_0^k+A_1^kx+A_2^kx^2+\cdots+A_{n-1}^kx^{n-1}\\ \widetilde{B}_k(x)=\widetilde{B}_0^k+\widetilde{B}_1^kx+\widetilde{B}_2^kx^2+\cdots+\widetilde{B}_{m-1}^kx^{m-1}$$
- $\sum_{i=0}^{m-1}A_{k+i}^3\widetilde{B}_{m-i-1}$ 是 $A_3(x)\cdot \widetilde{B}_1(x)$ 中 $x^{k+m-1}$ 前的系数;
- $\sum_{i=0}^{m-1}A_{k+i}\widetilde{B}_{m-i-1}^3$ 是 $A_1(x)\cdot \widetilde{B}_3(x)$ 中 $x^{k+m-1}$ 前的系数;
- $\sum_{i=0}^{m-1}A_{k+i}^2\widetilde{B}_{m-i-1}^2$ 是 $A_2(x)\cdot \widetilde{B}_2(x)$ 中 $x^{k+m-1}$ 前的系数。
进行三次 $\text{FFT}$ 即可。
模板 #
// put FFT algorithm template here
vector<int> stringMatchingFFT(const string& text, string pattern) {
int n = text.size(), m = pattern.size();
vector<Comp> A1, A2, A3, B1, B2, B3;
for (auto i : text) {
int value = i == '*' ? 0 : i - 'a' + 1;
A1.push_back(value);
A2.push_back(value * value);
A3.push_back(value * value * value);
}
reverse(pattern.begin(), pattern.end());
for (auto i : pattern) {
int value = i == '*' ? 0 : i - 'a' + 1;
B1.push_back(value);
B2.push_back(value * value);
B3.push_back(value * value * value);
}
vector<Comp> part1 = multiply(A3, B1);
vector<Comp> part2 = multiply(A1, B3);
vector<Comp> part3 = multiply(A2, B2);
vector<int> matches;
for (int i = 0; i <= n - m; i ++) {
if (part1[i + m - 1].real() + part2[i + m - 1].real() - part3[i + m - 1].real() * 2 == 0)
matches.push_back(i);
}
return matches;
}
int main() {
string text, pattern;
cin >> text >> pattern;
vector<int> matches = stringMatchingFFT(text, pattern);
cout << matches.size() << "\n";
for (int i : matches) cout << i << " ";
cout << "\n";
return 0;
}