动态规划
动态规划思路
-
刻画一个最优解的结构特征
-
递归定义最优解的值
-
计算最优解的值, 通常采取自低向上的方法
-
利用计算出的信息构造一个最优解
钢条切割问题
给定一段长度为n英寸的钢条和一个价格表 p i p_i pi( i = 1 , 2 , ⋯ , n i = 1, 2,\cdots,n i=1,2,⋯,n), 求切割钢条方案, 使得销售收益 r n r_n rn 最大。注意,如果长度为n英寸的钢条的价格 p i p_i pi足够大,最优解可能就是完全不需要切割
-
刻画一个最优解的结构特征
将钢条从左边切割长度为 i i i的一段, 只对右边剩下长度为 n − i n-i n−i的一段继续切割,对左边的一段不再切割。
-
递归定义最优解的值
r n = max 1 ≤ i ≤ n ( p i + r n − 1 ) r_n = \max\limits_{1\leq i \leq n}(p_i + r_{n-1}) rn=1≤i≤nmax(pi+rn−1)
-
计算最优解的值
def cut_rot(p: list) -> int: n = len(p) r = [0] * (n+1) for j in range(1, n+1): q = -1 for i in range(j + 1): q = max(q, p[i] + r[j - i]) r[j] = q return r[n]
-
利用计算出的信息构造一个最优解
def cut_rot(p: list): n = len(p) r = [0] * (n+1) s = [0] * (n+1) for j in range(1, n+1): q = -1 for i in range(j+1): if q < p[i] + r[j - i]: q = p[i] + r[j - i] s[j] = i r[j] = q return (r[n], s[n])
活动选择问题
假定有 n n n个活动的集合 S = { a 1 , a 2 , ⋯ , a n } S=\{a_1,a_2,\cdots,a_n\} S={a1,a2,⋯,an},这些活动使用同一个资源,而这个活动在某个时刻只能供一个活动使用。每个活动 a i a_i ai都有一个开始时间 s i s_i si和一个结束时间 f i f_i fi,其中 0 ≤ s i < f i < ∞ 0\leq s_i<f_i<\infty 0≤si<fi<∞。如果被选中,任务 a i a_i ai 发生在半开区间 [ s i , f i ) [s_i, f_i) [si,fi)期间。如果两个活动 a i a_i ai和 a j a_j aj 满足 [ s i , f i ) [s_i, f_i) [si,fi)和 [ s j , f i ) [s_j, f_i) [sj,fi)不重叠,则称它们是兼容的。在活动选择问题中,我们希望选出一个最大兼容集。假定活动已按结束时间的单调递增顺序排序:
f 1 ≤ f 2 ≤ f 3 ≤ ⋯ ≤ f n − 1 ≤ f n f_1\leq f_2\leq f_3\leq \cdots\leq f_{n-1}\leq f_n f1≤f2≤f3≤⋯≤fn−1≤fn
解法一
-
刻画一个最优解的结构特征
假设 S i j S_{ij} Sij表示在活动 a i a_i ai结束之后, a j a_j aj开始之前的所有活动的集和,记 A i j A_{ij} Aij为 S i j S_{ij} Sij 中活动的最大兼容子集且 a k ∈ A i j a_k\in A_{ij} ak∈Aij ,于是可以得到两个子问题: 寻找 S i k S_{ik} Sik中的兼容子集,寻找 S k j S_{kj} Skj中的兼容子集。令 A i k = A i j ⋂ S i k A_{ik} = A_{ij} \bigcap S_{ik} Aik=Aij⋂Sik A k j = A i j ∪ a k ∪ A k j A_{kj} = A_{ij} \cup {a_k}\cup A_{kj} Akj=Aij∪ak∪Akj, A i j = A i k ∪ a k ∪ A k j A_{ij} = A_{ik}\cup{a_k}\cup A_{kj} Aij=Aik∪ak∪Akj
于是得到
∣ A i j ∣ = ∣ A i k ∣ + ∣ A k j ∣ + 1 |A_{ij}| = |A_{ik}| + |A_{kj}| + 1 ∣Aij∣=∣Aik∣+∣Akj∣+1 -
递归定义最优解的值
c [ i , j ] = { 0 , S i j = ∅ c [ i , k ] + c [ k , j ] + 1 , S i j ≠ ∅ c_{[i,j]} = \begin{cases} 0 ,S_{ij} = \empty \\c_{[i,k]} + c_{[k,j]}+1 , S_{ij} \neq \empty\end{cases} c[i,j]={0,Sij=∅c[i,k]+c[k,j]+1,Sij=∅
-
计算最优解的值
def act_sel(s: list[int], f: list[int]): n = len(s) c = [[0 for j in range(n+2)] for i in range(n+1)] for j in range(1, n+2): for i in range(j): for k in range(i+1, j): if s[k] > s[i] and f[k] < s[j]: c[i][j] = c[i][k] + c[k][j] + 1 return c[0][n+1]
-
利用计算出的信息构造一个最优解
def act_sel(s: list[int], f: list[int]): n = len(s) r = [] c = [[0 for j in range(n+2)] for i in range(n+1)] for j in range(1, n+2): for i in range(j): for k in range(i+1, j): if s[k] > s[i] and f[k] < s[j]: c[i][j] = c[i][k] + c[k][j] + 1 r.append(k) return (c[0][n+1], r)
解法二
-
刻画一个最优解的结构特征
假设 S S S为所有活动的集和,若 a K ∈ S a_K \in S aK∈S ,那么可以对 a K a_K aK执行两种操作:选和不选,定义 A k − 1 A_{k-1} Ak−1为 a k a_k ak之前在此模式下执行的最优解,当不选择 a k a_k ak时,显然 ∣ A k ∣ = ∣ A k − 1 ∣ |A_k| = |A_{k-1}| ∣Ak∣=∣Ak−1∣,当选择 a k a_k ak时,记 a m a_m am为结束时间最接近但小于 a k a_k ak开始时间的活动,由此可以推出 ∣ A k ∣ = ∣ A m ∣ + 1 |A_k| = |A_m| + 1 ∣Ak∣=∣Am∣+1
-
递归定义最优解的值
c i = max 0 ≤ m < i ( c i − 1 , c m + 1 ) c_{i} = \max\limits_{0\leq m<i}(c_{i-1}, c_{m} + 1) ci=0≤m<imax(ci−1,cm+1)
-
计算最优解的值
def act_sel(s: list[int], f: list[int]): n = len(s) c = [0] * (n+1) for i in range(1, n+1): j = i - 1 pos = s[j] while f[j - 1] > pos: j -= 1 c[i] = max(c[i - 1], c[j-1] + 1) return c[n]
-
利用计算出的信息构造一个最优解
def act_sel(s: list[int], f: list[int]): n = len(s) c = [0] * (n+1) s = [] for i in range(1, n+1): j = i - 1 pos = s[j] while f[j - 1] > pos: j -= 1 c[i] = max(c[i - 1], c[j-1] + 1) if c[i-1] > c[j - 1] + 1: c[i] = c[i-1] else: c[i] = c[j-1] + 1 s.append(i) return (c[n], s)
0-1背包问题
一个正在抢劫商店的小偷发现了 n n n 件商品,第 i i i件商品价值 v i v_i vi美元,重 w i w_i wi磅, v i v_i vi和 w i w_i wi都是整数。这个小偷希望拿走价值尽量高的商品,但他的背包最多容纳 w w w磅重的商品, w w w是一个整数。他应该拿哪些商品?
-
刻画一个最优解的结构特征
假设 d p i j dp_{ij} dpij表示 j j j磅第 i i i件商品的最优解,第 i i i件有两种决策:选和不选。
- 如果从第 i i i件商品考虑,不选第 i i i件商品后,包内最大容纳量为 j j j。在此容纳量下,我们完成了 i − 1 i-1 i−1次决策,最优解可表示为 d p ( i − 1 ) j dp_{(i-1)j} dp(i−1)j,同理选择第 i i i件商品后,包内最大容纳量为 j − w e i g h t i j-weight_i j−weighti在此容纳量下,我们完成了 i − 1 i-1 i−1次决策,最优解可表示为 d p ( i − 1 ) ( j − w e i g h t i ) dp_{(i-1)(j-weight_i)} dp(i−1)(j−weighti)
-
递归定义最优解的值
d p i j = max ( d p ( i − 1 ) j , d p ( i − 1 ) ( j − w e i g h t i ) + v a l u e i ) dp_{ij} = \max(dp_{(i-1)j},dp_{(i-1) (j - weight_i)} + value_i ) dpij=max(dp(i−1)j,dp(i−1)(j−weighti)+valuei)
-
计算最优解的值
def com_sel(value:list[int], weight:list[int], W:int): n = len(value) dp = [[0 for j in range(W+1)] for i in range(n)] #初始化 for j in range(weight[0], W+1): dp[0][j] = value[0] for j in range(1, W+1): for i in range(1, n): dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) return dp[n-1][W]
-
利用计算出的信息构造一个最优解
def com_sel(value: list[int], weight: list[int], W: int): n = len(value) dp = [[0 for j in range(W+1)] for i in range(n)] s = [] # 初始化 for j in range(weight[0], W+1): dp[0][j] = value[0] for j in range(1, W): for i in range(1, n): dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) for i in range(1, n): if dp[i-1][W-weight[i]] + value[i] < dp[i-1][W]: dp[i][W] = dp[i-1][W] else: dp[i][W] = dp[i-1][W-weight[i]] + value[i] s.append(i) return dp[n-1][W], s
在本章中,每个步骤都要进行一次选择,但选择通常依赖子问题的解。因此,我们通常以一种自底向上的方式求解,先求子问题,然后是较大的问题。然而,是否可以做出局部最优选择,并将该选择加入最优解中,这引出了我们的下一章—贪心选择