动态规划-python版本

 

目录

1、斐波那契数列

2、爬楼梯

3、使⽤最⼩花费爬楼梯

4、不同路径

5、不同路径 II

6、整数拆分

背包问题+背包回溯

7、01背包

(1)分割等和⼦集 

(2)最后⼀块⽯头的重量 II

(3)目标和

​(4)一和零

8、完全背包

(1)518题:零钱兑换II

(2)377题:组合总和IV

(3)322题:零钱兑换

(4)279题:完全平方数

(5)139题:单词拆分 

9、多重背包

(1)打家劫舍

(2)打家劫舍II

(3)打家劫舍III

(4)买卖股票的最佳时机

(5)买卖股票的最佳时机II

(6)买卖股票的最佳时机III

(7)买卖股票的最佳时机IV

(8)最佳买卖股票时机含冷冻期

(9)买卖股票的最佳时机含⼿续费

(10)最⻓递增⼦序列

(11)最⻓连续递增序列

(12)最⻓重复⼦数组

(13)最⻓公共⼦序列

(14)不相交的线

(15)最⼤⼦序和

(16)判断⼦序列

(17)不同的⼦序列

(18)两个字符串的删除操作

(19)编辑距离

(20)回⽂⼦串

(21)最⻓回⽂⼦序列


1、斐波那契数列

# -*- coding:utf-8 -*-
def fib(n):
    """
    递归方式实现斐波那契数列---时间复杂度O(2^n),空间复杂度O(n)
    :param n:
    :return:
    """
    if n < 2:
        return n
    else:
        return fib(n-1) + fib(n-2)

def fib1(n):
    """
    dp方式实现斐波那契数列---时间复杂度O(n),空间复杂度O(1)
    :param n:
    :return:
    """
    if n < 2:
        return n
    dp = [0, 1]  # 初始化
    for i in range(2, n + 1):
        # dp.append(dp[i-1] + dp[i - 2])     # 空间复杂度高 O(n)
        dp[0], dp[1] = dp[1], dp[0] + dp[1]  # 空间复杂度 O(1)----滚动dp一维数组
    return dp[-1]
if __name__ == '__main__':
    print(fib(8))
    print(fib1(8))

变长的dp动画 

 滚动的dp动画

2、爬楼梯

题目:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。  

每次你可以爬 1 或 2 个台阶。你有多少种不同的⽅法可以爬到楼顶呢?

思路:

递推公式:dp[i] = dp[i-1] +dp[i-2]

dp[i]存储的是上第i阶台阶的方法总数:要不就是i-1阶爬一阶上来,要不就是i-2阶爬2个台阶上来

初始化:和斐波那契数列一波相比只是初始不一样,第一个台阶的方法1种,第二个台阶方法是2种

遍历顺序:遍历台阶,从第一阶向后

def fib1(n):
    """
    dp方式实现爬楼梯---时间复杂度O(n),空间复杂度O(1)
    :param n:
    :return:
    """
    if n < 2:
        return n
    dp = [1, 2]  # 初始化这里和斐波那契不一样
    for i in range(2, n + 1):
        # dp.append(dp[i-1] + dp[i - 2])     # 空间复杂度高 O(n)
        dp[0], dp[1] = dp[1], dp[0] + dp[1]  # 空间复杂度 O(1)----滚动dp一维数组
    return dp[1]
    return dp[-1]

3、使⽤最⼩花费爬楼梯

LeetCode地址:力扣

题目:数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

思路:

递推公式:dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]

到达第i个台阶所花费的最少体⼒为dp[i]

初始化:dp[0],dp[1] = cost[0],cost[1] 

              这里dp[1]并没有取min(cost[0],cost[1])因为这里如果去了cost[0],那就没有从

             cost[1]作为起点的情况就给排除了,最后的结果也是取最后两个值的最小值也体现

             了这个点

到达第i个台阶所花费的最少体⼒为dp[i]

遍历顺序:遍历台阶,即是遍历cost

def fun1(cost):
    dp = cost[0:2]
    for i in range(2, len(cost)):
        dp[0], dp[1] = dp[1], min(dp[0], dp[1]) + cost[i]

    return min(dp)

4、不同路径

LeetCode地址:https://leetcode-cn.com/problems/unique-paths/

题目:

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

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

问总共有多少条不同的路径?

思路:

递推公式:[i][j]=dp[i - 1][j] + dp[i][j - 1] 

走到[i][j]的网格的路径总数,是来自从上、从左来的路径总数

初始化:dp[m][n] = 都是1

遍历顺序:第一行和第一列只有一种路径,所以遍历行从1,列也从1

def uniquePathsWithObstacles(obstacleGrid):
    """二维dp数组解决不同路径问题"""
    m, n = len(obstacleGrid), len(obstacleGrid[0])
    dp = [[1 for x in range(n)] for y in range(m)]

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

    return dp[-1][-1]

def uniquePathsWithObstacles(obstacleGrid):
    """一维dp数组解决不同路径问题"""
    # 不同路径1中可以使用一维dp数组def uniquePaths(m, n):
    dp = [1 for x in range(n)]

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

    return dp[-1]

5、不同路径 II

题⽬链接:https://leetcode-cn.com/problems/unique-paths-ii/

与不同路径的区别

(1)遇到障碍物,说明到达这个网格的路径总数为0

(2)遍历顺序:第一行如果有障碍物,从障碍物向后就不能走了

                           第一列如果有障碍物,从障碍物向下就不能走了

def uniquePathsWithObstacles(obstacleGrid):
    """二维dp数组解决不同路径问题"""
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [[1 for x in range(n)] for y in range(m)]

        for i in range(0, m):
            for j in range(0, n):
                if obstacleGrid[i][j] == 1:#遇到障碍物走到这个网格的路径就为0
                    dp[i][j] = 0
                elif i == 0 and j == 0:
                    pass
                elif i == 0 and j > 0:
                    dp[i][j] = dp[i][j - 1]
                elif j == 0 and i > 0:
                    dp[i][j] = dp[i - 1][j]
                else:
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

        return dp[-1][-1]

def uniquePathsWithObstacles(obstacleGrid):
    """一维dp数组解决不同路径问题"""
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [1 for x in range(n)]

        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 1:
                    dp[j] //= 2   #遇到障碍物只是阻挡了一个方向所以除以2
                else:
                    dp[j] += dp[j - 1]

        return dp[-1]

6、整数拆分

 LeetCode地址343题: 力扣

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

思路:

递推公式:dp[i] = max(dp[i], dp[i-j]*j, (i-j)*j)

                 列举拆分所有的情况,dp[i]依赖dp[i-j]的写法

         比如:dp[3]拆分情况----

                       首先尝试拆出来1----dp[3]=0、1*2=2、1*dp[2]=1----dp[3]=2

                       再次尝试拆出来2----dp[3]=2、2*1=2、2*dp[1]=0----dp[3]=2

                   dp[4]拆分情况----

                        首先尝试拆出来1------dp[4]=0、1*3=3、1*dp[3]=2-----dp[4]=3

                        再次尝试拆出来2------dp[4]=3、2*2=4、2*dp[2]=2-----dp[4]=4

                        再次尝试拆出来3------dp[4]=4、3*1=3、3*dp[1]=0-----dp[4]=4                    

初始化:dp[2] = 1

               dp[0],dp[1]没有意义,0、1不能拆分,题目也说了从2开始

遍历顺序:遍历dp从3开始,应为2已经初始化了

                  拆分的场景就是从1到i的所有情况

#方法1:dp动态规划
    def integerBreak(n):
        dp = [0] * (n + 1)
        dp[2] = 1
        for i in range(3, n + 1):
            for j in range(1, i):
                dp[i] = max(dp[i],dp[i-j]*j, (i-j)*j)
        return dp[-1]

#方法2:贪心--3这个特殊的数么有啥理论依据
    def integerBreak(n):
        #贪心
        if n == 2:
            return 1
        if n == 3:
            return 2
        if n == 4:
            return 4
        result = 1
        while n > 4:
            result *= 3
            n -= 3
        result *= n
        return result

背包问题+背包回溯

7、01背包

学习视频:【动态规划】背包问题_哔哩哔哩_bilibili

学习文章:【动态规划】一次搞定三种背包问题 - 弗兰克的猫 - 博客园

题目:一个背包可以装M重的东西,物品5个,重量为[2, 2, 6, 5, 4],价值为[3, 6, 5, 4, 6], 问:如何装可以得到最大的价值?

 

def bag(bag_w, num, weights, values):
    """
    01背包问题
    :param bag_w: 背包容量
    :param num: 物品个数
    :param weights: 物品重量list
    :param values: 物品价值list
    :return: 描绘的二维数组
    """
    # 初始化一个二维数据,放入各种情况下最佳价值,右下角就是最优解
    dp = [[0 for row in range(bag_w + 1)] for column in range(num + 1)]

    for row in range(1, num + 1):
        for col in range(1, bag_w + 1):
            if weights[row - 1] > col:  # 此时背包容量col小于row物品的容量,放不下row物品
                dp[row][col] = dp[row - 1][col]
            else:
                # 找到去掉row物品容量col - weights[row - 1]对应的row-1个物品的最大价值
                t1 = dp[row - 1][col - weights[row - 1]] + values[row - 1]
                t2 = dp[row - 1][col]
                dp[row][col] = max(t1, t2)

    # 找到装入了那些物品----背包回溯
    # 从右下角开始找,判断第五个物品是否装进口袋看:装第五个物品的最优解是否大于装第四个物品的最优解,大说明装入了,不大就是没有装入
    good_value = dp[-1][-1]
    good_n = []

    while num > 0 and bag_w > 0:
        if dp[num][bag_w] > dp[num - 1][bag_w]:
            good_n.append(num)
            bag_w -= weights[num - 1]
        num -= 1

    print("背包最大价值:%s" % good_value)
    print("背包物品编号:%s" % good_n[::-1])


def bag1(bag_w, num, weights, values):
    # 01背包不做回溯的话,可以使用一维dp滚动数组
    # 初始化一个一维数据,放入不同背包容量时最大价值
    # 难点在循环顺序
    dp = [0 for row in range(bag_w + 1)]

    for row in range(0, num):
        for col in range(bag_w, weights[row]-1, -1):
            dp[col] = max(dp[col], dp[col - weights[row]] + values[row])

    return dp[-1]

if __name__ == '__main__':
    num = 5                       # 5个物品
    bag_w = 10                    # 背包总容量
    weights = [2, 2, 6, 5, 4]     # 物品重量集合
    values = [6, 3, 5, 4, 6]      # 物品价值集合
    bag(bag_w, num, weights, values)
    bag1(bag_w, num, weights, values)

二维图解

一维图解

(1)分割等和⼦集 

 LeetCode地址: 力扣

题目:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

思路:将这些nums的元素当做物品,重量=价值=nums对应元素的值,背包假设为nums的和sum(nums)的一半

           转化为:背包容量sum(nums)/2,是否有恰好装满的情况(装满了,剩下的一半也是剩下物品的重量了)

递推公式:   dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])     (一维)   

                     dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i]] + nums[i])     (二维) 

初始化:初始值都为0

遍历顺序:和上面的01背包一致

  def canPartition(self, nums: List[int]) -> bool:
        "01背包的一维数组解法"
        total = sum(nums)
        if total <= 1 or total % 2 != 0:
            return False
        target = total // 2
        dp = [0] * (target + 1)

        for i in range(len(nums)):
            for j in range(target, nums[i] - 1, -1):
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

        return dp[-1] == target

def canPartition1(self, nums: List[int]) -> bool:
        "01背包的二维数组解法"
        total = sum(nums)
        if total <= 1 or total % 2 != 0:
            return False
        target = total // 2
        dp = [[0] * (target + 1)] * (len(nums)+1)

        for i in range(len(nums)+1):
            for j in range(nums[i], target+1):
                dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i]] + nums[i])

        return dp[-1][-1] == target

(2)最后⼀块⽯头的重量 II

 LeetCode地址: 力扣

题目:

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

思路:可以转化为分割等和⼦集的问题,target=total//2

          不同点(1)total不能等分的特殊情况不用处理

                      (2)子集问题中dp[-1]==target,

                        这里(total -dp[-1]) -dp[-1],含义将total分2份,一份是dp[-1],一份是total-dp[-1],他们差就是击碎后的最小值了,不理解我们在分析下,(total -dp[-1]) -dp[-1]  === 2(target -dp[-1]),这个式子是不是target和dp[-1]的值越相近那结果是不是越小?dp[-1]如果等于target就是等分子集了,是不是就是完美粉碎了?如果还不理解dp[-1]的意义,那就在讲,转化01背包问题:target是一个背包,装石头或者上面等分子集的nums的元素,石头重量即是价值,这样取如何装找到最大价值

递推公式:   dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])     (一维)   

                     dp[i][j] = max(dp[i-1][j], dp[i-1][j - nums[i-1]] + nums[i-1])     (二维) 

初始化:初始值都为0

遍历顺序:和上面的01背包一致

def lastStoneWeightII(self, stones: List[int]) -> int:
        """一维数组dp解法"""
        total = sum(stones)
        target = total // 2
        dp = [0] * (target + 1)

        for i in range(len(stones)):
            for j in range(target, stones[i] - 1, -1):
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])

        return total- dp[-1] - dp[-1]


    def lastStoneWeightII(self, stones: List[int]) -> int:
        """二维数组dp解法"""
        total = sum(stones)
        target = total // 2
        dp = [[0 for x in  range(target + 1)] for y in range(len(stones)+1)]

        for i in range(1, len(stones)+1):
            for j in range(1, target+1):
                if j < stones[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j - stones[i-1]] + stones[i-1])

        return total- dp[-1][-1] - dp[-1][-1]

(3)目标和

 LeetCode地址: 力扣

题目:

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

思路:每个元素有正负的两种情况,是否也是和分割等和⼦集、最后⼀块⽯头的重量 II问题类似,都是将nums分成两份,一份装,一份不装入,装入的是正号,不装入的是负号,是不是也像粉碎石头了?

          不同点:这个背包的容量是多少呢?分隔等和子集的背包容量是total的一半,这里呢?一份正数和、一份负数和

                 正数和 + 负数和 = total

                 正数和 -  负数和 = target

                 正数和 = (total + target)/ 2

所以就转化为了正数和的表达式 的数目

递推公式:   dp[j] = dp[j]+dp[j - nums[i]]   (一维)   

                     dp[i][j] = dp[i-1][j]+dp[i-1][j - nums[i-1]] (二维) 

初始化:dp[0] =1、dp[i][0]=0 这个是二维的情况

遍历顺序:和上面的01背包一致

def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 二维数组dp解法
        total = sum(nums)
        if target > total or (target + total) % 2 != 0 or target < -total:
            # 特殊情况处理:target不能大于total,小于-total,如果功能不能整除2,就得不到正数和的部分
            return 0
        bag_size = (target + total) // 2
        dp = [[0 for x in range(bag_size + 1)] for y in range(len(nums) + 1)]
        #初始化
        for z in range(len(nums)+1):
            dp[z][0] = 1

        for i in range(1, len(nums)+1):
            for j in range(0, bag_size+1): # 这个变量背包从0开始,这里的0有具体的含义了,之前价值0是无意义的了
                if j < nums[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j - nums[i-1]] + dp[i-1][j]

        return dp[-1][-1]


def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 一维数组dp解法
        total = sum(nums)
        if target > total or (target + total) % 2 != 0 or target < -total:
            # 特殊情况处理:target不能大于total,小于-total,如果功能不能整除2,就得不到正数和的部分
            return 0

        bag_size = (target + total) // 2
        dp = [0] * (bag_size + 1)
        dp[0] = 1
        for i in range(len(nums)):
            for j in range(bag_size, nums[i] - 1, -1):
                dp[j] = dp[j] + dp[j - nums[i]]

        return dp[-1]

​(4)一和零

题目链接:474. 一和零

题目:

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

思路:

dp[i][j]的含义:i个0和j个1组成的最大子集长度

递推公式:   dp[i][j] = max(dp[i][j], dp[i - m_sum][j - n_sum] + 1) 

初始化:0,组成0个0和0个1的字符串是0个

遍历顺序:和上面的01背包一致

题目特点:这里的物品是每一个字符串,背包是m个0和n个1的背包,这个背包不是之前只考虑物品的重量属性了,需要考虑物品的两个属性才能装入这个背包。动图效果多理解下

    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0 for x in range(n + 1)] for y in range(m + 1)]

        for w in strs:
            m_sum = w.count("0")
            n_sum = w.count("1")
            for i in range(m, m_sum - 1, -1):
                for j in range(n, n_sum - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - m_sum][j - n_sum] + 1)

        return dp[-1][-1]

8、完全背包

(1)518题:零钱兑换II

题目链接:518. 零钱兑换 II

题目:

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

思路:

dp[j]的含义:凑成总⾦额j的货币组合数为dp[j]

递推公式:  dp[j] += dp[j - coins[i]]

初始化:dp[0]=1

遍历顺序:外层coins,内层顺序吧变量金额,这一点区分了01背包,这里状态覆盖

这里的解释很清楚:动态规划:关于01背包问题,你该了解这些!(滚动数组)

    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0 for x in range(amount + 1)]
        dp[0] = 1

        for w in coins:
            for j in range(w, amount + 1):
                dp[j] += dp[j - w]

        return dp[-1]

(2)377题:组合总和IV

题目链接:377. 组合总和 Ⅳ

题目:

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

思路:

dp[j]的含义:凑成⽬标正整数为i的排列个数为dp[i]

递推公式:  dp[j] += dp[j - nums[i]]

初始化:因为题⽬中也说了:给定⽬标值是正整数! 所以 dp[0] = 1是没有意义的,仅仅是为

               了推导递推公式。

遍历顺序:外层for遍历背包,内层for循环遍历物品。

    def combinationSum4(self, nums: List[int], target: int) -> int:
        # 完全背包 
        # 求排列数就是外层for遍历背包,内层for循环遍历物品。
        dp = [0] * (target + 1)
        dp[0] = 1

        for i in range(target + 1):
            for j in range(len(nums)):
                if nums[j] <= i: #能放入物品nums[j]的情况
                    dp[i] += dp[i - nums[j]]
        
        return dp[-1]

(3)322题:零钱兑换

题目链接:322. 零钱兑换

题目:

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

思路:

dp[j]的含义:凑成⽬标正整数为i的最少硬币个数

递推公式:  dp[j] = min(dp[j], dp[j - nums[i]]+1)

初始化:因为题⽬中也说了:给定⽬标值是正整数! 所以 dp[0] = 1是没有意义的,仅仅是为

               了推导递推公式。

遍历顺序:外层for遍历物品,内层for循环背包。

    def coinChange(self, coins: List[int], amount: int) -> int:
        # 完全背包
        # dp[i]组成i的硬币最少个数,组成不了用-1表示,这里就初始了-1
        dp = [float("inf")] * (amount + 1)
        dp[0] = 0

        for i in coins:
            for j in range(i, amount+1):
                if dp[j - i] != float("inf"): #不添加coins[i]的情况,没有办法组成j的金额
                                              #入参:coins = [2, 3, 10], amount = 11
                    dp[j] = min(dp[j - i] + 1, dp[j])
        
        if dp[-1] == float("inf"):
            return -1
        return dp[-1]

(4)279题:完全平方数

题目:279. 完全平方数

和零钱兑换一模一样,区别就是物品是完全平方数

    def numSquares(self, n: int) -> int:
        import math
        # 完全背包
        dp = [float("inf")] * (n + 1)
        dp[0] = 0
        temp = math.ceil(math.sqrt(n))
        for i in range(1, temp + 1):
            for j in range(i * i, n + 1): # 这里不用考虑i组成不了dp[j]的情况,因为1的平方还是1,让任何数都有解
                dp[j] = min(dp[j - i * i] + 1, dp[j])
        print(dp)
        return dp[-1]

(5)139题:单词拆分 

题目:139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        """动态规划"""
        # 单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
        # 递归公式:dp[i]= dp[j] and s[j:i+1] in wordDdict
        #                 在i之前找到一个位置j---j之前满足拆分,dp[j] = True
        #                                   ----j到i之间满足拆分,s[j:i+1] in wordDdict 
        n = len(s)
        dp = [True] + [False] * n

        for i in range(1, n+1): 
            for j in range(i): 
                if dp[j] and s[j:i] in wordDict:
                    dp[i] = True
                    break #这里找到就结束了,减少循环次数
        return dp[-1]

9、多重背包

 类似面试题目(360的题):

有彩色粉笔m个,白色粉笔n个。
a个彩色粉笔+b个白色粉笔可以打包卖x元;
c个彩色粉笔打包卖y元;
d个白色粉笔打包卖z元。
问粉笔不一定用完,得到的最多价钱是多少?

举例

输入: 
m=5, n=5, a=1, b=2, c=3, d=3, x=2, y=1, z=3, 
输出: 
6

# 第一种:dp动态规划
def chalk1(m, n, a, b, c, d, x, y, z):
    # dp[i][j]表示i个彩色粉笔和j个白色粉笔的最大价值
    dp = [[0 for _ in range(m + 1)] for _ in range(n + 1)]
    # 物品做一个数据结构,方便下面遍历物品
    temp = {(a, b): x, (c, 0): y, (0, d): z}
    if temp_n and temp_m:
        max_n = min(n // temp_n, m // temp_m)
    elif temp_n:
        max_n = n // temp_n
    elif temp_m:
        max_n = m // temp_m
    for _ in range(max_n):
        for temp_m, temp_n in temp:
            for i in range(m, temp_m - 1, -1):
                 for j in range(n, temp_n - 1, -1):
                    # 递推公式和零和一的题目一模一样,寻找不装temp这个物品的最大价值+temp物品的价值
                    dp[i][j] = max(dp[i][j], dp[i - temp_m][j - temp_n] + temp[(temp_m, temp_n)])
    return dp[-1][-1]


#第二种:递归
def chalk2(m, n, a, b, c, d, x, y, z):
    res={} #这里放所有购买的彩色和白色粉笔的总数组合

    def temp(m, n):  # 此方法就是寻找m,n的不同组合的价值是多少
        if (m, n) not in res:
            r = 0
            # 找到三种购买方式的最大价值
            if m >= a and n >= b: 
                r = max(r, temp(m - a, n - b) + x)
            if m >= c:
                r = max(r, temp(m - c, n) + y)
            if n >= d:
                r = max(r,temp(m, n - d) + z)
            res[(m, n)] = r
        return res[(m, n)]

    return temp(m, n)

#第三种:递归,相比较第二种,没有创建递归子函数
def chalk3(m, n, a, b, c, d, x, y, z):
    res={}
    if (m, n) not in res:
        r = 0
        if m >= a and n >= b:
            r = max(r, chalk3(m - a, n - b, a, b, c, d, x, y, z) + x)
        if m >= c:
            r = max(r, chalk3(m - c, n, a, b, c, d, x, y, z) + y)
        if n >= d:
            r = max(r, chalk3(m, n - d, a, b, c, d, x, y, z) + z)
        res[(m, n)] = r

    return res[(m, n)]            

(1)打家劫舍

198. 打家劫舍

01背包问题

    def rob(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        if len(nums) == 1:
            return nums[0]
        dp = [0] * len(nums)
        dp[0], dp[1] = nums[0], max(nums[:2])
        for i in range(2, len(nums)):
            dp[i] = max(dp[i-1], dp[i-2] + nums[i])

        return dp[-1]

(2)打家劫舍II

213. 打家劫舍 II

    def rob(self, nums: List[int]) -> int:
        # 拆分两个部分的打家劫舍I, nums[1:]最后一家偷,那就不偷第一家
        #                        nums[:-1]不偷最后一家,偷第一家
        if len(nums) == 0:
            return 0
        if len(nums) == 1:
            return nums[0]

        def __dp(temp_nums):
            if len(temp_nums) == 1:
                return temp_nums[0]
            dp = [0] * len(temp_nums)
            dp[0] = temp_nums[0]
            dp[1] = max(temp_nums[:2])
            for i in range(2, len(temp_nums)):
                dp[i] = max(dp[i-1], dp[i-2] + temp_nums[i])
            return dp[-1]

        one = __dp(nums[:-1])
        two = __dp(nums[1:])    
        return max(one, two)

(3)打家劫舍III

(4)买卖股票的最佳时机

121. 买卖股票的最佳时机

 

    def maxProfit(self, prices: List[int]) -> int:
        """贪心方法"""
        res = 0
        low = float("inf")

        for i in prices:
            low = min(low, i)
            res = max(i-low, res)

        return res

    def maxProfit(self, prices: List[int]) -> int:
        """动态规划"""
        # dp[i]第i天:第一个值表示持股状态金额最大,第二值表示不持股状态最大值
        #不持股状态dp[i][1]:(1)i-1天就不持股保持dp[i-1][1],(2)i天买股票:i天价格prices[i] + 第i-1持股的最大价值dp[i-1][0]
        #持股状态dp[i][0]:(1)i-1天就持股保持dp[i-1][0],(2)i天卖股票:-prices[i]
        dp=[[0,0] for _ in range(len(prices))] 
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1, len(prices)):
            dp[i][0] = max(-prices[i],dp[i-1][0])
            dp[i][1] = max(dp[i-1][1],prices[i]+dp[i-1][0]) 
        return dp[-1][-1] #返回最后一天不持股的最大价值

(5)买卖股票的最佳时机II

122. 买卖股票的最佳时机 II

 

    def maxProfit(self, prices: List[int]) -> int:
        """贪心算法"""
        res = 0
        for i in range(1,len(prices)):
            res += max(prices[i]-prices[i-1], 0)

        return res

    def maxProfit(self, prices: List[int]) -> int:
        """动态规划"""
        dp = [[0, 0] for _ in range(len(prices))]
        dp[0][0] = -prices[0] 
        dp[0][1] = 0
        for i in range(1,len(prices)):
            # 第i天持股:(1)i-1天持股-->保持i-1持股状态最大收益:dp[i-1][0]
            #           (2)i-1天不持股-->i天买入:i-1不持股的最大收益dp[i - 1][1]-i天的价格prices[i]
            dp[i][0] = max(dp[i-1][0], dp[i - 1][1] - prices[i])
            # 第i天不持股:(1)i-1天持股-->i天卖出:i-1持股的最大收益dp[i - 1][0]+i天的价格prices[i]
            #             (2)i-1天不持股-->保持i-1不持股状态最大收益:dp[i-1][1]
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i])

        return dp[-1][-1]

   def maxProfit(self, prices: List[int]) -> int:
        """动态规划----一维滚动dp"""
        dp = [0, 0]
        dp[0] = -prices[0] 
        dp[1] = 0
        for i in range(1,len(prices)):
            dp[0] = max(dp[0], dp[1] - prices[i])
            dp[1] = max(dp[1], dp[0] + prices[i])

        return dp[-1]

(6)买卖股票的最佳时机III

 123. 买卖股票的最佳时机 III

    def maxProfit(self, prices: List[int]) -> int:
        """动态规划"""
        dp = [[0, 0, 0, 0, 0] for _ in range(len(prices))]
        dp[0][1] = -prices[0]
        dp[0][3] = -prices[0]

        for i in range(1, len(prices)):
            dp[i][0] = dp[i-1][0]
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
            dp[i][2] = max(dp[i-1][2], dp[i-1][1]+prices[i])
            dp[i][3] = max(dp[i-1][3], dp[i-1][2]-prices[i])
            dp[i][4] = max(dp[i-1][4], dp[i-1][3]+prices[i])
        print(dp)
        return dp[-1][-1]    


    def maxProfit(self, prices: List[int]) -> int:
        """动态规划----一维滚动dp"""
        dp = [0, 0, 0, 0, 0]
        dp[1] = -prices[0]
        dp[3] = -prices[0]

        for i in range(1, len(prices)):
            dp[1] = max(dp[1], dp[0]-prices[i])
            dp[2] = max(dp[2], dp[1]+prices[i])
            dp[3] = max(dp[3], dp[2]-prices[i])
            dp[4] = max(dp[4], dp[3]+prices[i])
        print(dp)
        return dp[-1]

(7)买卖股票的最佳时机IV

188. 买卖股票的最佳时机 IV

这里就是就要参考买卖股票的最佳时机III,买卖股票最多2次,这里是k次,观察规律

dp[i]的含义                               初始值                   递归公式

dp[0]  表示不操作状态             0                 
dp[1] 第⼀次买⼊状态             -prices[0]              max(dp[1], dp[0] - prices[1] )
dp[2] 第⼀次卖出状态             0                           max(dp[2], dp[1] +prices[2] )
dp[3] 第⼆次买⼊状态             -prices[0]              max(dp[3], dp[2] - prices[3] )
dp[4] 第⼆次卖出状态             0                           max(dp[4], dp[3] +prices[4] )
dp[5] 第三次买⼊状态             -prices[0]              max(dp[5], dp[4] - prices[5] )
dp[6] 第三次卖出状态             0
..................
dp[2*k-1] 第k次买⼊状态       -prices[0]             max(dp[2*k-1], dp[2*k-2] - prices[2*k-1] )
dp[2*k]    第k次卖出状态          0                           max(dp[2*k], dp[2*k-1] - prices[2*k] )
              
    def maxProfit(self, k: int, prices: List[int]) -> int:
        """动态规划----一维滚动dp"""
        if len(prices) == 0 or k == 0: 
            # 前面的股票prices长度限制是最小1,这里有0的风险
            return 0
        dp = [0 for _ in range(2*k+1)]
        # 初始化
        for i in range(1,len(dp)):
            if i % 2 == 1:
                dp[i] = -prices[0]

        for i in range(1,len(prices)):
            for j in range(1,len(dp)):
                if j % 2 == 1:
                    dp[j] = max(dp[j], dp[j-1]-prices[i])
                else:
                    dp[j] = max(dp[j], dp[j-1]+prices[i])
        
        return dp[-1]

    def maxProfit(self, k: int, prices: List[int]) -> int:
        """动态规划"""
        if len(prices) == 0 or k == 0:
            return 0
        dp = [[0 for _ in range(2*k+1)] for _ in range(len(prices))]
        # 初始化
        for i in range(1,len(dp[0])):
            if i % 2 == 1:
                dp[0][i] = -prices[0]

        for i in range(1,len(prices)):
            for j in range(1,len(dp[0])):
                if j % 2 == 1:
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]-prices[i])
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]+prices[i])
        
        print(dp)
        return dp[-1][-1]

(8)最佳买卖股票时机含冷冻期

309. 最佳买卖股票时机含冷冻期

 

    def maxProfit(self, prices: List[int]) -> int:
        """动态规划"""
        if len(prices) == 0:
            return 0
        dp = [[0, 0, 0, 0] for _ in range(len(prices))]
        dp[0][0] = -prices[0]

        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][3]-prices[i], dp[i-1][1]-prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][3])
            dp[i][2] = dp[i-1][0] + prices[i]
            dp[i][3] = dp[i-1][2]
        print(dp)
        return max(dp[-1][1:])

(9)买卖股票的最佳时机含⼿续费

714. 买卖股票的最佳时机含手续费

    def maxProfit(self, prices: List[int], fee: int) -> int:
        """贪心算法"""
        res = 0
        min_price = prices[0]

        for i in range(1,len(prices)):
            if min_price > prices[i]:
                min_price = prices[i]
            if prices[i] > min_price + fee:
                res += prices[i]-min_price-fee
                min_price = prices[i]-fee  
                #计算利润每次都要减去手续费,所以要让重置minPrice = prices[i] - fee;这样在明天收获利润的时候,才不会多减一次手续费!

        return res

    def maxProfit(self, prices: List[int], fee: int) -> int:
        """动态规划"""
        dp = [-prices[0], 0]

        for i in range(1, len(prices)):
            dp[0] = max(dp[0], dp[1]-prices[i])
            dp[1] = max(dp[1], dp[0]+prices[i]-fee)  #和股票II中一模一样,卖出计算的时候减去手续费就行了
        return dp[1]

(10)最⻓递增⼦序列

300. 最长递增子序列

    def lengthOfLIS(self, nums: List[int]) -> int:
        dp = [1] * len(nums)
        result = 1
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[j] < nums[i]:
                    dp[i] = max(dp[i], dp[j]+1)
            if dp[i] > result:
                result = dp[i]
            

        print(dp)
        return result

(11)最⻓连续递增序列

674. 最长连续递增序列

    def findLengthOfLCIS(self, nums: List[int]) -> int:
        res = 1
        temp = 1
        for i in range(1,len(nums)):
            if nums[i] > nums[i-1]:
                temp += 1
            else:
                temp = 1
            res = max(res,temp)
        return res

    def findLengthOfLCIS(self, nums: List[int]) -> int:
        """动态规划"""
        dp = [1] * len(nums)
        res = 1

        for i in range(1, len(nums)):
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1] + 1
            res = max(res, dp[i])
        return res

(12)最⻓重复⼦数组

718. 最长重复子数组

dp[i][j]含义:表示nums1前i个元素和nums2前j个元素的最长重复子数组

初始化:dp = [[0 for _ in range(len(nums1)+1)] for _ in range(len(nums2)+1)],最后一列一行是为了dp[0][0] = dp[-1][-1]准备的,要不然需要遍历从1开始

递归公式:dp[i][j] = dp[i-1][j-1] + 1

遍历:遍历nums1的每个index,内层遍历nums2的每个index

好理解的办法就是,遍历从1开始

    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        """动态规划"""
        res = 0
        dp = [[0 for _ in range(len(nums2))] for _ in range(len(nums1))]

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

(13)最⻓公共⼦序列

1143. 最长公共子序列

dp[i][j]含义:表示text1前i个元素和text2前j个元素的最长公共子序列

初始化:dp = [[0 for _ in range(len(text1)+1)] for _ in range(len(text2)+1)],最后一列一行是为了dp[0][0] = dp[-1][-1]准备的,要不然需要遍历从1开始

递归公式:情况1-----text1[i] == text2[j]----------dp[i][j]=dp[i][j]+1

                  情况2-----text1[i] != text2[j]-----------dp[i][j]=max(dp[i-1][j],dp[i][j-1])---text1的[0....i-1]和text2的[0....j] 、text1的[0....i]和text2的[0....j-1]这两个范围的最长公共子序列取大值 

遍历:遍历nums1的每个index,内层遍历nums2的每个index

  

从1开始遍历,似乎更好理解

    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        """动态规划"""
        dp = [[0 for _ in range(len(text1)+1)] for _ in range(len(text2)+1)]

        for i in range(len(text2)):
            for j in range(len(text1)):
                if text2[i] == text1[j]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

        return dp[-2][-2]

(14)不相交的线

1035. 不相交的线

最⻓公共⼦序列是一模一样的,这里需要想明白这点

    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        """动态规划-求两者的最长公共子序列"""
        dp = [[0 for _ in range(len(nums2)+1)] for _ in range(len(nums1)+1)]

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

        return dp[-2][-2]

(15)最⼤⼦序和

(16)判断⼦序列

(17)不同的⼦序列

(18)两个字符串的删除操作

(19)编辑距离

(20)回⽂⼦串

(21)最⻓回⽂⼦序列

(22)毕业旅行问题

题目描述
小明目前在做一份毕业旅行的规划。打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。由于经费有限,希望能够通过合理的路线安排尽可能的省一些路上的花销。给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。

输入描述
城市个数n(1<n≤20,包括北京)
城市间的车票价钱 n行n列的矩阵 m[n][n]

输出描述
最小车费花销 s

示例1
输入

4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0

输出

13

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值