首页 > 其他分享 >图解动态规划

图解动态规划

时间:2024-07-21 17:08:51浏览次数:12  
标签:动态 nums max 递增 len 序列 长度 图解 规划

总结自:10 分钟彻底搞懂“动态规划”算法

下面用实例讲解什么是动态规划算法(Dynamic Programming,DP)。

首先我们来看一个经典的动态规划问题。

输入一个无序的数组,要求找出其中最长的递增的子序列。

如:对于[1, 5, 2, 4, 3],其最长递增子序列为1,2,41,2,3

但这里我们对这个问题进行简化,我们只要求返回最长递增子序列的长度即可。对于[1, 5, 2, 4, 3],最长递增子序列的长度为3

暴力搜索

最容易想到的解法是暴力搜索。比如从1出发,下一个数字可以是524或者3,因为它们都是递增的:

image-20240109225634910

假如下一个数字我们取5的话,再下一个数字都取不了,因为剩下的243都比5小,不能够构成一个递增序列:

image-20240109225906615

如果下一个数字是2的话,下一个数字可以是43,此时构成的递增序列长度为3

image-20240109230113745

以此类推,如果下一个数字我们取4,下一个数字依然不能选,因为34小:

image-20240109230313481

算法就这样循环往复地执行,直到遍历完所有以1开头的子序列,并且在遍历过程中,我们会实时记录当前的最长递增子序列长度:

image-20240109230512505
image-20240109230531644

最后可以知道,从1出发,最长的递增子序列长度为3

image-20240109230942436

我们按照同样的方法计算从5243出发的最长递增子序列长度:

image-20240109231246389

得到max_len数组,选出最长的那个,即取max_len数组的最大值,算法结束:

image-20240109231409888

要实现上述算法,我们可以定义函数L(nums, i),该函数返回从数组nums的第i个数字开始的最长子序列长度:

def L(nums, i):
    """Returns the length of longest increasing subsequence starting from i"""
[ 1, 5, 2, 4, 3 ]
  ↑
  i

在函数L中,我们检查i后面的所有数字(数字索引标记为j),只要该数字比i位置的数大,那么以i开始的最长递增子序列长度就等于以j开始的最长递增子序列长度加1,求以j开始的最长递增子序列长度也执行这个过程,所以我们递归地调用L函数:

def L(nums, i):
    """Returns the length of longest increasing subsequence starting from i"""
    if i == len(nums) - 1: # last number
        return 1
    max_len = 1
    for j in range(i, len(nums)):
        if nums[j] > nums[i]:
            max_len = L(nums, j) + 1
    return max_len

使用L函数,求从各位置开始的最长递增子序列长度,并取最大值,即可得到最长递增子序列的长度:

"""longest increasing subsequence"""
def LIS(nums):
    return max(L(nums, i) for i in range(len(nums)))

这个算法的问题是,它的时间复杂度不可接受,假设数组长度为 n,那么其子序列个数为 2n,每个子序列最多遍历 n 次,所以时间复杂度为 O(2n) * O(n) = O(n * 2n)。

优化

观察遍历树,会发现其中存在大量重复计算:

image-20240206155432978

比如我们遍历子序列1,2,4的时候就已经计算过“从 4 开始的最大子序列的长度”:

image-20240206155543784

后面遍历1,4的时候又重复计算了一次:

image-20240206155716439

为了避免重复的计算,我们可以在第一次计算的时候,将结果保存起来:

image-20240206155805647

后遍历到相同的节点我们就不再需要再重复计算了,直接将之前的结果返回:

image-20240206155833277

代码如下:

memo = {} # 用于记录计算结果

def L(nums, i):
    if i in memo:
        return memo[i] # 如果已经计算过,则直接返回
    if i == len(nums) - 1:
        return 1
    max_len = 1
    for j in range(i, len(nums)):
        if nums[j] > nums[i]:
            max_len = L(nums, j) + 1
    memo[i] = max_len # 记录结果
    return max_len

def LIS(nums):
    return max(L(nums, i) for i in range(len(nums)))

由于用到了字典/哈希表来保存计算的中间结果,因此我们也称之为“记忆化”搜索(Recursion With Memoization),有人也称其为带“备忘录”的递归,或者叫递归树的“剪枝”(Pruning)。

迭代的形式

由于1可以和5243构成递增序列,所以要计算从1开始的最长子序列长度,可以先计算从5243开始的最长子序列长度:

image-20240206163139371

取其中的最大值,然后加一:

image-20240206163203630

同样地,要计算从5开始最长子序列长度,先找到之后比5大的数,计算各数的最长子序列长度,取其中的最大值,再加一:

image-20240206163252704

这里没有比5大的数,结果为1

image-20240206163652166

以此类推,直到最后一个数(最长递增子序列长度为 1):

image-20240206163744864

从右侧的公式可以知道,只要从后往前依次计算,就可以将所有答案推算出来:

image-20240206163853586
image-20240206163907366

具体实现用到了两个循环,外面的循环表示从下往上依次计算:

image-20240206164354413

里面的循环用于遍历括号中的这些数值:

image-20240206164432150

运算的结果用数组L存放:

image-20240206164536100

对于后面的比当前数大的所有数(能构成递增序列的数),在L[i]L[j]+1中取较大值更新到L[i],效果等价于先找到后面比当前数大的数,计算各数的最长子序列长度并取其中的最大值保存到L[i]

image-20240206164649381

最后返回L数组中的最大值:

image-20240206182939930

代码实现:

def length_of_LIS(nums):
    n = len(nums)  # 5
    L = [1] * n # initial value: [1,1,1,1,1]
    
    for i in reversed(range(n)):  # i -> 4,3,2,1,0
        for j in range(i + 1, n):
            if nums[j] > nums[i]: # is increasing seq
                L[i] = max(L[i], L[j] + 1)
                
    return max(L)

其中有两个循环,每个循环最多执行 N 次,时间复杂度 O(n2)。

一般思路

穷举法 / 暴力搜索

使用动态规划解决问题,可以先考虑用暴力穷举的方法解决,并画出递归树,尝试写一个递归函数来求解。

记忆化搜索 / 剪枝

如果发现遍历中存在大量的重复计算,可以尝试用哈希表将数据缓存下来,之后遍历到相同的节点就直接查表,避免重复的计算。

改写成迭代形式

最后,可以将计算的过程表示出来,然后观察公式求解的顺序,并尝试将递归形式改写成更加高效的迭代形式。

标签:动态,nums,max,递增,len,序列,长度,图解,规划
From: https://www.cnblogs.com/Higurashi-kagome/p/18010199

相关文章

  • 无法使用“from x import y”动态导入
    我正在编写一个函数来检查导入文件/模块是否包含任何错误。当我使用模块名称作为变量时,该函数由于某些原因失败。有效defimportfile1():try:fromtest2importbeaprint('1stscenario:','importissuccessful')except:print('1s......
  • 代码随想录算法训练营第35天 | 动态规划1:509.斐波那契数、70.爬楼梯、746.使用最小花
    代码随想录算法训练营第35天|动态规划理论基础https://programmercarl.com/动态规划理论基础.html#算法公开课509.斐波那契数https://leetcode.cn/problems/fibonacci-number/submissions/548309803/代码随想录https://programmercarl.com/0509.斐波那契数.html#算法公开......
  • 使用Spring Boot实现业务数据动态脱敏
     ​ 博客主页:   南来_北往......
  • Android Studio项目中的重复类、动态版本控制及其他优化方法
    本文介绍在Android开发过程中,我们常常会遇到一些棘手的问题,如重复类冲突、动态版本控制及依赖打包等。本文将介绍如何解决这些问题,并提供一些有用的优化方法。1.解决重复类冲突问题在引入多个JAR包或AAR包时,可能会遇到类重复的问题,导致编译失败。这里提供了两种解决方......
  • 动态规划-1:穷举遍历->map缓存->取消递归
    importjava.util.HashMap;importjava.util.Map;publicclassDynamicProgrammingAlgorithm{publicstaticvoidmain(String[]args){//比如要求一个数组的最长递增子序列的长度//比如是[1,4,2,5,3],那么[1,2,5],或者[1,2,3]都是最长递增子序......
  • 动态规划2:计算最大连续子序列和
    importjava.util.HashMap;importjava.util.Map;publicclassDynamicProgramming2{publicstaticvoidmain(String[]args){int[]arr={3,-4,2,-1,2,6,-5,4};//暴力枚举法System.out.println(getMaxSumSubArr(arr));//加......
  • 深度学习图解,第 1 部分:神经网络如何工作?神经网络的图解和直观介绍
            欢迎来到雲闪世界。神经网络是一种机器学习模型。这只是我计划撰写的关于深度学习的整个系列文章的第一篇。它将重点介绍一个简单的人工神经网络如何学习,并为您提供对神经网络如何逐个神经元构建的深入(哈哈,双关语)理解,这在我们继续构建这些知识时至关重......
  • 如何在Python中使用装饰器动态创建类方法?
    我正在开发一个Python项目,我需要在运行时动态地为类创建方法。我想使用装饰器根据一些外部配置将这些方法添加到类中。要求是:装饰器应该从外部配置(例如字典)读取方法定义。装饰器应该动态地将这些方法添加到类中。每个生成的方法都应具有配置中指定的自己唯一的实现。以......
  • Leetcode 中的动态规划
    对于初学者来说,Leetcode中的动态规划可以做哪些问题?我想知道可以使用Leetcode中的动态规划来解决哪些问题,对于初学者来说很容易。我一直在LeetCode上练习问题,我注意到有些问题被专门标记为“动态编程”(DP)。我了解DP的基础知识,例如将问题分解为子问题并存储这些子问题的......
  • 如何使用 for 循环在 python jupyter 笔记本中创建动态图?
    我正在学习本课关于用Python求解热方程。该课程指出,在求解热方程后,我们可以通过在循环中简单地调用pyplot.plot()来可视化解的动画图,其中下面的代码将动态绘制每次每个点的温度,从而得到一个动画情节(课程帖子中提供了动画情节的示例)。importnumpyfrommatplotlibi......