⎨ ⎧ 0 c [ i − 1 , j − 1 ] + 1 max ( c [ i , j − 1 ] , c [ i − 1 , j ]) if i = 0 or j = 0 if i , j > 0 and x i = y j if i , j > 0 and x i = y j 第3步:采用自底向上的方法计算最优解的值 考虑每个长度下的X和每个长度下的Y对应的c[i][j]
如果X或Y当前长为0,则对应的LCS都是0 X最长为1Y最长为1:LCS(1,1)的长度取决于LCS(0,1),LCS(1,0),LCS(0,0),而这三个都在之前得到 Y最长为2:LCS(1,2)的长度取决于LCS(0,2),LCS(1,1),LCS(0,1),而LCS(1,1)在之前已经得到 以此类推,直到Y最长为n X最长为2Y最长为1:LCS(2,1)的长度取决于LCS(1,1),LCS(2,0),LCS(1,0),而LCS(1,1)在之前已经得到 Y最长为2:LCS(2,2)的长度取决于LCS(1,2),LCS(2,1),LCS(1,1),而LCS(2,1)在之前已经得到 以此类推,直到Y最长为n 以此类推,直到X最长为m 第4步:利用结果构造得到最优解的过程 由定理可知,只有同时改变X和Y,即当x m = y n x_m = y_n x m = y n 时,才会在LCS中加入元素x m x_m x m ,其他情况下都是单独改变X或单独改变Y去寻找x m = y n x_m = y_n x m = y n 的情况,所以一个二维数组记录每次情况
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def optimal (road, X, len_X, len_Y ): if len_X == 0 or len_Y == 0 : return '' if road[len_X][len_Y] == 'bingo' : return optimal(road, X, len_X - 1 , len_Y - 1 ) + X[len_X] elif road[len_X][len_Y] == 'left' : return optimal(road, X, len_X, len_Y - 1 ) elif road[len_X][len_Y] == 'up' : return optimal(road, X, len_X - 1 , len_Y) def LCS (X, Y ): X = '$' + X Y = '$' + Y len_X = len (X) len_Y = len (Y) common = [[0 for _ in range (len_Y)] for _ in range (len_X)] road = [['' for _ in range (len_Y)] for _ in range (len_X)] for i in range (1 , len_X): for j in range (1 , len_Y): if X[i] == Y[j]: common[i][j] = common[i - 1 ][j - 1 ] + 1 road[i][j] = 'bingo' else : X_left = common[i][j - 1 ] Y_up = common[i - 1 ][j] if X_left > Y_up: common[i][j] = X_left road[i][j] = 'left' else : common[i][j] = Y_up road[i][j] = 'up' res = optimal(road, X, len_X - 1 , len_Y - 1 ) return common, res
2.4 最优二叉搜索树 问题介绍 问题描述:给定n个关键字< k 1 , k 2 , k 3 , … , k n > <k_1,k_2,k_3,\ldots,k_n> < k 1 , k 2 , k 3 , … , k n > ,其中还存在n+1个伪关键字< d 0 , d 1 , d 2 , … , d n > <d_0,d_1,d_2,\ldots,d_n> < d 0 , d 1 , d 2 , … , d n > ,满足d n > k n , k i < d i < k i + 1 d_n>k_n,\,k_i<d_i<k_{i+1} d n > k n , k i < d i < k i + 1 ,关键字没找到就会找到伪关键字,每个关键字有一个访问概率p,每个伪关键字也有一个访问概率q,假定搜索代价是找到关键字所访问的结点个数,即d e p t h ( i ) + 1 depth(i)+1 d e pt h ( i ) + 1 ,要求构造出一个二叉搜索树,使得在该树中的搜索各种关键字的代价最小,即:1 + ∑ i = 1 n d e p t h ( k i ) × p i + ∑ i = 0 n d e p t h ( d i ) × q i 1 + \sum_{i=1}^{n}{depth(k_i) \times p_i} + \sum_{i=0}^{n}{depth(d_i) \times q_i} 1 + ∑ i = 1 n d e pt h ( k i ) × p i + ∑ i = 0 n d e pt h ( d i ) × q i 最小,也称为OBST问题(Optimal Binary Search Tree)
案例分析:对一个n=5的关键字集合及如下搜索概率,有以下两种树结构,其中第二种代价最小为2.75
i 0 1 2 3 4 5 p_i 0.00 0.15 0.10 0.05 0.10 0.20 q_i 0.05 0.10 0.05 0.05 0.05 0.10
最优二叉搜索树不一定是高度最矮的,搜索频率越大的也不一定出现在越上面
第1步:刻画一个最优子结构 如果一颗树是最优二叉搜索树,则它的根节点的左子树和右子树也都是最优二叉搜索树
第2步:递归地定义重叠子问题 假设当前树存在按序关键字< k i , k i + 1 , … , k j > <k_i,k_{i+1},\ldots,k_j> < k i , k i + 1 , … , k j > ,其中选取k r , i ≤ r ≤ j k_r,i\leq r \leq j k r , i ≤ r ≤ j 作为根结点,那么它的左子树就只有关键字< k i , … , k r − 1 > <k_i,\ldots,k_{r-1}> < k i , … , k r − 1 > 和伪关键字< d i − 1 , … , d r − 1 > <d_{i-1},\ldots,d_{r-1}> < d i − 1 , … , d r − 1 > ,右子树就只有关键字< k r + 1 , … , k j > <k_{r+1},\ldots,k_j> < k r + 1 , … , k j > 和伪关键字< d r , … , d j > <d_r,\ldots,d_j> < d r , … , d j >
但子树的搜索代价是以其高度计算的,将子树插到原树的根节点的位置会导致高度增加1,因此需要计算子树插入原树导致的搜索代价增量,即w [ i ] [ j ] = 1 × ( ∑ l = i j p l + ∑ l = i − 1 j q l ) w[i][j]= 1 \times (\sum_{l=i}^{j}{p_l} + \sum_{l=i-1}^{j}{q_l}) w [ i ] [ j ] = 1 × ( ∑ l = i j p l + ∑ l = i − 1 j q l ) ,它也满足递归式w [ i ] [ j ] = w [ i ] [ r − 1 ] + w [ r + 1 , j ] + 1 × p r w[i][j] = w[i][r-1] + w[r+1,j] + 1 \times p_r w [ i ] [ j ] = w [ i ] [ r − 1 ] + w [ r + 1 , j ] + 1 × p r
令e[i][j]表示以关键字< k i , … , k j > <k_i,\ldots,k_j> < k i , … , k j > 和伪关键字< d i − 1 , … , d j > <d_{i-1},\ldots,d_j> < d i − 1 , … , d j > 组成的最优二叉搜索树的搜索代价则有:
e [ i ] [ j ] = { q i if i > j min i ≤ r ≤ j { e [ i ] [ r − 1 ] + e [ r + 1 ] [ j ] + w [ i ] [ j ] } if i ≤ j e[i][j] = \begin {cases} q_i & \text{if } i > j \\ \min_{i\leq r \leq j} \{e[i][r-1] + e[r+1][j] + w[i][j]\} & \text{if } i \leq j \end{cases} e [ i ] [ j ] = { q i min i ≤ r ≤ j { e [ i ] [ r − 1 ] + e [ r + 1 ] [ j ] + w [ i ] [ j ]} if i > j if i ≤ j
第3步:采用自底向上的方法计算最优解的值 考虑每个子树的关键字的起始下标是i,终止下标是j,则结点总数为l = j - i + 1
l=0,就是伪关键字,搜索代价为q[i-1] l=1,r只能是i(左0右0) l=2,r可以是i(左0右1),i+1(左1右0),其中结点总数为0、1的在之前已经算出 l=3,r可以是i(左0右2),i+1(左1右1),i+2(左2右0),其中结点总数为0、1、2的在之前已经算出 以此类推,直到l=n,其中长为0、1、2、3、…、n-1的在之前已经算出 第4步:利用结果构造得到最优解的过程 用一个二维数组root[i][j]记录划分子树选取的最佳根节点,那么从root[1][n]选取的划分根节点开始划分成左子树和右子树,然后再选取左子树的划分根节点和右子树的划分根节点,以此类推,直到无法划分
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def optimal (root, i, j ): if i == j + 1 : print ('d_' + str (j)) return r = root[i][j] print ('k_' + str (r)) optimal(root, i, r-1 ) optimal(root, r+1 , j) def OBST (p,q ): p = [0.0 ] + p length = len (q) w = [[0.0 for _ in range (length)] for _ in range (length)] e = [[0.0 for _ in range (length)] for _ in range (length)] root = [[0 for _ in range (length)] for _ in range (length)] for i in range (1 , length): e[i][i-1 ] = q[i-1 ] w[i][i-1 ] = q[i-1 ] for l in range (1 , length): for i in range (1 , length - l + 1 ): j = i + l - 1 e[i][j] = float ('inf' ) w[i][j] = w[i][j-1 ] + p[j] + q[j] for r in range (i, j+1 ): if r + 1 > j: min_e = e[i][r-1 ] + q[j] + w[i][j] else : min_e = e[i][r-1 ] + e[r+1 ][j] + w[i][j] if min_e < e[i][j]: e[i][j] = min_e root[i][j] = r optimal(root, 1 , 5 ) min_e = e[1 ][length-1 ] return min_e
3. 总结 计算机没有人类的思维,但是他有人类无可比拟的计算能力,也就是说,任何基础问题计算机理论上都可以穷举出结果,然而很多时候穷举是愚蠢的,计算机会花大量时间甚至90%的时间去穷举错误/不合理/不存在的情况,导致穷举的代价(时间和空间)变得巨大
因此算法实际上就是思考“如何聪明高效地穷举”,上述列出状态转移方程的本质就是找到一种合理的穷举方式
总的来说,我认为,解决动态规划问题,就是要构建出一张DP TABLE
DP TABLE的第0行和第0列代表着平凡情况,也就是可以直接判断,不需要递归分解的情况 DP TABLE通常是从左到右(一维),从左上到右下(二维)填充数据的 DP TABLE中任一元素的值通常取决于其左上部分元素的值 4. 经典DP 接下来的题目不会像之前那么详细地分析,只会给出题目描述,算法思路和具体代码
4.1 斐波那契数列 构建一张一维DP表,dp[i]的值取决于前几个元素即dp[i-1], dp[i-2]等
4.1.1 爬楼梯 问题描述:给定一个整数n表示楼梯级数,一个人每次能上1、2或3级,问走到n级有多少种走法
算法描述:令dp[i]表示走i级一共有的走法,则dp[i]=dp[i-3]+dp[i-2]+dp[i-1],且dp[1]=1,dp[2]=2,dp[3]=4
具体代码
1 2 3 4 5 6 7 8 9 10 11 def climb_stairs (n ): dp = [0 for _ in range (n + 1 )] dp[1 ] = 1 dp[2 ] = 2 dp[3 ] = 4 for i in range (4 , n + 1 ): dp[i] = dp[i-3 ] + dp[i-2 ] + dp[i-1 ] print (dp[n]) n = 5 climb_stairs(n)
4.1.2 解码数字串 问题描述:给定一个只包含数字的表示已编码的非空字符串s(长度大于等于2),使用如下映射进行解码:“1” -> ‘A’,“2” -> ‘B’,…,“26” -> ‘Z’,你需要计算并返回解码该字符串的所有可能方法的总数,并给出全部解码方案。(编码不包含前导0,即"06"不存在映射,但是"10"会映射到"J")
算法思路:令dp[i]表示从第1个字符到第i个字符的解码方案数量,对于当前字符str[i],分为三种情况:
str[i+1]是0,因此必须和这个0结合:dp[i]=dp[i-1],dp[i+1]=dp[i-1]; str[i-1]和str[i]组成的数字位于1~26:dp[i]=dp[i-1]+dp[i-2]; str[i]只能独自编码:dp[i]=dp[i-1]。 具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 def decode (string,mapping ): n = len (string) string = '#' + string dp = [0 for _ in range (n+1 )] dp[0 ] = 1 codes = [[] for _ in range (n + 1 )] codes[0 ] = ["" ] i = 1 while i != (n + 1 ): if string[i] == '0' : print ("can't decode!" ) return 0 if i != n and string[i+1 ] == '0' : dp[i] = dp[i-1 ] dp[i+1 ] = dp[i-1 ] for code in codes[i - 1 ]: codes[i].append(code + mapping[string[i:i+2 ]]) codes[i+1 ].append(code + mapping[string[i:i+2 ]]) i += 2 elif string[i-1 ] != '0' and string[i-1 ] != '#' and 1 <= int (string[i-1 :i+1 ]) <= 26 : dp[i] = dp[i-1 ] + dp[i-2 ] for code in codes[i - 1 ]: codes[i].append(code + mapping[string[i]]) for code in codes[i - 2 ]: codes[i].append(code + mapping[string[i-1 :i+1 ]]) i += 1 else : dp[i] = dp[i-1 ] for code in codes[i - 1 ]: codes[i].append(code + mapping[string[i]]) i += 1 print (f"{string[1 :n]} decode: {codes[n]} " ) return dp[n] string = '01101016' mapping = {} for i in range (1 , 27 ): mapping[str (i)] = chr (i + 64 ) print (f"there are {decode(string,mapping)} ways to decode {string} " )
4.2 回合制博弈 构建一张二维DP表,假定博弈双方都采取DP表的最优方案,当前回合的获胜情况取决于当前回合过后,对方回合的获胜情况
4.2.1 拿硬币 问题描述:假设有n个硬币,两个玩家 A 和 B 轮流从中拿硬币。每个玩家在自己的回合中可以选择从 1 到 k 个硬币(k 是一个固定的数且k<=n)进行拿取,拿完最后一个硬币的玩家获胜。如果A先拿,判断A是否必赢,如果必赢给出A必胜的方案
算法思路:令dp[i][j]表示己方还剩下i个硬币,最多可以拿j个硬币的情况下,赢1还是输0,如果至少存在一个k使得dp[i-k][j]=0,也就是存在一定击败对方的方法,那么己方就必赢,如果对全部k都有dp[i-k][j]=1,也就是无论怎么样对方都必赢,那么己方就必输
具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 def take_coin (n,k ): dp = [[-1 for _ in range (k+1 )] for _ in range (n+1 )] take = [[-1 for _ in range (k+1 )] for _ in range (n+1 )] for i in range (1 , n+1 ): for j in range (1 , k+1 ): if j >= i: dp[i][j] = 1 take[i][j] = i else : if all (dp[i-t][j] == 1 for t in range (1 , j+1 )): dp[i][j] = 0 else : for t in range (1 , j+1 ): if dp[i-t][j] == 0 : dp[i][j] = 1 take[i][j] = t break if dp[n][k] == 1 : print (f"There are {n} coins and people can only take 1-{k} coins at a time!" ) rest = n while 1 : a_take = take[rest][k] rest -= a_take print (f"A takes {a_take} and left {rest} " ) if rest == 0 : print ("A wins!" ) break b_take = int (input ("please enter amount that B takes: " )) rest -= b_take print (f"B takes {b_take} and left {rest} " ) if rest == 0 : print ("B wins!" ) break else : print ("A must lose!" ) take_coin(10 , 3 )
4.2.2 拿石子 问题描述:有n堆石头放在一行(n为偶数),第i堆石头一共有array[i]个石头,每次只能拿最左边石堆或最右边的石堆,最后谁拿的石头总数最多谁获胜。如果A先拿,给出A能拿到最多石头的方案,并判断最后是否能获胜
算法思路:令dp[i][j]只剩下从第i堆到第j堆石头时可以获取的最多石头数量,令sum表示从第i堆到第j堆石头的石头总数,如果拿最左边大则dp[i][j] = sum - dp[i+1][j],如果拿最右边大则dp[i][j] = sum - dp[i][j-1]
具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 def take_stone (array ): n = len (array) array = [0 ] + array dp = [[0 for _ in range (n + 1 )] for _ in range (n + 1 )] stone = [[-1 for _ in range (n + 1 )] for _ in range (n + 1 )] for l in range (1 , n + 1 ): for i in range (1 , n - l + 2 ): j = i + l - 1 if i == j: dp[i][j] = array[i] else : total = sum (array[i:j+1 ]) left = total - dp[i+1 ][j] right = total - dp[i][j-1 ] if left > right: dp[i][j] = left stone[i][j] = 0 else : dp[i][j] = right stone[i][j] = 1 left = 1 right = n while left <= right: print (f"there are {array[left:right+1 ]} " ) direct_A = stone[left][right] if direct_A == 1 : print (f"A takes {array[right]} " ) right -= 1 else : print (f"A takes {array[left]} " ) left += 1 print (f"there are {array[left:right+1 ]} " ) direct_B = int (input ("enter your choice (0--left and 1--right): " )) if direct_B == 1 : print (f"you take {array[right]} " ) right -= 1 else : print (f"you take {array[left]} " ) left += 1 print (f"A got {dp[1 ][n]} and you got {sum (array) - dp[1 ][n]} " ) print ("A wins" if dp[1 ][n] > (sum (array) - dp[1 ][n]) else "you win" ) array = [28 , 15 , 1 , 33 , 24 , 9 , 17 , 10 ] take_stone(array)
4.3 背包问题 这类问题通常给定一个限制,然后每个元素都可以选择执行或不执行,构建一张二维DP表,其中行遍历元素个数,列遍历限制数
4.3.1 0/1背包 问题描述:给定n个物品,每个物品都有体积volume和价值value,目标是找到能够装入固定容量为c的背包中的最大价值,其中每个物品只能选择放入背包或不放入
算法思路
按顺序排好物体,dp[i][j]表示容量为i时将前j个物品放入背包的最大价值,其中i的取值为[1,c],j的取值为[1,n],那么结果就是dp[n][c] 先遍历容量,再遍历物品,假设此时已经遍历到dp[i][j],对于第j个物体如果它的体积已经大于背包容量,那么肯定无法放下,则dp[i][j]=dp[i][j-1] 如果它的体积小于等于背包容量,那么可以选择放和不放,放的话就是剩下体积放前j-1个物品的最大价值加上第j个物品的价值,因此dp[i][j]=max(dp[i][j-1],dp[i-volume(j)][j-1]+value(j)) 具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 def Knapsack (volume, value, n, c ): dp = [[0 for _ in range (n+1 )] for _ in range (c+1 )] index = [[[] for _ in range (n+1 )] for _ in range (c+1 )] for i in range (1 , c+1 ): for j in range (1 , n+1 ): if volume[j] > i: dp[i][j] = dp[i][j-1 ] index[i][j] = index[i][j-1 ] else : not_put = dp[i][j-1 ] put = dp[i-volume[j]][j-1 ] + value[j] if not_put > put: index[i][j] = index[i][j-1 ] dp[i][j] = not_put else : index[i][j] = index[i-volume[j]][j-1 ] index[i][j].append(j) dp[i][j] = put print (dp[c][n]) print (index[c][n]) n = 5 c = 10 volume = [0 ,5 ,4 ,3 ,2 ,1 ] value = [0 ,1 ,2 ,3 ,4 ,5 ] Knapsack(volume, value, n, c)
4.3.2 最小划分数组 问题描述:给出一个正整数数组,把它分成S1、S2两部分,使S1的数字和与S2的数字和的差的绝对值最小
算法思路
已知SUM=S1+S2是不变的,令|S1-S2|最小,即令|S1-(SUM-S1)|最小,即令|2S1-SUM|最小,即令|S1-SUM/2|最小,且S1和S2中肯定有一个小于等于SUM/2,不妨令S1小于等于SUM/2,即在S中找到一个子集S1,使得S1尽可能接近SUM/2,因此对于每个元素,他要么放进S1,要么不放进S1,可以看成类背包问题 令dp[i][j]表示前j个元素最接近i的和,则结果就是dp[sum/2][n] 具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def minimum_partition (array,n ): sumnum = sum (array) total = sumnum // 2 s1 = [[[] for _ in range (n+1 )] for _ in range (total+1 )] dp = [[0 for _ in range (n+1 )] for _ in range (total+1 )] for i in range (1 ,total+1 ): for j in range (1 ,n+1 ): if array[j] > i: dp[i][j] = dp[i][j-1 ] s1[i][j] = s1[i][j-1 ][:] else : not_put = dp[i][j-1 ] put = dp[i-array[j]][j-1 ] + array[j] if not_put > put: dp[i][j] = not_put s1[i][j] = s1[i][j-1 ][:] else : dp[i][j] = put s1[i][j] = s1[i-array[j]][j-1 ][:] s1[i][j].append(array[j]) print (abs (2 * dp[total][n] - sumnum)) print (s1[total][n]) array = [0 , 1 , 6 , 11 , 5 ] minimum_partition(array,4 )
4.3.3 子集和 问题描述:给定一个非负整数的集合S,一个值M,问S中是否有一个子集,使得子集和等于M(注意集合里不存在相同的元素)
算法思路:令dp[i][j]表示前j个数是否可以刚好凑成i
对于i=0,平凡凑成 如果array[j]>i,则dp[i][j]=dp[i][j-1] 如果array[j]<=i且dp[i-array[j]][j-1]==1,即用前j-1个数正好也可以凑成i-array[j],则dp[i][j]=1,且往后可以不用计算了,即使可能存在不同方案 如果array[j]<=i且dp[i-array[j]][j-1]==0,即用前j-1个数不能凑成i-array[j],则dp[i][j]=0 具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def subsetsum (array,key ): n = len (array) array = [0 ] + array dp = [[0 for _ in range (n+1 )] for _ in range (key+1 )] num = [[[] for _ in range (n+1 )] for _ in range (key+1 )] for j in range (n+1 ): dp[0 ][j] = 1 for i in range (1 ,key+1 ): for j in range (1 ,n+1 ): if array[j] > i: dp[i][j] = dp[i][j-1 ] elif dp[i-array[j]][j-1 ] == 1 : for k in range (j,n+1 ): dp[i][k] = 1 num[i][k] = num[i-array[j]][j-1 ][:] num[i][k].append(array[j]) break if dp[key][n] == 1 : print (f"there is a subset {num[key][n]} whose sum is {key} !" ) else : print (f"there is no subset whose sum is {key} !" ) array = [6 , 2 , 9 , 8 , 3 , 7 ] print (f"set is {array} " )subsetsum(array,17 ) subsetsum(array,26 ) subsetsum(array,31 ) subsetsum(array,33 )
4.4 最优选择策略 这类问题通常每种情况下有几个选项可供选择,dp[i]的值往往与其相邻元素有关
4.4.1 最小路径和 问题描述:给定一个包含非负整数的二维网格grid,每个格子中的数值表示从该位置到其相邻格子的移动成本,只允许向下或向右移动,找到从左上角到右下角的最小路径和
算法思路:令dp[i][j]表示从左上角到(i,j)的最小路径和,因为只允许向下或向右移动,也就是说dp[i][j] = max(dp[i-1][j] + cost[i-1][j], dp[i][j-1] + cost[i][j-1]),同时,最上面一行和最左边一列的dp是可以直接确定的
具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def MinimumPath (array,row,col ): dp = [[0 for _ in range (col)] for _ in range (row)] direct = [['' for _ in range (col)] for _ in range (row)] direct[0 ][0 ] = 'begin->' for j in range (1 , col): dp[0 ][j] = dp[0 ][j-1 ] + array[0 ][j-1 ] direct[0 ][j] = direct[0 ][j-1 ] + 'right->' for i in range (1 , row): dp[i][0 ] = dp[i-1 ][0 ] + array[i-1 ][0 ] direct[i][0 ] = direct[i-1 ][0 ] + 'down->' for i in range (1 , row): for j in range (1 , col): down = dp[i-1 ][j] + array[i-1 ][j] right = dp[i][j-1 ] + array[i][j-1 ] if down < right: dp[i][j] = down direct[i][j] = direct[i-1 ][j] + 'down->' else : dp[i][j] = right direct[i][j] = direct[i][j-1 ] + 'right->' direct[-1 ][-1 ] = direct[-1 ][-1 ] + 'end' print (direct[-1 ][-1 ]) print (dp[-1 ][-1 ]) array = [ [1 , 3 , 4 ], [2 , 5 , 9 ], [7 , 6 , 0 ] ] MinimumPath(array,3 ,3 )
4.4.2 股票买卖 问题描述:给定一个数组price,其中price[i]表示第i天的股票价格,可以进行多次交易,但是每次交易必须先买入然后卖出,通过选择合适的买入和卖出时机,计算在第n天可以获得的最大利润,注意不可以在同一天进行买入和卖出,同时认为每次买入和卖出的量都是1
算法思路:dp[i]表示第i天获得的最大收益,假设已经遍历到第i天,那么可以两种选择,选择其中的较大值
不进行任何操作,那么dp[i]=dp[i-1] 卖股票,那么就要在之前选择一天j买股票,那么dp[i]=dp[j-1]+price[i]-price[j] 具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def stock_jobbing (price,n ): dp = [0 for _ in range (n + 1 )] deal = [[] for _ in range (n + 1 )] for i in range (1 ,n+1 ): not_sold = dp[i-1 ] sold = -float ('inf' ) buy_day = -1 for j in range (1 ,i): temp = dp[j-1 ] + price[i] - price[j] if temp > sold: sold = temp buy_day = j if sold > not_sold: dp[i] = sold deal[i] = deal[buy_day-1 ] deal[i].append((buy_day,i)) else : dp[i] = not_sold deal[i] = deal[i-1 ] print (dp[n]) print (deal[n]) price = [0 , 7 , 1 , 5 , 3 , 6 , 4 ] n = 6 stock_jobbing(price, n)
4.4.3 编辑距离 问题描述:给定两个字符串 word1 和 word2,计算将 word1 转换为 word2 所需的最小编辑操作次数,允许的操作有插入一个字符、删除一个字符或替换一个字符。示例:intention->替换i为e->entention->删除t->enention->替换n为c->enection->替换n为x->exection->插入u->execution,一共5次(其中一种)
算法思路:
令dp[i][j]表示将word1(长度为n)的前i个字符变为word2(长度为m)的前j个字符所需要的最小编辑次数,则最终结果就是dp[n][m] 假设遍历到dp[i][j],我们保证知道dp[0~i][0~j]的值,也就是说我们可以认为我们知道:如何最简单地将word1[1:i]转换为word2[1:j-1],那么只需要在word1的最后添加一个word2[j]就行了:dp[i][j] = dp[i][j-1] + 1 如何最简单地将word1[1:i-1]转换为word2[1:j],那么只需要在word1的最后删除一个word2[j]就行了:dp[i][j] = dp[i-1][j] + 1 如何最简单地将word1[1:i-1]转换为word2[1:j-1],那么只需要在word1的最后把word[i]变为word[j]就行了:dp[i][j] = dp[i-1][j-1] + 1 考虑平凡情况dp[i][0] = i:删除word1的全部前i个字符 dp[0][j] = j:插入word2的全部前j个字符 dp[i][j] = dp[i-1][j-1]:字符一样,不需要更改,word[i] = word[j] 具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 def edit_distance (word1, word2 ): n = len (word1) m = len (word2) word1 = '0' + word1 word2 = '0' + word2 dp = [[0 for _ in range (m + 1 )] for _ in range (n + 1 )] operations = [[[] for _ in range (m + 1 )] for _ in range (n + 1 )] for i in range (1 , n + 1 ): dp[i][0 ] = i operations[i][0 ].append("Delete " + word1[i]) for j in range (1 , m + 1 ): dp[0 ][j] = j operations[0 ][j].append("Insert " + word2[j]) for i in range (1 , n + 1 ): for j in range (1 , m + 1 ): if word1[i] == word2[j]: dp[i][j] = dp[i - 1 ][j - 1 ] operations[i][j] = operations[i - 1 ][j - 1 ] else : insert = dp[i][j - 1 ] + 1 delete = dp[i - 1 ][j] + 1 replace = dp[i - 1 ][j - 1 ] + 1 if insert <= delete and insert <= replace: dp[i][j] = insert operations[i][j] = operations[i][j - 1 ][:] operations[i][j].append("Insert " + word2[j]) elif delete <= insert and delete <= replace: dp[i][j] = delete operations[i][j] = operations[i - 1 ][j][:] operations[i][j].append("Delete " + word1[i]) else : dp[i][j] = replace operations[i][j] = operations[i - 1 ][j - 1 ][:] operations[i][j].append("Replace " + word1[i] + " with " + word2[j]) print ("edit distance:" , dp[n][m]) print (f"from {word1[1 :]} to {word2[1 :]} opearations:" ) for op in operations[n][m]: print (op) word1 = "intention" word2 = "execution" edit_distance(word1, word2)
4.5 子序列 这类问题的做法比较统一,就是先从小到大遍历长度l,然后dp[i][j]表示不同的起点i和终点j,根据长度l填充表
4.5.1 最长回文子序列(Longest Palindrome Subsequence,LPS) 问题描述:回文子序列是指正着读和反着读都相同的子序列,给定一个字符串,求该字符串的最长回文子序列的长度
算法思路:令dp[i][j]表示从i到j子串的最长回文子序列,对于长度为l且从i到j的子串,如果i和j相同,则dp[i][j] = dp[i+1][j-1] + 2,否则dp[i][j] = max(dp[i+1][j],dp[i][j-1])
具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def LPS (str ): n = len (str ) str = '0' + str char = [[str [k] for k in range (n+1 )] for _ in range (n+1 )] dp = [[1 for _ in range (n+1 )] for _ in range (n+1 )] for l in range (2 , n + 1 ): for i in range (1 , n - l + 2 ): j = i + l - 1 if str [i] == str [j]: dp[i][j] = dp[i+1 ][j-1 ] + 2 char[i][j] = str [i] + char[i+1 ][j-1 ] + str [j] else : if dp[i+1 ][j] > dp[i][j-1 ]: dp[i][j] = dp[i+1 ][j] char[i][j] = char[i+1 ][j] else : dp[i][j] = dp[i][j-1 ] char[i][j] = char[i][j-1 ] print (char[1 ][n]) print (dp[1 ][n]) str = 'zabxcdyefxghaz' LPS(str )
4.5.2 最长上升子序列(Longest Increasing Subsequence,LIS) 问题描述:给定一个长度为n的数组,找出一个最长的单调递增子序列
算法思路:令dp[i][j]表示从i到j的子串的最长单调递增子序列,有两种情况,选择两者中的最大值
array[j]作为最后的数字,找到所有小于array[j]的索引k,则dp[i][j] = max(dp[i][k] + 1)对于i<=k<j array[j]不作为最后的数字,则dp[i][j]=dp[i][j-1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def LIS (array ): n = len (array) array = [-1 ] + array dp = [[0 for _ in range (n+1 )] for _ in range (n+1 )] sequence = [[[] for _ in range (n+1 )] for _ in range (n+1 )] for l in range (1 ,n+1 ): for i in range (1 ,n - l + 2 ): j = i + l - 1 if i == j: dp[i][j] = 1 sequence[i][j].append(array[i]) else : islast = -float ('inf' ) index = -1 for k in range (i,j): if array[k] < array[j] and islast < (dp[i][k] + 1 ): islast = dp[i][k] + 1 index = k notlast = dp[i][j-1 ] if islast > notlast: dp[i][j] = islast sequence[i][j] = sequence[i][k][:] sequence[i][j].append(array[j]) else : dp[i][j] = notlast sequence[i][j] = sequence[i][j-1 ][:] print (f"LIS is {sequence[i][j]} whose length is {dp[1 ][n]} " ) array = [4 ,5 ,1 ,7 ,3 ,8 ,9 ] print (array)LIS(array) array = [3 ,2 ,7 ,1 ,5 ,4 ,6 ,9 ,8 ,0 ,10 ] print (array)LIS(array)