动态规划
动态规划的三种类型题
- 计数
- 有多少种方式走到右下角
- 有多少种方式选处k个数使得和为sum
- 求最大最小值
- 求左上角走到右下角路径的最大最小值
- 最长上升子序列长度
- 求存在性
- 取石子游戏,先手是否必胜
- 能不能选出k个数使得和为sum
硬币问题
你有几种不同面值的硬币(2,5,7),它们的面额存储在coin[]数组中,你需要刚好用最少的硬币数量买价值为n的商品),求最少的硬币数量
思路:动态规划
-
确定状态
(假设n为27)若所有步骤都是最优解,那么最后一个步骤与倒数第 二个步骤也都是最优解。
-
转移方程
假设最后一个步骤为f(n),代表着花了硬币的数量,那么倒数第二个步骤就是f(n-x)+1个数量。可得f(n)=min{f(n-2)+1,f(n-5)+1,f(n-7)+1}
-
初始条件和边界情况
初始条件:f(0)=0,零元花了零个硬币
边界情况:未知数n不能为负数,并且f(n)不能为最大值(一开始设置数组每个为最大正整数) -
计算顺序
自底向上,先满足方程式右边的条件,即从最小的f(0)开始计算到f(27)
可得代码:
def leastCoin(coins,n):
dp=[2**31-1 for i in range(n+1)] #数组容量要包含n
dp[0]=0
for i in range(1,n+1):
for j in range(len(coins)):
if i>=coins[j] and dp[i-coins[j]]!=2**31-1:
dp[i]=min(dp[i-coins[j]]+1,dp[i])
return dp[n]
Q5 最长回文子串 Longest Palindromic Substring
给你一个字符串 s,找到 s 中最长的回文子串。
状态转移方程为dp[i][j]=(dp[i+1][j-1] and s[i]==s[j],在这道题中dp[i][j]为一个bool值,当判断为true的时候可以得到i和j为起始下标与终点下标,从而得到完整的字符串
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)]
ans = ""
# 枚举子串的长度为 l+1,因为l是从0开始计算的
for l in range(n):
# 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
for i in range(n):
j = i + l #j+l不是加1,加的是长度
if j >= len(s):
break
if l == 0:
dp[i][j] = True
elif l == 1:
dp[i][j] = (s[i] == s[j])
else:
dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
if dp[i][j] and l + 1 > len(ans):
ans = s[i:j+1]
return ans
LintCode Q116 跳跃游戏 Jump Game
给出一个非负整数数组,你最初定位在数组的第一个位置。
数组中的每个元素代表你在那个位置可以跳跃的最大长度。
判断你是否能到达数组的最后一个位置。
状态转移方程:dp[j]=True的前提是dp[i] and i+A[i]>=j为True,代码如下
class Solution:
"""
@param A: A list of integers
@return: A boolean
"""
def canJump(self, A):
n=len(A)
dp=[False]*n
dp[0]=True
for i in range(n):
for j in range(i+1,n):
if dp[i] and i+A[i]>=j:
dp[j]=True
return dp[n-1]
Q45 跳跃游戏 Ⅱ Jump Game Ⅱ
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
这道题和跳跃游戏不同的是它的动态转移方程代表的是最少的跳跃次数。根据题意可以得到动态转移方程为dp[j] = min(1 + dp[i], dp[j])
。同时还得注意判断是否可以跳跃到当前位置。
class Solution:
def jump(self, nums: List[int]) -> int:
dp=[2**31-1]*len(nums)
dp[0]=0
for i in range(len(nums)-1):
for j in range(i+1,len(nums)):
if nums[i]+i>=j:
dp[j]=min(1+dp[i],dp[j])
return dp[len(nums)-1]
Q53 最大子序和 Maximum Subarray
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:动态规划
这道题用暴力破解法肯定是会超时的,我自己就超时了。评论区有一种很好用的方法,由于必须要求是连续数组,只有加上不为负数的数才能增大,那么如果之前的和小于0,那么便不加上了,重新开始求和,具体在代码里面 体现为:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
ans=nums[0]
sum=0
for num in nums:
if sum>0:
sum+=num
else:
sum=num
ans=max(ans,sum)
return ans
还有一种更简便的写法,但是思路是相同的:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
for i in range(1, len(nums)):
nums[i]= nums[i] + max(nums[i-1], 0)
return max(nums)
Q70 爬楼梯 Climbing
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
思路:动态规划
这道题需要知道第n个台阶只能从第n-1或者n-2个上来(仅此两种路径)。到第n-1个台阶的走法 + 第n-2个台阶的走法 = 到第n个台阶的走法,已经知道了第1个和第2个台阶的走法,一路加上去。
即 f(n) = f(n-1) + f(n-2)
class Solution:
def climbStairs(self, n: int) -> int:
if n<=2: #仅知第一步和第二步是固定的路径数
return n
i1=1
i2=2
for i in range(3,n+1):#第三步开始就是前两步路径相加了,斐波那契
temp=i1+i2
i1=i2
i2=temp
return i2
然后是用了动态转移方程的第二种,需要注意的是边界问题:(耗时更少)
class Solution:
def climbStairs(self, n: int) -> int:
dp=[0]*(n+1)
if n==1:
return 1
if n==2:
return 2
dp[1]=1
dp[2]=2
for i in range(3,n+1):
dp[i]=dp[i-1]+dp[i-2]
return dp[n]
Q121 买卖股票的最佳时机 Best Time to Buy and Sell Stock
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路:动态规划
前i天的最大收益 = max{前i-1天的最大收益,第i天的价格-前i-1天中的最小价格}
f(n)=max{f(n-1),prices[i]-minPrice}
class Solution:
def maxProfit(self, prices: List[int]) -> int:
maxPro=0 #利润最小不能小于0
minPrice=2**31-1
for i in range(len(prices)):
maxPro=max(maxPro,prices[i]-minPrice)
minPrice = min(minPrice, prices[i])
return maxPro
通过动态转移方程写出的代码:(耗时会更多一点)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
dp=[0]*len(prices)
minPrice=2**31-1
profit=0
for i in range(len(prices)):
dp[i]=max(dp[i-1],prices[i]-minPrice)
minPrice=min(minPrice,prices[i])
profit=max(dp[i],profit)
return profit