$0, ~ 1, ~ 1, ~ 2, ~ 3, ~ 5, ~ 8, ~ 13, \ldots$
$$ \begin{align*} \quad F(n) = \begin{cases} n, & n \le 1 \\ F(n-1) + F(n-2), & n > 1 \end{cases} \end{align*} $$
分治法实现
def fibo_rec(n): if n <= 1: return n else: # 递归求解两个子问题 return fibo_rec(n - 1) + fibo_rec(n - 2)
效率如何?
设$T(n)$为求$F(n)$需调用 fibo_rec() 的次数
$$ \begin{align*} \quad \underbrace{T(n) + 1}_{G(n)} & = \underbrace{T(n-1) + 1}_{G(n-1)} + \underbrace{T(n-2) + 1}_{G(n-2)}, ~ \ldots, ~ G(0) = G(1) = 2 \\[10pt] & \Longrightarrow G(n) = 2 F(n+1) \\[5pt] & \Longrightarrow T(n) = 2 F(n+1) - 1 \end{align*} $$
$F(n) = \frac{1}{\sqrt{5}} ( (\frac{1 + \sqrt{5}}{2})^n - (\frac{1 - \sqrt{5}}{2})^n )$,$T(n)$亦为指数增长
问题 | F(6) | F(5) | F(4) | F(3) | F(2) | F(1) | F(0) |
---|---|---|---|---|---|---|---|
个数 | 1 | 1 | 2 | 3 | 5 | 8 | 5 |
问题 F(n-k) 的个数为 F(k+1)
保持分治递归的形式,每个子问题第一次求解后将结果存下来,每碰到一个子问题,先检查是否已求解过,若否则求解保存
def fibo_dp_memoized(n): F = [-float("inf")] * (n + 1) # 备忘录初始化为负无穷 return fibo_dp_memoized_aux(n, F) def fibo_dp_memoized_aux(n, F): if n <= 1: return n else: if F[n] >= 0: return F[n] else: F[n] = fibo_dp_memoized_aux(n - 1, F) + fibo_dp_memoized_aux(n - 2, F) return F[n]
备忘录英文为Memoization,注意i前面没有r,由英国人工智能先驱唐纳德·米奇于1967年提出,但其实米奇本人只提出了memo function的概念,并没有用这个词,而更早的是香农在他1950年制造的机械鼠Theusus中就使用该词了
既然 F[ ] 的填写顺序为 F[2]、F[3]、F[4]、……、F[n],为何不直接用循环顺序填写?
def fibo_dp_iter(n): F = [0] * (n + 1) F[1] = 1 for i in range(2, n + 1): F[i] = F[i - 1] + F[i - 2] return F[n]
只有一重循环,时间复杂度为$O(n)$
有时也未必需要记录所有子问题的结果,节省存储空间
def fibo_dp_iter2(n): prev, curr = 0, 1 for i in range(n - 1): next = prev + curr prev, curr = curr, next return curr
动态规划 (dynamic programming, DP) 由美国数学家理查德·贝尔曼于 50 年代中期正式提出
programming 意为规划、计划、调度,不是编程的意思
1940 年左右
贝尔曼提出动态规划用于优化美国空军的训练和后勤保障
当时的国防部长是通用电气前首席执行官威尔逊,他非常讨厌数学研究,因此贝尔曼在起名上想方设法隐藏动态规划的数学味
We had a very interesting gentleman in Washington named Wilson. He was secretary of Defense, and he actually had a pathological fear and hatred of the word "research". I'm not using the term lightly; I'm using it precisely. His face would suffuse, he would turn red, and he would get violent if people used the term "research" in his presence. You can imagine how he felt, then, about the term "mathematical" ...... I felt I had to do something to shield Wilson and the Air Force from the fact that I was really doing mathematics inside the RAND Corporation. What title, what name, could I choose?
--- Bellman's autobiography, 1984
事实上贝尔曼首次用动态规划这个词是在 1952 年,早于威尔逊去五角大楼任职数月
根据递推关系有
$$ \begin{align*} \quad & \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} F(n-1) \\ F(n) \end{bmatrix} = \begin{bmatrix} F(n) \\ F(n-1) + F(n) \end{bmatrix} = \begin{bmatrix} F(n) \\ F(n+1) \end{bmatrix} \\[4pt] & \qquad \Longrightarrow \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}^n \begin{bmatrix} F(0) \\ F(1) \end{bmatrix} = \begin{bmatrix} F(n) \\ F(n+1) \end{bmatrix} \end{align*} $$
利用矩阵乘法的结合律
$$ \begin{align*} \quad & \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}^2 = \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}, \quad \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}^4 = \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}^2 \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}^2, \quad \ldots \end{align*} $$
时间复杂度为$O(\lg n)$?
根据数学归纳法易证
$$ \begin{align*} \quad & F(n) = F(m) F(n-m-1) + F(m+1) F(n-m), \quad \forall n,m \in \Nbb \\[5pt] & \qquad \Longrightarrow \begin{cases} F(2n-1) = F(n-1)^2 + F(n)^2 \\ F(2n) = F(n) (2 \cdot F(n-1) + F(n)) \end{cases} \end{align*} $$
def fibo_rec_faster(n): if n == 1: return 0, 1 m = int(n / 2) prev_, curr_ = fibo_rec_faster(m) prev = prev_**2 + curr_**2 curr = curr_ * (2 * prev_ + curr_) next = prev + curr if n & 1: return curr, next else: return prev, curr
$F(n) = \frac{1}{\sqrt{5}} ( (\frac{1 + \sqrt{5}}{2})^n - (\frac{1 - \sqrt{5}}{2})^n )$,对于充分大的$n$
无论二进制还是十进制,打印一个斐波那契数就要$\Theta(n)$
动态规划未必一定比分治递归快,当分治递归产生的子问题个数更少且没有重复子问题时,动态规划就没有优势了
动态规划是一种聪明的、不求解重复子问题的递归方式
通过保存部分子问题的解,以空间换时间,动态规划是一种时空权衡 (time-memory trade-off)
动态规划常用来求解最优化问题
第一阶段:递归 (recursive) 表示
第二阶段:递推 (recurrent) 求解
长度$i$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
价格$p[]$ | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
假设切割钢条的工序本身没有成本
如何将$n$的最优分解用$m ~ (< n)$的最优分解表示出来?
设最优分解为$n = n_1 + \cdots + n_k$
任取$\Ical \subseteq \{1, \ldots, k\}$,记$\overline{\Ical} = \{1, \ldots, k\} \setminus \Ical$,设$m = \sum_{i \in \Ical} n_i$,则
反设$\sum_{j \in \Jcal} n'_j = m$是$m$的更优分解,则$\sum_{j \in \Jcal} n'_j + \sum_{i \in \overline{\Ical}} n_i$是$n$的更优分解
最优解的每个子部分都是子问题的最优解,称为最优子结构性
双子问题:
分解为$m$、$n-m$两部分未必是最优的,需遍历所有分解取最大
$$ \begin{align*} \quad r_n = \begin{cases} \max \{ r_1 + r_{n-1}, ~ r_2 + r_{n-2}, ~ \ldots, ~ r_{\lfloor n/2 \rfloor} + r_{\lceil n/2 \rceil}, ~ p[n] \}, & n \le \text{len}(p) \\ \max \{ r_1 + r_{n-1}, ~ r_2 + r_{n-2}, ~ \ldots, ~ r_{\lfloor n/2 \rfloor} + r_{\lceil n/2 \rceil} \}, & n > \text{len}(p) \end{cases} \end{align*} $$
从$(1,n-1)$到$(\lfloor n/2 \rfloor, \lceil n/2 \rceil)$即可遍历所有分解,在$n$不超过$p[]$的长度时还需考虑$p[n]$,它对应的是$r_0 + r_n$,但不能以这样的形式写,否则会出现无限递归
单子问题:将最优分解中的某个数视为一部分,其余为另一部分
分解为$i$、$n-i$两部分未必是最优的,需遍历所有分解取最大
$$ \begin{align*} \quad r_n = \begin{cases} \max \{ p[1] + r_{n-1}, ~ \ldots, ~ p[n-1] + r_1, ~ p[n] \}, & n \le \text{len}(p) \\ \max \{ p[1] + r_{n-1}, ~ \ldots, ~ p[\text{len}(p)] + r_{n - \text{len}(p)} \}, & n > \text{len}(p) \end{cases} \end{align*} $$
单子问题产生的子问题更少,效率更优
def cut_rod_rec2(n, p): # 分治 双子问题 if n <= 1: return p[n] else: if n < len(p): v = p[n] else: v = -float("inf") for i in range(1, math.ceil(n / 2)): # i = 1, 2, ... v = max(v, cut_rod_rec2(i, p) + cut_rod_rec2(n - i, p)) return v
def cut_rod_rec(n, p): # 分治 单子问题 if n <= 1: return p[n] else: v = -float("inf") for i in range(1, min(n + 1, len(p))): # i = 1, 2, ... v = max(v, p[i] + cut_rod_rec(n - i, p)) return v
令$T(n)$表示求 cut_rod_rec(n,p) 的递归调用次数,则
$$ \begin{align*} \quad T(n) & = \begin{cases} 1, & n = 1 \\ 1 + \sum_{i=1}^{n-1} T(i), & 1 < n \le \text{len}(p) \\ 1 + \sum_{i=n-\text{len}(p)}^{n-1} T(i), & n > \text{len}(p) \end{cases} \end{align*} $$
$T(n)$的增长速度快于斐波那契数列,至少是指数增长速度
带备忘录的自顶向下法 (top-down with memoization)
def cut_rod_dp_memoized(n, p): r = [-float("inf")] * (n + 1) # 备忘录初始化为负无穷 return cut_rod_dp_memoized_aux(n, r, p) def cut_rod_dp_memoized_aux(n, r, p): if r[n] >= 0: # 查备忘录 若之前已计算过就直接用 return r[n] if n <= 1: r[n] = p[n] else: for i in range(1, min(n + 1, len(p))): # i = 1, 2, ... r[n] = max(r[n], p[i] + cut_rod_dp_memoized_aux(n - i, r, p)) return r[n]
每个子问题求解需执行第 12 ~ 13 行的一重 for 循环
规模为$1,2,\ldots,n$的子问题各求解一次,时间复杂度$\Theta(n^2)$
自底向上法 (bottom-up method)
def cut_rod_dp_bottom_up(n, p): r = [0] * (n + 1) for j in range(1, n + 1): # 依次求解 r[1], r[2], ... v = -float("inf") for i in range(1, min(j + 1, len(p))): # 求解 r[j] 时遍历 i = 1, 2, ... v = max(v, p[i] + r[j - i]) # 此时 r[j-1], r[j-2], ... 均已求好 r[j] = v return r[n]
二重 for 循环时间复杂度$\Theta(n^2)$,一维表格空间复杂度$\Theta(n)$
两者有相同的渐进时间复杂度$\Theta(n^2)$
自顶向下
自底向上
def cut_rod_dp_bottom_up_print_sol(n, p): r = [0] * (n + 1) s = [0] * (n + 1) # s[i]是长度为i的钢条的第一刀最优切割位置 for j in range(1, n + 1): v = -float("inf") for i in range(1, min(j + 1, len(p))): if v < p[i] + r[j - i]: v = p[i] + r[j - i] s[j] = i # 更新最优切割位置 r[j] = v print("cut colution:", end=' ') while n > 0: # 打印最优切割方案 print(s[n], end=' ') n = n - s[n]
长度$i$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
价格$p[]$ | 0 | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 | - | - | - | - |
收益$r[]$ | 0 | 1 | 5 | 8 | 10 | 13 | 17 | 18 | 22 | 25 | 30 | 31 | 35 | 38 | 40 |
$s[]$ | 0 | 1 | 2 | 3 | 2 | 2 | 6 | 1 | 2 | 3 | 10 | 1 | 2 | 3 | 2 |
动态规划子问题之间的依赖关系,可用子问题图描述
自底向上处理子问题的顺序:
算法运行时间等于所有子问题求解时间之和
子问题的求解时间与对应顶点的出度成正比
动态规划的运行时间一般与顶点和边的数量至少呈线性关系
输入:$n$个正整数的集合$W = \{ w_1, w_2, \ldots, w_n \}$和正整数$M$
输出:$W$中的和数等于$M$的所有子集
例 1:
例 2:
子问题:$\{ w_i, w_2, \ldots, w_n \}$的子集和为$m$
令$\text{ss}[i, m]$表示子问题是否有解,$\text{ss}[1, M]$即为所求目标
综上,$\text{ss}[i, m] = \text{ss}[i+1, m] \vee \text{ss}[i+1, m-w[i]]$
$i$从 1 开始每增大 1,产生两个新的子问题,子问题总数为$2^n$
子问题:$\{ w_i, w_2, \ldots, w_n \}$的子集和为$m$
令$\text{ss}[i, m]$表示子问题是否有解,$\text{ss}[1, M]$即为所求目标
$\text{ss}[i, m] = \text{ss}[i+1, m] \vee \text{ss}[i+1, m-w[i]]$
边界:
优化:
子问题:$\{ w_i, w_2, \ldots, w_n \}$的子集和为$m$
令$\text{ss}[i, m]$表示子问题是否有解,$\text{ss}[1, M]$即为所求目标
$$ \begin{align*} \quad \text{ss}[i, m] = \begin{cases} \text{True}, & m = 0 \\ \text{False}, & m < 0 ~ \text{or} ~ i > n \\ \text{ss}[i+1, m], & m < w_i \\ \text{ss}[i+1, m] \vee \text{ss}[i+1, m-w_i], & \ow \end{cases} \end{align*} $$
令$\text{ss}[i, m]$表示子问题是否有解
$$ \begin{align*} \quad \text{ss}[i, m] = \begin{cases} \text{True}, & m = 0 \\ \text{False}, & m < 0 ~ \text{or} ~ i > n \\ \text{ss}[i+1, m], & m < w_i \\ \text{ss}[i+1, m] \vee \text{ss}[i+1, m-w_i], & \ow \end{cases} \end{align*} $$
def subset_sum_dp(w, M): n = len(w) ss = np.zeros((n, M + 1), dtype="bool") ss[:, 0] = True # 第0列全为真 if w[-1] <= M: ss[-1, w[-1]] = True # 填最后一行 for i in range(n - 2, 0, -1): # 从下往上填 for m in range(1, M + 1): # 每行从左往右填 if m < w[i]: # 如果目标和小于w[i] ss[i, m] = ss[i + 1, m] else: ss[i, m] = ss[i + 1, m] or ss[i + 1, m - w[i]] return ss
def print_sol(ss, w, i, m, M, list): if w[i] == m: # 如果当前目标和等于w[i] 则找到一个解 for j in list: print("%d + " % j, end="") print("%d = %d" % (w[i], M)) else: if ss[i + 1, m - w[i]]: # 如果w[i]是加数之一 list.append(w[i]) # 入栈 print_sol(ss, w, i + 1, m - w[i], M, list) list.pop() # 出栈 if ss[i + 1, m]: print_sol(ss, w, i + 1, m, M, list) w, M = [0, 5, 10, 12, 13, 15, 18], 30 ss = subset_sum_dp(w, M) print(ss[1, M]) print_sol(ss, w, 1, M, M, []) ------------------------------------------ True 5 + 10 + 15 = 30 5 + 12 + 13 = 30 12 + 18 = 30
def subset_sum_dp(w, M): n = len(w) ss = np.zeros((n, M + 1), dtype="bool") ss[:, 0] = True # 第0列全为真 if w[-1] <= M: ss[-1, w[-1]] = True # 填最后一行 for i in range(n - 2, 0, -1): # 从下往上填 for m in range(1, M + 1): # 每行从左往右填 if m < w[i]: # 如果目标和小于w[i] ss[i, m] = ss[i + 1, m] else: ss[i, m] = ss[i + 1, m] or ss[i + 1, m - w[i]] return ss
二重 for 循环,复杂度$O(nM)$
不要无脑动态规划,它并不是总能有提升的
设$\Av \in \Rbb^{p \times q}$、$\Bv \in \Rbb^{q \times r}$,则$\Cv = \Av \Bv \in \Rbb^{p \times r}$
计算$c_{ij} = \sum_{1 \le k \le q} a_{ik} b_{kj}$需做$q$次标量乘法
计算$\Cv$需$pqr$次标量乘法
设$\Av_1 \in \Rbb^{10 \times 100}$、$\Av_2 \in \Rbb^{100 \times 5}$、$\Av_3 \in \Rbb^{5 \times 50}$
矩阵连乘时,怎么两两相乘才能使总的代价最小呢?
输入:$n$个矩阵,$\Av_1 \in \Rbb^{p_0 \times p_1}, \Av_2 \in \Rbb^{p_1 \times p_2}, \ldots, \Av_n \in \Rbb^{p_{n-1} \times p_n}$
输出:计算代价最小的连乘顺序对应的加括号方案
令$P(n)$表示$n$个矩阵连乘时总的括号化方案的数量
$$ \begin{align*} \quad P(n) = \begin{cases} 1, & n = 1 \\ \sum_{k=1}^{n-1} P(k) P(n-k), & n > 1 \end{cases} \end{align*} $$
可以证明$P(n) = \binom{2n}{n} / (n+1) = \Omega(4^n / n^{3/2}) $,穷举法不可行!
$P(n)$称为卡特兰数 (Catalan number)
子问题:$\Av_i \cdots \Av_j$的最优加括号方案
用上划线表示加括号方案$\overline{\Av_i \cdots \Av_j}$,$\star$表示最优
假设$\Av_i \cdots \Av_j$的最优加括号方案在$\Av_k$和$\Av_{k+1}$之间分开:
$$ \begin{align*} \quad \overline{\Av_i \cdots \Av_j}^\star = (\overline{\Av_i \cdots \Av_k}) (\overline{\Av_{k+1} \cdots \Av_j}) \end{align*} $$
则必然有
问题:如何确定最优的分开位置$k$呢?
$\Av_i \in \Rbb^{p_{i-1} \times p_i}$、$\Av_k \in \Rbb^{p_{k-1} \times p_k}$、$\Av_{k+1} \in \Rbb^{p_k \times p_{k+1}}$、$\Av_j \in \Rbb^{p_{j-1} \times p_j}$
$\Av_i \cdots \Av_k \in \Rbb^{p_{i-1} \times p_k}$、$\Av_{k+1} \cdots \Av_j \in \Rbb^{p_k \times p_j}$
令$m[i,j]$表示计算$\Av_i \cdots \Av_j$所需的最少标量乘法次数
$$ \begin{align*} \quad m[i,j] = \begin{cases} 0, & n = 1 \\ \min_{i \le k < j} \{ m[i,k] + m[k+1,j] + p_{i-1} p_k p_j \}, & n > 1 \end{cases} \end{align*} $$
采用自底向上法,子问题$\Av_i \cdots \Av_j$的矩阵个数为$l = 2 \rightarrow n$
$m[i,j] = \begin{cases} 0, & n = 1 \\ \min_{i \le k < j} \{ m[i,k] + m[k+1,j] + p_{i-1} p_k p_j \}, & n > 1 \end{cases}$
$s[i,j]$表示$\Av_i \cdots \Av_j$的最优分开位置,用来重构最优解
def matrix_chain(p, n): m, s = np.zeros((n + 1, n + 1)), np.zeros((n + 1, n + 1)) for l in range(2, n + 1): # 子问题长度 l = 2 -> n for i in range(1, n - l + 2): # 从第i个矩阵开始 j = i + l - 1 # 到第j个矩阵结束 m[i, j] = float("inf") # 初始化为无穷大 for k in range(i, j): # 遍历最优分开位置 cost = m[i, k] + m[k + 1, j] + p[i - 1] * p[k] * p[j] if cost < m[i, j]: m[i, j] = cost s[i, j] = k return m.astype(int), s.astype(int)
三重 for 循环时间复杂度$\Theta(n^3)$,二维表格空间复杂度$\Theta(n^2)$
矩阵 | $\Av_1$ | $\Av_2$ | $\Av_3$ | $\Av_4$ | $\Av_5$ | $\Av_6$ |
---|---|---|---|---|---|---|
尺寸 | $30 \times 35$ | $35 \times 15$ | $15 \times 5$ | $5 \times 10$ | $10 \times 20$ | $20 \times 25$ |
采用自底向上法,子问题$\Av_i \cdots \Av_j$的矩阵个数为$l = 1 \rightarrow 6$
$$ \begin{align*} \quad l=1: m[1,1] & = m[2,2] = \cdots = m[6,6] = 0, ~ s[i,i] = null \\[5pt] l=2: m[1,2] & = 30 \times 35 \times 15 = 15750, ~ s[1,2] = 1 \\ m[2,3] & = 35 \times 15 \times 5 = 2625, ~ s[2,3] = 2 \\ m[3,4] & = 15 \times 5 \times 10 = 750, ~ s[3,4] = 3 \\ m[4,5] & = 5 \times 10 \times 20 = 1000, ~ s[4,5] = 4 \\ m[5,6] & = 10 \times 20 \times 25 = 5000, ~ s[5,6] = 5 \end{align*} $$
$p_0$ | $p_1$ | $p_2$ | $p_3$ | $p_4$ | $p_5$ | $p_6$ |
---|---|---|---|---|---|---|
$30$ | $35$ | $15$ | $5$ | $10$ | $20$ | $25$ |
递推关系$m[i,j] = \min_{i \le k < j} \{ m[i,k] + m[k+1,j] + p_{i-1} p_k p_j \}$
$$ \begin{align*} \quad m[1,1] & = m[2,2] = \cdots = m[6,6] = 0 \\ m[1,2] & = 15750, ~ m[2,3] = 2625, ~ m[3,4] = 750 \\ m[4,5] & = 1000, ~ m[5,6] = 5000 \\[5pt] m[1,3] & = \min \{m[1,1] + m[2,3] + p_0 p_1 p_3, ~ m[1,2] + m[3,3] + p_0 p_2 p_3 \} \\ & = \min \{ 2625 + 30 \times 35 \times 5, ~ 15750 + 30 \times 15 \times 5 \} \\ & = \min \{ \class{blue}{2625 + 5250}, ~ 15750 + 2250 \} = 7875, ~ s[1,3] = 1 \\ m[2,4] & = \min \{m[2,2] + m[3,4] + p_1 p_2 p_4, ~ m[2,3] + m[4,4] + p_1 p_3 p_4 \} \\ & = \min \{ 750 + 35 \times 15 \times 10, ~ 2625 + 35 \times 5 \times 10 \} \\ & = \min \{ 750 + 5250, ~ \class{blue}{2625 + 1750} \} = 4375, ~ s[2,4] = 3 \end{align*} $$
$p_0$ | $p_1$ | $p_2$ | $p_3$ | $p_4$ | $p_5$ | $p_6$ |
---|---|---|---|---|---|---|
$30$ | $35$ | $15$ | $5$ | $10$ | $20$ | $25$ |
递推关系$m[i,j] = \min_{i \le k < j} \{ m[i,k] + m[k+1,j] + p_{i-1} p_k p_j \}$
$$ \begin{align*} \quad m[1,1] & = m[2,2] = \cdots = m[6,6] = 0 \\ m[1,2] & = 15750, ~ m[2,3] = 2625, ~ m[3,4] = 750 \\ m[4,5] & = 1000, ~ m[5,6] = 5000 \\ m[1,3] & = 7875, ~ m[2,4] = 4375, ~ m[3,5] = 2500, ~ m[4,6] = 3500 \\[5pt] m[1,4] & = \min \{m[1,1] + m[2,4] + p_0 p_1 p_4, ~ m[1,2] + m[3,4] + p_0 p_2 p_4, \\ & \qquad \qquad m[1,3] + m[4,4] + p_0 p_3 p_4 \} \\ & = \min \{ 4375 + 30 \times 35 \times 10, ~ 15750 + 750 + 30 \times 15 \times 10, \\ & \qquad \qquad 7875 + 30 \times 5 \times 10 \} \\ & = \min \{ 14875, ~ 21000, ~ \class{blue}{9375} \} = 9375, ~ s[1,4] = 3 \end{align*} $$
递推关系$m[i,j] = \min_{i \le k < j} \{ m[i,k] + m[k+1,j] + p_{i-1} p_k p_j \}$
$m[1,6] = 15,125$即为$\Av_1 \cdots \Av_6$的最优加括号方案的乘法次数
$s[i,j]$表示$\Av_i \cdots \Av_j$的最优分开位置
最优方案:$(\Av_1 (\Av_2 \Av_3)) ((\Av_4 \Av_5) \Av_6)$
def print_sol(s, i, j): if i == j: print("A%d" % (i), end='') else: print("(", end='') print_sol(s, i, s[i, j]) print_sol(s, s[i, j] + 1, j) print(")", end='')
p = [30, 35, 15, 5, 10, 20, 25] n = len(p) - 1 # 矩阵个数 m, s = matrix_chain(p, n) print(m[1, n]) print(m[1:, 1:]) print(s[1:, 1:]) print_sol(s, 1, n) ------------------------------------------ 15125 [[ 0 15750 7875 9375 11875 15125] [ 0 0 2625 4375 7125 10500] [ 0 0 0 750 2500 5375] [ 0 0 0 0 1000 3500] [ 0 0 0 0 0 5000] [ 0 0 0 0 0 0]] [[0 1 1 3 3 3] [0 0 2 3 3 3] [0 0 0 3 3 3] [0 0 0 0 4 5] [0 0 0 0 0 5]
第一阶段:递归 (recursive) 表示
第二阶段:递推 (recurrent) 求解
对最优化问题,递归表示中第二步就是在判断最优子结构性
有向图上的无权最短路径具有最优子结构性
设$u \ne v$,$u$到$v$的最短路径为$p$,记为$u \overset{p}{\rightsquigarrow} v$
路径$p$上的中间结点$w$将$p$分为$p_1$、$p_2$两部分,$u \overset{p_1}{\rightsquigarrow} w \overset{p_2}{\rightsquigarrow} v$
最优子结构性均可用上述反证法来证,书上称剪切-粘贴技术
有向图上的无权最长简单(无环)路径不具有最优子结构性
区别的根源:子问题无关性,同一个原问题的子问题相互独立
最短路径的两个子问题相互独立,$p_1$、$p_2$除$w$外没有公共点,否则设公共点为$x$,则$u \rightsquigarrow x \rightsquigarrow w \rightsquigarrow x \rightsquigarrow v$可优化为$u \rightsquigarrow x \rightsquigarrow v$
最长路径的两个子问题不独立,子问题 ① 到 ② 的最长路径经过的点,在子问题 ② 到 ③ 的最长路径中不能再经过,否则构成环,换言之子问题 ① 到 ② 的求解影响了子问题 ② 到 ③ 的求解
DNA 由 A、C、G、T 四种碱基组成,现有两个有机体的 DNA
两个有机体有多“相似”,基因序列比对
考虑序列 S3,其中的基以同样的顺序出现在 S1 和 S2 中,但不一定连续,例如 S3 = GTCGTCGGAAGCCGGCCGAA,两个序列的最长公共非连续子序列称为最长公共子序列 (longest common subsequence, LCS)
将其中一个序列转换成另一个所需的最少的插入/删除/替换次数,编辑距离 (edit distance)
序列$X = \langle x_1, x_2, \ldots, x_m \rangle$,序列$Y = \langle y_1, y_2, \ldots, y_n \rangle$
记$X$的前$i$个元素构成的前缀子序列为$X_i = \langle x_1, x_2, \ldots, x_i \rangle$
设序列$Z = \langle z_1, z_2, \ldots, z_k \rangle$是$X$和$Y$的任意 LCS
$X_{m-1} = \langle x_1, x_2, \ldots, x_{m-1} \rangle$ | $x_m$ |
---|---|
$Y_{n-1} = \langle y_1, y_2, \ldots, y_{n-1} \rangle$ | $y_n$ |
LCS:$Z_{k-1} = \langle z_1, z_2, \ldots, z_{k-1} \rangle, ~ z_k$ |
可否考虑序列的第一个元素和后缀?
$X_{m-1} = \langle x_1, x_2, \ldots, x_{m-1} \rangle$ | $x_m$ |
---|---|
$Y_{n-1} = \langle y_1, y_2, \ldots, y_{n-1} \rangle$ | $y_n$ |
LCS:$Z_{k-1} = \langle z_1, z_2, \ldots, z_{k-1} \rangle, ~ z_k$ |
① 若$x_m = y_n$,则$z_k = x_m = y_n$且$Z_{k-1}$是$X_{m-1}$和$Y_{n-1}$的 LCS
反设$z_k \ne x_m = y_n$,将$x_m$接到$Z$的末尾可以得到一个长为$k+1$的公共子序列,这与$Z$的最优性矛盾
已证$z_k = x_m = y_n$,故$Z_{k-1}$是$X_{m-1}$和$Y_{n-1}$的公共子序列,若其不是最长,则另有一个长度大于$k-1$的公共子序列,将$x_m$接到其末尾可以得到$X$和$Y$的一个长度大于$k$的公共子序列,矛盾
$X_{m-1} = \langle x_1, x_2, \ldots, x_{m-1} \rangle$ | $x_m$ |
---|---|
$Y_{n-1} = \langle y_1, y_2, \ldots, y_{n-1} \rangle$ | $y_n$ |
LCS:$Z_{k-1} = \langle z_1, z_2, \ldots, z_{k-1} \rangle, ~ z_k$ |
② 若$x_m \ne y_n$,则$z_k \ne x_m$蕴含$Z$是$X_{m-1}$和$Y$的一个 LCS
若$z_k \ne x_m$,$Z$显然是$X_{m-1}$和$Y$的公共子序列,若其不是最长,则另有一个长度大于$k$的子序列,该子序列也是$X$和$Y$的子序列,这与$Z$的最优性矛盾
③ 若$x_m \ne y_n$,则$z_k \ne y_n$蕴含$Z$是$X$和$Y_{n-1}$的一个 LCS
$X_{m-1} = \langle x_1, x_2, \ldots, x_{m-1} \rangle$ | $x_m$ |
---|---|
$Y_{n-1} = \langle y_1, y_2, \ldots, y_{n-1} \rangle$ | $y_n$ |
LCS:$Z_{k-1} = \langle z_1, z_2, \ldots, z_{k-1} \rangle, ~ z_k$ |
记$c[i,j]$为$X_i$和$Y_j$的 LCS 的长度:
$$ \begin{align*} \quad c[i,j] = \begin{cases} 0, & i = 0 ~ \text{或} ~ j = 0 \\ c[i-1,j-1]+1, & i,j > 0 ~ \text{且} ~ x_i = y_j \\ \max \{ c[i,j-1], ~ c[i-1,j] \}, & i,j > 0 ~ \text{且} ~ x_i \ne y_j \end{cases} \end{align*} $$
$c[i,j]$为$X_i$和$Y_j$的 LCS 的长度,$b[i,j]$记录$c[i,j]$的计算情况
$$ \begin{align*} \quad c[i,j] = \begin{cases} 0, & i = 0 ~ \text{或} ~ j = 0 \\ c[i-1,j-1]+1, & i,j > 0 ~ \text{且} ~ x_i = y_j \\ \max \{ c[i,j-1], ~ c[i-1,j] \}, & i,j > 0 ~ \text{且} ~ x_i \ne y_j \end{cases} \end{align*} $$
def lcs(X, Y): m, n = len(X), len(Y) c, b = np.zeros((m + 1, n + 1), dtype="int"), np.empty((m, n), dtype="str") for i in range(1, m + 1): for j in range(1, n + 1): if X[i - 1] == Y[j - 1]: # 如果当前两个子串的最后一个字符相同 c[i, j] = c[i - 1, j - 1] + 1 b[i - 1, j - 1] = "↖" elif c[i - 1, j] >= c[i, j - 1]: c[i, j] = c[i - 1, j] b[i - 1, j - 1] = "↑" else: c[i, j] = c[i, j - 1] b[i - 1, j - 1] = "←" return c, b
$X = \langle A, B, C, B, D, A, B \rangle$,长度 7
$Y = \langle B, D, C, A, B, A \rangle$,长度 6
$c[i,j]$为$X_i$和$Y_j$的 LCS 的长度
$b[i,j]$记录$c[i,j]$的计算情况
| $j$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|---|
$i$ | | $y_j$ | B | D | C | A | B | A |
0 | $x_i$ | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | A | 0 | | | | | | |
2 | B | 0 | | | | | | |
3 | C | 0 | | | | | | |
4 | B | 0 | | | | | | |
5 | D | 0 | | | | | | |
6 | A | 0 | | | | | | |
7 | B | 0 | | | | | | |
for i in range(1, m + 1): for j in range(1, n + 1): if X[i - 1] == Y[j - 1]: c[i, j] = c[i - 1, j - 1] + 1 b[i - 1, j - 1] = '↖' elif c[i - 1, j] >= c[i, j - 1]: c[i, j] = c[i - 1, j] b[i - 1, j - 1] = '↑' else: c[i, j] = c[i, j - 1] b[i - 1, j - 1] = '←'
二重 for 循环时间复杂度$\Theta(n^2)$,二维表格空间复杂度$\Theta(n^2)$
$X = \langle A, B, C, B, D, A, B \rangle$,长度 7
$Y = \langle B, D, C, A, B, A \rangle$,长度 6
$c[i,j]$为$X_i$和$Y_j$的 LCS 的长度
$b[i,j]$记录$c[i,j]$的计算情况
| $j$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|---|
$i$ | | $y_j$ | Ⓑ | D | Ⓒ | A | Ⓑ | Ⓐ |
0 | $x_i$ | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | A | 0 | 0↑ | 0↑ | 0↑ | 1↖ | 1← | 1↖ |
2 | Ⓑ | 0 | ①↖ | 1← | 1← | 1↑ | 2↖ | 2← |
3 | Ⓒ | 0 | 1↑ | 1↑ | ②↖ | 2← | 2↑ | 2↑ |
4 | Ⓑ | 0 | 1↖ | 1↑ | 2↑ | 2↑ | ③↖ | 3← |
5 | D | 0 | 1↑ | 2↖ | 2↑ | 2↑ | 3↑ | 3↑ |
6 | Ⓐ | 0 | 1↑ | 2↑ | 2↑ | 3↖ | 3↑ | ④↖ |
7 | B | 0 | 1↖ | 2↑ | 2↑ | 3↑ | 4↖ | 4↑ |
打印最长公共子序列
def restore_lcs(i, j, LCS): if i == -1 or j == -1: return if b[i, j] == "↖": restore_lcs(i - 1, j - 1, LCS) LCS.append(X[i]) elif b[i, j] == "↑": restore_lcs(i - 1, j, LCS) else: restore_lcs(i, j - 1, LCS)
将一个序列转换成另一个所需的最少的插入/删除/替换次数
$X = \langle A, B, C, B, D, A, B \rangle$、$Y = \langle B, D, C, A, B, A \rangle$
$3$次删除、$2$次插入
A | B | C | B | D | A | B | ||
---|---|---|---|---|---|---|---|---|
B | D | C | A | B | A |
$2$次删除、$2$次替换、$1$次插入
A | B | C | B | D | A | B | |
---|---|---|---|---|---|---|---|
B | D | C | A | B | A |
LCS 的长度与不带替换的编辑距离是相关的,从前一个例子可以看出
$X$、$Y$各插入一些空格变成等长,然后以某种方式对齐,产生的最少的不相同的列数就是编辑距离
最优子结构性:将$X$、$Y$按编辑距离的方式对齐后,去掉最后一列,剩余列的对齐方式依然是前缀的编辑距离
记$c[i,j]$为$X_i$和$Y_j$的编辑距离
最后一列分 3 种情况:
A | B | C | B | D | A | B | |
---|---|---|---|---|---|---|---|
B | D | C | A | B | A |
记$c[i,j]$为$X_i$和$Y_j$的编辑距离
最后一列分 3 种情况:
3 种情况取最小即为最优
$$ \begin{align*} \quad c[i,j] = \begin{cases} i, & j = 0 \\ j, & i = 0 \\ \min \left\{ \begin{array}{l} c[i-1,j] + 1, \\ c[i,j-1] + 1, \\ c[i-1,j-1] + \Ibb(x_m \neq y_n), \end{array} \right\} & \ow \end{cases} \end{align*} $$
def edit_dis(X, Y): m, n = len(X), len(Y) c, b = np.empty((m + 1, n + 1), dtype='int'), np.empty((m, n), dtype='str') for i in range(m + 1): c[i, 0] = i for j in range(n + 1): c[0, j] = j for i in range(1, m + 1): for j in range(1, n + 1): a = [c[i - 1, j - 1] + int(X[i - 1] != Y[j - 1]), c[i, j - 1] + 1, c[i - 1, j] + 1] index = np.argmin(a) c[i, j] = a[index] if index == 0: b[i - 1, j - 1] = '↖' elif index == 1: b[i - 1, j - 1] = '←' else: b[i - 1, j - 1] = '↑' return c, b
def restore_sol(i, j, b, l): if i == -1: for k in range(j + 1): l.append([' ', Y[j]]) return if j == -1: for k in range(i + 1): l.append([X[i], ' ']) return if b[i, j] == '↖': restore_sol(i - 1, j - 1, b, l) l.append([X[i], Y[j]]) elif b[i, j] == '↑': restore_sol(i - 1, j, b, l) l.append([X[i], ' ']) else: restore_sol(i, j - 1, b, l) l.append([' ', Y[j]]) X = ['A', 'B', 'C', 'B', 'D', 'A', 'B'] Y = ['B', 'D', 'C', 'A', 'B', 'A'] c, b = edit_dis(X, Y) l = [] restore_sol(len(X) - 1, len(Y) - 1, b, l) l = np.transpose(l) print(c) print(b) print(c[len(X) - 1, len(Y) - 1]) print(l) ---------------------------------------- [[0 1 2 3 4 5 6] [1 1 2 3 3 4 5] [2 1 2 3 4 3 4] [3 2 2 2 3 4 4] [4 3 3 3 3 3 4] [5 4 3 4 4 4 4] [6 5 4 4 4 5 4] [7 6 5 5 5 4 5]] [['↖' '↖' '↖' '↖' '←' '↖'] ['↖' '↖' '↖' '↖' '↖' '←'] ['↑' '↖' '↖' '←' '←' '↖'] ['↖' '↖' '↖' '↖' '↖' '←'] ['↑' '↖' '↖' '↖' '↖' '↖'] ['↑' '↑' '↖' '↖' '↖' '↖'] ['↖' '↑' '↖' '↖' '↖' '←']] 5 [['A' 'B' 'C' 'B' 'D' 'A' 'B' ' '] [' ' 'B' ' ' 'D' 'C' 'A' 'B' 'A']]
输入:元素各不相同的序列$X$
输出:$X$的最长递增子序列 (longest increasing subsequence, LIS)
最优子结构性:设以$X[i]$结尾的 LIS 为$X[i_1], \ldots, X[i_k], X[i]$
则$X[i_1], \ldots, X[i_k]$必然是以$X[i_k]$结尾的 LIS
记$d[i]$为以$X[i]$结尾的 LIS 的长度,显然所有$d[i]$的最小值是 1
$d[0] = 1$,以$X[0]$结尾的 LIS 就是$X[0]$
$d[1]$根据$X[1]$是否可以接在$X[0]$后面分两种情况:
$X[i]$接在以$X[0], X[1], \ldots, X[i-1]$结尾的哪个 LIS 后面?
在所有可接的 LIS 后面选一个最长的,若无则为$1$:
$$ \begin{align*} \quad d[i] = \begin{cases} 1, & i = 0 \\ \max_{j < i, X[j] < X[i]} \{ 1, d[j] + 1 \}, & i > 0 \\ \end{cases} \end{align*} $$
def lis_dp(X, n): d, b = [1] * n, [-1] * n for i in range(1, n): for j in range(i): if X[j] < X[i] and d[i] < d[j] + 1: # X[i]可接在X[j]后面 d[i] = d[j] + 1 b[i] = j # 更新前一个元素的索引 return d, b
二重 for 循环时间复杂度$\Theta(n^2)$,一维表格空间复杂度$\Theta(n)$
$$ \begin{align*} \quad d[i] = \begin{cases} 1, & i = 0 \\ \max_{j < i, X[j] < X[i]} \{ 1, d[j] + 1 \}, & i > 0 \\ \end{cases} \end{align*} $$
$i$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
$X$ | ② | 8 | ④ | 9 | 1 | ⑥ | ⑦ | 3 | 0 | 5 |
$d$ | 1 | 2 | 2 | 3 | 1 | 3 | 4 | 2 | 1 | 3 |
$b$ | -1 | 0 | 0 | 1 | -1 | 2 | 5 | 0 | -1 | 2 |
def restore_lis_dp(X, b, index, LIS): if index == -1: return restore_lis_dp(X, b, b[index], LIS) LIS.append(X[index])
维护表$e[]$,初始为空
$e[j]$始终记录当前长度为$j$的递增子序列的末位元素最小值
以$X = [2, 8, 4, 9, 1, 6, 7, 3, 0, 5]$为例,最终$e = [0, 3, 5, 7]$
$e$的长度为 4 代表 LIS 的长度为$4$
$e[-1] = 7$代表 LIS 末位元素为$7$,$[2, 4, 6, 7]$是一个 LIS
$e$的元素是单调递增的,这是提升速度的关键
$e[j]$记录长度为$j$的递增子序列的末位元素最小值,初始为空
$X$ | $e$ | $\text{len(LIS)}=1$ | $\text{len(LIS)}=2$ | $\text{len(LIS)}=3$ | $\text{len(LIS)}=4$ |
---|---|---|---|---|---|
$2$ | $[2]$ | $[2]$ | | | |
$8$ | $[2, \class{blue}{8}]$ | $[2]$ | $[2, \class{blue}{8}]$ | | |
$4$ | $[2, \class{blue}{4}]$ | $[2]$ | $[2, \class{blue}{4}]$ | | |
$9$ | $[2, 4, \class{blue}{9}]$ | $[2]$ | $[2, 4]$ | $[2, 4, \class{blue}{9}]$ | |
$1$ | $[\class{blue}{1}, 4, 9]$ | $[\class{blue}{1}]$ | $[2, 4]$ | $[2, 4, 9]$ | |
$6$ | $[1, 4, \class{blue}{6}]$ | $[1]$ | $[2, 4]$ | $[2, 4, \class{blue}{6}]$ | |
$7$ | $[1, 4, 6, \class{blue}{7}]$ | $[1]$ | $[2, 4]$ | $[2, 4, 6]$ | $[2, 4, 6, \class{blue}{7}]$ |
$3$ | $[1, \class{blue}{3}, 6, 7]$ | $[1]$ | $[2, \class{blue}{3}]$ | $[2, 4, 6]$ | $[2, 4, 6, 7]$ |
$0$ | $[\class{blue}{0}, 3, 6, 7]$ | $[\class{blue}{0}]$ | $[2, 3]$ | $[2, 4, 6]$ | $[2, 4, 6, 7]$ |
$5$ | $[0, 3, \class{blue}{5}, 7]$ | $[0]$ | $[2, 3]$ | $[2, 4, \class{blue}{5}]$ | $[2, 4, 6, 7]$ |
在任意时刻$e$都是严格单调递增的,若$i<j$且$e[i] > e[j]$,则有
$$ \begin{align*} \quad \text{LIS}_i:~ \underbrace{\square ~ \square ~ \cdots ~ \square ~ e[i]}_{\text{len}~=~i}, \quad \text{LIS}_j:~ \underbrace{\square ~ \square ~ \square ~ \square ~ \cdots ~ \square ~ \square ~ e[j]}_{\text{len}~=~j} \end{align*} $$
去掉$\text{LIS}_j$前$j-i$个元素可得长度$i$、末位元素$< e[i]$的递增序列
在遍历$X$时,对于$X[i]$
二分查找时间复杂度为$O(\lg n)$,总时间复杂度为$O(n \lg n)$
def lis_dp_plus(X, n): e = [] b = [-1] * n # b[i]记录i在LIS中的前一个元素的值 for i in range(n): j = bisect_left(e, X[i]) # 在e中对X[i]进行二分查找 if j == len(e): # X[i] > e[-1] if j > 0: b[i] = e[-1] e.append(X[i]) # 将其接在e后面表示找到了更长的递增子序列 else: # e[j-1] < X[i] < e[j] if j > 0: b[i] = e[j - 1] e[j] = X[i] # 将e[j]改为X[i] 可以改进现有长度为j的递增子序列 return e, b def restore_lis_dp_plus(X, n, e, b): dict = {key: value for key, value in zip(X, range(n))} # 倒排索引字典 X[i]: i pre = e[-1] # e的最后一个元素是LIS的最后一个元素 LIS = [] restore_lis_dp_plus_aux(dict, b, pre, LIS) # 从最后一个元素开始 向前将LIS构造出来 return LIS def restore_lis_dp_plus_aux(dict, b, pre, LIS): if pre < 0: return restore_lis_dp_plus_aux(dict, b, b[dict[pre]], LIS) LIS.append(pre)
对每个单词,在字典里找该词的翻译,有的能找到,有的找不到
When | you | play | the | game | of | thrones | , | you | win |
---|---|---|---|---|---|---|---|---|---|
当…时 | 你 | 玩 | 游戏 | 的 | 权力 | , | 你 | 赢 | |
or | you | die | . | There | is | no | middle | ground | . |
或 | 你 | 死 | . | 有 | 没 | 中间 | 地带 | . |
Daenerys Stormborn of House Targaryen, the First of Her Name, Queen of the Andals, the Rhoynar and the First Men, Queen of Meereen, Protector of the Realm, Lady Regnant of the Seven Kingdoms, Mother of Dragons, Khaleesi of the Great Grass Sea, the Unburnt, Breaker of Chains.
方法:以单词作关键字建二叉搜索树
目标:尽快找到单词,期望搜索时间最少
本页15个结点的二叉树根据字典序而来
频繁被查询的单词靠近根可减少期望比较次数
$n$个关键字产生$n+1$个区间$d_i = (k_i, k_{i+1})$
这些区间称为伪关键字
输入:
输出:以$k_1, k_2, \ldots, k_n$为内部结点、$d_0, d_1, \ldots, d_n$为叶子结点的最优二叉搜索树 (optimal binary search tree, OBST),期望搜索时间最少
$$ \begin{align*} \quad \Ebb & [\text{搜索时间}] = \sum_{i=1}^n (\dep (k_i) + 1) \cdot p_i + \sum_{i=0}^n (\dep (d_i) + 1) \cdot q_i \\ & \overset{\sum_{i=1}^n p_i + \sum_{i=0}^n q_i = 1}{=} 1 + \sum_{i=1}^n \dep (k_i) \cdot p_i + \sum_{i=0}^n \dep (d_i) \cdot q_i \end{align*} $$
有$n = 5$个关键字,$6$个伪关键字
$i$ | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
$p_i$ | | 0.15 | 0.1 | 0.05 | 0.1 | 0.2 |
$q_i$ | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 |
关键字 | $k_1$ | $k_2$ | $k_3$ | $k_4$ | $k_5$ | $d_0$ | $d_1$ | $d_2$ | $d_3$ | $d_4$ | $d_5$ | 总计 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
概率 | 0.15 | 0.1 | 0.05 | 0.1 | 0.2 | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 | 1 |
深度 | 1 | 0 | 2 | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | |
贡献 | 0.3 | 0.1 | 0.15 | 0.2 | 0.6 | 0.15 | 0.3 | 0.2 | 0.2 | 0.2 | 0.4 | 2.8 |
深度 | 1 | 0 | 3 | 2 | 1 | 2 | 2 | 4 | 4 | 3 | 2 | |
贡献 | 0.3 | 0.1 | 0.2 | 0.3 | 0.4 | 0.15 | 0.3 | 0.25 | 0.25 | 0.2 | 0.3 | 2.75 |
如果$\Tcal$是关于关键字$k_1, \ldots, k_n$和伪关键字$d_0, \ldots, d_n$的 OBST,则$\Tcal$中仅包含$k_i, \ldots, k_j$和$d_{i-1}, \ldots, d_j$的子树$\Tcal'$必然也是 OBST
反设$\Tcal'$不是最优,则存在更优的$\Tcal''$,将$\Tcal$中的$\Tcal'$替换成$\Tcal''$可以得到比$\Tcal$更优的 OBST
子问题:仅包含$k_i, \ldots, k_j$和$d_{i-1}, \ldots, d_j$的 OBST
子问题:仅包含$k_i, \ldots, k_j$和$d_{i-1}, \ldots, d_j$的 OBST
设$e[i,j]$是子问题 OBST 的期望搜索代价
$$ \begin{align*} \quad e[i,j] & = \overbrace{p_r}^{\text{根}} + \overbrace{e[i,r-1] + w (i,r-1)}^{\text{左子树}} + \overbrace{e[r+1,j] + w (r+1,j)}^{\text{右子树}} \\ & = e[i,r-1] + e[r+1,j] + w(i,j) \end{align*} $$
其中左、右子树分别$+ w (i,r-1)$、$+w (r+1,j)$是因为它们成为$k_r$的子结点时,所有结点深度加$1$
$w(i,j) = \sum_{l=i}^j p_l + \sum_{l=i-1}^j q_l$是结点概率和
设$root[i,j]$是仅包含$k_i, \ldots, k_j$和$d_{i-1}, \ldots, d_j$的 OBST 的根节点
$$ \begin{align*} \quad e[i,j] = \begin{cases} q_{i-1}, & j = i - 1 \\ \min_{i \le r \le j} ~ \{ e[i,r-1] + e[r+1,j] + w(i,j) \}, & i \le j \end{cases} \end{align*} $$
def optimal_bst(p, q): n = len(p) e = np.full((n + 2, n + 1), float("inf")) w = np.zeros((n + 2, n + 1)) root = np.zeros((n + 1, n + 1), dtype="int") for i in range(1, n + 2): e[i, i - 1] = w[i, i - 1] = q[i - 1] # 边界情况 for l in range(1, n + 1): # 子问题 l = 1 -> n for i in range(1, n - l + 2): # 从第i个关键字 j = i + l - 1 # 到第j个关键字 w[i, j] = w[i, j - 1] + p[j - 1] + q[j] # 填写w表 for r in range(i, j + 1): # 遍历寻找最优根节点 t = e[i, r - 1] + e[r + 1, j] + w[i, j] if t < e[i, j]: e[i, j] = t # 更新期望搜索代价 root[i, j] = r # 更新最优根节点 return e, w, root
三重 for 循环时间复杂度$\Theta(n^3)$,二维表格空间复杂度$\Theta(n^2)$
$p_1$ | $p_2$ | $p_3$ | $p_4$ | $p_5$ | $q_0$ | $q_1$ | $q_2$ | $q_3$ | $q_4$ | $q_5$ |
---|---|---|---|---|---|---|---|---|---|---|
0.15 | 0.1 | 0.05 | 0.1 | 0.2 | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 |
$$ \begin{align*} w(i,j) & = q_{i-1} + p_i + q_i + \cdots + p_j + q_j \\[5px] w(1,0) & = q_0 = 0.05, ~ w(2,1) = q_1 = 0.1, ~ w(3,2) = q_2 = 0.05 \\ w(4,3) & = q_3 = 0.05, ~ w(5,4) = q_4 = 0.05, ~ w(6,5) = q_5 = 0.1 \\[5px] w(1,1) & = q_0 + p_1 + q_1 = 0.05 + 0.15 + 0.1 = 0.3, ~ \ldots, ~ w(5,5) = 0.35 \\[5px] w(1,2) & = q_0 + p_1 + q_1 + p_2 + q_2 = 0.45, ~ \ldots, ~ w(4,5) = 0.5 \\[5px] w(1,3) & = q_0 + p_1 + q_1 + p_2 + q_2 + p_3 + q_3 = 0.55, ~ \ldots, ~ w(3,5) = 0.6 \\[5px] w(1,4) & = q_0 + p_1 + q_1 + p_2 + q_2 + p_3 + q_3 + p_4 + q_4 = 0.7, ~ w(2,5) = 0.8 \\[5px] w(1,5) & = 1 \end{align*} $$
$p_1$ | $p_2$ | $p_3$ | $p_4$ | $p_5$ | $q_0$ | $q_1$ | $q_2$ | $q_3$ | $q_4$ | $q_5$ |
---|---|---|---|---|---|---|---|---|---|---|
0.15 | 0.1 | 0.05 | 0.1 | 0.2 | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 |
$$ \begin{align*} e[i,j] & = \begin{cases} q_{i-1}, & j = i - 1 \\ \min_{i \le r \le j} ~ \{ e[i,r-1] + e[r+1,j] + w(i,j) \}, & i \le j \end{cases} \\[5px] e[1,0] & = q_0 = 0.05, ~ e[2,1] = q_1 = 0.1, ~ e[3,2] = q_3 = 0.05 \\ e[4,3] & = q_4 = 0.05, ~ e[5,4] = q_5 = 0.05, ~ e[6,5] = q_6 = 0.1 \\[5px] e[1,1] & = e[1,0] + e[2,1] + w(1,1) = 0.05 + 0.1 + 0.3 = 0.45, ~ root[1,1] = 1 \\ e[2,2] & = e[2,1] + e[3,2] + w(2,2) = 0.1 + 0.05 + 0.25 = 0.4, ~ root[2,2] = 2 \\ e[3,3] & = e[3,2] + e[4,3] + w(3,3) = 0.05 + 0.05 + 0.15 = 0.25, ~ root[3,3] = 3 \\ e[4,4] & = e[4,3] + e[5,4] + w(4,4) = 0.05 + 0.05 + 0.2 = 0.3, ~ root[4,4] = 4 \\ e[5,5] & = e[5,4] + e[6,5] + w(5,5) = 0.05 + 0.1 + 0.35 = 0.5, ~ root[5,5] = 5 \\[5px] e[1,2] & = \min \{ e[1,0] + e[2,2], ~ e[1,1] + e[3,2]\} + w(1,2) \end{align*} $$
$p_1$ | $p_2$ | $p_3$ | $p_4$ | $p_5$ | $q_0$ | $q_1$ | $q_2$ | $q_3$ | $q_4$ | $q_5$ |
---|---|---|---|---|---|---|---|---|---|---|
0.15 | 0.1 | 0.05 | 0.1 | 0.2 | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 |
$$ \begin{align*} e[i,j] & = \begin{cases} q_{i-1}, & j = i - 1 \\ \min_{i \le r \le j} ~ \{ e[i,r-1] + e[r+1,j] + w(i,j) \}, & i \le j \end{cases} \\[5px] e[1,2] & = \min \{ e[1,0] + e[2,2], ~ e[1,1] + e[3,2]\} + w(1,2) \\ & = \min \{ \class{blue}{0.05 + 0.4}, ~ 0.45 + 0.05 \} + 0.45 = 0.9, ~ root[1,2] = 1 \\ e[2,3] & = \min \{ e[2,1] + e[3,3], ~ e[2,2] + e[4,3]\} + w(2,3) \\ e[3,4] & = \min \{ e[3,2] + e[4,4], ~ e[3,3] + e[5,4]\} + w(3,4) \\ e[4,5] & = \min \{ e[4,3] + e[5,5], ~ e[4,4] + e[6,5]\} + w(4,5) \\[5px] e[1,3] & = \min \{ e[1,0] + e[2,3], ~ e[1,1] + e[3,3], ~ e[1,2] + e[4,3] \} + w(1,3) \\ e[2,4] & = \min \{ e[2,1] + e[3,4], ~ e[2,2] + e[4,4], ~ e[2,3] + e[5,4] \} + w(2,4) \\ & \qquad \vdots \end{align*} $$
跨越中点的最大子数组涉及求以$A[mid]$作结尾的最大子数组
左子问题递归涉及求以$A[mid/2]$作结尾的最大子数组
对任意$i \le mid/2$,求和$A[i] + \cdots + A[mid/2]$会被重复计算
分治法求最大子数组的时间复杂度$\Theta(n \lg n)$
子问题$i$:求以$A[i]$作结尾的最大子数组$A[j, \ldots, i]$的和
将这$n$个子问题求解完,其中和最大的就是原问题的最大子数组
最优子结构性:设$A[j, \ldots, i-1]$是以$A[i]$作结尾的最大子数组,其中$j < i$,则$A[j, \ldots, i-1]$也是以$A[i-1]$作结尾的最大子数组
反设$A[j', \ldots, i-1]$是以$A[i-1]$作结尾的最大子数组,$j' \ne j$,则$A[j', \ldots, i]$是更大的以$A[i]$作结尾的最大子数组
最优子结构性:设$A[j, \ldots, i-1]$是以$A[i]$作结尾的最大子数组,其中$j < i$,则$A[j, \ldots, i-1]$也是以$A[i-1]$作结尾的最大子数组
设$dp[i]$是以$A[i]$作结尾的最大子数组的和,两种情况:
$$ \begin{align*} \quad dp[i] = \begin{cases} dp[i-1] + A[i], & dp[i-1] \ge 0 \Longleftrightarrow j < i \\ A[i], & dp[i-1] < 0 \Longleftrightarrow j = i \end{cases} \end{align*} $$
def find_max_subarray_dp2(): dp = [0] * n # dp[i]为以A[i]作结尾的最大子数组的和 s = [0] * n # s[i]为以A[i]作结尾的最大子数组的起始索引 dp[0], s[0] = A[0], 0 for i in range(1, n): if dp[i-1] >= 0: dp[i] = dp[i-1] + A[i] # A[i]接在A[j,...,i-1]后面 s[i] = s[i-1] # 继承其起始索引 else: dp[i] = A[i] # A[i]不接在A[j,...,i-1]后面 另起炉灶 s[i] = i # 起始索引就是当前位置 max_index = np.argmax(dp) # 遍历dp获取最大元的索引 return s[max_index], max_index, dp[max_index]
$i$ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$A[]$ | 13 | -3 | -25 | 20 | -3 | -16 | -23 | 18 | 20 | -7 | 12 | -5 | -22 | 15 | -4 | 7 |
$dp[]$ | 13 | 10 | -15 | 20 | 17 | 1 | -22 | 18 | 38 | 31 | 43 | 38 | 16 | 31 | 27 | 34 |
$s[]$ | 0 | 0 | 0 | 3 | 3 | 3 | 3 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 |
算法导论 3rd
计算题:15.2-1、15.4-1、15.5-2
设计题:15.1-3、15-9、15-11
证明题:15.2-5、15.3-6