动态规划的三个步骤:
1.找到状态转移方程
2.将每个状态的结果按顺序存储
3.指定输出结果位于存状态的数组的位置
最大上升子序列和
对于给定的序列,会有很多个上升子序列,对每个上升子序列求和,找到一个数组的所有上升子序列中 和最大的哪个上升子序列,输出这个值。比如序列(1,7,3,5,9,4,8) 有两个上升子序列 如(1,7),(3,4,8) 这些子序列中和最大的为18 即子序列(1,3,5,9)的和。
用一维dp,dp的状态是每次找到以当前字符为结尾的list中的最大上升子序列和。
#用一维dp,dp的状态是每次找到以当前字符为结尾的list中的最大上升子序列和。
#也就是说dp[i]表示以nums[i]结尾(必须包含nums[i])的list中的当前最大上升子序列和。
if __name__ == "__main__":
l= list(map(int, input().split()))
n=l[0]
l = list(map(int, input().split()))
dp=[[] for _ in range(n)]
res=-1
for i in range(n):
dp[i]=l[i]
for j in range(i):
#如果nums[j]加进来当前子序列符合上升且加进来能增加当前上升子序列的和
if dp[j]+l[i]>dp[i] and l[j]<l[i]:
#就把当前这个数加进来并且更新当前上升子序列的和
dp[i]=dp[j]+l[i]
res=max(res,dp[i])
print(res)
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
思路:
用f(i,j)表示从左上角走到(i,j)的路径数量,其中i,j范围分别是[0,m)与[0,n)
可以得到状态转移方程:
f(i,j)=f(i-1,j)+f(i,j-1)
代码
class Solution(object):
def uniquePaths(self, m, n):
l=[[1]*n]+[[1]+[0]*(n-1) for _ in range(m-1)] #由于是走最短路径,所以初始化时到达边界上每个点的方法都只有1种,所以边界初始化为1
for i in range(1,m):
for j in range(1,n):
l[i][j]=l[i-1][j]+l[i][j-1]
return l[-1][-1]
优化
上面代码的空间复杂度是O(mn),时间复杂度也是O(mn),注意到f(i,j) 仅与第i行的状态(l[i][j-1])与第 i-1行的状态(l[i-1][j])有关,所以考虑到可以不用建立整个状态空间的二维数组,只需要建立当前第第i行与第 i-1行的状态数组即可,即用两个滚动数组来代替上面的二维数组,即由于pre[j]=f[i-1][j] 此时状态转移方程为:cur[j]=cur[j-1]+pre[j]
可使得使空间复杂度降为O(n) 优化后代码如下:
class Solution(object):
def uniquePaths(self, m, n):
pre=[1]*n #上一行
cur=[1]+[0]*(n-1) #当前行 这里初始化方法和最开始的方法一样的
for i in range(1,m):
for j in range(1,n):
cur[j]=cur[j-1]+pre[j]
pre=cur #每一行遍历完之后当前行的值就存在上一行,即当前行的值cur赋给上一行pre
return pre[-1] #原则上 上一步cur赋值给了pre,这里返回pre[-1]或者cur[-1]都可以,但是考虑到如m=1 n=2这种地图只有一行的情况,此时返回pre[-1]就是1,而cur[-1]是0 所以应该返回pre[-1]
因为存了两个数组,此时空间复杂度为O(2N) 那么还可以优化吗?
再次优化:
针对上一版本,初始化时cur=[1]*N,即这里的cur就相当于上一版本中初始化的pre,上一版本中每次cur也要赋值给pre,所以这种初始化方法之后一开始的cur本身就等于pre,后面在迭代中,每次的cur[j]实际也就是pre[j],所以此时状态转移方程由:
cur[j]=cur[j-1]+pre[j] 变为 cur[j]=cur[j-1]+cur[j]
可使得使空间复杂度降为O(n)
class Solution(object):
def uniquePaths(self, m, n):
cur=[1]*n
for i in range(1,m):
for j in range(1,n):
cur[j]=cur[j-1]+cur[j]
return cur[-1]
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
思路
采用DP算法,第0阶到第0阶有1种方法,第0到第1阶有一种方法,第0到第2阶梯有2中方法,到第三阶有三种。。。。。得到状态转移方程:f(n)=f(n-1)+f(n-2)
代码:
class Solution(object):
def climbStairs(self, n):
l1=l2=1
for i in range(1,n):
l1,l2=l2,l1+l2
return l2
优化:
注意到每一时刻的状态f(n)只与前一刻,前前一刻的状态有关,所以用l1表示前前一刻状态(初始化为第0阶),l2表示为前一刻的状态(初始化为第1阶),又可以把当前时刻的状态存在l2中,所以此时状态方程为:l2=l1+l2
代码如下:
class Solution(object):
def climbStairs(self, n):
l1=l2=1
for i in range(1,n):
l1,l2=l2,l1+l2
return l2
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
思路:
自底向上
首先凑状态方程 状态就是凑成每个金额的最小硬币数,知道了金额就能够知道其最小硬币数(自顶向下) 也就是说只要知道要凑的金额 就有一个确定的状态
所以当前的状态(金额)挨个减去各个硬币的面值就是上一个状态(比如当前是100,减去面值为1的硬币,就是99,说明此时上一个状态是
99,取了面值为1的硬币到当前状态)硬币面值有多个,对应的上一个状态也有多个,怎么上一个状态是哪一个?就将每一轮中取每个硬币对应的上一个状态的
最小硬币数进行对比(比如这里的例子,就用99和95(假设此时取了5元硬币)所要凑的最小硬币数对比)相当于递归,一直比到初始状态,也就是要凑0元,只有一个结果。
class Solution:
def coinChange(self, coins, amount):
# dp
if amount < 0:
return -1
res = [0] + [amount+1] * amount # 备忘录长度为amount+1因为0元也需要一个点存 用amount+1初始化各子问题对应的硬币数量,因为找零数量不可能超过amount
for i in range(1, len(res)): #从总额为1块钱开始比
for coin in coins: #因为每个状态都要用每个硬币去比一遍 所以内循环是硬币
if i < coin: #考虑到前期某些点不能用某些硬币来表示 比如用5元硬币凑4块 所以如果i<coin则跳过这一步操作进行下一次循环
continue
res[i] = min(res[i], 1+res[i-coin])
return res[-1] if res[-1] != amount+1 else -1
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:
动态规划题首先要明确状态是什么 变量是什么
状态就是最终要求的值,也就是最大收益 那么中间状态就是某天的最大收益 变量是某天是否卖出(这里可能会考虑哪天买入的问题,但是动态规划不用考虑这些),根据数据推理可得第n天卖出的最大利润为前一天的最大利润加上这两天的差值,这就是状态方程。
首先想到建立长度为n的备忘录,之后考虑优化,由于每天的最大收益为前一天的最大利润加上这两天的差值,而且每天的最大利润最小为0,得到如下代码,其中memo每次迭代前存的就是上一天的最大收益。
class Solution(object):
def maxProfit(self, prices):
memo=0
m=0
for i in range(1,len(prices)):
memo=max(memo+prices[i]-prices[i-1],0)
m=max(m,memo)
return m
因为这里要输出最大收益,而不是最后一天卖出的最大收益,所以设一个m存最大收益。
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [0]
输出:0
示例 4:
输入:nums = [-1]
输出:-1
示例 5:
输入:nums = [-100000]
输出:-100000
思路
首先分析状态是子数组的最大和,改变状态的变量是数组的元素,采用自底向上建模,可以得到其递推公式是以当前项为数组结尾的最大和是以前一项为结尾的最大和加上当前项的值。这里要注意,算最大和时如果当前项算出来最大和小于0,不能直接把当前项最大和取0,这样在计算全为负数的list时就会报错,如[-1,-2],此时会输出0,而不是-1,错误代码如下所示
class Solution(object):
def maxSubArray(self, nums):
a=len(nums)
if a==1:
return nums[0]
memo[0]=max(nums[0],0) #第一处错误:哪怕nums[0]是负数,第一项最大和也应该取nums[0]而不是0,因为题目要求最大和至少包括一个元素,这里取0的话相当于长度为1的只有一个负数的数组,其最大和是0,也就是一个都不取,这不符合题意,会报错
maxp=memo[0]#maxp用来保存遍历过程中最大的数组最大和
for i in range(1,a):
memo[i]=max(memo[i-1]+nums[i],0)#第二处错误,这里把当前项的最大和直接和0比较,小于0当前项最大和直接取0,这是错的,原因也是我上面说的全为负数的样例。这里应该先比较,前一项的最大和和0比较,如果为负数,就舍弃前一项的最大和不取,再加上当前值nums[i] 这样就能够保存负的最大和
maxp=max(maxp,memo[i])
return maxp
正确代码如下:
首项不管正负memo[0]都应该是nums[0],之后如果dp[i-1]小于0,我们直接把前面的舍弃,也就是说重新开始计算,否则会越加越小的,直接让dp[i]=num[i]。所以转移公式如下
dp[i]=num[i]+max(dp[i-1],0);
class Solution(object):
def maxSubArray(self, nums):
a=len(nums)
if a==1:
return nums[0]
memo[0]=max(nums[0],0) #第一处错误:哪怕nums[0]是负数,第一项最大和也应该取nums[0]而不是0,因为题目要求最大和至少包括一个元素,这里取0的话相当于长度为1的只有一个负数的数组,其最大和是0,也就是一个都不取,这不符合题意,会报错
memo[0]=nums[0]
maxp=memo[0]#maxp用来保存遍历过程中最大的数组最大和
for i in range(1,a):
memo[i]=max(memo[i-1]+nums[i],0)
# memo[i]=max(memo[i-1],0)+nums[i]
# maxp=max(maxp,memo[i])
maxp=max(maxp,memo[i])
return maxp
代码优化
class Solution(object):
def maxSubArray(self, nums):
a=len(nums)
if a==1:
return nums[0]
# memo[0]=max(nums[0],0)
memo=nums[0]
maxp=memo
for i in range(1,a):
memo=max(memo,0)+nums[i]
maxp=max(maxp,memo)
return maxp
我们申请了一个长度为length的数组,但在转移公式计算的时候,每次计算当前值的时候只会用到前面的那个值,再往前面就用不到了,这样实际上是造成了空间的浪费。这里不需要一个数组,只需要一个临时变量memo用来存储上一个最大和即可,代码如上面所示
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路:
这里注意一个误区 不是一定要隔家偷,比如有1 2 3 4 5 五家,不是说只有偷1 3 5 或者2 4 两种偷法,只要不挨着偷,每家都可以决定偷 或者不偷 偷得话就是dp[i]=dp[i-2]+dp[i] 不偷就是dp[i-1]
还有一个误区,初始化的时候dp[0]确实是第一家的金额,但是dp[1]不是第二家的金额,而应该是max(nums[0],nums[1])即不一定是偷第二家才是当前的最大收益,也可能第一家钱多,只偷第一家 不偷第二家收益更多
代码如下:
class Solution(object):
def rob(self, nums):
a=len(nums)
maxm=0#最大金额
if a==1:
return nums[0]
if a==2:
return max(nums[0],nums[1])
dp=[0]*len(nums)
dp[0]=nums[0]
dp[1]=max(nums[0],nums[1])
for i in range(2,a):
dp[i]=max(dp[i-2]+nums[i],dp[i-1])
maxm=max(maxm,dp[i])
return maxm
优化
由于只需要上一家和上上一家的金额就可以决策,所以可以优化,注意优化之后循环中间不要忘记迭代dp1和dp2
class Solution(object):
def rob(self, nums):
a=len(nums)
maxm=0#最大金额
if a==1:
return nums[0]
if a==2:
return max(nums[0],nums[1])
dp1,dp2=0,0 #dp1存上上一家,dp2存上一家
dp1=nums[0]
dp2=max(nums[0],nums[1])
for i in range(2,a):
dp1,dp2=dp2,max(dp1+nums[i],dp2)
maxm=max(maxm,dp2)
return maxm
42. 接雨水
双指针法真的妙,那么如何理解双指针法呢?听我来给你捋一捋。
双指针o1空间,dp优化过来的:
法1我们维护了所有位置左右两侧的最高挡板
然而对于某侧挡板,若存在更高的挡板则可以丢弃
因此我们可以哪侧挡板小,就更新哪侧挡板
初始话我们选择两侧即为挡板
我们先明确几个变量的意思:
left_max:左边的最大值,它是从左往右遍历找到的
right_max:右边的最大值,它是从右往左遍历找到的
left:从左往右处理的当前下标
right:从右往左处理的当前下标
定理一:在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个。
定理二:当我们从左往右处理到left下标时,左边的最大值left_max对它而言是可信的,但right_max对它而言是不可信的。(见下图,由于中间状况未知,对于left下标而言,right_max未必就是它右边最大的值)
定理三:当我们从右往左处理到right下标时,右边的最大值right_max对它而言是可信的,但left_max对它而言是不可信的。
对于位置left而言,它左边最大值一定是left_max,右边最大值“大于等于”right_max,这时候,如果left_max<right_max成立,那么它就知道自己能存多少水了。无论右边将来会不会出现更大的right_max,都不影响这个结果。 所以当left_max<right_max时,我们就希望去处理left下标,反之,我们希望去处理right下标。即当前位置取决于左右挡板的高度.
class Solution(object):
def trap(self, height):
"""
:type height: List[int]
:rtype: int
"""
left=0
right=len(height)-1
leftm=0
rightm=0
ans=0
while left<=right:
if leftm>rightm:#如果左边最大值大于右边最大值 由于木桶短板原理 所以此时循环右边 按右边最大值为挡板基准 也就是哪边最大值小就走哪边指针判断
ans+=max(0,rightm-height[right])
rightm = max(rightm, height[right])
right-=1
else:#注意等号的判断,上面if判断如果不写if leftm>rightm 就是写if leftm<=rightm 一定不能忘了等号,因为左=右的那一格也是有水的也要算
ans+=max(0,leftm-height[left])
leftm = max(leftm, height[left])
left+=1
return ans
5.最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成
这里有两种方法。
基础方法是中心扩散法,也就是如果一个字符串是回文串,那么从这个字符串的最中心元素开始,他的左右两边必有元素与中心元素相等,或者左右两边元素对称相等。元素与中心元素相等的回文,如“aa”,"bbb"这样的回文串,或左右两边元素对称相等,如“aba”这样的。如果一个回文串既有元素与中心元素相等,又有左右两边元素对称相等,那一定是中心元素附近有相等字符,再往外扩散有左右对称相等的,如“abcccba” c是中心元素,或者“abba”,b是中心元素。
所以总结可知,从头开始遍历,把每个元素当作中心元素,先从该元素往左(或往右)判断是否有相等元素,再判断另一个方向是否有相等元素,直到找到左右两边第一个与中心元素不相等的元素,现在开始判断左右两边元素是否对称相等即可。代码如下
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
size=len(s)
l=1
maxl=1
ml=0
for i in range(size):
left=i-1 #初始left应该为s[i]左边一个位置,right为s[i]右边一个位置
right=i+1
while left>=0 and s[i]==s[left]:#如果是回文 那中心字符左一字符可能和中心字符s[i]相同,所以先向左找到第一个和s[i]不同的字符,相同则左指针left-=1,回文川长度l+=1 注意left的边界判断条件left>=0
left-=1
l+=1
while right<size and s[i]==s[right]:#同理,找到右边第一个和中心字符不同的。因为如果一个回文串里出现连续相同字符,那么相同字符一定在中心字符旁边。所以先判断左右相同字符。
right+=1
l+=1
while left>=0 and right<size and s[right]==s[left]:#相同字符判断完了,对于aa,bbb的情况上两个while已经解决。这个while解决bab这种情况。此时left和right分别指向中心字符两侧第一个与中心不重复的字符。此时只有left和right指向的字符相同才对称,才符合回文串
left-=1
right+=1
l+=2
if l>maxl:
maxl=l
ml=left+1
l=1
return s[ml:ml+maxl]
更进一步方法是动态规划方法:
定义二维的dp table
动规五部曲:
确定dp数组(dp table)以及下标的含义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
确定递推公式
在确定递推公式时,就要分析如下几种情况。
整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
情况二:下标i 与 j相差为1,例如aa,也是文子串
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
以上三种情况分析完了,那么递归公式如下:
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
dp[i][j] = true;
}
}
注意这里我没有列出当s[i]与s[j]不相等的时候,因为在下面dp[i][j]初始化的时候,就初始为false。
代码如下
class Solution(object):
def longestPalindrome(self, s):
size=len(s)
if size<2:
return s
dp=[[False]*size for _ in range(size)]
maxl=1
l=0
start=0
for i in range(size):
for j in range(i+1):
if i-j<2 and s[i]==s[j]:
dp[i][j]=True
l=i-j+1
elif s[i]==s[j] and dp[i-1][j+1]:
dp[i][j]=True
l=i-j+1
if l>maxl:
maxl=l
start = j
return s[start:start+maxl]