算法 - 动态规划(一)

动态规划

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,
然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值