Leetcode 动态规划 I

每个状态都是由上一个状态推导而来

1)确定dp数组及下标含义;2)确定递推公式;3)dp数组及初始化;4)确定遍历顺序;5)举例推导dp数组

01背包:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

1)先物品再背包,可否颠倒?

二维dp可颠倒,一维dp不可以颠倒。因为一维dp要倒序遍历,若先背包再物品,每个dp[j]只会放一次物品。

2)二维dp背包容量从小到大遍历/一维dp背包容量从大到小遍历。 为什么?

因为一维dp若正序遍历,物品0会被重复放入,所以从后往前遍历,每次取得状态不会和之前取得状态重合。

完全背包:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

01背包(从大到小)和完全背包(从小到大)只有遍历顺序不同。

完全背包中一维dp遍历顺序(物品、背包)可颠倒

求装满背包有几种方法,递推公式一般为:dp[j]+=dp[j-nums[i]]

组合和排列的区别:物品、背包顺序->组合;背包、物品顺序->排列

多重背包:有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用(物品有数量限制),每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

方法:1)改变物品数量为01背包格式

for i in range(len(nums)):
    while nums[i]>1:
        weight.append(weight[i])
        vaklue.append(value[i])
        nums[i]-=1

2)改变遍历个数

for i in range(len(weight)):
    for j in range(bag_weight,weight[i]-1,-1):
        for k in range(1,nums[i]+1):
            if j-k*weight[i]>=0:
                dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i])

背包递推公式:

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

问装满背包有几种方法:dp[j] += dp[j - nums[i]]

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j])

509. 斐波那契数

class Solution(object):
    def fib(self, n):
        if n<=1:return n
        dp=[0]*(n+1)
        dp[0],dp[1]=0,1
        for i in range(2,n+1):
            dp[i]=dp[i-1]+dp[i-2]
        return dp[-1]

70. 爬楼梯

从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

所以dp[i] = dp[i - 1] + dp[i - 2] 。

class Solution(object):
    def climbStairs(self, n):
        if n==1:return n 
        dp=[0]*(n+1)
        dp[1]=1
        dp[2]=2
        for i in range(3,n+1):
            dp[i]=dp[i-1]+dp[i-2]
        return dp[n]

746. 使用最小花费爬楼梯

第0个和第1个台阶不消耗体力,所以dp[0]和dp[1]都为0

class Solution(object):
    def minCostClimbingStairs(self, cost):
        dp=[0]*(len(cost)+1)
        dp[0],dp[1]=0,0
        for i in range(2,len(cost)+1):
            dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
        return dp[-1]

62. 不同路径

class Solution(object):
    def uniquePaths(self, m, n):
        dp=[[0]*n for _ in range(m)]
        for i in range(n):
            dp[0][i]=1
        for j in range(m):
            dp[j][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[-1][-1]

63. 不同路径 II

初始化时若遇到障碍物直接break

class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        m=len(obstacleGrid)
        n=len(obstacleGrid[0])
        dp=[[0]*n for _ in range(m)]
        for i in range(n):
            if obstacleGrid[0][i]==1:
                break
            dp[0][i]=1
        for j in range(m):
            if obstacleGrid[j][0]==1:
                break
            dp[j][0]=1
        for i in range(1,m):
            for j in range(1,n):
                if obstacleGrid[i][j]!=1:
                    dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[-1][-1]

343. 整数拆分

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

j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

拆分成m个相似的数得到结果才最大,所以j的范围为(1,i/2+1)

class Solution(object):
    def integerBreak(self, n):
        dp=[0]*(n+1)
        dp[2]=1
        for i in range(3,n+1):
            for j in range(1,i/2+1):
                dp[i]=max(dp[i],max((i-j)*j,dp[i-j]*j))
        return dp[-1]

96. 不同的二叉搜索树

 dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 

class Solution(object):
    def numTrees(self, n):
        dp=[0]*(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[-1]

494. 目标和

01背包问题,一维dp先物品再背包

假设加法的总和为x,那么减法对应的总和就是sum - x。所以我们要求的是 x - (sum - x) = target,得到x = (target + sum) / 2

class Solution(object):
    def findTargetSumWays(self, nums, target):
        sum_=sum(nums)
        if (target+sum_)%2==1 or abs(target)>sum_:return 0
        
        bagsize=(target+sum_)//2
        dp=[0]*(bagsize+1)
        dp[0]=1
        for i in range(len(nums)):
            for j in range(bagsize,nums[i]-1,-1):
                dp[j]+=dp[j-nums[i]]
        return dp[-1]

1049. 最后一块石头的重量 II

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了,01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。物品的重量为stones[i],物品的价值也为stones[i]。

我们要求的target其实只是最大重量的一半,最后dp[target]里是容量为target的背包所能背的最大重量。那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

class Solution(object):
    def lastStoneWeightII(self, stones):  
        sum_=sum(stones)
        target=sum_//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 sum_-2*dp[-1]

416. 分割等和子集

01背包,求集合里能否出现总和为 target=sum / 2 的子集。

dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。

那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。

class Solution(object):
    def canPartition(self, nums):
        if len(nums)==1:return False
        sum_=sum(nums)
        if sum_%2==1:return False
        bagsize=sum_//2
        dp=[0]*(bagsize+1)
        for i in range(len(nums)):
            for j in range(bagsize,nums[i]-1,-1):
                dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
        if dp[-1]==bagsize:return True
        else:return False

698. 划分为k个相等的子集

473. 火柴拼正方形

回溯

class Solution(object):
    def canPartitionKSubsets(self, nums, k):
        if sum(nums)%k!=0:return False
        target=sum(nums)//k
        self.res=[0]*(k)
        nums.sort(reverse=True)
        return self.back(nums,k,target,0)
    def back(self,nums,k,target,index):
        if index==len(nums):
            #既然所有数字都放进集合中了,那么所有bucket中的元素和一定为target。
            return True
        for i in range(k):
            if self.res[i]+nums[index]>target:continue
            if i>0 and self.res[i]==self.res[i-1]:continue
            self.res[i]+=nums[index]
            if self.back(nums,k,target,index+1):return True
            self.res[i]-=nums[index]
        return False

474. 一和零

01背包

class Solution(object):
    def findMaxForm(self, strs, m, n):
        dp=[[0]*(m+1) for _ in range(n+1)]
        for s in strs:
            zero,one=0,0
            for i in range(len(s)):
                if s[i]=='0':zero+=1
                else:one+=1
            for i in range(n,one-1,-1):
                for j in range(m,zero-1,-1):
                    dp[i][j]=max(dp[i][j],dp[i-one][j-zero]+1)
        return dp[-1][-1]

518. 零钱兑换 II

完全背包。求装满背包有几种方法,递推公式都是:dp[j] += dp[j - nums[i]]

遍历顺序的区别:先物品再背包,计算的是组合数;先背包再物品,计算的是排列数

eg:组合({1,5});排列({1,5},{5,1})

class Solution(object):
    def change(self, amount, coins):
        dp=[0]*(amount+1)
        dp[0]=1
        for i in range(len(coins)):#物品
            for j in range(coins[i],amount+1):#背包
                dp[j]+=dp[j-coins[i]]
        return dp[-1]

377. 组合总和 Ⅳ

class Solution(object):
    def combinationSum4(self, nums, target):
        dp=[0]*(target+1)
        dp[0]=1
        for j in range(1,target+1):
            for i in range(len(nums)):
                if j>=nums[i]:
                    dp[j]+=dp[j-nums[i]]
        return dp[-1]

70. 爬楼梯

排列问题

class Solution(object):
    def climbStairs(self, n):
        dp=[0]*(n+1)
        dp[0]=1
        for j in range(n+1):
            for i in range(1,3):
                if j>=i:
                    dp[j]+=dp[j-i]
        return dp[-1]

322. 零钱兑换

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

class Solution(object):
    def coinChange(self, coins, amount):
        dp=[float('inf')]*(amount+1)
        dp[0]=0
        for i in range(len(coins)):
            for j in range(coins[i],amount+1):
                dp[j]=min(dp[j],dp[j-coins[i]]+1)
        if dp[-1]!=float('inf'):return dp[-1]
        else:return -1

279. 完全平方数

完全背包

class Solution(object):
    def numSquares(self, n):
        dp=[float('inf')]*(n+1)
        dp[0]=0
        nums=[i**2 for i in range(1,n+1) if i**2<=n]
        for j in range(1,n+1):
            for i in range(len(nums)):
                dp[j]=min(dp[j],dp[j-nums[i]]+1)
        return dp[-1]

139. 单词拆分

完全背包

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

class Solution(object):
    def wordBreak(self, s, wordDict):
        dp=[False]*(len(s)+1)
        dp[0]=True
        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[-1]

198. 打家劫舍

如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。

如果不偷第i房间,那么dp[i] = dp[i - 1],即考虑i-1房,然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

class Solution(object):
    def rob(self, nums):
        if len(nums)==1:return nums[0]
        dp=[0]*(len(nums))
        dp[0],dp[1]=nums[0],max(nums[0],nums[1])
        for j in range(2,len(nums)):
            dp[j]=max(dp[j-1],dp[j-2]+nums[j])
        return dp[-1]

213. 打家劫舍 II

两种情况,有首无尾,有尾无首

class Solution(object):
    def rob(self, nums):
        if len(nums)==1:return nums[0]
        res1=self.robrange(nums[1:])
        res2=self.robrange(nums[:-1])
        return max(res1,res2)
        
    def robrange(self,num):
        dp=[0]*len(num)
        dp[0]=num[0]
        for j in range(1,len(num)):
            if j==1:dp[j]=max(dp[j-1],num[j])
            else:dp[j]=max(dp[j-1],dp[j-2]+num[j])       
        return dp[-1]

337. 打家劫舍 III

动态规划+递归

本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。

如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子

dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

class Solution(object):
    def rob(self, root):
        res=self.robtree(root)
        return max(res)
    def robtree(self,root):
        #0不偷,1偷
        if not root:return [0,0]
        left=self.robtree(root.left)
        right=self.robtree(root.right)
        val1=root.val+left[0]+right[0]
        val2=max(left[0],left[1])+max(right[0],right[1])
        return [val2,val1]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值