动态规划问题综合

参考代码

 

最长公共子串

def slt(str1,str2):
    value=[[0 for i in range(len(str2)+1)] for j in range(len(str1)+1)]
    substr=''
    length = 0
    for i in range(len(str1)):
        for j in range(len(str2)):
            if str1[i] == str2[j]:
                value[i+1][j+1] = value[i][j]+1
                if value[i+1][j+1]>length:
                    length = value[i][j]
                    substr += str1[i]
    return substr,length
print(slt('abcdef','abcd'))

最长公共子序列

def slt(str1,str2):
    value=[[0 for i in range(len(str2)+1)] for j in range(len(str1)+1)]
    substr=''
    length = 0
    for i in range(len(str1)):
        for j in range(len(str2)):
            if str1[i] == str2[j]:
                value[i+1][j+1] = value[i][j]+1
                if value[i + 1][j + 1] > length:
                    length = value[i][j]
                    substr += str1[i]
            else:
                value[i + 1][j + 1] = max(value[i+1][j],value[i][j+1])
    return substr,length
print(slt('abcdef','abcf'))

 

0/1背包问题三角形最小路径和

def solve(vlist,wlist,weight,n):
    value=[[0 for i in range(weight+1)] for j in range(n+1)]
    for i in range(1,n+1):
        for j in range(1,weight+1):
            if wlist[i] > j:
                value[i][j] = value[i-1][j]
            else:
                value[i][j] = max(value[i-1][j - wlist[i]] + vlist[i], value[i-1][j])
    return value[-1][-1]

if __name__ == '__main__':
    n = 6
    weight = 10
    w = [0,2, 2, 3, 1, 5, 2]
    v = [0,2, 3, 1, 5, 4, 3]
    print(solve(v,w,weight,n))

矩阵类动态规划问题

最大子序和

递推方程

拆解问题的时候也提到了,有两种情况,即当前元素自成一个子数组,另外可以考虑前一个状态的答案,于是就有了

dp[i] = Math.max(dp[i - 1] + array[i], array[i])

化简一下就成了:

dp[i] = Math.max(dp[i - 1], 0) + array[i]
a=[-2,1,-3,4,-1,2,1,-5,4]
dp=[0 for i in range(len(a))]
dp[0]=a[0]
for i in range(1,len(a)):
  dp[i]=max(dp[i-1]+a[i],a[i])
  # dp[i]=max(dp[i-1],0)+a[i]
print(max(dp))

三角形最短路径和

问题拆解:

这里的总问题是求出最小的路径和,路径是这里的分析重点,路径是由一个个元素组成的,和之前爬楼梯那道题目类似,[i][j] 位置的元素,经过这个元素的路径肯定也会经过 [i - 1][j] 或者 [i - 1][j - 1],因此经过一个元素的路径和可以通过这个元素上面的一个或者两个元素的路径和得到。

状态定义:

状态的定义一般会和问题需要求解的答案联系在一起,这里其实有两种方式,一种是考虑路径从上到下,另外一种是考虑路径从下到上,因为元素的值是不变的,所以路径的方向不同也不会影响最后求得的路径和,如果是从上到下,你会发现,在考虑下面元素的时候,起始元素的路径只会从[i - 1][j] 获得,每行当中的最后一个元素的路径只会从 [i - 1][j - 1] 获得,中间二者都可,这样不太好实现,因此这里考虑从下到上的方式,状态的定义就变成了 “最后一行元素到当前元素的最小路径和”,对于 [0][0] 这个元素来说,最后状态表示的就是我们的最终答案。

a=[[2],[3,4],[6,5,7],[4,1,8,3]]
n=len(a)
dp=[[0 for i in range(n)] for i in range(n)]
for i in range(n):
  dp[n-1][i]=a[n-1][i]
i=n-2
while i>=0:
  for j in range(i+1):
    dp[i][j]=min(dp[i+1][j],dp[i+1][j+1])+a[i][j]
  i = i - 1
print(dp)

不同路径:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

递推方程

有了状态,也知道了问题之间的联系,其实递推方程也出来了,就是

dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

m,n=3,7
dp=[[ 0 for i in range(n)] for i in range(m)]
for i in range(m):
  dp[i][0]=1
for i in range(n):
  dp[0][i]=1
for i in range(1,m):
  for j in range(1,n):
    dp[i][j]=dp[i-1][j]+dp[i][j-1]
print(dp[-1][-1])

不同路径II:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

在上面那道题的基础上,矩阵中增加了障碍物,这里只需要针对障碍物进行判断即可,如果当前位置是障碍物的话,状态数组中当前位置记录的答案就是 0,也就是没有任何一条路径可以到达当前位置,除了这一点外,其余的分析方法和解题思路和之前一样。

# obstacleGrid=[
#   [0,0,0],
#   [0,1,0],
#   [0,0,0]
# ]
obstacleGrid=[[0,0]]
if obstacleGrid[0][0]==1:print(0)
m=len(obstacleGrid)
n=len(obstacleGrid[0])
dp = [[0 for _ in range(n)] for _ in range(m)]
for i in range(m):
    if obstacleGrid[i][0] == 0:
        dp[i][0] = 1
    else:
        for j in range(i, m):
            dp[j][0] = 0
        break
for i in range(n):
    if obstacleGrid[0][i] == 0:
        dp[0][i] = 1
    else:
        for j in range(i, n):
            dp[0][j] = 0
        break
print(dp)
for i in range(1,m):
    for j in range(1,n):
        if obstacleGrid[i][j]==1:
            dp[i][j] = 0
        else:
            dp[i][j] = dp[i][j - 1] + dp[i - 1][j]
print(dp)
print(dp[-1][-1])

 最小路径和

状态定义

因为是要求路径和,因此状态需要记录的是 “从起始点到当前位置的最小路径和

递推方程

有了状态,以及问题之间的联系,我们知道了,当前的最短路径和可以由其上方和其左方的最短路径和对比得出,递推方程也可以很快写出来:

dp[i][j] = Math.min(dp[i - 1][j] + dp[i][j - 1]) + grid[i][j]

a=[
  [1,3,1],
  [1,5,1],
  [4,2,1]]
m,n=len(a),len(a[0])
dp=[[ 0 for i in range(n)] for i in range(m)]
dp[0][0]=a[0][0]
for i in range(1,n):
  dp[0][i]=dp[0][i-1]+a[0][i]
for i in range(1,m):
  dp[i][0]=dp[i][0]+a[i][0]
print(dp)
for i in range(1,m):
  for j in range(1,n):
    dp[i][j]=min(dp[i - 1][j],dp[i][j - 1])+a[i][j]
print(dp[-1][-1])

最长回文子串

1、如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;

2、如果一个字符串的头尾两个字符相等,才有必要继续判断下去。

(1)如果里面的子串是回文,整体就是回文串;

(2)如果里面的子串不是回文串,整体就不是回文串。

即在头尾字符相等的情况下,里面子串的回文性质据定了整个子串的回文性质,这就是状态转移。因此可以把“状态”定义为原字符串的一个子串是否为回文子串。

第 1 步:定义状态
dp[i][j] 表示子串 s[i, j] 是否为回文子串。

第 2 步:思考状态转移方程
这一步在做分类讨论(根据头尾字符是否相等),根据上面的分析得到:

dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]

分析这个状态转移方程:

(1)“动态规划”事实上是在填一张二维表格,i 和 j 的关系是 i <= j ,因此,只需要填这张表的上半部分;

(2)看到 dp[i + 1][j - 1] 就得考虑边界情况。

边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。

这个结论很显然:当子串 s[i, j] 的长度等于 2 或者等于 3 的时候,我其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。

如果子串 s[i + 1, j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,当然是回文;
如果子串 s[i + 1, j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。

s='aaaa'
n=len(s)
if n<2:print(s)
dp=[[False for i in range(n)] for i in range(n)]
start=0
maxlen=1
for i in range(n):
    dp[i][i]=True
for j in range(1,n):
    for i in range(j):
        if s[i]==s[j]:
            if j-i<3:
                dp[i][j]=True
            else:
                dp[i][j]=dp[i+1][j-1]
        else:
            dp[i][j]=False
        if dp[i][j]:
            curlen=j-i+1
            if curlen>maxlen:
                maxlen=curlen
                start=i
print(dp)
print(s[start:start+maxlen])

序列类动态规划问题

最长上升子序列

状态定义

问题拆解中我们提到 “第 i 个问题和前 i - 1 个问题有关”,也就是说 “如果我们要求解第 i 个问题的解,那么我们必须考虑前 i - 1 个问题的解”,我们定义 dp[i] 表示以位置 i 结尾的子序列的最大长度,也就是说 dp[i] 里面记录的答案保证了该答案表示的子序列以位置 i 结尾。

递推方程

对于 i 这个位置,我们需要考虑前 i - 1 个位置,看看哪些位置可以拼在 i 位置之前,如果有多个位置可以拼在 i 之前,那么必须选最长的那个,这样一分析,递推方程就有了:

dp[i] = Math.max(dp[j],...,dp[k]) + 1, 
a=[10,9,2,5,3,7,101,18]
dp=[1 for i in range(len(a))]
for i in range(len(a)):
   for j in range(i):
     if a[i]> a[j]:
       dp[i]=max(dp[j]+1,dp[i])
print(dp)

粉刷房子

问题拆解

对于每个房子来说,都可以使用三种油漆当中的一种,如果说不需要保证相邻的房子的颜色必须不同,那么整个题目会变得非常简单,每个房子直接用最便宜的油漆刷就好了,但是加上这个限制条件,你会发现刷第 i 个房子的花费其实是和前面 i - 1 个房子的花费以及选择相关,如果说我们需要知道第 i 个房子使用第 k 种油漆的最小花费,那么你其实可以思考第 i - 1 个房子如果不用该油漆的最小花费,这个最小花费是考虑从 0 到当前位置所有的房子的。

状态定义

通过之前的问题拆解步骤,状态可以定义成 dp[i][k],表示如果第 i 个房子选择第 k 个颜色,那么从 0 到 i 个房子的最小花费

递推方程

基于之前的状态定义,以及相邻的房子不能使用相同的油漆,那么递推方程可以表示成:

dp[i][k] = Math.min(dp[i - 1][l], ..., dp[i - 1][r]) + costs[i][k], l != k, r != k
cost=[[17,2,17],
      [16,16,5],
      [14,3,19]]
dp=[[0 for i in range(3)] for i in range(len(cost))]
for i in range(3):
    dp[0][i]=cost[0][i]
for i in range(1,len(cost)):
    dp[i][0]=min(dp[i-1][1],dp[i-1][2])+cost[i][0]
    dp[i][1]=min(dp[i-1][0],dp[i-1][2])+cost[i][1]
    dp[i][2]=min(dp[i-1][0],dp[i-1][1])+cost[i][2]
print(min(dp[-1]))

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

问题拆解

如果我们要求解抢完 n 个房子所获得的最大收入,因为题目的要求,我们可以思考第 i 个房子是否应该抢,如果要抢,那么第 i - 1 个房子就不能抢,我们只能考虑抢第 i - 2 个房子。如果不抢,那么就可以抢第 i - 1 个房子,这样一来,第 i 个房子就和第 i - 1 个房子,以及第 i - 2 个房子联系上了。

状态定义

通过之前的问题拆解,我们知道,如果我们从左到右去抢房子,抢到当前房子可以获得的最大值其实是和抢到前两个房子可以获得的最大值有关,因此我们可以用 dp[i] 表示抢到第 i 个房子可以获得的最大值

递推方程

如果我们抢第 i 个房子,那么我们就只能去考虑第 i - 2 个房子,如果不抢,那么我们可以考虑第 i - 1 个房子,于是递推方程就有了:

dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1])
a=[1,2,3,1]
a=[2,7,9,3,1]
dp=[0 for i in range(len(a)+1)]
dp[0]=0
dp[1]=a[0]
print(dp)
for i in range(2,len(a)+1):
    dp[i]=max(dp[i-1],dp[i-2]+a[i-1])
print(dp)

打家劫舍II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 

前面那道题目的 follow up,问的是如果这些房子的排列方式是一个圆圈,其余要求不变,问该如何处理。

房子排列方式是一个圆圈意味着之前的最后一个房子和第一个房子之间产生了联系,这里有一个小技巧就是我们线性考虑 [0, n - 2] 和 [1, n - 1],然后求二者的最大值。

其实这么做的目的很明显,把第一个房子和最后一个房子分开来考虑。实现上面我们可以直接使用之前的实现代码。

买卖股票

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

双指针发

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices)<=0:
            return 0
        minnum=prices[0]
        maxProfit=0
        for i in range(len(prices)):
            minnum=min(minnum,prices[i])
            maxProfit=max(prices[i]-minnum,maxProfit)
        return maxProfit

动态规划

由牛顿莱布尼兹公式

\int_{a}^{b}f(x)=F(a)-F(b)

因此,区间和跟求差可以相互转换。题目就变为求最大连续子数组和,用动态规划很好做。可以理解为题目要求最大价格差,我们可以构造一个数组,prices[i]-prices[i-1],代表每日能获得的利润值,这个数组的最大连续子数组和就是最大利润,即最大价格差。
具体表现为:

  • 首先构造diff数组,求连续两天的价格差
  • 状态转移方程dp[i] = max(dp[i-1]+diff[i], 0), dp[i]指以i元素结尾的子数组的最大和
  • 接着遍历diff数组,根据状态转移方程求出dp数组
  • 最后dp数组中的最大值就是最大价格差
class Solution:
    def maxProfit(self, prices):
        if len(prices)<=0:
            return 0
        diff=[0 for i in range(len(prices))]
        for i in range(len(prices)-1):
            diff[i]=prices[i+1]-prices[i]
        dp=[0 for i in range(len(diff))]
        dp[0]=max(0,diff[0])
        for i in range(len(diff)):
            dp[i]=max(dp[i-1]+diff[i],0)
        return max(dp)
Solution=Solution()
print(Solution.maxProfit([7,6,4,3,1]))

数字解码

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

特判,若ss为空或者s[0]=="0"s[0]=="0",返回00

初始化dp=[0,...,0]dp=[0,...,0],长度为n+1n+1,dp[0]=1,dp[1]=1dp[0]=1,dp[1]=1,dp[1]=1dp[1]=1表示第一位的解码方法,dp[0]dp[0]的作用,在于两位时,如:"12",dp[2]=dp[1]+dp[0]dp[2]=dp[1]+dp[0]。

遍历ss,遍历区间[1,n)[1,n):

若s[i]=="0"s[i]=="0":
若s[i-1]=="1" or s[i-1]=="2"s[i−1]=="1"ors[i−1]=="2":此时,到当前位置的解码方法dp[i+1]dp[i+1]和上上一位的相同,因为上一位和本位置结合在了一起。dp[i+1]=dp[i-1]dp[i+1]=dp[i−1]
否则,返回00,表示无法解码
否则:
判断何时既可以自身解码也可以和前一位结合:若上一位s[i-1]=="1"s[i−1]=="1",则当前位既可以单独解码也可以和上一位结合。或者上一位s[i]=="2"s[i]=="2"则此时,若"1"<=s[i]<="6""1"<=s[i]<="6",也是可以的。综上,s[i-1]=="1" or (s[i-1]=="2" and "1"<=s[i]<="6")s[i−1]=="1"or(s[i−1]=="2"and"1"<=s[i]<="6") 。此时,dp[i+1]=dp[i]+dp[i-1]dp[i+1]=dp[i]+dp[i−1],等于上一位和上上位的解码方法之和。
否则,dp[i+1]=dp[i]dp[i+1]=dp[i]
返回dp[n]dp[n]

class Solution:
    def numDecodings(self, s: str) -> int:
        n=len(s)
        if(not s or s[0]=="0"):
            return 0
        dp=[0]*(n+1)
        dp[0]=1
        dp[1]=1
        for i in range(1,n):
            if(s[i]=="0"):
                if(s[i-1]=="1" or s[i-1]=="2"):
                    dp[i+1]=dp[i-1]
                else:
                    return 0
            else:
                if(s[i-1]=="1" or (s[i-1]=="2" and "1"<=s[i]<="6")):
                    dp[i+1]=dp[i]+dp[i-1]
                else:
                    dp[i+1]=dp[i]
        return dp[-1]

参考文献:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值