算法之动态规划
题源皆来自于力扣(LeetCode)
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
来源(https://leetcode-cn.com/problems/maximum-subarray/)
70.爬楼头
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
121. 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber/
# 动态规划总结是看了九章算法的一个视频 (链接:https://www.bilibili.com/video/av45990457/?redirectFrom=h5)
动态规划:
动态规划的题目的特点:
1、计数 2、求最值 3、存在性(暂时我还没有弄懂)
动态规划4步曲:
1、首先需要的便是确定状态:
1、先考虑问题的最后一步
2、然后在根据最后一步确定子问题。
以力扣的53题为例子:
53题中的题目要求为求最大子序列和,那么这是一个最值问题,可以使用动态规划来做;
我们可以假设f(i)为数组nums的最大子序列和,
1、首先确定最后一步,这最后一步是什么呢,首先我们可以肯定的是给出的数组nums的最大子序列的最后一个整数肯定是nums[i]
2、所以我们只需要求到nums[i]前面的数组的最大子序列和在去加上nums[i]
2、确定转移方程
那么f[i-1]就代表了以nums[i]结尾的最大子序列和,然后我们就可以写出状态转移方程
f[i] = max(f[i-1] + nums[i],nums[i])
此时会有疑虑,为什么要与nums[i]做比较,首先子序列数组中可以只有单独的一个数,且f[i-1]可能是一个负数
3、考虑初始条件和边界的问题
此时我们便需要考虑其初始条件,当数组中只有一个元素的时候,我们很容易知道,他的最大子序列和肯定就只能是nums[0]
那么我们就可以定义
f[0] = nums[0]
边界条件: 转移方程算不出来的,需要我们自己定义,这就是边界条件
而这道题的边界条件就是,当指定数组为空的时候,我们用转移方程便无法计算出。此时就需要定义 if len(nums)==0:return 0
4、计算顺序
我们应该是从左到右计算还是从右到左计算。
首先,要明确一点,动态规划的意义便是存储我们计算过的值。
那么我们就可以明确计算顺序了。那便是,当我们计算等式左边的时候,等式右边的状态已经计算过了。
当我们要计算f[3]的时候,我们可以发现f[2]已经计算过了,此时我们就可以知道,此题是从左到右去计算。
由上面的分析4步曲我们就可以将第53题解出。
class Solution(object): def maxSubArray(self, nums): # 首先边界情况我们需要将其判断出 if not nums: return 0 n = len(nums) f = [0 for _ in range(n)] # 用来存储遍历到不同的i时,数组中的最大子序列和 f[0] = nums[0] # 定义初始条件 for i in range(1, n): # 从1开始遍历,应为f中已经有了i为0时,此时的最大子序列和。 f[i] = max(f[i-1]+nums[i], nums[i]) # 此时直接写出转移方程就行 return max(f) # 其中最大的f数组中最大的值便是最大nums的最大子序列和。
时间复杂度:O(n) 空间复杂度:O(n)
改进:既然最后的状态只与上一个状态有关,那么就可以压缩空间,使空间复杂度为O(1)
class Solution(object): def maxSubArray(self, nums): # 首先边界情况我们需要将其判断出 if not nums: return 0 n = len(nums) previous = nums[0] result = previous # 定义初始条件 for i in range(1, n): # 从1开始遍历,应为f中已经有了i为0时,此时的最大子序列和。 previous = f[i-1]+nums[i] res = max(res,previous) # 此时的最后的结果只需要与上一个比较哪个大 return result # 其中最大的f数组中最大的值便是最大nums的最大子序列和。
70题分析
1、确定状态
我们就可以f(n)为走到n个台阶的方法,
最后一步:由题意可以知道,一次只能走1个台阶或者2个台阶。那么我们可以确定要走上n街台阶,最后一步就只能走1个台阶或者2个台阶
子问题:由最后一步我们就只需要知道走到n-1步有多少种方法,走到n-2步有多少种方法,此时只需要两者相加,便是走到n个台阶的方法。
2、状态转移方程
那么f(n-1)为走到n-1个台阶的方法,f(n-2)是走到n-2个台阶的方法。由此我们就可以很快的确定状态转移方程
f(n) = f(n-1) + f(n-2)
3、初始状态和边界条件
我们很容易知道,走到n = 1的时候只有1种方法,n=2的时候,有2种方法。后面的便不需要考虑了,因为n=3的时候,可以由f(n-1)+f(n-2)得到
边界条件:当没有台阶的时候,我们需要去定义他。
4、计算顺序
我们可以很容易知道,是从左往右,因为,当n=3的时候,我们就已经可以得出f(2) 和f(1)
代码:
class Solution(object): def climbStairs(self, n): """ :type n: int :rtype: int """ if n == 0: return 0 f_li = [1,2] result = 0 for i in range(2,n): f_li.append(f_li[i-1]+f_li[i-2]) return f_li[n-1]
121题分析:
解买卖股票此题需要一个技巧,那便是将问题转换一下。题给出的是获得的最大利益,我们如何求得最大利益呢。
求最大利益,我们可以发现,将每隔一天的利益计算出来. 此时的数组是[7,1,5,3,6,4],我们可以将第一天和第二天的利益算出来,第二天与第三天的利益算出来,以此类推,然后将算出来的值存入新的列表中,此时我们就可以发现所求的最大利益就是求新数组的最长子序列和。只是我们需要考虑当新的数组中全为负数的时候。其指定的数组肯定是降序排列的,那么此时只需要判断一下求出的新数组最长子序列和是否是负数就行了。
代码:
class Solution(object): def maxProfit(self, prices): """ :type prices: List[int] :rtype: int """ n = len(prices) if n <=1 : return 0 # 判断特例,数组为0或者为1的时候,都没有最大利益 one_day_prices = [] for i in range(n - 1): one_day_prices.append(prices[i+1] - prices[i]) # 将每一天的利润存入数组中 sum_li = [0 for _ in range(n-1)] sum_li[0] = one_day_prices[0] for i in range(1, n-1): sum_li[i] = max(sum_li[i-1]+one_day_prices[i], one_day_prices[i]) return max(max(sum_li),0) # 将最大值与0做比较,若比0小,肯定是降序,直接返回0就行
198题分析:
1、确定状态
最后一步:我们可以肯定的是,小偷要么最后偷的肯定是nums[i],那么上一个偷的便是[i-2]的那个,但是我们忽略了一个问题,便是小偷可以最后偷[i-1]的那个;因为是求小偷最多可以偷多少钱,所以由此我们就可以确定这最后一步便是要么小偷最后偷的是i那个,或者是i-1那个。
子问题:由此我们就可以得到子问题便是,偷到i-2的时候偷了多少钱,偷到i-1的时候偷了多少钱。
2、确定状态转移方程:
f[i] = max(f[i-2]+nums[i], f[i-1])
3、初始状态和边界条件
初始状态:当只有一家能够偷的时候,f[0] = nums[0]
边界条件:当没有可以偷的时候,返回0。
4、计算顺序
...
代码:
class Solution(object): def rob(self, nums): """ :type nums: List[int] :rtype: int """ n = len(nums) if n == 0: return 0 f_li = [0 for _ in range(n)] f_li[0] = nums[0] for i in range(1, n): f_li[i] = max(f_li[i-1], f_li[i-2]+ nums[i]) return max(f_li)