leetcode2021年度刷题分类型总结(七)动态规划 (python/c++)

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划五部曲:

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

前菜:509. 斐波那契数

class Solution {
public:
    
    int fib(int n) {
        //基本解法:递归
        if(n==0 || n==1){
            return n;
        }
        return fib(n-1)+fib(n-2); //缺点:会有很多重复计算

        //优化解法:记忆化搜索
        

        //优化解法二:动态规划
        vector<int> dp(n+1,0);
        if(n==0||n==1){
            return n;
        }
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<=n;++i){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
};

例一:746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。

  • 支付 15 ,向上爬两个台阶,到达楼梯顶部。
    总花费为 15 。
class Solution(object):
    def minCostClimbingStairs(self, cost):
        """
        :type cost: List[int]
        :rtype: int
        """
        ##dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。 第i个台阶的体力值包括在内
        n=len(cost)
        dp=[0]*(n)
        dp[0]=cost[0]
        dp[1]=cost[1]

        for i in range(2,n):
            dp[i]=min(dp[i-1],dp[i-2])+cost[i] 

        return min(dp[n-1],dp[n-2]) #最后楼梯顶部可以从倒数第一个台阶到达,也可以从倒数第二个台阶到达,取最小

        ##优化空间复杂度
        n=len(cost)
        dp0=cost[0]
        dp1=cost[1]

        for i in range(2,n):
            sums=min(dp0,dp1)+cost[i]
            dp0=dp1
            dp1=sums
        
        return min(dp0,dp1)

例二:62.不同路径

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

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

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

class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        #深度优先搜索(超出时间限制)
        def dfs(i,j,m,n):
            if i>m or j>n:
                return 0
            if i==m or j==n:
                return 1
            return dfs(i+1,j,m,n)+dfs(i,j+1,m,n)

        return dfs(1,1,m,n)

        #动态规划
        dp=[[0]*n]*m #dp[i][j] 代表机器人到达i j 位置的路径数目


        for i in range(n): 
            dp[0][i]=1
        
        for i in range(m):
            dp[i][0]=1

        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[m-1][n-1]

        #动态规划优化时间复杂度
        # 简单理解,以m=3,n=7为例,
        # 就是先把第一行初始化(同时第一列也同时初始化了,因为dp[0]=1),然后到第二行时dp[j]+=dp[j-1] 相当于加上了上面格子的路径数dp[j]和左边格子的dp[j-1]
        # 最后提交dp[n-1]就是到最后一行最后一列的方案数
        dp=[0]*n
        
        for i in range(n):
            dp[i]=1

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

        return dp[n-1]

C++版:

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m,vector(n,0));
        
        for(int i=0;i<m;i++){
            dp[i][0]=1;
        }
        for(int j=0;j<n;j++){
            dp[0][j]=1;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        } 
        //
        return dp[m-1][n-1];

    }
};

例三.63. 不同路径 II

重点在想明白对于有障碍的格子怎么处理:遇到障碍,则此格子无法到达,此格子方案数为0

class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        """
        :type obstacleGrid: List[List[int]]
        :rtype: int
        """
        m=len(obstacleGrid)
        n=len(obstacleGrid[0])
        #需要考虑m=1 n=1

        dp = [[0 for _ in range(n)]  for _ in range(m)]

        for i in range(m):  ##在初始化首行首列过程中,考虑到如果遇到障碍,障碍之后都不可到达
            if obstacleGrid[i][0]==1:
                break
            else:
                dp[i][0]=1

        for j in range(n):
            if obstacleGrid[0][j]==1:
                break
            else:
                dp[0][j]=1

        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-1][j]+dp[i][j-1]

        return dp[m-1][n-1]

C++版

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
    int m = obstacleGrid.size();  //获取行数
    int n = obstacleGrid[0].size();  //获取列数

    vector<vector<int>> dp(m,vector(n,0));

    for(int i=0;i<m;i++){
        if(obstacleGrid[i][0]==0){
            dp[i][0]=1;
        }else{
            break;
        }
    }
    for(int j=0;j<n;j++){
        if(obstacleGrid[0][j]==0){
            dp[0][j]=1;
        }else{
            break;
        }
    }
    for(int i=1;i<m;i++){
        for(int j=1;j<n;j++){
            if(obstacleGrid[i][j]==0){
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
    }
    return dp[m-1][n-1];

    }
};

例四:343. 整数拆分

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

class Solution(object):
    def integerBreak(self, n):
        """
        :type n: int
        :rtype: int
        """
        #dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。 当dp采用这种定义时,dp[1]无法再拆分 dp[2]=1 
        #递推公式:dp[i]=max(dp[i],max((i-j)*j,dp[i-j]*j)) 而不是dp[i]=max(dp[i],dp[i-j]*dp[j])
        #max((i-j)*j,dp[i-j]*j)指权衡分拆成两个数(i-j)*j和分拆成两个以上的更多数dp[i-j]*j哪个乘积更大
        
        if n<3:
            return 1

        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],max((i-j)*j,dp[i-j]*j))

        return dp[n]

        # 使用递推公式:dp[i]=max(dp[i],dp[i-j]*dp[j])的正确写法
        # 此时dp[i]不再明确表示分拆数字i可以得到的最大乘积为dp[i],因为当大于3的数拆分到了以3为因子的一部分时,再往下拆分乘积会变的更小
        # 所以当拆分到了1 2 3为因子时,不再往下拆分,而是令dp[1]=1 dp[2]=2 dp[3]=3
        # 虽然这个初始化明确的违背dp[i]"分拆数字i可以得到的最大乘积为dp[i]"的定义
        if n<3:
            return 1
        if n==3:
            return 2

        dp=[0]*(n+1)
        dp[1]=1
        dp[2]=2
        dp[3]=3

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

        return dp[n]

        ##数论:将数字nn拆分为尽量多的33,可以保证乘积最大(证明见题目评论区)
        if n <= 3:
            return n - 1
        
        quotient, remainder = n // 3, n % 3
        if remainder == 0:
            return 3 ** quotient
        elif remainder == 1:
            return 3 ** (quotient - 1) * 4
        else:
            return 3 ** quotient * 2

留待解决的问题(分配座位,不能并排)

例五:198. 打家劫舍

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

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

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

class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n=len(nums)
        dp=[0]*(n+1)

        dp[1]=nums[0]
        # dp[2]=nums[1]

        for i in range(2,n+1):
            dp[i]=max(dp[i-1],dp[i-2]+nums[i-1])

        return dp[n]

例六:213. 打家劫舍 II

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

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

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n=len(nums)
        if n==1:
            return nums[0]
        if n==2:
            return max(nums[0],nums[1])

        val1=self.roblist(nums[1:n])
        val2=self.roblist(nums[:n-1])
        # print(n)
        # print(val1)
        # print(val2)

        return max(val1,val2)

    def roblist(self,nums):
        n=len(nums)
        dp=[0]*(n+1)
        # print(n)

        dp[1]=nums[0]

        for i in range(2,n+1):
            dp[i]=max(dp[i-1],dp[i-2]+nums[i-1])


        # print(dp[n])
        return dp[n]

例七:337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution(object):
    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        ##树形DP:就是在树上进行递归公式的推导。

        ##关键是要讨论当前节点抢还是不抢。每个当前节点可以分两种情况:root.val取或者不取
        def dfs(root):
            if root==None:
                return 0,0
            ly,ln=dfs(root.left)#ly,ln 左节点取和不取的最大值
            ry,rn=dfs(root.right)

            return root.val+ln+rn,max(ly,ln)+max(ry,rn)

        return max(dfs(root))

例八:121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        ##动态规划
        #对于每一天i,可以有三种动作,维持、买入、卖出
        #dp[i][0]:dp[i][0] 表示第i天持有股票所得最多现金 
        #dp[i][0]:表示第i天不持有股票所得最多现金
        n=len(prices)
        dp=[[0]*2 for _ in range(n)]
        dp[0][0]=-prices[0]
        dp[0][1]=0 

        for i in range(1,n):
            dp[i][0]=max(dp[i-1][0],-prices[i]) #i点之前买入的最大钱数
            dp[i][1]=max(dp[i-1][1],prices[i]+dp[i][0]) #i点之前卖出的最大钱数

        return dp[-1][1]


        ##贪心解法:
        #记录某一天之前的所有最小值,然后res用来记录当前节点前最大利润
        minval=float("inf")
        # maxval=-float("inf")
        n=len(prices)
        res=0

        for i in range(n):
            if prices[i]<minval:
                minval=prices[i] 
            if prices[i]-minval>res:
                res=prices[i]-minval

        return res

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

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        #动态规划
        #对于每天i,可以选择买入、卖出、不操作三种选项,存在两种状态
        #dp[i][0]  第i天持有股票后的最多现金(不操作、买入)
        #dp[i][1]  第i天持有的最多现金(不操作、卖出)
        #对于每个i都更新这两个状态
        n=len(prices)
        dp=[[0 for _ in range(2)] for _ in range(n)]

        dp[0][0]-=prices[0]
        dp[0][1]=0

        for i in range(1,n):
            #第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票)
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
            # 第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票)
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])

        return max(dp[n-1][0],dp[n-1][1])


        #贪心算法
        #对每一个点,都有买入、卖出、不操作三种选项。山谷买入、山峰卖出、斜坡上不操作
        #需要考虑削平的山峰和山谷怎么处理 比如[2 1 2 1 0 0 1]
        #所以买入点是prices[i]<=prices[i-1] and prices[i]
        #卖出点是prices[i]>prices[i-1] and prices[i]>=prices[i+1]
        n=len(prices)
        res=0
        buy=0 #默认起点卖出,如果不是,buy后续会被覆盖
        sell=0

        for i in range(1,n-1):
            if prices[i]<=prices[i-1] and prices[i]<prices[i+1]:
                buy=i
            if prices[i]>prices[i-1] and prices[i]>=prices[i+1]:
                sell=i
                res+=prices[sell]-prices[buy]

        if prices[n-1]>prices[n-2]:
            sell=n-1
            res+=prices[sell]-prices[buy]

        return res

C++版本

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        
        vector<long long> dp(amount+1,INT_MAX); 
        //INT_MAX代表2147483647 + 1 如果用int存回overflow
        // 2147483647 + 1cannot be represented in type 'int' (solution.cpp)
        dp[0]=0;
        for(auto coin:coins){
            for(int j=coin;j<amount+1;j++){
                dp[j]=min(dp[j],dp[j-coin]+1);
            }
        }
        // for(int i=0;i
        //     std::cout<
        // }
        
        if(dp[amount]!=INT_MAX)
            return dp[amount];
        
        return -1;
    }
};

例十:322. 零钱兑换

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

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

你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

class Solution(object):
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """

##此题优化过程
#1. 这种找路径,找方法的题一般可以使用回溯法来解决,回溯法也可以说是树形图法,解题的时候使用类似于树状图的结构,使用 自顶而下 的方法。

#2. 而在回溯法中,如果含有很多的重复的计算的时候,就可以使用记忆化的搜索,将可能出现的重复计算大状态使用一个数组来保存其值,在进行重复的计算的时候,就可以直接的调用数组中的值,较少了不必要的递归。

#3. 使用了记忆化搜索后,一般还可以进行优化,在记忆化搜索的基础上,变成 自底而上 的动态规划。


# 以上说明链接:https://leetcode.cn/problems/coin-change/solution/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/


        ##回溯算法(超时)
        if amount==0:
            return 0

        res=[]

        def backtracking(coins,cnt,tmp):
            if tmp==amount:
                res.append(cnt)
                return 
            if tmp>amount:
                return 
            for coin in coins:
                cnt+=1
                tmp+=coin
                backtracking(coins,cnt,tmp)
                cnt-=1
                tmp-=coin

        backtracking(coins,0,0)

        if res:
            return min(res)
        else:
            return -1 

        #记忆化搜索
        #在回溯的基础上将可能出现的重复计算大状态使用一个数组来保存其值

        #动态规划
        #dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
        dp=[float("inf")]*(amount+1) #考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
        dp[0]=0

        for coin in coins:
            for x in range(coin,amount+1):
                dp[x]=min(dp[x],dp[x-coin]+1) #递归逻辑:dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的

        return dp[amount] if dp[amount]!=float("inf") else -1
记忆化搜索

用python3.x来实现,因为使用到了@functools.lru_cache(amount)装饰器

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
   ##该函数是一个装饰器,为函数提供缓存功能。在下次以相同参数调用时直接返回上一次的结果。
        @functools.lru_cache(amount) 
        def dp(rem) -> int:
            if rem < 0: return -1
            if rem == 0: return 0
            mini = int(1e9)
            for coin in self.coins:
                res = dp(rem - coin)
                if res >= 0 and res < mini:
                    mini = res + 1
            return mini if mini < int(1e9) else -1

        self.coins = coins
        if amount < 1: return 0
        return dp(amount)

例十一:96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

class Solution(object):
    def numTrees(self, n):
        """
        :type n: int
        :rtype: int
        """
        # dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
        # 元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
        # 元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
        # 元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
        # 有2个元素的搜索树数量就是dp[2]。
        # 有1个元素的搜索树数量就是dp[1]。
        # 有0个元素的搜索树数量就是dp[0]。
        # 所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

        dp=[0]*(n+1)
        dp[0]=1
        dp[1]=1
        #对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
        #一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
        for i in range(2,n+1):
            for j in range(1,i+1):
                dp[i]+=dp[j-1]*dp[i-j]

        return dp[n]

01背包:

例十二:416. 分割等和子集

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

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

class Solution(object):
    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        ##二维背包
        n=len(nums)
        total=sum(nums)
        if total%2!=0:
            return False

        total//=2 #背包最多能装
        ##dp[i][j] 装0-i个物品时的最大元素和,j为bagweight,最大 total//=2,当dp[i][j]= total//2,则return True
        #dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) 这里特别的是value[i]==weight[i]
        #初始化
        dp=[[0 for _ in range(total+1)] for _ in range(n)]
        for j in range(1,total+1):
            if j>=nums[0]: #当背包的大小大于第一个数的weight
                dp[0][j]=nums[0] #把第一个数的value放进背包,刚好这里value[i]和weight[i]都是nums[i]

        for i in range(1,n): #n行对应n个物品,第一行已经初始化
            for j in range(1,total+1): #
                if j<nums[i]:
                    dp[i][j]=dp[i-1][j]
                else:
                    dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i])

        return dp[n-1][total]==total #如果大小为sum/2的背包最多可以装入sum/2的值,那么剩下的也为sum/2,即两个子集元素和相等



        ##优化
        #一维背包
        # 递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        # 其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
        # 与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
        # 这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
        n=len(nums)
        total=sum(nums)
        dp=[0]*(total+1)
        if total%2!=0:
            return False

        total//=2
        for i in range(n):
            for j in range(total,nums[i]-1,-1):
                dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]) #dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

        return dp[total]==total

例十三:1049. 最后一块石头的重量 II

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

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

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

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

class Solution(object):
    def lastStoneWeightII(self, stones):
        """
        :type stones: List[int]
        :rtype: int
        """
        #主旨在于把石头分成尽量相等的两堆,和分割等和子集很像,但是返回值不再是True False
        ##二维背包
        totalweight=sum(stones)
        total=totalweight//2
        n=len(stones)

        dp=[[0 for _ in range(total+1)] for _ in range(n)]
        for j in range(1,total+1):
            if j>=stones[0]: #当背包的大小大于第一个数的weight
                dp[0][j]=stones[0] #把第一个数的value放进背包,刚好这里value[i]和weight[i]都是nums[i]

        for i in range(1,n): #n行对应n个物品,第一行已经初始化
            for j in range(1,total+1): #
                if j<stones[i]:
                    dp[i][j]=dp[i-1][j]
                else:
                    dp[i][j]=max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i])


        return (totalweight-dp[n-1][total])-dp[n-1][total]

        ##一维背包
        totalweight=sum(stones)
        total=totalweight//2
        n=len(stones)
        dp=[0]*(total+1)

        for i in range(n):
            for j in range(total,stones[i]-1,-1): #一定要注意这里的循环是倒序,从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
            ##二维背包不用考虑这个问题,对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
                dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])


        return (totalweight-dp[total])-dp[total]
        #上面这段循环等价于:
        # for i in range(n):
        #     for j in range(total,stones[i]-1,-1):
        #         if j
        #             continue
        #         else:
        #             dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])

例十四:518. 零钱兑换 II

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

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

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

题目数据保证结果符合 32 位带符号整数。
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

class Solution(object):
    def change(self, amount, coins):
        """
        :type amount: int
        :type coins: List[int]
        :rtype: int
        """
        #dp[j]:可以凑成总金额j的硬币组合数
        #求装满背包有几种方法,一般公式都是:dp[j] += dp[j - nums[i]]

        ##要注意的地方有两点:
        #1。如果求组合数就是外层for循环遍历物品,内层for遍历背包。
        # 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
        
        #2.01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
        # 而完全背包的物品是可以添加多次的,所以要从小到大去遍历

        dp=[0]*(amount+1)

        dp[0]=1

        for coin in coins:
            # print('########')
            # print(coin)
            for j in range(coin,amount+1):
                # dp[j]=max(dp[j],dp[j-coin])
                dp[j]+=dp[j-coin]
                # print(dp[j])

        return dp[amount]

例十五:139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。

回溯算法 和 记忆化搜索

class Solution {
// private:
//     //回溯算法
//     bool backtracking (const string& s, const unordered_set& wordSet, int startIndex) {
//         if (startIndex >= s.size()) {
//             return true;
//         }
//         for (int i = startIndex; i < s.size(); i++) {
//             string word = s.substr(startIndex, i - startIndex + 1);
//             if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) {//枚举分割所有字符串,判断是否在字典里出现过。
//                 return true;
//             }
//         }
//         return false;
//     }
// public:
//     bool wordBreak(string s, vector& wordDict) {
//         unordered_set wordSet(wordDict.begin(), wordDict.end());
//         return backtracking(s, wordSet, 0);
//     }

//记忆化搜索
private:
    bool backtracking(const string& s,const unordered_set<string>& wordSet,vector<bool>& memory,int startIndex){
        if(startIndex>=s.size()){
            return true;
        }

        if(!memory[startIndex]) return memory[startIndex];  如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果
        for(int i=startIndex;i<=s.size();i++){
            string word= s.substr(startIndex,i-startIndex+1);
            if(wordSet.find(word)!=wordSet.end()&&backtracking(s,wordSet,memory,i+1)){
                return true;
            }
        }

        memory[startIndex] = false; // 记录以startIndex开始的子串是不可以被拆分的
        return false;

    }
public:
    bool wordBreak(string s,vector<string>& wordDict){
        unordered_set<string> wordSet(wordDict.begin(),wordDict.end());
        vector<bool> memory(s.size(),1); //以memory数组记录中间结果
        return backtracking(s,wordSet,memory,0);
    }
};

动规

class Solution(object):
    def wordBreak(self, s, wordDict):
        """
        :type s: str
        :type wordDict: List[str]
        :rtype: bool
        """
        ##最直观:
        ##回溯算法一个爆搜,查以i为下标位置的子串能否被拆分
        #但是太费时间
        #dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
        ##注意,这里要求返回True or False 才有用动规的可能,若要返回字符串之类的一般只能回溯



        dp=[False]*(len(s)+1) ##初始化以i开始的子串是不可以被拆分的

        dp[0]=True ##在0位置之前 拆
        for j in range(1,len(s)+1):
            for word in wordDict:
                if j>=len(word):
                    dp[j]=dp[j] or (dp[j-len(word)] and word==s[j - len(word):j])

        return dp[len(s)]

例十六:123. 买卖股票的最佳时机 III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        #错误版本
        ###
        # n=len(prices)
        # dp=[[0]*5 for _ in range(n)]
        # dp[0][1]=-prices[0]

        # if n>=4:
        #     dp[2][3]=-prices[2]

        # for i in range(1,n):
        #     dp[i][1]=max(dp[i-1][1],-prices[i])#i之前第一次买入还是i第一次买入
        #     dp[i][2]=max(dp[i-1][2],prices[i]+dp[i-1][1])#i之前第一次卖出还是i第一次卖出
            
        #     #第二次交易要严格在第一次之后
        #     if i>=3:
        #         dp[i][3]=max(dp[i-1][3],-prices[i])##i之前第二次买入还是i第一次买入
        #         dp[i][4]=max(dp[i-1][4],prices[i]+dp[i-1][3])

        # return dp[-1][4]

        # 一天一共就有五个状态,
        # 没有操作
        # 第一次买入
        # 第一次卖出
        # 第二次买入
        # 第二次卖出
        ##有两点说明
        # 1.第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
        #2.dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票 dp[i][0]等同理
        if len(prices) == 0:
            return 0
        dp = [[0] * 5 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])
        return dp[-1][4]

参考:
【1】代码随想录
【2】力扣

你可能感兴趣的:(动态规划,python,算法)