【动态规划】总结

概念

动态规划,英文:Dynamic Programming,简称DP。如果当前状态可以由之前的状态推导出来,那么这个问题可以用dp解决

五部曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

背包问题

来自Carl哥的代码随想录的图片
在这里插入图片描述

01 背包

一般来说给你一组物品/一组数,每个都有且只有选/不选两种选择,并且每次只能取一个,那么就可以用01背包来解决。

dp含义

常见的dp[i][j]的含义是从0-i个数字/物品中选择,装到体积/容量/上限为j的包里,取得的最大价值/最多种取法/最少种取法

递推公式

  • 求价值最大:
    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
    前者相当于不选下标为i的物品的价值,后者相当于在满足背包当前容量j的情况下,选当前物品的价值(容量固定、选了当前物品,那么肯定就得往前推到体积为j - weight[i]的状态)

  • 求装满背包有几种方法(排列组合有多少种取法):
    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
    一共j个数,每个数的体积为nums[i],假设我不取当前第i个数,那么就直接继承 dp[i-1][j],如果取了当前的数,那么我又会多出几种方法呢,需要dp[j-nums[i]]个数。

举个例子:dp[j],j 为5,
已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包
已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包
已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

  • 能否装满背包(最多装多少):
    dp[i][j] = max(dp[i][j], dp[i-1][j-nums[i]]+nums[i])

  • 装满背包所有物品的最小个数
    dp[i][j] = min(dp[i][j], dp[i-1][j-nums[i]]+1)

初始化

初始化需要根据递推公式来判断,如果递推公式中出现[i-1]的索引(通常是二维dp的写法),那么一定是从1开始,因为如果从0开始的话,dp[-1]并不越界(python),但是它却表示最后一个值,因此可能会出问题。因此要具体情况具体分析,尽量不出现索引是-1的情况。
此外,无论是一维dp还是二维dp,都要注意dp[0]、dp[0][0]的初始化,要具体情况具体分析,不一定要解释它的具体含义,而是保证递推公式正常计算为主!

确定遍历顺序

二维dp:物品[i]、体积[j]都是正序遍历,并且二者可以交换顺序【排列和组合不能调换顺序】。
一维dp:物品[i]是正序遍历,体积[j]是【倒序】遍历(因为要保证每个东西最多取一次,如果正序遍历则前面的值已经改变了,再在这个基础上递推的话,那就相当于取了多次了),并且二者不可以交换顺序。

code实例

  • 二维dp
    初始化
    在这里插入图片描述
# 初始化,先创建二维空间,然后初始化第一行
dp = [[0 for j in range(maxweight+1)] for i in range(n)]
for j in range(maxweight+1):
    if j>=weight[0]: dp[0][j] = value[0]

# 先遍历物品,再遍历背包体积
for i in range(1,n):  # 注意i从1开始遍历
    for j in range(maxweight+1):
        if j>=weight[i]:
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
        else:
            dp[i][j] = dp[i-1][j]

# 先遍历背包体积,再遍历物品
# 交换遍历顺序,不影响,因为当前值取决于左上角的值
for j in range(maxweight+1):
    for i in range(1, n):
        if j>=weight[i]:
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
        else:
            dp[i][j] = dp[i-1][j]
  • 一维dp
# 初始化,只需要创建空间并设为0即可,
# 不需要对第一行额外初始化,这是区别于二维dp的一点
dp = [0 for j in range(maxweight+1)]
# i从【0】开始遍历了
for i in range(n):
	# j是【倒序】遍历,并且只需要到weight[i]即可,小于直接继承
    for j in range(maxweight,weight[i]-1,-1):
        dp[j] = max(dp[j], dp[j-weight[i]]+value[i])

一维dp不可以交换物品和背包数量,因为如果背包体积在外循环,物品在内循环,那么当前体积下,后一个物品就会覆盖前一个物品,每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品

完全背包

与01背包的区别

  • 与01背包不同的是:每个物品可以取无数次
  • 区别只在于遍历顺序:(1)前面提到,01背包在一维dp时倒序遍历j,是为了防止利用已经更新的值再更新,这样就相当于重复取了,因此要倒序遍历;(2)此外01背包在一维dp时不能交换遍历顺序,只能先遍历物品,再遍历背包体积,而完全背包可以交换二者顺序。

code实例

  • 一维dp
# 先遍历物品,再遍历背包体积
for i in range(n):
	# 和01背包遍历的不同,这里是正序遍历,因为可以取无数个物品
    for j in range(weight[i],maxweight+1):   
        dp[j] = max(dp[j], dp[j-weight[i]]+value[i])

# 先遍历背包体积,再遍历物品
# 注意区分和上面的j遍历,这个必须从0开始遍历,因为要遍历所有的体积
for j in range(maxweight+1):  
    for i in range(n):
        if j>=weight[i]:
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
  • 二维dp
    比较麻烦。实际和一维dp一样。
# 初始化,创建dp空间并初始化第一行
dp = [[0 for j in range(maxweight + 1)] for i in range(n)]

for j in range(maxweight + 1):
    if j >= weight[0]:
        dp[0][j] = dp[0][j-weight[0]] + value[0]

for i in range(1, n):
    for j in range(1, maxweight + 1):
        if j >= weight[i]:
        	#  这里改成dp[i][j - weight[i]] + value[i],只要每次取前面更新过的值就行
            dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
        else:
            dp[i][j] = dp[i - 1][j]

背包中的排列与组合问题

  • 初始化dp数组全为0,dp[0]须为1

dp[j] += dp[j-weight[i]] 求解排列组合的递推公式,在前面01背包的地方提到了
装满背包有几种方法?

# 先遍历物品,再遍历背包体积 =》 求【组合数】
for i in range(n):
	# 注意区分和二维dp的遍历,以及和01背包遍历的不同,这里是正序遍历,因为可以取无数个物品
    for j in range(weight[i],maxweight+1):   
        dp[j] += dp[j-weight[i]]

# 先遍历背包体积,再遍历物品 =》 求【排列数】
for j in range(maxweight+1):  # 注意区分和上面的j遍历,这个必须从0开始遍历
    for i in range(n):
        if j>=weight[i]:
            dp[j] += dp[j-weight[i]]

排列可以重复,组合不可以重复,比如{1,2}和{2,1}算作两个排列,但是是同一个组合。
先遍历物品,后遍历背包体积时,1后面可以出现2,而2后面不能出现1,所以只会有{1,2}这种情况,不会出现{2,1},适用于组合问题
在这里插入图片描述

同理,先遍历背包体积时,后遍历物品时,根据遍历顺序,2后面可以出现1的情况,因此{1,2}和{2,1}都能出现。适用于排列问题
在这里插入图片描述
上面两幅图标明了遍历顺序,可以很明显看出两种遍历方式的区别。

多重背包

每种物品不能无限取,最多取Ci件,可以看作变种01背包问题,只需要将Ci件i号物品全部展开,看作Ci种物品。

打家劫舍问题

问题描述

给一个数组相邻之间不能连着偷,如何偷才能得到最大金钱
变种:

  • 房子之间首位相连,那么就分两种情况:从第一个开始,忽略最后一个,即nums[:len(nums)];忽略第一个,直到最后一个,即nums[1:]。
  • 房子以二叉树的方式遍历:那么就考虑两种情况:偷当前节点,跳过左右节点;不偷当前节点,偷左右节点,并取左右节点偷到的最大值作为当前节点的dp值,这个逻辑就是后续遍历

dp数组含义

dp[i] 表示下标0-i(包括i)的房屋能取得的最大值

递推公式

因为不一定非要隔一个房间偷一次,我们只要保证值最大、且相邻房间不同时偷就可以。递推公式如下:
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
有两种选择:

  • 当前节点不偷,那么就保持上一个节点的值dp[i-1],但上一个节点偷或不偷,我们完全不用在意【假设上个 节点偷了,那么当前节点不偷,符合规则;假设上个节点不偷,那么当前节点可以不偷,也符合规则】;
  • 当前节点偷,那么就得忽略上一个节点,从i-2这个节点考虑,dp[i-2]+nums[i]

这个递推公式可以随着题目变种而改变,比如某团笔试题分糖问题规定:取了当前节点后,前后四个节点都不能再取。那么就相当于必须相隔2个间隙,所以如果当前节点取了,只需要从i-3这个节点考虑就可以了,即dp[i-3]+nums[i]

初始化

根据递推公式,需要知道i-1i-2的dp值,因此必须给出dp[0]dp[1]的值。
dp[0]不必说,肯定是取0号房间的值才能最大。
dp[1]表示0-1房间能偷到的最大值,那么肯定是从房间0和1中取最大值了。

遍历顺序

根据递推公式,i至少从2开始遍历才不会越界,并且是正向遍历。

举例

在这里插入图片描述

code实例

n = len(nums)
dp = [0] * n
if n<3:
    return max(nums)
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2,n):
    dp[i] = max(dp[i-1], dp[i-2]+nums[i])

股票问题

推荐阅读【美国站老哥题解】及【国内翻译】
Most consistent ways of dealing with the series of stock problems
股票问题系列通解(转载翻译)

通用问题描述

给定一个整数数组 prices ,它的第i个元素 prices[i] 是一支给定的股票在第i天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成k笔交易。
【注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。】
根据k的不同,可以将问题分为以下几种:

dp含义

首先,我们定义两种状态:持有股票不持有股票

  • 持有股票又包含两种状态:第i天买入股票->第i天持有股票;第i天没买股票,但i-1天持有股票->第i天持有股票。
  • 不持有股票也包含两种状态:第i天卖出股票->第i天不持有股票;第i天啥也没干,但i-1天就不持有股票->第i天不持有股票。

我们以k∀N这个一般情况为例,

dp[j][i]表示第i天,持有/不持有股票的状态下持有的现金

其中j为奇数时表示持有股票,j为偶数时表示不持有股票。

一个k对应两个j,即j=1表示第一次交易持有股票,j=2表示第一次交易不持有股票(之前一定买入股票,这次卖掉)。
同理,j=3表示第二次交易持有股票(之前一定买入并卖出了一次股票,这次新买入一只股票),j=4表示第二次交易不持有股票(之前卖出了一次股票又买入了新的股票,这次卖掉了)。
j=0是一个特殊状态,表示既不买入也不卖出,所以初始化为0,这个主要是为了后面方便递推,以及写成统一形式。

递推公式

根据前面的介绍,可以写出下面的递推公式

for i in range(1,n):
    for j in range(1,k+1):
    	# 第k次持有股票状态下第i天的现金 dp[2*j-1][i]
    	# 昨天持有股票,今天不买新的(昨天有啥今天就有啥)dp[2*j-1][i-1]
    	# 昨天不持有股票,今天买新的 dp[2*j-2][i-1]-prices[i]
        dp[2*j-1][i] = max(dp[2*j-1][i-1], dp[2*j-2][i-1]-prices[i])
        # 第k次不持有股票状态下第i天的现金 dp[2*j][i]
        # 今天没得卖(昨天就没有股票)dp[2*j][i-1]
        # 今天把昨天持有股票的卖了dp[2*j-1][i-1]+prices[i]
        dp[2*j][i] = max(dp[2*j][i-1], dp[2*j-1][i-1]+prices[i])

初始化

根据递推公式,需要用到dp[0][:]以及dp[2*j-1][0]的值,因此要先定义好,dp[0][:]根据定义全部设为0,表示不买入也不卖出的现金为0。dp[2*j-1][0]表示头天持有股票的现金(无论第几次买入),全部初始化为当天的股票价格的负数,因为买入股票花钱了嘛。

dp = [[0 for _ in range(n)]for _ in range(2*k+1)]
for j in range(1,k+1):
	dp[2*j-1][0] = -prices[0]

遍历顺序

根据递推公式,外层循环按照天数遍历,内层循环按照交易次数遍历,全部正序遍历。

其他变种

  • k=1,不需要遍历天数了,此外由于最多买入一次,所以购买股票当天现金一定为0-prices[i],因为前一天不可能有利润。
for i in range(1,n):
	dp = [[0 for _ in range(n)]for _ in range(2)]
	# 0-当天持有股票
    # 1-当天不持有股票
    # 今天不买新的,保持昨天的股票 今天买新的股票
    dp[0][i] = max(dp[0][i-1], -prices[i])
    # 今天啥也不干,保持昨天没有股票的状态  把昨天持有的股票卖了
    dp[1][i] = max(dp[1][i-1], dp[0][i-1] + prices[i])
  • k=∞,不需要遍历天数,但是在购买时需要考虑到昨天的利润dp[i-1][1],因此现金为dp[i-1][1]-prices[i]。【当k为任意值时,且k>n//2,那么股票交易次数不影响dp值,该问题变为股票交易次数无限次的情况】
dp = [[0 for _ in range(n)]for _ in range(2)]
# 0-当天持有股票
# 1-当天不持有股票
dp[0][0] = -prices[0]
dp[1][0] = 0
for i in range(1,n):
    # 今天不买新的,保持昨天的股票 今天买新的股票(用昨天的现金【需要考虑到昨天可能有利润】)
    dp[0][i] = max(dp[0][i-1], dp[1][i-1]-prices[i])
    # 今天啥也不干,保持昨天没有股票的状态  把昨天持有的股票卖了
    dp[1][i] = max(dp[1][i-1], dp[0][i-1] + prices[i])
  • 考虑冷冻期的话可以按照打家劫舍的做法,假如当天买股票的话,则前一天就不能卖股票了,就得从i-2天开始考虑。即将dp[1][i-1]-prices[i]改为dp[1][i-2]-prices[i]
dp = [[ 0 for _ in range(n)] for _ in range(2)]
# 0-当天持有股票
# 1-当天不持有股票
dp[0][0] = -prices[0]
for i in range(1,n):
	# 今天不买新的,保持昨天的股票 今天买新的股票(需要从前天开始考虑)
    dp[0][i] = max(dp[0][i-1], dp[1][i-2]-prices[i])
    # 今天啥也不干,保持昨天没有股票的状态  把昨天持有的股票卖了
    dp[1][i] = max(dp[1][i-1], dp[0][i-1]+prices[i])
dp = [[0 for j in range(n)]for i in range(2)]
dp[0][0] = -prices[0]-fee
for i in range(1,n):
    dp[0][i] = max(dp[0][i-1], dp[1][i-1]-prices[i]-fee)
    dp[1][i] = max(dp[1][i-1], dp[0][i-1]+prices[i])

子序列问题

首先需要明确,子序列与连续子序列的区别:子序列包括连续子序列。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。关键词:不改变顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
连续子序列条件更为苛刻,该子序列必须是原数组中连续的部分。
根据问题不同,dp和递推公式也都不同

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

dp含义

dp[i]表示以nums[i]结尾的子序列的最大长度。

递推公式

因为dp[i]记录了以nums[i]结尾的子序列的最大长度,那么只要当前数nums[i]比之前的nums[j]大,就可以把当前数作为结尾加入到之前的子序列中,此时长度在之前的长度dp[j]上+1,所以递推公式如下:
dp[i] = max(dp[i], dp[j]+1)
该公式表示在之前所有子序列+1中取最大的那个。

初始化

因为每一个数都可以看作一个递增子序列,所以最短都是1,dp[i]都初始化为1

遍历顺序

根据递推公式可知,需要双重循环,外层循环遍历结尾的数,内层循环遍历之前的序列。都是正向遍历

递推实例

nums = [10, 9, 2, 5, 3, 7, 101, 18]
dp  =  [1,  1, 1, 2, 2, 3,  4,  4]

code实例

n = len(nums)
dp = [1] * n
for i in range(1,n):
    for j in range(0,i):
        if nums[i]>nums[j]:
            dp[i] = max(dp[i], dp[j]+1)

最长连续递增子序列

与上面的区在于需要保证连续,因此递推公式也更简单,只需要比较num[i-1]nums[i]的关系即可,如果nums[i]大,那么直接在dp[i-1]长度上+1。

code实例

n = len(nums)
dp = [1] * n
for i in range(1,n):
    if nums[i]>nums[i-1]:
        dp[i] = dp[i-1]+1

最长重复子数组(连续子序列)

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

dp含义

dp[i][j]: 以nums[i-1]和nums[j-1]结尾的连续子序列的最大长度

听起来很抽象,其实是为了初始化方便这么设计的,实际上也可以用以nums[i]和nums[j]结尾的连续子序列的最大长度。可以看下面的递推图,因为在前面多加了一行和一列,所以dp[i][j]的下标i和j相比于原始数组偏移了一位,所以在这里要-1。
在这里插入图片描述
如果不增加一行和一列,也可以,看下面这个图,实际上是一样的,但是需要对第一行和第一列额外初始化,所以方便起见采用上面那种方法。
在这里插入图片描述

递推公式

如果nums[i-1]和nums[j-1]相等,说明长度至少为1,然后两个序列分别往前看一位,即dp[i-1][j-1],如果以他们为结尾的也相等(表现为dp[i-1][j-1]>0),那么再分别往后加一位也满足重复子数组;如果不相等(表现为dp[i-1][j-1]=0),那么dp[i][j]就是1了。综上所述可以写成统一的形式:
if nums1[i-1]==nums2[j-1]: dp[i][j] = dp[i-1][j-1] + 1

初始化

按照这种方式,所有位置初始都填0就可以,(否则要先求出来第一列和第一行的dp值,即相等就赋值1)

遍历顺序

根据递推公式,从左往右,从上往下遍历即可。两个for循环的遍历顺序无所谓。

code实例

n1 = len(nums1)
n2 = len(nums2)
res = 0
dp = [[0 for j in range(n2+1)]for i in range(n1+1)]
for i in range(1,n1+1):
    for j in range(1,n2+1):
        if nums1[i-1]==nums2[j-1]:
            dp[i][j] = dp[i-1][j-1] + 1
            res = max(dp[i][j],res)

最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
与最长重复子数组(连续子序列)相比,缺少了连续的约束,因此计算时麻烦一些。

初始化和遍历顺序都一样,主要是dp含义递推公式不同。
在前面最长递增子序列问题中我们介绍了,最长序列要以nums[i]为结尾,遍历之前的dp值取最大值并+1。(实际上最长递增子序列问题dp也可以写成 二维dp[i][j]的形式,只是我们把它压缩成了一维。)
在这里我们依然可以用这个方法。
如下图所示(ps:在carl哥例图上改的QAQ)
在这里插入图片描述

dp含义

dp[i][j]: 表示nums1[0:i](实际上是0->i-1)和nums2[0:j](0->j-1)的最长公共子序列

与最长相同连续子序列的区别在于不一定需要以nums1[i-1]nums2[j-1]结尾了,它可以继承之前的结果。比如[1,2,3,2,1]与[3,4,2,1]的最长公共子序列是[3,2,1],长度是3;[1,2,3,2,1]与[3,4,2,1,4,7]的最长公共子序列还是[3,2,1],长度是3。但是最长相同连续子序列不可以继承,可以结合上面的图理解一下。

递推公式

相等的情况就不说了,当nums1[i-1]nums2[j-1]不等时,就要继承之前的值,看看nums1[0, i - 2]nums2[0, j - 1]的最长公共子序列 和 nums1[0, i - 1]nums[0, j - 2]的最长公共子序列,取最大的,体现在dp上就是dp[i][j-1], dp[i-1][j]

if nums1[i-1]==nums2[j-1]:
	dp[i][j] = dp[i-1][j-1] + 1
else:
	dp[i][j] = max(dp[i][j-1], dp[i-1][j])

在这里插入图片描述

code实例

n1 = len(text1)
n2 = len(text2)
res = 0
dp = [[0 for j in range(n2+1)]for _ in range(n1+1)]
for i in range(1,n1+1):
    for j in range(1,n2+1):
        if text1[i-1] == text2[j-1]:
            dp[i][j] = dp[i-1][j-1]+1
        else:
            dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[-1][-1]

相关题目(我要打三个)
1143. 最长公共子序列
1035. 不相交的线
392.判断子序列
115. 不同的子序列
注意有些题目区分了子串和主串,此时递推公式就不能考虑去掉子串的情况了,即dp[i-1][j],其中i是子串下标,j是主串下标。
583. 两个字符串的删除操作

编辑距离问题

上面的两个字符串的删除操作题目也可以看作编辑距离的问题,即给定任意两个字符串,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

dp含义

dp[i][j]: 以word1[i-1]word2[j-1]结尾的子串变得相等,需要删除字符的最少次数

可以看到,涉及到连续子序列的问题都增加了以某个字母结尾的约束,而子序列则没有这一限定,因此在递推时可以“继承”上一步的dp值。

递推公式

分为两种情况:

  • word1[i-1]==word2[j-1],如果两个子串结尾字符相同,那么当前俩字母肯定就不管了,就往前回退一位,看看他们一不一样,即dp[i][j] = dp[i-1][j-1]
  • word1[i-1]!=word2[j-1],如果两个子串结尾字符不同,那么就有三种情况:(1)各自回退一位,把俩字母都删掉,即dp[i][j] = dp[i-1][j-1] + 2;(2)word1回退一位,删掉word1[i-1]这个字符,即dp[i][j] = dp[i-1][j] + 1;(3)word2回退一位,删掉word2[j-1]这个字符,即dp[i][j] = dp[i][j-1] + 1。从三种情况中取最小值min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+2)
    另一种简化的方式:min(dp[i-1][j]+1, dp[i][j-1]+1),Carl哥说:同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[i][j-1] + 1

初始化

第一行和第一列,比如第一行,表示空子串与另一个子串需要删除的步骤,只需要把另一个子串全部删掉就等于空集啦!所以很自然dp[0][j] = j,同样dp[i][0] = i

遍历顺序

根据递推公式,应该是从左往右,从上往下递推。

递推举例

在这里插入图片描述

code实例

n1 = len(word2)
n2 = len(word1)
# dp[i][j]表示让word1[0:j]与word[0:i]相同最少需要删除的字符数
dp = [[0 for _ in range(n2+1)]for _ in range(n1+1)]
for i in range(1,n1+1):
    dp[i][0] = i
for j in range(1,n2+1):
    dp[0][j] = j 
for i in range(1,n1+1):
    for j in range(1, n2+1):
        # 如果相同就不用删字符,直接继承上一个字母
        if word2[i-1]==word1[j-1]:
            dp[i][j] = dp[i-1][j-1]
        else:
            # 如果不同,那么看删除word2的i-1的字符还是删除word1的j-1的字符需要的步骤更少
            dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+2)

变种

72. 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数。
你可以对一个单词进行如下三种操作:插入一个字符,删除一个字符,替换一个字符。
与上面不同的是,可以替换字符了,相当于原来dp[i-1][j-1]+2变成了dp[i-1][j-1]+1,本来要删除俩字符,现在只需要一步替换操作就可以了。所以只需要改一下递推公式就可以轻松解决这道困难题了。

回文子串问题

dp含义

dp[i][j]表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false

递推公式

主要有两种情况:

  • s[i]==s[j],如果i==j,显然是回文串,比如字母‘a’;如果i和j相差1,那显然也是,比如字母‘aa’;其他情况需要掐头去尾看中间是不是回文串,即dp[i][j]=dp[i+1][j-1],比如‘bb’是回文串,‘abba’就是回文串,‘bc’不是回文串,‘abbc’就不是回文串。
  • s[i]!=s[j],显然s[i->j]不是回文串。

初始化

全部初始化为False

遍历顺序

根据递推公式dp[i][j]=dp[i+1][j-1]i要从后往前遍历,j要从i开始从前往后遍历(因为j表示子串尾)。
在这里插入图片描述

code实例

n = len(s)
res = 0
dp = [[0 for j in range(n)]for i in range(n)]
for j in range(n):
    for i in range(j+1):
        if s[i]==s[j]:
            if j==i or j-i == 1:
                dp[i][j] = 1
            else:
                dp[i][j] = dp[i+1][j-1]
        res += dp[i][j]

回文子序列

与回文子串不同,没有必须连续的约束。
主要是dp含义和递推公式不同

dp含义

dp[i][j]:字符串s在[i, j](左闭右闭)范围内最长的回文子序列的长度。

递推公式

主要有两种情况:

  • s[i]==s[j],说明头尾加上这俩字符,最长回文子序列长度可以再加2。dp[i][j]=dp[i+1][j-1]+2
  • s[i]!=s[j],显然同时增加这俩字符并不能增加回文子序列的长度,那么就考虑两种情况,只加头s[i],或者只加尾s[j],并取二者最大值。

递推例子

在这里插入图片描述

code实例

n = len(s)
dp = [[0 for j in range(n)]for i in range(n)]
for i in range(n):
    dp[i][i] = 1
for i in range(n-2,-1,-1):
    for j in range(i+1,n):
        if s[i]==s[j]:
        # 如果s[i]==s[j],那么最长回文子序列长度就是中间的长度+2
        # 即使中间的不是回文串,那么最小也是1,
        # 这样加上左右两边相等的字符,构成的长度为3的字符串仍然是回文串
            dp[i][j] = dp[i+1][j-1] + 2
        else:
        # 如果s[i]!=s[j],肯定是没办法同时加入最长回文序列的
        # 那么就能在一边加字符,看看加哪边构成的串更长
        # dp[i+1][j] 表示在[i+1][j-1]基础上在尾部添加新的字符s[j]构成的最长回文序列
        # dp[i][j-1]表示在[i+1][j-1]基础上在头部添加新的字符s[i]构成的最长回文序列
            dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][-1]

猜你喜欢

转载自blog.csdn.net/LoveJSH/article/details/129753562