动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,
然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
1、最长连续序列(中等) - 128
题目描述;
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
解题思路:
用哈希表存储每个端点值对应连续区间的长度。
若数已在哈希表中:跳过不做处理。
若是新数加入:
- 取出其左右相邻数已有的连续区间长度 left 和 right
- 计算当前数的区间长度为:cur_length = left + right + 1
- 根据 cur_length 更新最大长度 max_length 的值
- 更新区间两端点的长度值
def longest_consecutive(nums):
"""
:param nums: list
:return:
"""
hash_dict = dict()
max_length = 0
for num in nums:
if num not in hash_dict:
left_len = hash_dict.get(num-1, 0)
right_len = hash_dict.get(num+1, 0)
# 如左长为2,右长为1,当前数字为5,那必有3,4,6这三个数字都在hash表中,
# 所以记5的长度为4,后面更新3和6的长度都为4.
cur_length = left_len + right_len + 1
max_length = max(cur_length, max_length)
# 如果左右长度有一个为0那么下面也记录了当前值的长度,若都不为0不记录当前值的长度也没影响。
# hash_dict[num] = cur_length
# 更新最左端点的值,如果left_len=n存在,left_len=0相当于记录本身num的长度,
# 那么证明当前数的前n个都存在哈希表中
hash_dict[num-left_len] = cur_length
# 更新最右端点的值,如果right_len=n存在,right_len=0相当于记录本身num的长度,
# 那么证明当前数的后n个都存在哈希表中
hash_dict[num+right_len] = cur_length
return max_length
2、若不考虑时间复杂度,下面一种解法:先排序,然后再统计相邻数字的长度即可
def longest_consecutive(nums):
"""
因为排序了,所以时间复杂度不为 O(n)
:param nums: list
:return:
"""
# 排序
nums.sort()
max_length = 1
record_length = 1
for i in range(1, len(nums)):
if nums[i] == nums[i-1]+1:
record_length += 1
else:
if record_length > max_length:
max_length = record_length
# 有多轮情况:例如[1, 2, 3, 100, 101, 102, 103],不满足元素相邻之后需要重新计算长度
record_length = 1
# 只有一轮情况如:[1, 2, 3, 4],执行不到else语句,max_length还是1,所以这里取两个值的最大值返回
return max(max_length, record_length)
2、跳台阶(简单)
题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
示例:
输入:2 输入:3 输入:7
返回值:2 返回值:3 返回值:21
解题思路:
- 一只青蛙一次可以跳1阶或2阶,直到跳到第n阶:
- 第一步如果选择跳1阶:那么剩下n-1阶(不就相当于求n-1台阶的跳法吗?)
- 第一步如果选择跳2阶:那么剩下n-2阶(不就相当于求n-2台阶的跳法吗?)
- 如果用函数表示:那么它总的跳法就为f(n−1) + f(n−2) 这就变成了斐波那契数列。因此可以按照斐波那契数列的做法来做:即输入n, 输出第n个斐波那契数列的值。
def jump_floor(num):
if num <= 2:
return num
return jump_floor(num-1) + jump_floor(num-2)
这道题推荐使用递归方法更简单
下面不用递归也简单
def jump_floor(num):
if num <= 2:
return num
# 通过分析发现a是当前值,b下一个值,a+b是下下一个值
cur, next = 1, 2
for _ in range(num-2):
# a,b分别保存下一个值
cur, next = next, cur + next
return next
3、最小花费爬楼梯(简单)
题目描述:
给定一个整数数组 cost,其中 cost[i] 是从楼梯第i 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例:
输入:[2,5,20]
返回值:5
说明:认真看题应知cost[0]为2代表要支付2元,然后选择往上跳;cost[1]为5代表要支付5元然后往上跳;cost[2]为20代表要支付20元,然后选择往上跳。显然最小花费为:从下标为1的台阶开始,支付5 ,向上爬两个台阶,到达楼梯顶部。总花费为5
输入:[1,100,1,1,1,90,1,1,80,1]
返回值:6
说明:你将从下标为 0 的台阶开始。
1.支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
2.支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
3.支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
4.支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
5.支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
6.支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
解题思路:
- step 1:可以用一个数组记录每次爬到第i阶楼梯的最小花费,然后每增加一级台阶就转移一次状态,最终得到结果。
- step 2:(初始状态)因为可以直接从第0级或是第1级台阶开始,因此这两级的花费都直接为0.
- step 3:(状态转移) 每次到一个台阶,只有两种情况,要么是它前一级台阶向上一步,要么是它前两级的台阶向上两步,因为在前面的台阶花费我们都得到了,因此每次更新最小值即可,转移方程为:dp[i]=min(dp[i−1]+cost[i−1], dp[i−2]+cost[i−2])。
此题关键是理解题意,仔细列出来过程就是上图所示:
def min_cost_climb_stairs(cost):
"""
:param cost: list
:return:
"""
if len(cost) <= 2:
return min(cost)
# 记录每一步的最小花费
dp = [0, 0]
for i in range(2, len(cost)+1):
# 当前在i层,到达i层时通过i-1 或者 i-2 这两种路径向上爬得来的。
# 所以下面比较这两种路径哪一个路径花费最小
# cost[i-1]是从i-1向上爬的实际花费,dp[i-1]是爬到i-1时的最小花费
# 所以dp[i-1] + cost[i-1]即为从i-1向上爬的最小花费。同理i-2。
cur_cost = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
dp.append(cur_cost)
# dp[-1]就是总的最小花费
return dp
4、打家劫舍(一)(中等)
题目描述:
你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。
给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。
示例:
输入:[1,2,3,4]
返回值:6
说明:最优方案是偷第 2,4 个房间输入:[2,10,5] 输入:[1,3,6]
返回值:10 返回值:7
说明:最优方案是偷第 2 个房间 说明:最优方案是偷第1, 3个房间输入:[1,2,7,4,5,6]
返回值:14
说明:1+7+6
解题思路:
或许有人认为利用贪心思想,偷取最多人家的钱就可以了,要么偶数家要么奇数家全部的钱,但是有时候会为了偷取更多的钱,或许可能会连续放弃两家不偷,因此这种方案行不通,我们依旧考虑动态规划。(跟上一题很类似,主要总结出公式)
- step 1:用dp(列表)表示到当前nums[i]时最多能偷取到多少钱。
- step 2:(初始状态) 如果nums长度为1或者2时,dp[0]=nums[0], dp[1]=max(nums[0], nums(1))。
- step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为dp[i]=max(nums[i]+dp[i-2], dp[i-1])。
def rob(nums):
"""
:param nums: list
:return:
"""
if len(nums) <= 2:
return max(nums)
# 保存到当前房间为止的最大偷窃金额
dp = [nums[0], max(nums[0], nums[1])]
for i in range(2, len(nums)):
# nums[i]+dp[i-2]表示偷的时候当前最大的金额,dp[i-1]表示不偷的时候当前最大的金额
cur_money = max(nums[i]+dp[i-2], dp[i-1])
dp.append(cur_money)
# dp[-1]或者max(dp)即为所求
return dp
5、打家劫舍(二)(中等)
题目描述:
你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。
给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。
示例:
输入:[1,3,6]
返回值:6
说明:由于1和3是相邻的,因此最优方案是偷第3个房间。
解题思路:
在上一题的思路基础上:
第一家与最后一家不能同时取到,那么我们可以分成两种情况讨论:
情况1:偷第一家的钱,不偷最后一家的钱。初始状态与状态转移不变,遍历的时候数组最后一位不去遍历。
情况2:偷最后一家的钱,不偷第一家的钱。初始状态与状态转移不变,遍历的时候数组第一位不去遍历。
最后取两种情况的较大值即可。
def rob(nums):
"""
:param nums: list
:return:
"""
if len(nums) <= 2:
return max(nums)
return max(sub_rob(nums[1:]), sub_rob(nums[:-1]))
def sub_rob(nums):
"""
:param nums: list
:return:
"""
dp = [nums[0], max(nums[0], nums[1])]
for i in range(2, len(nums)):
cur_money = max(nums[i] + dp[i - 2], dp[i - 1])
dp.append(cur_money)
return max(dp)