1.动态规划
动态规划 是由前一个状态推导出
贪心算法 是直接取局部最优
动态规划需要直到状态转移公式
解题过程分为5步
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
2.简单和中等题
509. 斐波那契数 - 力扣(LeetCode) (leetcode-cn.com)
其实也不用写dp数组,这道题就可以解出来,但是这道题还是用了动态规划的思想,每个数之间是有关系的
class Solution:
def fib(self, n: int) -> int:
if n<2:return n
a, b = 0, 1
for i in range(n-1):
a=a+b
a,b=b,a
return b
70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)
这道题同上一道题
class Solution:
def climbStairs(self, n: int) -> int:
#每次爬台阶相当于是 n-1情况 和n-2情况和,
dp=[]
for i in range(n):
if i<2: dp.append(i+1)
else:
dp[0]+=dp[1]
dp[0],dp[1]=dp[1],dp[0]
return dp[-1]
746. 使用最小花费爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)
进阶版的爬楼梯
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
#dp[i]为某一层花费 dp[i] =min(dp[i-1] dp[i-2])+cost[i]
#dp[0]=1 dp[1]=100
dp = [0]*len(cost) #提前配置好空间,再赋值会快很多
for i in range(len(cost)):
if i <2: dp[i]=cost[i]
else:dp[i]= min(dp[i-1],dp[i-2]) + cost[i]
return min(dp[-2:])
62. 不同路径 - 力扣(LeetCode) (leetcode-cn.com)
这道题和爬楼梯类似,不过是一个二维的
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
#第mn 行肯定是 第m-1 n 行 和第m n-1行移动过来的
#dp表示mn位置的条数 dp[m][n]
#初始化 m=0位置为1 n=0位置为1 因为边上只有一条路径可以走
dp= [[1]*n for _ 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]
63. 不同路径 II - 力扣(LeetCode) (leetcode-cn.com)
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
#同前一道题,但是加入了障碍物,所以要将障碍物位置变为0,
dp = obstacleGrid
for i in range(len(dp)):
for j in range(len(dp[0])):
if not dp[i][j]-1: dp[i][j]=0
elif not i and not j :dp[i][j]=1
else: dp[i][j] =(dp[i-1][j] if i>0 else 0)+(dp[i][j-1] if j >0 else 0)
return dp[-1][-1]
343. 整数拆分 - 力扣(LeetCode) (leetcode-cn.com)
这道题的确不好想,而且 是个二维的循环,以后在动态规划,可能会经常用到。
class Solution:
def integerBreak(self, n: int) -> int:
#第n个 相当于 第n 分解成 n-i和i乘积 和dpn-i和i乘积比较 选最大的
#虽然n是从2开始的 相当于只有n-2+1 个 但是为了遍历方便扩充成n个
dp= [_+1 for _ in range(n)]
for i in range(n):
for j in range(1,i):
dp[i] = max(dp[i] , max(dp[i-j]*j,(i-j)*j))
#print(dp)
return dp[-1] if n>=4 else n-1
96. 不同的二叉搜索树 - 力扣(LeetCode) (leetcode-cn.com)
class Solution:
def numTrees(self, n: int) -> int:
#dp 为n情况下二叉搜索树的个数
#dp[i] +=dp[i-j-1]dp[j] for j in range(i)
dp= [0]*(n+1)
for i in range(n+1):
if i < 2: dp[i]=1
else:
for j in range(i):dp[i] +=dp[i-j-1]*dp[j]
#print(dp)
return dp[-1]
3.01背包问题基础
1.01背包基础
背包问题是个啥,我一直很好奇,今天终于看到这里了,先看下基础理论。
# -*- coding: gbk -*-
#暴力解法
nmax=3
nlist=[15,20,30]
nweigt=[1,3,4]
res= []
def backtracking(n,weight,hw):
if not n - nmax :return res.append(hw)
for i in [0,1]:
if i: #选择
hw+= nlist[n-1]
weight-=nweigt[n-1]
backtracking(n+1,weight,hw)
backtracking(1,4,0)
print('所有可能的情况为:',res)
print('可以放入最大为:',max(res))
暴力解法肯定是不可取的,用的时间为o(2^n)当这个物体n特别多的时候,这样就太浪费时间了,肯定暴力的方法是不可取的
2.01背包问题的动态规划(二维数组)
1)定义
2)递推关系
如果某一行 即某一个物品i 在j小于weight[i]时候,直接取 dp[i-1][j]
如果大于weight[i]时候,就可以比较他放入与不放入的总重量的大小,肯定选最大的那个
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
3)初始化
全部初始化为0,但是每次都是和i-1比较,所以要初始化i=0行
4)遍历顺序
选择一行一行遍历,就是一个物品一个物品来
5)对比dp数组并修改程序
weight=[1,3,4]
value=[15,20,30]
n=4
dp= [[0]*(n+1) for _ in range(len(weight))]
for j in range(n+1): #初始化第一行
if j>=weight[0]: dp[0][j] = value[0]
for i in range(1,len(weight)):
for j in range(1,n+1):
if j<weight[i]:dp[i][j] =dp[i-1][j]
else:dp[i][j] =max(dp[i-1][j-weight[i]] +value[i],dp[i-1][j])
print(dp)
3.01背包问题的动态规划(一维数组)
一维数组是同过二维数组改动来的,滚动这个变现很生动,之前是把物品按照二维展开,其实可以用一维数组,只是需要不断变动前面的数组
#dp[j] i 表示j表示此时的还剩下的重量 dp表示总价值
#dp[j] =max(dp[j-weight[i]]+value[i],dp[j])
weight=[1,3,4]
value=[15,20,30]
n=4
dp= [0 for _ in range(n+1)]
for i in range(len(weight)):
for j in range(n,weight[i]-1,-1): #这里用了从大到小的顺序,而且只取到物品的大小,前面全是原来的 很巧妙
dp[j] =max(dp[j-weight[i]] +value[i],dp[j])
print(dp)
4.01背包问题
416. 分割等和子集 - 力扣(LeetCode) (leetcode-cn.com)
这道题就是典型的背包问题
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums)%2: return False
target=sum(nums)//2
pd=[0]*(target+1)
for i in range(len(nums)):
for j in range(target,nums[i]-1,-1):
pd[j]=max(pd[j-nums[i]]+nums[i],pd[j])
if not pd[-1]-target :return True
else:return False
1049. 最后一块石头的重量 II - 力扣(LeetCode) (leetcode-cn.com)
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
#把石头分成两堆 让两堆的数量尽量接近
#这就变成01背包问题,就是让选出数量尽量接近平均数
sumer = sum(stones)
target = sumer//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])
#print(dp,sumer)
return sumer-2*dp[-1]
494. 目标和 - 力扣(LeetCode) (leetcode-cn.com)
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
#dp为填满target大小的可能情况
suber =(sum(nums)-target)
target = suber//2
if suber%2 or target<0 :return 0
dp = [0]*(target+1)
dp[0]=1 #装满容量为0的背包,有1种方法,就是装0件物品。
for i in range(len(nums)):
for j in range(target,nums[i]-1,-1):
dp[j] +=dp[j-nums[i]]
return dp[-1]
474. 一和零 - 力扣(LeetCode) (leetcode-cn.com)
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
#dp 为i个0和j个1情况下最大子集
dp = [[0]*(n+1) for _ in range(m+1)]
for strer in strs:
zeroer= strer.count('0')
oneser= strer.count('1')
for i in range(m, zeroer-1,-1):
for j in range(n,oneser-1,-1):
dp[i][j] = max(dp[i][j], dp[i-zeroer][j-oneser]+1)
#print(dp)
return dp[-1][-1]
5.完全背包
完全背包内物品可以无限取,01背包内物品每个物品只能取一次。
01背包为了保证每个物品只取一次,所以取值时候是从后向前取的
for i in range(len(weight)):# 遍历物品
for j in range(target, nums[i]-1,-1):#遍历背包
dp[j] = max(dp[j], dp[j-nums[i]]+values[i])
完全背包不同,可以重复取之前的值,从前往后取
for i in range(len(weight)):# 遍历物品
for j in range(nums[i], target+1):#遍历背包
dp[j] = max(dp[j], dp[j-nums[i]]+values[i])
518. 零钱兑换 II - 力扣(LeetCode) (leetcode-cn.com)
这道题必须外层是物品,内层是背包,这样计算出来的是排除顺序的,是组合,
排列就需要反过来,一定要分的清楚这两种情况。
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
#完全背包问题
#dp[i] 为总金额i下组合数
#dp[j]+=dp[j-coins[i]]
dp = [0]*(amount+1)
dp[0] =1 #总金额0 只有一种组合情况
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j] += dp[j-coins[i]]
#print(dp)
return dp[-1]
6.总结
1)动态规划问题,类似就是爬楼梯问题,可以一次爬1阶,也可以一次爬两阶,
这样在爬i阶情况下有多少中情况,应该用dp[i]表示i台阶下情况数,为dp[i-1]+dp[i-2]
当不是确定1介两阶情况,就可以抽象成数组nums
这样就需要将每个nums,加到一起 即 dp[i]+= dp[i-num[j]]
需要遍历数组将所有情况加起来。
2)之后就出来了背包问题,这相当于在爬楼梯情况下,加入了台阶数还会变化。
背包问题还是挺难的。
01背包都是遍历背包时候都是从后向前遍历的,为了保证物品只用一次,这和01背包定义有关系。
完全背包没有这样的限制,所以应该从前向后遍历。
这里涉及到排列组合区别,带来外内层遍历选择问题,明天应该就看到了。