算法和数据结构(Python)——动态规划

适用情况

求最值,有子序列相关

模式

  1. dp表法

思路

  1. 根据题目建立dp表,明确dp[i]的含义。
  2. 设置 base case ,初始化 dp 数组。
  3. 运用数学归纳法的思想,假设 dp[0…i-1] 都已知,想办法求出 dp[i]。动态找dp[i], dp[j]的变化关系,在什么条件下满足题目要求的变化。可以把初始化的值也考虑进来
    (但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。)
def f(s1, s2):
	<1.创建dp表(初始化)>
    dp = [[0 for i in range(len(s2)+1)] for j in range(len(s1)+1)]

    <2.循环修改dp表>
    for i in range(1,len(s1)+1):     # 大循环表示列
        for j in range(1,len(s2)+1): # 小循环表示行
            <3.如果相等>
            if s1[i-1] == s2[j-1]:  
                dp[i][j] = 
            <4.如果不等>
            else:
                dp[i][j] = 最值()
    return dp[-1][-1]  
  1. 备忘录法 递归

思路

  1. 找原问题和子问题的变化量
  2. 根据题目确定函数的因变量
  3. 确定状态转移方程,子问题可以做出什么选择得到原问题
<1.定义备忘录>
memo = dict()

def f(n, arr):
	<2.查备忘录:避免重复>
	if n in	memo: return memo[n]
	
	<3.递归中止条件>
	if n ==	0: return 0
	if n < 0: return -1
	
	res = float('INF')
	for i in arr:
		<4.子问题无解>
		if f(n - i) == -1: continue
		
		<5.动态转移方程递归>
		res = min(res, 1 + f(n - i))
		
	<6.记入备忘录>
	memo[n] = res if res != float('INF') else -1
	return memo[n]

例题1 双字符二维dp表

leetcode 1143. (medium) 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。

解题思路:
创建dp表,注意dp表跟循环内ij的对应,大循环的索引i代表行,小循环的索引j代表列。
在这里插入图片描述

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        # <1.创建dp表(初始化)> 注意dp表的对应关系
        dp = [[0 for i in range(len(text2)+1) ] for j in range(len(text1)+1 )]
        # <2.循环修改dp表>注意是先i行后j列
        for i in range(1, len(text1)+1):
            for j in range(1, len(text2)+1):
            	# <3.如果相等>
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                # <4.如果不相等>
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[-1][-1]

例题二 双字符二维的dp表

leetcode 72 (hard)编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符;删除一个字符;替换一个字符

输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

解题思路

  1. 列出dp表
    在这里插入图片描述
  2. 格子的左、左上和上格子到本格的状态转移
    在这里插入图片描述
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        dp = [[0 for i in range(len(word2)+1)] for j in range(len(word1)+1)]
        
        # <1.创建dp表(初始化)>
        for i in range(len(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j

        # <2.循环修改dp表>
        for i in range(1,len(word1)+1):
            for j in range(1,len(word2)+1):
                # <3.如果相等>则跳过
                if word1[i-1] == word2[j-1]:  
                    dp[i][j] = dp[i-1][j-1]
                # <4.如果不相等>则删除、插入或替换
                else:
                    dp[i][j] = min(               # 都是相对于s2,改s1,加一表示进行一个操作
                                    dp[i-1][j]+1, # 直接把 s[i] 这个字符删掉,前移i
                                    dp[i][j-1]+1, # 在s[i]插入跟s[j]一样的字符,前移j
                                    dp[i-1][j-1]+1) # 把s[i]替换成s[j],然后i、j要一起前移
        return dp[-1][-1]  # dp表的最后一个值就是最终编辑步数

例题三 双重一维dp表

leetcode 673 (medium)最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

解题思路

  1. 以终为始:最长递增子序列的个数记为res,建立双重dp表,参考求最长递增子序列的方法length[i]表示对应nums[i]的子序列长度,count[i]表示对应nums[i]的子序列长度的个数
  2. 数学归纳法,假设 dp[0…i-1] 都已知,想办法求出 dp[i],找length(i), length(j), count[i], count[j]的变化关系。基于初始化的值,正常都是length前面的值小于等于length后面的值(初始化为1),即这个长度的递增子序列第一次出现。
    在这里插入图片描述
    如果这个i前面有两个相同的值,第二次进入这个条件时,这个i已经是做过length[i] = length[j] + 1,即有两个长度相同的子序列
    在这里插入图片描述
  3. 上面求出来的count[i]只是以nums[i]结尾的最长子序列个数,要求最长递增子序列个数还要遍历找最长递增子序列(可能有多个),求count的和
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0
		 # <1.创建dp表(初始化)>
        count = [1 for i in range(len(nums))]     # 以x结尾的最长子序列个数
        length = [1 for i in range(len(nums))]    # 以x结尾的最长子序列长度
        res = 0
		# <3.循环修改dp表>
        for i in range(len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:      
                    # 在初始化的length表基础上,正常都是length前面的值小于等于length后面的值(初始化为1)           
                    if length[j] >= length[i]:
                        length[i] = length[j] + 1                       
                        count[i] = count[j]
                    # 如果这个i前面有两个相同的值,第二次进入这个条件时,这个i已经是做过length[i] = length[j] + 1,即有两个长度相同的子序列
                    elif length[j]+1 == length[i]:
                        count[i] += count[j]
        
        maxSub = max(length)
        # 是找最长递增子序列,要在length中找长度最长子序列的个数之和
        for i in range(len(nums)):
            if length[i] == maxSub:
                res += count[i]

        return res

例题五 回文系列 一维dp转为二维

leetcode 516(medium)最长回文子序列
给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。

输入: “bbbab”
输出: 4
一个可能的最长回文子序列为 “bbbb”。

解题思路

  1. 根据题目建立dp表,dp[i…j]表示范围从i到j的最长回文子序列长度
    在这里插入图片描述
    为了便于找状态转移方程,将最长回文子序列的长度呈现,而且这里有i和j,将一维dp表转为二维,一维为正序s,一维为逆序s,dp[i][j]相当于dp[i…j]表示范围从i到j的最长回文子序列长度。因为i始终小于j,所以左下方的dp[i][j]都为0。
    在这里插入图片描述
  2. 初始化,只有一个字符的最长回文子序列长度为1,即i == j时的dp都为1
    在这里插入图片描述
  3. 数学归纳找规律:
    如果s[i] == s[j] 则在上一个回文子序列长度上dp[i+1][j-1](即dp[i+1…j-1])加上两个字符的长度+2。
    如果s[i] != s[j] 则把s[i]和s[j]分别加入s[i+1][j-1]中,即变为s[i][j-1]和s[i+1][j],看哪一边的回文子序列更长(max)。
    在这里插入图片描述
  4. 遍历dp表时
    对i要反向遍历for i in range(len(s)-1, -1, -1),
    对j正向遍历并避开左下方的范围for j in range(i+1, len(s))
    在这里插入图片描述
  5. 最后要求的是dp[0][len(s)-1]即dp[0…len(s)-1]的最长回文子序列
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        # <1.创建dp表(初始化)>
        dp = [[0 for i in range(len(s))] for j in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1
        
        # <2.循环修改dp表>
        for i in range(len(s)-1, -1, -1): # 对i要反向遍历
            for j in range(i+1, len(s)):  # 对j正向遍历且避开j小于i的情况
            
                # <3.如果相等>则在上一个回文子序列长度上加上两个字符的长度
                if s[j] == s[i]:
                    dp[i][j] = dp[i+1][j-1] + 2
                    
                # <4.如果不相等>则对比哪边的回文子序列更长
                elif s[j] != s[i]:
                    dp[i][j] = max(dp[i][j-1], dp[i+1][j])
        
        return dp[0][len(s)-1] # 返回dp[0...len(s)-1]的最长回文子序列

leetcode (medium) 5 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。

解题思路
从上到下思考状态:

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

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

  • 如果s[i][j]长度j-i+1<=3,则一定是回文
  • 如果s[i][j]长度j-i+1>3,则无法判断,要看里面的子串是否是回文,如果里面子串是回文,就是回文,如果不是则一定不是(判断里面子串是不是回文则又回到判断里面子串长度的问题,因为循环的伸缩窗口是从小到大,从长度最小的开始判断,所以按这个逻辑进行迭代)
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if len(s) < 2: 
            return s
        # 以终为始,求子串时只要获取起始点
        start = 0
        maxlen = 0

        # <1.创建dp表(初始化)>
        dp = [[0 for i in range(len(s))] for j in range(len(s))]
        for i in range(len(s)):
            dp[i][i] = 1

        # <2.循环修改dp表>
        for i in range(len(s)-1,-1,-1):   # 为了排除错误,还是要逆序,从少到多循环
            for j in range(i,len(s)):
                # <3.如果相等>
                if s[i] == s[j]:
                    # 如果子串长度小于等于3,一定是回文
                    if j - i + 1 <= 3:
                        dp[i][j] = 1
                    # 否则再看子问题
                    else:
                        dp[i][j] = dp[i+1][j-1]
                
                # <4.如果不等>情况可以忽略,因为这里只要判断是否为回文子串

                # 循环比较,找最长子串
                if dp[i][j] == 1:
                    curlen = j-i+1
                    if curlen > maxlen:
                        maxlen = curlen
                        start = i

        # 到return的时候再截取,不用循环截取,占用内存
        return s[start:start+maxlen]

练习1 leetcode 322 零钱兑换(最少硬币) medium

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

解题思路

  1. 根据题目构建dp表,变化的状态是总金额amount设为i,明确dp[i]的含义,即总额为1时的最少硬币数
  2. base case要把dp表除了首位,其他设为amount+1,便于后续取最小值
  3. 状态转移方程为
    在这里插入图片描述
    贴一个分析贴 动态规划算法思想
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # <1.创建dp表(初始化)>
        dp = [amount + 1] * (amount + 1)
        dp[0] = 0
        # <2.循环修改dp表>
        for i in range(1, amount + 1):   # 大循环表示金额
            for j in range(len(coins)):  # 小循环表示对应的硬币面值
                # 金额大于硬币面值时才有必要计算
                if i >= coins[j]:
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1) # 对比dp[i]和选择不同硬币的情况

        return dp[-1] if dp[-1] != amount + 1 else -1

在这里插入图片描述

练习2 《剑指offer》 面试题10- II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:
输入:n = 2
输出:2

解题思路

  1. 斐波那契数列的同类型题,区别在于
    青蛙跳台阶f(0)=1, f(1)=1, f(2)=2。斐波那契数列f(0) = 0, f(1) = 1, f(2)=1
var numWays = function(n) {
    if (n == 0){
        return 1
    }
    if (n == 1 || n == 2){
        return n
    }
    var cur = 1n,
    pre = 1n
    for (var i = 2n; i <= n; i++){
        var sum = cur + pre
        pre = cur
        cur = sum
    }
    return sum % 1000000007n
};
语法笔记
  1. 在数值后面加n把Number类型转成BigInt类型,防止数值大于 2 53 − 1 2^{53} - 1 2531 超出Number类型的最大数值

参考资料:
labuladong算法小抄

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值