leetcode_动态规划_python

总的要点:

  • 明确dp的含义。初始化时注意限制输入,初始化后注意迭代时要从初始化之后开始!从初始化之后开始!从初始化之后开始!
  • 步骤:明确含义和递推公式,创建dp;初始化dp;迭代dp.
  • 二维dp和滚动数组的区别:二维dp最后一个for内更新所用的参量在该循环内不会改变,但滚动数组会改变所用的参数(这也是为什么可以写成滚动数组的原因)。写成滚动数组要注意遍历的顺序。不是把dp定义成二维就是二维dp。
  • 无论元素能否重复使用,二维dp中for循环的嵌套顺序是无所谓的,因为最内层的for循环不会改变该层循环所需要的参数。且迭代时都是顺序。
  • 而对于滚动数组,如果元素不能重复使用,则元素在外背包在内,顺序不能颠倒,且背包要倒序,以使得物品不会重复放置;如果元素可以重复使用,嵌套顺序可以交换,且背包是正序。
  • 组合类问题,如各种是否能凑到目标和,dp与元素排列顺序无关的,外层物品内层包->这样相当于物品遍历的顺序就是该物品在集合中的相对次序,先出现的不会排在后出现的后面
  • 排列类问题,需要各种顺序,所以外层包内层物品。

总而言之,bugfree的写法:先确定dp的含义和递推公式,并根据递推公式确定初始化的值(因为有很多初始化的实际意义很难解释,只是方便计算)。然后确定是组合问题还是排列问题,组合问题则物品在外背包在内,排列问题则背包在外物品在内;如果物品可以重复则顺序,如物品不能重复则倒序([a,b] 的倒序为range(a,b-1,-1)。

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

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

思路:dp[i]表示第i天手里可能有的现金的最大值,dp[i][0]表示该天手中没有股票,dp[i][1]表示该天手中有股票。则当天手中没有股票可能是因为:1.前一天也没有股票。2.前一天有股票今天卖了。当天有股票可能是因为:1.昨天有股票今天继续持有;2.昨天没有股票今天卖了。统一交易费在卖出股票时交。

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        #易错点:利润和不等于每天利润和的拆解,因为每次拆解都需要手续费
        #dp[i][0]:最多现金:第i天没有股票 dp[i][1]:有股票
        dp = [[0, 0] for i in range(len(prices)) ]
        dp[0][1] = -prices[0]
        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]-fee)
            dp[i][1] = max(dp[i-1][0]-prices[i], dp[i-1][1])
        return max(dp[len(prices)-1])

 leetcode70.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。

思路:dp[i]为爬到第i个台阶的方法数量。则按最后一次上的台阶数量,总方法分为两类:最后上1个和最后上2个。所以dp[i] = dp[i-1] + dp[i-2]。(注意不要再将最后上两个台阶拆成两种方案,因为拆开->上两次一个台阶的方案属于dp[i-1]中。所以直接按最后一步走多少个台阶把方法分成两类。)初始化时初始化第一和第二个台阶,即选项阶梯,所以要在前面判断给定的n是否大于等于2。因为(1,2)和(2,1)属于两种方案,所以其实是排列问题,所以背包在外物品在内。可以重复选择物品,所以顺序遍历。

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        dp = [0 for i in range(n+1)]
        dp[1] = 1
        dp[2] = 2
        step_choice = [1, 2]
        for i in range(3, n+1):
            for j in range(len(step_choice)):
                step = step_choice[j]
                if step <= i:
                    dp[i] += dp[i-step]
        return dp[n]

leetcode746.使用最小花费爬楼梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

思路:实际上需要爬到第i+1个楼梯。dp[i]表示爬到第i个楼梯所需要的最小花费,则该花费的方案是:上一次爬一个、或上一次爬两个中的最小值。比较绕的是到底要爬到哪一层。

leetcode62.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

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

 

 

 思路:方法1:排列组合。相当于一共需要向下走m-1步,向右走n-1步,即从m+n-2中选m-1向下其余均向右Cm+n-1n-1。计算时为了防止溢出,每次都除。Cmn类公式上下都有n项,所以直接range(0,n) (此处为n-1),减去即可。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        res = 1
        num1 = m+n-2
        count = n-1
        num2 = n-1
        for i in range(count):
            res = res*(num1-i)/(num2-i)
        return round(res)

方二:动态规划。dp[i][j]走到(i,j)的路径数目。初始化时注意最左边的一列和最上边的一行只有一种路径。且在迭代时,从1开始,即不要修改初始化的值!因为python里list索引-1也是有效的,不会报错但结果不正确。所以动态规划类问题注意在迭代时避开初始化的位置!避开初始化的位置!避开初始化的位置!

 leetcode63.不同路径2

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

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

思路:类似62.区别在于1.初始化时,对于最上面和最左边,一旦有障碍,则后面所有点都不可达,路径数目为0。2.中间如果是障碍,直接路径为0。

leetcode343.整数拆分

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

思路:方法一:可以证明(利用多项的均值不等式,对拆成m个加和的m求导,算出最佳均值为e),将数拆成3的加和乘积最大,如果不能,若余1则退一个3补两个2,余2则直接补2.注意判断数字是否小于3,3以下单论。

class Solution:
    def integerBreak(self, n: int) -> int:
        if n==2: #注意这两种情况需要单独考虑 因为推公式时单独考虑了
            return 1
        if n == 3:
            return 2
        order_3 = n//3
        if n%3 == 0:
            return int(math.pow(3, order_3))
        elif n%3 == 1:#退1个3补两个2
            return int(math.pow(3, order_3-1)*4)
        else:
            return int(math.pow(3, order_3)*2)

方法二:dp[i]表示拆分数字i可以获得的最大乘积。假设其中一个差分数为j,则dp[i]为dp[i]本身,(拆分成两个,i-j不拆分)j*(i-j),(拆分成三个及以上,i-j拆分)j*dp[i-j]中的最大值。注意j的取值范围,因为另一个数要拆分则必然大于等于2,所以j最大取到i-2,因此是i-1的半开区间。

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [0 for i in range(n+1)]
        dp[2] = 1
        # dp[3] = 2
        for i in range(3,n+1): #i=num1+(i-num1)
            for num_1 in range(1,i-1):#注意范围 从1开始 因为要保证另一个索引大于等于2 所以半开为i-1
                dp[i] = max(dp[i], num_1*(i-num_1), num_1 * dp[i-num_1])
        
        return dp[n]

leetcode96.不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

思路:首先要明确一个概念,对于递增序列,相同数量的节点意味着相同数量的BST种类,因为BST与各个元素间的相对值大小有关,但与具体是多少值无关。dp[i]:i个节点的递增序列能构成的BST种类。根节点可以选择1到i,所以j的范围取[1,i+1),二叉树种类数等于所有根节点带来的种类之和。注意迭代时要跳过初始化!跳过初始化!跳过初始化!

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0 for i in range(n+1)]
        dp[0] = 1
        dp[1] = 1
        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]

 leetcode416.分割等和子集

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

思路:即找到集合使得集合内元素的和为数组总和的一半。因为所有数字都是整数,所以如果总和不能被2整除则返回False。相当于nums为物品的重量,dp[i]为向容量为i的背包里装东西。方法一:dp[i]表示容量为[i]的背包在目前已经见过物品的组合情况下是否能装满。注意dp[0]要初始化为True。方法二:dp[i]表示容量为i的背包在目前已见过物品品的组合情况下最多能装多少,最后判断target是否能装满。

易错点:1.因为每个物品只能选择一次,所以外层写物品内层写背包,且背包是倒序。2.正序闭区间[a,b]写法为range(a,b+1),即末尾的加上步长,所以[a,b]倒叙的写法为[b,a-1],加上步长-1。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        #有一个重量为sum/2的背包,最多可以放多少东西 等于则可以
        
        sum_all = sum(nums)
        if sum_all%2:
            return False
        else:
            capacity_bag = sum_all//2
        dp = [0 for i in range(capacity_bag+1)] #dp[i]:dp[重量]
        for i in range(len(nums)):
            weight_object = nums[i]
            for j in range(capacity_bag,weight_object-1,-1): #[weight_object, capacity_bag]倒序
                dp[j] = max(dp[j], dp[j-weight_object]+weight_object)
            if dp[capacity_bag] == capacity_bag:
                return True
        return dp[capacity_bag] == capacity_bag

leetcode1049.最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

思路:将一堆石头分成两部分,每部分接近总重量的一半。注意写倒序的两个要点:范围,范围,范围,要写-1,要写-1,要写-1。

leetcode494.目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

思路:相当于凑目标和,加号集合和减号集合的和已知,差已知,所以凑加号集合的目标和。首先考虑目标:方法数。因此dp[i]设置为装满i容量包的方法数。初始化:dp[0] = 1。迭代:因为不能重复,所以倒序。边界条件:因为都是整数,所以目标和非整数不行;S的绝对值不能大于所有数的和

class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
        #left+right = sum(nums) left-right = S->left = (sum(nums)+S)//2
        sum_positive_set = (sum(nums)+S)//2
        if (sum(nums)+S)%2 or abs(S)>sum(nums): #注意
            return 0
        dp = [0 for i in range(sum_positive_set+1)] #dp[i] 凑到i有多少种方法
        dp[0] = 1
        for i in range(len(nums)):
            value_num = nums[i]
            for j in range(sum_positive_set,value_num-1,-1):
                dp[j] += dp[j-value_num] #已有的方法加上通过nums[i]可以凑出的方法
        return dp[sum_positive_set]

leetcode474.一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

思路:最大子集元素数量dp[0的个数][1的个数]。需要注意两点:1.迭代时最大的情况有两个:最大子集没有该元素,即已经是最大的;最大子集有该元素,则总大小为另一半集合的大小加上1(该元素的大小)。3.虽然dp设置为二维,但并不是二维dp,而是滚动数组,因为迭代时涉及到本轮for的变量。而本题不能元素不能重复取用,所以要用倒序。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0 for i in range(n+1)] for i in range(m+1)] #dp[0的个数][1的个数]子集大小 注意次序

        def getNumZeroAndOne(string):
            num_one = 0
            num_zero = 0
            for i in range(len(string)):
                if string[i] == '0':
                    num_zero += 1
                elif string[i] == '1':
                    num_one += 1
            return num_zero, num_one
        
        for i_str in range(len(strs)):
            string_temp = strs[i_str]
            num_zero, num_one = getNumZeroAndOne(string_temp)
            for i in range(m, num_zero-1, -1): #注意倒序
                for j in range(n, num_one-1,-1):
                    dp[i][j] = max(dp[i][j], dp[i-num_zero][j-num_one]+1) #注意+1
        return dp[m][n]

 leetcode518.零钱兑换2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 

思路:可以重复使用,所以遍历顺序从前往后。dp[i]表示组合数,外层for循环表示在num[:i]中的组合数。

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        if not coins:
            return 1 if amount == 0 else 0
         
        dp = [0 for i in range(amount+1)]
        dp[0] = 1

        for i in range(len(coins)):
            value_coin = coins[i]
            for i_dp in range(value_coin, amount+1):
                dp[i_dp] += dp[i_dp-value_coin]
        return dp[amount]

leetcode377.组合总和4

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的排列个数(长度不限)。

思路:排列问题,物品的顺序要求各种都有,所以背包在外物品在内。可以重复,所以顺序遍历。注意:背包是否能放下物品。

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0 for i in range(target+1)]
        dp[0] = 1

        for i in range(1, target+1): #排列问题先背包后物品 
            for num_temp in nums:
                if num_temp <= i:
                    dp[i] += dp[i-num_temp]
        return dp[target]

leetcode279.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

思路:和为i的集合最小元素数量dp[i]。num dp[i] = min(dp[i], dp[i-num]+1)。所以dp[i]初始化为无限大,dp[0]=0。强调数量,所以是组合问题。可以重复取用->顺序遍历。注意平方数中要考虑1,1的平方=1而其他数的平方均比本身大。所以可以单拿1出来或者在平方数内考虑到n。

class Solution:
    def numSquares(self, n: int) -> int:
        #排列还是组合->for谁在外谁在内 是否可以重复选元素:顺序还是倒序
        dp = [float('inf') for i in range(n+1)]
        dp[0] = 0
        for i in range(1, n+1):
            num = i*i
            for j in range(num, n+1):
                dp[j] = min(dp[j], dp[j-num]+1)
        return dp[n]

leetcode139.单词拆分

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

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

思路:dp[i]表示s[:i)是否可以拆分,则i的最大值为len(s),所以range内取len(s)+1。dp[i] = dp[i] or dp[j] and s[j:i] in wordDict。因此dp[0]取True

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False for i in range(len(s)+1)] #dp[i]:s[:i]是否在字典里
        dp[0] = True
        for i in range(len(s)+1):
            for j in range(i):
                dp[i] = dp[i] or s[j:i] in wordDict and dp[j]
        return dp[len(s)]
posted @ 2021-02-17 17:02  bokeyuan6  阅读(62)  评论(0编辑  收藏  举报