知识焦点
先建立概念地图
- FFT
- inverse FFT
- roots of unity
- polynomial multiplication
- unitary matrix
- conjugate transpose
- matrix decomposition
- geometric series
CS170 · 作业 3
CS 170 Fall 2023 第三次作业,包含学习小组信息与逆 FFT(Inverse FFT)两大题;逆 FFT 部分讨论多项式在单位根处的取值、傅里叶矩阵的逆、酉矩阵性质、以及矩阵的递归分块分解。
知识焦点
建议顺序
先完整作答,再按需展开提示;最后才展开官方参考解答并逐步核对证明与计算。
本地来源
题面与解答均由本地一手资料整理:hw03.pdf、hw03-sol.pdf
本地一手资料完整提取 · 作业
本节按本地一手资料完整整理,共 10 道大题。本地官方解答已完整收录并默认折叠;建议先独立完成,再逐题核对推导。
hw03.pdf、hw03-sol.pdf列出你学习小组中所有成员的姓名和 SID(学生编号)。如果你没有合作者,则必须明确写出 "none"。
若没有其他组员,必须显式写出 "none",不可留空。
若没有其他组员,答案为:none。
(若有组员,则列出每位成员的姓名与 SID。)
解答来源:hw03-sol.pdf · 1 · confidence: high
题面来源:hw03.pdf · 1 · confidence: high
考虑两个多项式 \(A(x) = 1 + 5x + 3x^2 + 4x^3\) 与 \(B(x) = 3 + 4x + 2x^3\)。选取一个适当的 \(n\),并分别写出 \(A(x)\)、\(B(x)\) 与 \(C(x) = A(x)B(x)\) 在全部 \(n\) 个单位根处的取值。
你选取的 \(n\) 应足够大,以便在对这 \(n\) 个点上的取值应用逆 FFT 后能够恢复 \(C\) 的系数。你选取的 \(n\) 是多少?(注意:\(n\) 应为 2 的幂。)
提示:\(C\) 的次数是多少?
A(x)=1+5x+3x^2+4x^3,B(x)=3+4x+2x^3,C(x)=A(x)B(x) 在 8 次单位根处的取值
来源:hw03.pdf · 1
先确定 \(C(x)=A(x)B(x)\) 的次数:\(\deg A = 3\),\(\deg B = 3\),所以 \(\deg C = 6\)。要恢复系数至少需要 \(6+1=7\) 个点,而 \(n\) 必须是 2 的幂,故取 \(n=8\)。
\(C\) 的次数为 \(\deg A + \deg B = 3 + 3 = 6\),因此至少需要 \(6+1=7\) 个点才能通过逆 FFT 恢复 \(C\) 的系数。由于 \(n\) 必须是 2 的幂,故取 \(n = 8\)。
官方解答仅给出 \(n=8\) 而未列出 \(A\)、\(B\)、\(C\) 在各 8 次单位根处具体的取值。
解答来源:hw03-sol.pdf · 1 · confidence: high
题面来源:hw03.pdf · 1 · confidence: high
定义
\[M_n(\omega^{-1}) = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega^{-1} & \omega^{-2} & \cdots & \omega^{-(n-1)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{-(n-1)} & \omega^{-2(n-1)} & \cdots & \omega^{-(n-1)(n-1)} \end{bmatrix}\]
回顾 \(\omega^{-1} = 1/\omega = \bar{\omega} = \exp(-2\pi i/n)\)。
证明 \(\frac{1}{n} M_n(\omega^{-1})\) 是 \(M_n(\omega)\) 的逆矩阵,即证明
\[\frac{1}{n} M_n(\omega^{-1})\, M_n(\omega) = I\]
其中 \(I\) 为 \(n\times n\) 单位矩阵。
M_n(omega) 与 M_n(omega^{-1}) 矩阵
来源:hw03.pdf · 1-2
考虑矩阵乘积的第 \((j,k)\) 项,利用等比数列求和公式;当 \(j\neq k\) 时利用 \(\omega^n=1\)。
我们证明 \(M_n(\omega^{-1})M_n(\omega)\) 的第 \((j,k)\) 个元素(行列均从 0 开始计数)在 \(j=k\) 时为 \(n\),在 \(j\neq k\) 时为 \(0\)。
由矩阵乘法定义,第 \((j,k)\) 项为
\[\sum_{l=0}^{n-1} M_n(\omega^{-1})_{jl}\, M_n(\omega)_{lk} = \sum_{l=0}^{n-1} \omega^{-lj}\, \omega^{kl} = \sum_{l=0}^{n-1} \omega^{l(k-j)}.\]
若 \(j=k\),则上式化为
\[\sum_{l=0}^{n-1} \omega^{0} = \sum_{l=0}^{n-1} 1 = n.\]
若 \(j\neq k\),则 \(\omega^{k-j}\neq 1\),由等比数列求和公式
\[\sum_{l=0}^{n-1} \omega^{l(k-j)} = \frac{1 - \omega^{n(k-j)}}{1 - \omega^{k-j}}.\]
由于 \(\omega\) 是 \(n\) 次单位根,对任意整数 \(m\) 都有 \(\omega^{nm}=1\),故上式化为
\[\frac{1-1}{1-\omega^{k-j}} = 0.\]
另一种看法:因式分解 \(X^n - 1 = (X-1)(X^{n-1}+X^{n-2}+\dots+X+1)\)。\(\omega^{k-j}\) 是 \(X^n-1\) 的根;若 \(j\neq k\) 则 \(\omega^{k-j}\neq 1\),故不是 \(X-1\) 的根,必为 \(X^{n-1}+\dots+1\) 的根,即
\[\omega^{(n-1)(k-j)} + \omega^{(n-2)(k-j)} + \dots + \omega^{k-j} + 1 = 0,\]
恰好就是要证的等式。
因此 \(\frac{1}{n}M_n(\omega^{-1})\,M_n(\omega) = I\),即 \(\frac{1}{n}M_n(\omega^{-1})\) 是 \(M_n(\omega)\) 的逆矩阵。
解答来源:hw03-sol.pdf · 2-3 · confidence: high
题面来源:hw03.pdf · 1-2 · confidence: high
设 \(A\) 为复元素方阵。\(A\) 的共轭转置 \(A^\dagger\) 由对 \(A^T\) 的每个元素取复共轭得到。若 \(A\) 的逆等于其共轭转置,即 \(A^{-1}=A^\dagger\),则称 \(A\) 为酉矩阵(unitary)。
证明 \(\frac{1}{\sqrt{n}}\,M_n(\omega)\) 是酉矩阵。
M_n(omega) 矩阵
来源:hw03.pdf · 2
观察 \((M_n(\omega))^\dagger = M_n(\omega^{-1})\),再利用 (b) 的结论。
观察得 \((M_n(\omega))^\dagger = M_n(\omega^{-1})\)(对每个元素取共轭即把 \(\omega\) 换成 \(\omega^{-1}=\bar{\omega}\))。于是
\[\left(\frac{1}{\sqrt{n}}M_n(\omega)\right)\!\left(\frac{1}{\sqrt{n}}M_n(\omega)\right)^\dagger = \frac{1}{n}\,M_n(\omega)\,M_n(\omega^{-1}) = I,\]
其中最后一步用到 (b) 已证的 \(\frac{1}{n}M_n(\omega^{-1})M_n(\omega)=I\)(将 \(\omega\) 与 \(\omega^{-1}\) 角色对调即得)。故 \(\frac{1}{\sqrt{n}}M_n(\omega)\) 是酉矩阵。
解答来源:hw03-sol.pdf · 3 · confidence: high
题面来源:hw03.pdf · 2 · confidence: high
设已知一个次数至多为 \(n-1\) 的多项式 \(C(x)\) 在 \(C(1), C(\omega), \dots, C(\omega^{n-1})\) 处的取值。解释如何利用 \(M_n(\omega^{-1})\) 求出 \(C(x)\) 的系数。
M_n(omega) 乘以系数向量等于取值向量
来源:hw03.pdf · 2
利用 \(M_n(\omega)\) 把系数向量映到取值向量,再用其逆。
设 \(c_0,\dots,c_{n-1}\) 为 \(C(x)\) 的系数。由课堂结论
\[M_n(\omega)\begin{bmatrix}c_0\\ c_1\\ \vdots \\ c_{n-1}\end{bmatrix} =\begin{bmatrix}C(1)\\ C(\omega)\\ \vdots \\ C(\omega^{n-1})\end{bmatrix}.\]
于是
\[\begin{bmatrix}c_0\\ c_1\\ \vdots \\ c_{n-1}\end{bmatrix} = M_n(\omega)^{-1}\begin{bmatrix}C(1)\\ C(\omega)\\ \vdots \\ C(\omega^{n-1})\end{bmatrix}.\]
由 (b) 知 \(M_n(\omega)^{-1} = \frac{1}{n}M_n(\omega^{-1})\)。因此,为求 \(C(x)\) 的系数,只需将 \(\frac{1}{n}M_n(\omega^{-1})\) 乘以取值向量 \([C(1)\; C(\omega)\; \dots\; C(\omega^{n-1})]^T\) 即可。
解答来源:hw03-sol.pdf · 3 · confidence: high
题面来源:hw03.pdf · 2 · confidence: high
证明 \(M_n(\omega^{-1})\) 可以像 \(M_n(\omega)\) 一样被拆成四个 \(n/2 \times n/2\) 子矩阵。具体地,假设把 \(M_n\) 的列重新排列,使偶数列(下标从 0 计)在左、奇数列在右。证明重排后 \(M_n(\omega^{-1})\) 具有如下形式:
\[\begin{bmatrix} M_{n/2}(\omega^{-2}) & \omega^{-j}M_{n/2}(\omega^{-2}) \\ M_{n/2}(\omega^{-2}) & -\omega^{-j}M_{n/2}(\omega^{-2}) \end{bmatrix}\]
记号 \(\omega^{-j}M_{n/2}(\omega^{-2})\) 表示把 \(M_{n/2}(\omega^{-2})\) 的第 \(j\) 行(下标从 0 计)乘上 \(\omega^{-j}\) 所得的矩阵。可假设 \(n\) 是 2 的幂。
重排后的分块形式:左上 M_{n/2}(omega^{-2}),右上 omega^{-j} M_{n/2}(omega^{-2}),左下 M_{n/2}(omega^{-2}),右下 -omega^{-j} M_{n/2}(omega^{-2})
来源:hw03.pdf · 2
注意 \(\omega^{-1}\) 仍是本原 \(n\) 次单位根;将 \(\omega\mapsto \omega^{-1}\) 代入 \(M_n(\omega)\) 的分解即可。也可逐块直接验证各角的 \((j,k)\) 元素。
首先指出:若 \(\omega\) 是本原 \(n\) 次单位根,则 \(\omega^{-1}\) 也是本原 \(n\) 次单位根(因为 \(\omega^{-1}=e^{-i\theta}\) 当 \(\omega=e^{i\theta}\))。我们在证明 \(M_n(\omega)\) 可拆成 4 块时只用到了「\(\omega\) 是本原 \(n\) 次单位根」这一性质,因此把 \(\omega\mapsto\omega^{-1}\) 代入仍成立,即得所要的重排后形式。
下面逐块直接验证(列按偶、奇重排)。
左上块:第 \((j,k)\) 个元素为 \(\omega^{-j\cdot(2k)} = \omega^{-2jk}\)。因 \(n\) 为偶数,\(\omega^2\) 是本原 \((n/2)\) 次单位根,故该元素正是 \(M_{n/2}(\omega^{-2})\) 的第 \((j,k)\) 项。
左下块:第 \((j,k)\) 个元素为 \(\omega^{-(n/2+j)(2k)} = \omega^{-nk}\,\omega^{-2jk}\)。由 \(\omega^n=1\) 得 \(\omega^{-nk}=1\),故亦为 \(\omega^{-2jk}\),与左上块相同。
右上块:第 \((j,k)\) 个元素为 \(\omega^{-j(2k+1)} = \omega^{-j}\,\omega^{-2jk}\),即 \(\omega^{-j}\) 乘以左上块第 \((j,k)\) 项,故右上块 \(=\,\omega^{-j}M_{n/2}(\omega^{-2})\)。
右下块:第 \((j,k)\) 个元素为 \(\omega^{-(j+n/2)(2k+1)} = \omega^{-2jk}\,\omega^{-nk}\,\omega^{-j}\,\omega^{-n/2}\)。由 \(\omega^{-nk}=1\) 及 \((\omega^{-n/2})^2=1\) 但 \(\omega^{-n/2}\neq 1\) 可得 \(\omega^{-n/2}=-1\),于是该元素 \(= -\omega^{-j}\,\omega^{-2jk}\),即 \(-\omega^{-j}\) 乘以左上块对应项。
故重排后
\[M_n(\omega^{-1}) \rightarrow \begin{bmatrix} M_{n/2}(\omega^{-2}) & \omega^{-j}M_{n/2}(\omega^{-2}) \\ M_{n/2}(\omega^{-2}) & -\omega^{-j}M_{n/2}(\omega^{-2}) \end{bmatrix}.\]
附注:一般地,复数 \(x\) 与整数 \(n,m\) 下 \((x^n)^{1/m}\neq (x^{1/m})^n\),故 \(x^{n/m}\) 并不良好定义。但此处 \(n/2\) 是整数,而整数次幂在复数上远比非整数次幂表现良好,故上述推导并无问题。
解答来源:hw03-sol.pdf · 3-4 · confidence: high
题面来源:hw03.pdf · 2 · confidence: high
比特串 \(b\) 的一个 \(k\)-逆序 是指:某个 \(1\) 出现在某个 \(0\) 之前且恰好相隔 \(k\) 个下标,即存在某个 \(i\) 使得 \(b_i = 1\) 且 \(b_{i+k} = 0\)。
例如,串 \(010010\) 有两个 \(1\)-逆序(从第 2 和第 5 位开始)、一个 \(2\)-逆序(从第 2 位开始)、一个 \(4\)-逆序(从第 2 位开始)。
要求: 设计一个算法,给定长度为 \(n\) 的比特串 \(b\),对每个 \(k = 1, 2, \dots, n-1\) 统计所有的 \(k\)-逆序。算法必须快于 \(\Theta(n^2)\) 时间。可以假设实数算术运算耗时 \(\Theta(1)\)。
给出一个「三部分解法」(算法描述、正确性证明、运行时间分析)。
提示:利用课上讲的基于 FFT 的跨相关(cross-correlation)算法,构造适当的两个向量做卷积即可同时得到所有 \(k\) 的答案。
构造两个向量:
向量 \(\vec{p} = [0, \dots, 0, b_0, \dots, b_{n-1}]\) 是「填充」向量,长度为 \(2n\),由 \(n\) 个 0 后接 \(b\) 的每一位构成;
向量 \(\vec{f} = [(1-b_0), (1-b_1), \dots, (1-b_{n-1})]\) 是「翻转」向量,由 \(b\) 的每一位取反(0 变 1、1 变 0)构成。
对这两个向量运行跨相关算法。\(\vec{f}\) 与 \(\vec{p}[j : n+j]\) 的内积恰好是 \((n-j)\)-逆序的个数,对 \(j = 1, 2, \dots, n-1\) 报告即可。(注意第一个和最后一个内积会被忽略,因为不存在 0-逆序或 \(n\)-逆序。)
正确性证明从下标 \(i\) 开始存在一个 \(k\)-逆序 当且仅当 \(b_i = 1\) 且 \(b_{i+k} = 0\);这等价于 \(b_i(1-b_{i+k}) = 1\)。
考虑内积 \(\vec{f} \cdot \vec{p}[j : n+j] = \sum_{i=0}^{n-1} f_i p_{i+j}\)。由于当 \(i+j < n\) 时 \(p_{i+j} = 0\),因此 \[\sum_{i=0}^{n-1} f_i p_{i+j} = \sum_{i=n-j}^{n-1} f_i p_{i+j} = \sum_{i=0}^{j-1} f_{i+n-j} p_{n+i}.\]
由 \(\vec{f}\) 与 \(\vec{p}\) 的定义,上式等于 \(\sum_{i=0}^{j-1} (1-b_{i+n-j}) b_i\)。根据第一句话,求和中的每一项在从 \(i\) 开始存在 \((n-j)\)-逆序时为 1,否则为 0,因此该和恰好是 \((n-j)\)-逆序的个数。
既然课上已知跨相关算法能正确计算这些内积,故整个算法正确。
运行时间构造向量 \(\vec{p}\) 和 \(\vec{f}\) 共耗时 \(\Theta(n)\);由课上结论,跨相关算法耗时 \(\Theta(n \log n)\);整理输出结果耗时 \(\Theta(n)\)(或视实现为常数时间)。没有其他额外工作,故整体算法耗时 \(\Theta(n \log n)\)。
伪代码CountKInversions(b, n):
p ← array of length 2n, filled with 0
for i ← 0 to n-1:
p[n + i] ← b[i]
f ← array of length n
for i ← 0 to n-1:
f[i] ← 1 - b[i]
// 跨相关:等价于把 f 反转后与 p 做卷积
corr ← CrossCorrelate(f, p) // Θ(n log n)
for j ← 1 to n-1:
report corr[j] as number of (n-j)-inversions
复杂度:\(\Theta(n \log n)\) 时间,\(\Theta(n)\) 额外空间。
解答来源:hw03-sol.pdf · 1-2 · confidence: high
题面来源:hw03.pdf · 1 · confidence: high
在生物学中,我们常希望在物种的 DNA 中定位某个已知基因。由于基因突变,执行相同功能的基因可能相似但不完全相同,且基因在 DNA 序列中常多次出现。因此一个更实际的问题是:在 DNA 序列中找出所有与已知基因相似的基因。
将该问题建模:设 \(g\) 是长度为 \(n\) 的字符串(已知基因),\(s\) 是长度为 \(m\) 的字符串(完整 DNA 序列),\(m \ge n\)。问题是:找出 \(s\) 中所有与 \(g\) 精确匹配的长度为 \(n\) 的子串的起始位置(0 开始计数)。例如 \(g = \text{ACT}\),\(s = \text{ACTACTA}\),应输出 \(0\) 和 \(3\)。
给出一个 \(O(nm)\) 时间复杂度的算法。
提示:若卡住可参考习题课 Q4。
假设 \(g\) 与 \(s\) 以比特串形式给出,即每个字符为 0 或 1。给出一个 \(O(m \log m)\) 时间复杂度的算法。
提示:若卡住可参考习题课 Q4。
更一般地,假设 \(g\) 与 \(s\) 合起来至多使用 \(\alpha\) 个不同字符 \(c_1, c_2, \dots, c_\alpha\)(例如 (b) 中 \(\alpha = 2\),对 DNA 序列 \(\alpha = 4\))。给出 \(O(m \log m)\) 时间复杂度的算法,找出 \(s\) 中所有与 \(g\) 匹配的长度为 \(n\) 的子串。
提示:将字符串表示为复向量。
(a) 直接枚举每个起始位置并逐字符比对即可。 (b) 把 0 替换为 \(-1\),将 \(g\) 反转后构造多项式 \(p_1\),\(s\) 构造多项式 \(p_2\),用 FFT 求乘积 \(p_3 = p_1 \cdot p_2\),检查系数 \(c_{n-1+j} = n\)。 (c) 用 \(\alpha\) 次单位根 \(\omega_\alpha = e^{2\pi i/\alpha}\) 把字符编码为复平面上的单位根,使得匹配位置贡献 1、不匹配位置贡献非 1 的单位根,再按 (b) 的方式做卷积并检查系数。
对每个起始位置 \(i \in \{0, 1, \dots, m-n\}\),检查子串 \(s[i : i+n-1]\) 是否与 \(g\) 匹配。每次检查耗时 \(O(n)\),共有 \(O(m)\) 个起始位置,故总时间 \(O(mn)\)。
NaiveMatch(g, n, s, m):
matches ← empty list
for i ← 0 to m - n:
ok ← true
for j ← 0 to n - 1:
if g[j] ≠ s[i + j]:
ok ← false; break
if ok: append i to matches
return matches
时间 \(O(mn)\),额外空间 \(O(1)\)(不计输出)。
令 \(g'\) 为把 \(g\) 中的 \(0\) 替换为 \(-1\) 得到的串;\(s'\) 为把 \(s\) 中的 \(0\) 替换为 \(-1\) 得到的串。
构造两个多项式:
\(p_1(x) = a_0 + a_1 x + \cdots + a_{n-1} x^{n-1}\),其中 \(a_d = g'(n-d-1)\),即 \(g'\) 的系数顺序反转;
\(p_2(x) = b_0 + b_1 x + \cdots + b_{m-1} x^{m-1}\),其中 \(b_d = s'(d)\)。
令 \(p_3(x) = p_1(x) \cdot p_2(x) = c_0 + c_1 x + \cdots\)。则对任意 \(j \in \{0, 1, \dots, m-n\}\),\(x^{n-1+j}\) 的系数为 \[c_{n-1+j} = \sum_{i=0}^{n-1} a_{n-1-i} b_{j+i} = \sum_{i=0}^{n-1} g'(i) s'(j+i),\] 恰好是 \(s'\) 中从下标 \(j\) 开始的子串与 \(g'\) 的内积。若两串在至多 \(k\) 个位置不同,则该内积至少为 \(n - 2k\)。
因此只需计算 \(p_3\),并输出所有满足 \(c_{n-1+j} = n\) 的 \(j\)(此时子串与 \(g\) 完全匹配)。
在 \(\{-1, 1\}\) 编码下,\(g'(i) s'(j+i) = 1\) 当且仅当 \(g(i) = s(j+i)\),否则为 \(-1\)。因此内积 \(\sum_{i=0}^{n-1} g'(i) s'(j+i)\) 等于(匹配位置数)减(不匹配位置数)。该和为 \(n\) 当且仅当所有 \(n\) 个位置都匹配,即子串与 \(g\) 完全相同。由 FFT 正确计算卷积系数,算法输出的恰好是所有匹配位置。
BitstringMatch(g, n, s, m):
g' ← array of length n, g'[i] ← (g[i] == 1 ? 1 : -1)
s' ← array of length m, s'[i] ← (s[i] == 1 ? 1 : -1)
// p1: 系数为 g' 的反转
p1 ← array of length N (N 为不小于 m 的最小 2 的幂)
for d ← 0 to n-1: p1[d] ← g'[n-1-d]
// p2: 系数为 s'
p2 ← array of length N
for d ← 0 to m-1: p2[d] ← s'[d]
q1 ← FFT(p1); q2 ← FFT(p2) // O(m log m)
for i ← 0 to N-1: q1[i] ← q1[i] * q2[i] // 逐点乘, O(m)
p3 ← InverseFFT(q1) // O(m log m)
matches ← empty list
for j ← 0 to m - n:
if round(p3[n-1+j].real) == n:
append j to matches
return matches
一次 FFT、一次逆 FFT 各 \(O(m \log m)\),逐点乘与扫描系数各 \(O(m)\),故总时间 \(O(m \log m)\),空间 \(O(m)\)。
记 \(\omega_\alpha = e^{2\pi i / \alpha}\) 为 \(\alpha\) 次单位根。把字符编码为单位根的幂:
\(g'(i) = \omega_\alpha^j\) 当 \(g(i) = c_j\);
\(s'(i) = \omega_\alpha^{-j}\) 当 \(s(i) = c_j\)。
于是 \(g'(i) s'(j+i) = 1\) 当且仅当 \(g(i) = s(j+i)\),否则为一个不等于 1 的 \(\alpha\) 次单位根。
考虑内积 \(\sum_{i=0}^{n-1} g'(i) s'(j+i)\):若子串与 \(g\) 完全匹配,则内积为 \(n\);若存在任意不匹配位置,则和为 \(n\) 个 \(\alpha\) 次单位根之和,其中至少有一个不是 1。由于每个非 1 单位根的实部严格小于 1,\(n\) 个单位根之和为 \(n\) 当且仅当它们全是 1。因此内积为 \(n\) 当且仅当子串与 \(g\) 完全匹配。
按 (b) 的方式构造反转多项式 \(p_1\)(系数为 \(g'\) 反转)与 \(p_2\)(系数为 \(s'\)),用 FFT 计算 \(p_3 = p_1 \cdot p_2\),输出所有满足 \(c_{n-1+j} = n\) 的 \(j\)。
GeneralMatch(g, n, s, m, alpha, charToIdx):
omega ← exp(2πi / alpha)
g' ← array of length n
for i ← 0 to n-1: g'[i] ← omega^(charToIdx[g[i]])
s' ← array of length m
for i ← 0 to m-1: s'[i] ← omega^(-charToIdx[s[i]])
N ← 不小于 m 的最小 2 的幂
p1 ← array of length N
for d ← 0 to n-1: p1[d] ← g'[n-1-d] // 反转
p2 ← array of length N
for d ← 0 to m-1: p2[d] ← s'[d]
q1 ← FFT(p1); q2 ← FFT(p2)
for i ← 0 to N-1: q1[i] ← q1[i] * q2[i]
p3 ← InverseFFT(q1)
matches ← empty list
for j ← 0 to m - n:
if |p3[n-1+j].real - n| < ε and |p3[n-1+j].imag| < ε:
append j to matches
return matches
FFT 与逆 FFT 各 \(O(m \log m)\),其余线性,故总时间 \(O(m \log m)\),空间 \(O(m)\)。
解答来源:hw03-sol.pdf · 3-6 · confidence: high
题面来源:hw03.pdf · 1-3 · confidence: high
给定一个包含 \(n\) 个元素的数组 \(A[0..n-1]\),其中每个元素 \(A[i]\) 是满足 \(0 \leq A[i] \leq n\) 的整数(元素不一定互不相同)。请判断是否存在下标 \(i, j, k\)(不一定互不相同),使得
\[A[i] + A[j] + A[k] = n\]
设计一个时间复杂度为 \(O(n \log n)\) 的算法。注意:不需要返回具体的下标,只需回答「是」或「否」即可。
提示 1:元素可以用指数来编码!
提示 2:如果卡住了,参考讨论课 Q3。
主要思路:指数运算将乘法转化为加法。例如 \(x^3 \cdot x^2 = x^{2+3} = x^5\)。这提示我们将列表表示为多项式,其中列表的元素作为指数(这是一个非常常用的技巧,应当记住!)。例如,数组 \([1, 3]\) 可表示为 \(x^1 + x^3\)。将其与 \([2, 4]\) 对应的多项式 \(x^2 + x^4\) 相乘,得到 \(x^3 + 2x^5 + x^7\)。注意 \(3, 5, 7\) 分别对应 \([1,3]\) 中一个元素与 \([2,4]\) 中一个元素的所有可能和。
更正式地,定义
\[p(x) = x^{A[0]} + x^{A[1]} + \cdots + x^{A[n-1]}.\]
注意 \(p(x)^3\) 包含一系列项,每项形如 \(x^{A[i]} \cdot x^{A[j]} \cdot x^{A[k]} = x^{A[i]+A[j]+A[k]}\)。因此,我们只需检查 \(p(x)^3\) 是否包含 \(x^n\) 项。
正确性证明:观察
\[q(x) = p(x)^3 = \left(\sum_{0 \leq i < n} x^{A[i]}\right)^3 = \sum_{0 \leq i,j,k < n} x^{A[i]+A[j]+A[k]}.\]
因此,\(q\) 中 \(x^n\) 的系数非零当且仅当存在下标 \(i, j, k\) 使得 \(A[i] + A[j] + A[k] = n\)。故算法正确。(实际上,\(x^n\) 的系数告诉我们满足条件的三元组 \((i,j,k)\) 有多少个。)
复杂度分析:构造 \(p(x)\) 显然耗时 \(O(n)\)。\(p(x)\) 是次数至多 \(n = O(n)\) 的多项式。因此用 FFT 做两次乘法计算 \(q(x)\) 耗时 \(O(n \log n)\)。最后查找 \(x^n\) 的系数耗时 \(O(1)\),故总复杂度为 \(O(n \log n)\)。
备注:本题保证数组元素在 \(0 \ldots n\) 范围内。如果没有这个保证,上述 FFT 方法会变得低效(因为多项式次数与 \(A\) 中最大元素同阶)。此时容易得到 \(O(n^2)\) 算法,但尚未发现更快的算法。特别是,是否存在比 \(O(n^2)\) 更快的算法是一个著名的开放问题(称为 3SUM 问题)。该问题被广泛研究,因为它与计算几何中的许多问题密切相关。
解答来源:hw03-sol.pdf · 7 · confidence: high
题面来源:hw03.pdf · 3 · confidence: high
本周作业要求你实现 FFT,然后将 FFT 作为黑箱实现一些快速算法。有两种方式访问 notebook 并完成任务:
https://github.com/Berkeley-CS170/cs170-fa23-coding git clone(或如果已 clone 则 git pull),进入 hw03 文件夹。参考 README.md 获取本地配置说明。hw03 文件夹,在 Berkeley DataHub 上完成。注意:
提交说明:请下载完成的 fft.ipynb 文件或提交 .zip 文件,上传到 Gradescope 的 "Homework 3 Coding Portion" 作业。
OH/HWP 说明:指定的编程助教将在办公时间和作业派对提供概念和调试帮助。
学术诚信指南:我们意识到网上可能很容易找到这些算法的代码,但强烈建议不要直接复制。请参考 notebook 中提到的资源自己编写代码。不过我们也承认,某些算法的代码实现方式可能有限,你的解法可能与网上其他解法相似。
Notebook 内容概览 (fft.ipynb):
\(n\) 次单位根定义为满足 \(z^n = 1\) 的复数。等价定义为 \(e^{2\pi i k / n}\),其中 \(k = 0, 1, \cdots, n-1\)。
编写函数 roots_of_unities(n),给定 \(n\) 输出所有 \(n\) 次单位根(\(n\) 不必为偶数)。返回 \([\omega^0, \omega^1, \omega^2, \ldots, \omega^{n-1}]\),其中 \(\omega\) 是本原根。
编写函数 dft_naive(coeffs),输入多项式系数列表(coeffs[i] 为 \(x^i\) 的系数),返回该多项式在下一个 2 的幂次个单位根处的取值列表 \([P(\omega_0), P(\omega_1), \ldots]\)。
实现 \(O(n \log n)\) 的 FFT 算法。对于多项式
\[P(x) = a_0 + a_1 x + a_2 x^2 + \cdots + a_{n-1} x^{n-1}\]
定义偶次项多项式 \(E(x) := a_0 + a_2 x + \cdots + a_{n-2} x^{n/2-1}\) 与奇次项多项式 \(O(x) := a_1 + a_3 x + \cdots + a_{n-1} x^{n/2-1}\),满足
\[P(x) = E(x^2) + x O(x^2).\]
递归地对 \(E\) 和 \(O\) 在 \(n/2\) 次单位根上运行 FFT,再合并得到 \(P\) 在 \(n\) 次单位根处的取值。
函数签名:fft(coeffs, roots),其中 `roots = [1, \omega, \omega^2, \ldots, \omega^{n-1}]),\(n\) 为 2 的幂。
实现 ifft(vals, roots)。提示:可用 FFT 实现,但将单位根取倒数,最后除以 \(n\)(参考 DPV 教材第 75 页)。
使用 FFT 作为黑箱实现多项式乘法。步骤:对两个多项式系数分别做 FFT(补零到足够大的 2 的幂),逐点相乘,再做逆 FFT 转回系数形式。
给定过去 \(n\) 天的温度 \(y_0, y_1, \ldots, y_{n-1}\)(\(y_i \in \mathbb{R}\))和固定系数 \(c_0, \ldots, c_{n-2}\),第 \(t \geq 1\) 天的预测为
\[p_t = \sum_{k=0}^{t-1} c_k y_{t-1-k}\]
计算均方误差 (MSE):
\[\frac{1}{n-1} \sum_{t=1}^{n-1} (p_t - y_t)^2\]
在 \(O(n \log n)\) 时间内完成。提示:将预测计算转化为多项式乘法,注意对系数进行移位(prepend 0)。
Q1.1:可使用 np.exp(2 * np.pi * 1j / n) 计算本原根,再取各次幂;或求多项式 \(x^n - 1 = 0\) 的根并按辐角排序。
Q1.2:可用 np.poly1d 构造多项式再求值。
Q1.3:递归基例 \(n=1\) 直接返回系数;注意 np.array 不能用 append/+ 拼接,需用 np.append/np.concatenate。
Q1.4:iroots = np.array(roots) ** -1,返回 fft(vals, iroots) / n。
Q2:将系数前插 0 得到 coeffs_shift,再与 labels 做多项式乘法;取结果的第 1 到 \(n-1\) 项作为 \(p_t\),最后线性计算 MSE。
def roots_of_unities(n):
principal_root = np.exp(2 * np.pi * 1j / n)
roots = principal_root ** np.arange(n)
return roots
替代解法(求 \(x^n - 1 = 0\) 的根并按辐角排序):
def roots_of_unities(n):
poly = np.zeros(n + 1)
poly[0] = 1
poly[-1] = -1
roots = np.roots(poly)
sorted_roots = sorted(roots, key=lambda x: np.angle(x) % (2 * np.pi))
return sorted_roots
Q1.2) 朴素 DFT
def dft_naive(coeffs):
n = hyperceil(len(coeffs))
p = np.poly1d(coeffs[::-1])
return [p(omega) for omega in roots_of_unities(n)]
Q1.3) FFT
def fft(coeffs, roots):
n = len(coeffs)
assert n == hyperceil(n)
if n == 1:
return coeffs[:]
e_coeff = coeffs[0::2]
o_coeff = coeffs[1::2]
e = fft(e_coeff, roots[::2])
o = fft(o_coeff, roots[::2])
res = np.zeros(n, dtype=np.complex_)
for i in range(n // 2):
res[i] = e[i] + roots[i] * o[i]
res[i + n // 2] = e[i] + roots[i + n // 2] * o[i]
return res
Q1.4) 逆 FFT
def ifft(vals, roots):
n = len(vals)
assert n == hyperceil(n)
iroots = np.array(roots) ** -1
return np.array(fft(vals, iroots) / n)
Q1.5) 多项式乘法 (可选)
def pad(coeffs, to):
return np.pad(coeffs, (0, to - len(coeffs)), "constant", constant_values=0)
def poly_multiply(coeffs1, coeffs2):
n = hyperceil(len(coeffs1) + len(coeffs2) - 1)
v1 = fft(pad(coeffs1, n), roots_of_unities(n))
v2 = fft(pad(coeffs2, n), roots_of_unities(n))
v = v1 * v2
return ifft(v, roots_of_unities(n))
注意:FFT 使用复数运算会导致浮点误差,整数多项式乘法结果需取整并去除尾部零:
def round_complex_to_int(lst):
return [round(x.real) for x in lst]
def zero_pop(lst):
return np.trim_zeros(lst, "b")
Q2) 预测评估
def evaluate_predictions(coeffs, labels):
coeffs_shift = np.insert(coeffs, 0, 0)
n = hyperceil(len(coeffs_shift) + len(labels) - 1)
C = fft(pad(coeffs_shift, n), roots_of_unities(n))
Y = fft(pad(labels, n), roots_of_unities(n))
P = C * Y
p = ifft(P, roots_of_unities(n))[1 : len(labels)]
return np.mean((p - labels[1:]) ** 2)
算法思路:预测 \(p_t = \sum_{k=0}^{t-1} c_k y_{t-1-k}\) 是系数序列 \(c\)(前插 0 移位后)与标签序列 \(y\) 的卷积。通过 FFT 将两序列转为点值形式,逐点相乘后再逆 FFT 得到所有 \(p_t\),最后线性扫描计算均方误差。总复杂度 \(O(n \log n)\)。
解答来源:hw03-sol.pdf · 8-12 · confidence: high
题面来源:hw03.pdf · 3-4 · confidence: high
hw03.pdfhw03-sol.pdf