4.编辑距离问题。假设有两个单词,有三种编辑的手段:插入一个字符,替换一个字符,或者删除一个字符,从word1到word2,每用一次编辑算作一个编辑距离。给出word1和word2,求word1到word2的最短编辑距离。
【解】还是沿用之前的思维方式,首先确定用dp,因为是最优解问题,而且子问题之间有明显关联。然后确定dp的含义,有两个单词,那么dp[i][j]就是word1从头到i,到word2从头到j的index从i到j的最短距离。
公式的方向很重要,从当前推下一个有点麻烦,因为不知道是word1的长度加1还是word2的长度加1,或者都加。那么就从上一个推当前,因为只有三种编辑方式,那么分别使用三种方法,对应的初始的字符串肯定也不是一样的。
那么从之前到当前,也就是dp[i][j],根据三种操作分别来看:
替换:只用替换,两边长度同时加1,假如word1的i和word2的j字符相等,那么就是dp[i-1][j-1],否则 dp[i-1][j-1 ]+1;
删除:即删除word1的一个字符,能保证得到word2到j,这说明dp[i-1][j]时就已经满足了,然后删除多余的i字符,亦即dp[i-1][j]+1;
插入:依据之前的理解,插入之前word1长度已经是i,word2长度差1位,因此插入一位正好,dp[i][j-1]+1.
然后取最小值即可。
至于初始值,一个word为空的情况下,另外一个只能是删除,反过来也只能是插入,因此初始的就是从0开始直到非空的那个word的长度:
0 1 2 3 4
1
2
3
类似这种。
# Time: O(n * m)
# Space: O(n * m)
class example4:
# @return an integer
def minDistance(self, word1, word2):
distance = [[i] for i in range(len(word1) + 1)]
distance[0] = [j for j in range(len(word2) + 1)]
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
insert = distance[i][j - 1] + 1
delete = distance[i - 1][j] + 1
replace = distance[i - 1][j - 1]
if word1[i - 1] != word2[j - 1]:
replace += 1
distance[i].append(min(insert, delete, replace))
return distance[-1][-1]
if __name__ == "__main__":
print(example4().minDistance("Rabbit", "Racket"))#3
这段代码需要一点点解释:首先之所以distance的长度是word1的长度+1,是因为0也包括在内。用for的方式而不是之前的*来生成二维矩阵,是因为矩阵行的引用问题。
优化:之所以把优化单独拿出来,是因为这个优化没有之前的那么简单。从矩阵的角度而言,计算下一个值需要其左边、上边和左上的三个值,假如用之前的滚动dp,左边的和上边的解决了,但是左上角的如何解决呢?对了,我们可以用一个变量把它存起来,每计算一次更新一次:
# Time: O(n * m)
# Space: O(n + m)
class example4:
# @return an integer
def minDistance(self, word1, word2):
if len(word1) < len(word2):
return self.minDistance(word2, word1)
distance = [i for i in range(len(word2) + 1)]
for i in range(1, len(word1) + 1):
pre_distance_i_j = distance[0]
distance[0] = i
for j in range(1, len(word2) + 1):
insert = distance[j - 1] + 1
delete = distance[j] + 1
replace = pre_distance_i_j
if word1[i - 1] != word2[j - 1]:
replace += 1
pre_distance_i_j = distance[j]
distance[j] = min(insert, delete, replace)
这样下来空间节省的其实不少。另外可以看到,优化只是对计算优化,由于按照一定顺序计算,可以少用一些空间,本质上来说两种解法是一样的。
5.地下城游戏。骑士K要去救被恶魔囚禁在地下城的公主P,假设地下城是一个矩阵,里面充满各种陷阱和道具,每一个格子都会有陷阱或者道具,骑士遇到陷阱会扣去矩阵该单元标定的生命值,同样遇到道具会增加生命值。骑士生命值不能降到0或者小于0,否则游戏失败。所有值都是整数。骑士从左上角开始,公主在右下角,骑士走到右下角并且存活就算成功救出公主。现在给定一个矩阵,求骑士最少需要多少初始生命值才能救出公主?一开始进去的那格和最后那格的陷阱或者道具正常发动。
【解】这种一步一步的场景很适合dp发挥。要求的是初始生命,而已知的是要救出公主,从已知向未知推,肯定是从最后一格向左上角推。dp[i][j]的含义就设定为进入[i,j]之前所需的最小生命值,那么题目要求的就是dp[0][0]。
递推公式,这个一眼看不出来,从简单情况进行分析:
用d[i][j]代表mxn矩阵值,正的加负的扣。因为K的生命值总是大于等于1的,所以只要d[i][j]>=0,K就可以1血进来;否则,需要能承受伤害还保证1血,即1-d[i][j]。写成一个公式就是dp[m][n]=max(1,1-d[i][j])。
然后呢?对于相邻的两格,要能够保证至少有这么多生命出来。因为dp是分成子问题看的,可能实际上两条路只走一条,但计算的时候是分别计算,最后取最优即可。
先算左边的那个格子。之前最后一格只要保证留1血就行,这里不一样,但非常类似。dp[m][n-1]+d[m][n-1]>=dp[m][n],得到dp[m][n-1]>=dp[m][n]-d[m][n-1],同样还有dp[m][n-1]>=1,亦即dp[m][n-1]=max(1, dp[m][n ]-d[m][n-1 ] )。
那么,最后一行的问题解决了,往上走呢?最后一列往上走,公式应该是类似的: dp[m-1][n]=max(1, dp[m][n ]-d[m-1][n ] )。
那么问题解决了吗?并没有,因为前面说了,骑士很多时候有两种选择,如何选择还没有体现出来。
比如d[m-1][n-1]这一格,有两条路可以选择,选择让初始生命最小的:dp[m-1][n-1]=min(max(1,dp[m-1][n]-d[m-1][n-1]),max(1,dp[m][n-1]-d[m-1][n-1]))。
公式都写的差不多了,那么可以进行计算和优化了。由于只牵扯到两个值,可以一行一行来计算,也就是rolling dp:首先向左计算整行,然后往上翻一行再计算。公式的选择可以用条件语句来选择,或者这样:
# Time: O(m * n)
# Space: O(m + n)
class exmaple5:
# @param dungeon, a list of lists of integers
# @return a integer
def calculateMinimumHP(self, dungeon):
DP = [float("inf") for _ in dungeon[0]]
DP[-1] = 1
for i in reversed(range(len(dungeon))):
DP[-1] = max(DP[-1]- dungeon[i][-1], 1)
for j in reversed(range(len(dungeon[i]) - 1)):
min_HP_on_exit = min(DP[j], DP[j + 1])
DP[j] = max(min_HP_on_exit - dungeon[i][j], 1)
return DP[0]
if __name__ == "__main__":
dungeon = [[ -2, -3, 3],
[ -5,-10, 1],
[ 10, 30, -5]]
可以看到,首先设置inf初始值解决最后一排的选择问题,由于到某格的时候设置成无穷大,当然只能选择前一格,那也就是之前的公式;之后每行手动设置最后一列值,然后进行选择。这里使用了一个min_HP_on_exit作为中间值来简化问题,因为这个格子本身的值是固定的,那么右边和下边两个格子,谁要求越少谁就是最优解,然后根据这个值来确定dp[j],这样在中间就做出了选择,比原来的公式简便很多。
6.打气球游戏。假设有一排气球,上面有正整数值,每打爆一个,获得其紧挨着的左边的气球的值乘以被打爆的气球的值再乘以右边的气球的值的硬币,假如左边或者右边没有气球则以1代替。
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] ,
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
现给出nums,求能获得的最大coin数。
【解】这道题也符合之前说过dp的标准,dp[i][j]就设置为从i到j范围内能获得的最大coin数。由于没有气球用1代替,
为了简化问题,在nums两边加上1,然后求dp[0][n-1],此时不包括0和n-1。
然后就是公式推导了。第一枪打下去,假设第i个气球爆了,那么就是nums[i]*nums[i-1]*nums[i+1]。但是有一个问
题,就是之后不知道左边和右边的是哪个,因为可能原来左边右边的已经被打爆了。假如要写一个函数再去记录和寻找,
就有点复杂了。
那么换个思路,从最后一个气球开始看,往前推。
由于dp[i][j]是不包含边界的,可以认为边界的两个气球一直都在,那么最后一枪获得的coin就是
nums[x]*nums[i]*nums[j]。当然这还不够,因为定义dp是包含之前的。
之前的如何计算?可以想象最后一个气球把原来的一排分成两段,再加上这两段获得的分数即可:
dp[i][j]= nums[x ]*nums[i]*nums[j ]+dp[i][x]+dp[x][j],i<x<j
这里有个问题,就是怎么选择x。之前的选择方法都是取最优值,这里也是如此。不过选择不同的x,就需要不同dp值,
而计算这些dp值又需要更前面的dp值。这种计算思路很类似于斐波拉契数列,因为计算Fn需要Fn-1和Fn-2,而计算这
两个又需要之前的值,那么干脆就从前面往后计算。
对于这个问题,因为不包含边界,最小也要让里面有一个气球,即j-i=2,最大自然是全部包进去。这样从最小的开始
计算,然后存起来,之后计算更大的时候需要再拿出来。就像建金字塔,底层的是更上层的基础。
# Time: O(n^3)
# Space: O(n^2)
class example6(object):
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
coins = [1] + [i for i in nums if i > 0]+ [1]
n = len(coins)
max_coins = [[0 for _ in range(n)] for _ in range(n)]
for k in range(2, n):
for left in range(n - k):
right = left + k
for i in range(left + 1, right):
max_coins[left][right]= max(max_coins[left][right],
coins[left] * coins[i] * coins[right]+
max_coins[left][i] + max_coins[i][right])
return max_coins[0][-1]
if __name__ == '__main__':
print(example6().maxCoins([3, 1, 5, 8])) #167