算法之动态规划(一)

本文介绍了动态规划的基本概念,通过爬楼梯、最大子序和、最长上升子序列和三角形最小路径和等经典问题,阐述动态规划的解题思路、问题分析以及状态转移方程。动态规划通过记忆化存储避免重复计算,解决最优化问题,适用于存在重叠子问题的情况。
摘要由CSDN通过智能技术生成

动态规划(Dynamic programming)

动态规划是算法中经常用到的,通过将原问题分解成相对简单的子问题来求解复杂的问题的方法。动态规划与分治方法相似,通过组合子问题的解来求解原问题。与之不同的是,动态规划应用在子问题有重叠的情况下,即不同的子问题有共同的子子问题。解决重复子问题,并将子问题的结果保存在表格内,这就避免重复计算。

动态规划一般求解最优化问题,因为有很多可行性解,每个解都有一个值,我们需要寻找最优解的值,这个值称为这个问题的一个最优解,而不是最优解,因为可以有很多解都能达到最优值。

所以动态规划能解决的问题是能将原问题分解成小问题,并且小问题需要重复计算的情况,以空间换时间,用dp类似的表格记录小问题的计算解,从而计算最终结果的解。类似递推加上记忆化加上剪枝。

题目实例

1. 爬楼梯

Leetcode 70

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

问题分析:

根据数学思维,假如现在在n台阶,那上一步是在n-1或者n-2台阶上,所以很容易得出f(n)=f(n-1)+f(n-2),边界是f(1)=1 ,f(0)=0

因此代码可以写成

def climbStairs(self, n: int) -> int:
    if n <= 3:
        return n
    return self.climbStairs(n - 1) + self.climbStairs(n - 2)
						     fib(5)   
                     /                \
               fib(4)                fib(3)   
             /        \              /       \ 
         fib(3)      fib(2)         fib(2)   fib(1)
        /    \       /    \        /      \
  fib(2)   fib(1)  fib(1) fib(0) fib(1) fib(0)
  /     \
fib(1) fib(0)

这时候采用简单的递归是能解决问题。但是在递归调用的时候有很多重复计算的子问题,所以这时候我们加上记忆化的操作就有下面的这层代码。

def climbStairs(self, n: int) -> int:
  use = {0: 1, 1: 1}
  def dp(n):
    if n in use:
      return use[n]
    use[n] = dp(n - 1) + dp(n - 2)
    return use[n]
  return dp(n)

这次通过将字典的方法将已经计算的结果保存进去,从而避免重复计算,因为这题比较简单所以不涉及剪枝的概念,所以动态规划的解法为:

def climbStairs(self, n: int) -> int:
  if n<=2:
    return 2
  dp=[0]*n
  dp[0],dp[1]=12
  for i in range(2,n):
    dp[i]=dp[i-1]+dp[i-2]
  return dp[-1]

由上可以理解动态规划可以类似动态递推。而dp的解法空间复杂度为O(n),若想采用O(1)的方法可以直接用递推recursion的方式,自底向上求解。

def climbStairs(self, n: int) -> int:
  if n<=3:
    retrun n
  f1,f2=12
  for _ in range(2,n):
    f1,f2,=f2,f1+f2
  return f2

当然这个方法也不是最优解,因为时间复杂度为O(n),若想加快的话,需要借助矩阵幂与分治的方法。可以降到O(logn)

2. 最大子序和

Leetcode 53

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

思路:
  1. 一般可以采用递归+记忆化也就是递推来考虑,
  2. 若是采用dp的化:
    1. 定义状态:dp[n]
    2. 状态转移方程:dp[n]=best(dp[n-1],dp[n-2]…) 此处为理解的难点。
    3. 最优子结构。
问题分析
  1. 这个题目可以定义dp[i] 为nums[i]结尾的时候连续最大子数组和,子问题为nums[i] 跟nums[i-1]相加是否更大
  2. 状态转移方程
  3. 为,看dp[i] =max(nums[i],dp[i-1]+nums[i]),如果nums[i] 为正数,则dp[i-1]+nums[i] 更大,如果nums[i] 为负数,则需要更新dp[i]。此处的max类似剪枝的功能,这是一个比较简单的。dp问题的难点是状态转移中的类似剪枝的功能的寻找。
  4. 求出dp里面最大值。
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        dp=[0] * len(nums)
        dp[0]=nums[0]
        for i in range(1,len(nums)):
            dp[i]=max(nums[i],dp[i-1]+nums[i])  # 都为负数,取nums[i]
        return max(dp)

单独拿出状态转移方程的话可以考虑以下做法

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
      n=len(nums)
      max_value=nums[0]
      for i in range(1,n):
        if nums[i-1]>0:
          nums[i] +=num[i-1]
        max_value=max(nums[i],max_value)
      return max_value

此方法节省空间,只用原数组进行操作代替dp。

3. 最长上升子序列(300)

Leetcode 300

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1
问题分析
  1. 此处的难点是递增的子序列不是连续的,意思不止取前一个,可能是前j个都得去比较才能得到后续长度。默认dp[i]就是前i个元素的子序列长度了。
  2. 两层循环i,j i->n; j->i:
  3. 剪枝判断 nums[j]<nums[i];如果前面的的nums[j] <nums[i],就去比较前面j->i 中 dp[j]+1,dp[i]的大值。
  4. Dp[i]=max(dp[j]+1,dp[i]) ;此处需要在i->n的循环时将dp[i]初始化为1,因为i的前面全是递减的话初始值为1
  5. 返回max(dp)
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n=len(nums)
        dp=[1]*len(nums)
        for i in range(n):
            for j in range(i):
                if nums[i]>nums[j]:
                    dp[i]=max(dp[j]+1,dp[i])
        return max(dp)

4. 三角形最小路径和

Leetcode 120

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

示例1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
   2
  3 4
 6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
问题分析
  1. 定义dp[i] [j] 为第i层j列的路径和。
  2. 状态转移是:dp[i] [j] = min(dp[i-1] [j], dp[i-1] [j-1])+triangle[i] [j]
  3. 剪枝是j==0时,dp[i] [j] = dp[i-1] [j] +triangle[i] [j]; j 是最后一位时 dp[i] [j] = dp[i-1] [j-1] +triangle[i] [j],其他取状态转移方程
  4. 最后返回最后一层的最小值dp[-1]
from typing import List

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        dp = [[0] * len(i) for i in triangle]
        dp[0][0] = triangle[0][0]
        for i in range(1, len(triangle)):
            for j in range(len(triangle[i])):
                if j == 0:
                    dp[i][j] = dp[i - 1][j] + triangle[i][j]
                elif j == len(triangle[i]) - 1:
                    dp[i][j] = dp[i - 1][j - 1] + triangle[i][j]
                else:
                    dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]
        return min(dp[-1])
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值