算法之动态规划详解

定义

动态规划其实是一种运筹学方法,是在多轮决策过程中寻找最优解的方法。

应用场景

动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。

核心思想

求解动态规划的核心求解思路是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。但是我们在求解过程中, 需要避免重复计算从而更快速的找到答案 。

动态规划三要素

  1. 最优子结构:原问题的最优解所包含的子问题的解也是最优的,通过子问题的最值得到原问题的最值。
  2. 存在重叠子问题:子问题间不独立(这是动态规划区别于分治的最大不同);如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
  3. 无后效性:即后续的计算结果不会影响当前结果。

动态规划通用解题过程

动态规划没有标准的解题方法,即没有一个完全通用的解题方法。但在宏观上有通用的方法论:

下面的 k 表示多轮决策的第 k 轮:

  1. 问题分解,将原问题划分成几个子问题。一个子问题就是多轮决策的一个阶段。

  2. 找状态,选择合适的状态变量 S(k)。它需要具备描述多轮决策过程的演变,更像是决策可能的结果。

  3. 做决策,确定决策变量 u(k)。每一轮的决策就是每一轮可能的决策动作,即当前可以有哪些决策可以选择。

  4. 状态转移方程。这个步骤是动态规划最重要的核心,即 S(k+1)= uk(sk) 。

  5. 定目标。写出代表多轮决策目标的指标函数 V(k,n),即最终需要达到的目标。

  6. 寻找目标的终止条件。

动态规划的解题核心就是找到状态转移方程,其实所谓的状态转移方程说白了就是数学上的递推方程,没有什么高大上的。就是通过最基础的问题,一步步递推出所需要的最终目标结果。只是在定义状态转移方程的含义时,有不同的定义方式,不同的定义方式会有不同的递推方程式,但是只要定义正确,都可以得到最终正确的结果。

通常状态转移方程定义过程

  1. 明确当前可改变的状态有什么;
  2. 定义dp数组或者递归函数的具体含义;
  3. 明确每一步可以进行的选择有哪些;
  4. 编写递推方程,也就是状态转移方程;
  5. 明确baseline,也就是递推开始时的基础值是什么。

动态规划解题方式

动态规划的解题方式通常分为两种:

  1. 通过定义递归方程解决,这是一种自顶向下的求解方式,通常这种方式会有很多重复计算过程,因此可以通过建立备忘录记录中间过程来进行优化;
  2. 通过定义DP(Dynamic Programming)数组来求解,这是一种自底向上的求解方式。

至于选择哪一种解题方式,可以根据自己的习惯来。自己比较习惯那种方式思考就用哪种方式即可。

简单举例

下面以常见的斐波拉契数列为例来说明一下上述两种求解方式。

斐波那契数列(Fibonacci sequence)是指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……。即后一个数为前两个数之和。在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)

通过暴力递归求解

定义fib(n)函数返回的是斐波拉契数列第n项的值:

def fib(n):if n <= 2:return 1return fib(n-1) + fib(n-2)

此时的时间复杂度O(2^n)指数级,计算很慢。

我们来看一下递归的状态树:

从状态树我们可以看到有很多重复计算的节点,为了避免重复,我们可以通过建立一个备忘录memo记录,来记录中间节点的计算结果,避免重复计算,可以将时间复杂度直接降为O(n)线性复杂度,优化代码如下:

def fib(n):def helper(n):if n <= 2:return 1# 如果n的值已经计算过,则直接返回值memo[n]if n in memo: return memo[n]memo[n] = fib(n-1) + fib(n-2)return memo[n]memo = {} # 备忘录,记录已经计算过的值,防止重复计算return helper(n)

以fib(6)为例,递归的求解过程如下:

DP数组的迭代解法

上述递归过程是自顶向下求解的,也就是从目标值逐步向下 层递归,直至达到递归的终止条件,然后将值逐层返回;dp数组则是自底向上求解的,即从最开始的基础出发,然后逐步向上递推至目标值,由于DP数组自身存储了所有的计算过程,因此不存在重复计算。DP的解法如下:

def fib(n):dp = [0 for _ in range(n+1)]# 基础值,也就是baselinedp[1] = dp[2] = 1for i in range(3, n + 1):dp[i] = dp[i-1] + dp[i-2]return dp[n]

根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1):

def fib(n):if n <= 2:return 1prev = 1curr = 1for i in range(3,n+1):prev, curr = curr, prev + currreturn curr

零钱兑换问题

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的.

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

由于硬币coins是可以无限用的,因此该题唯一可变的状态为总金额amount。因此定义dp[n]表示:要凑出总金额为n,所需的最少硬币数为dp[n]。

状态转移方程为:

# 伪码框架
def coinChange(coins: List[int], amount: int):# 定义:要凑出金额 n,至少要 dp(n) 个硬币def dp(n):# 做选择,选择需要硬币最少的那个结果for coin in coins:res = min(res, 1 + dp(n - coin))return res# 我们要求的问题是 dp(amount)return dp(amount)

递归暴力解法

def coinChange(coins, amount):def dp(n):# 函数定义为目标金额为n时,所需的最少硬币数量# base case# 目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1if n == 0: return 0if n < 0: return -1# 求最小值,所以初始化结果为正无穷res = float('inf')for coin in coins:subpro = dp(n-coin)if subpro == -1:# 子问题无解,跳过continueres = min(res, 1 + subpro)return res if res != float('inf') else -1return dp(amount)

为了去除重复的计算,我们使用备忘录进行优化:

带备忘录的递归

def coinChange(coins, amount):def dp(n):# 函数定义为目标金额为n时,所需的最少硬币数量# base case# 目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1if n == 0: return 0if n < 0: return -1if n in memo:return memo[n]# 求最小值,所以初始化结果为正无穷res = float('inf')for coin in coins:subpro = dp(n-coin)if subpro == -1:# 子问题无解,跳过continueres = min(res, 1 + subpro)memo[n] = res if res != float('inf') else -1return memo[n]memo = {}return dp(amount)

DP数组迭代解法

dp[i] = x 表示,当目标金额为i时,至少需要x枚硬币

def coinChange(coins, amount):
# 数组大小为 amount + 1,初始值也为 amount + 1
# 因为总的零钱个数不会超过amount,所以初始化amount + 1即可dp = [amount + 1 for _ in (amount + 1)]dp[0] = 0for i in range(len(dp)):#  内层 for循环,求解的是所有子问题 + 1 的最小值for coin in coins:# 子问题无解,跳过if i - coin < 0:continuedp[i] = min(dp[i],1+dp[i-coin])if dp[amount] == amount + 1:return  -1else:return dp[amount]

以上便是关于动态规划的详细说明介绍,后续会出关于动态规划系列的相关常见leetcode题,目录如下(会持续更新):

  1. Leetcode 300.最长上升子序列
  2. Leetcode 1143.最长公共子序列
  3. Leetcode 198.打家劫舍
  4. Leetcode72.编辑距离
  5. Leetcode62.不同路径1
  6. Leetcode63.不同路径2
  7. Leetcode322.零钱兑换
  8. Leetcode64.最小路径和
  9. Leetcode53.最大子序和
    如果想和作者一起学习,欢迎扫描下面二维码关注公众号:阿旭算法与机器学习,一起讨论交流。

算法之【动态规划】详解(python)相关推荐

  1. python【数据结构与算法】动态规划详解从背包到最长公共子序列(看不懂你来打我)

    文章目录 1 引入 2 01背包 3 完全背包问题 4 多重背包问题 5 最长公共子序列 1 引入 0-1背包问题是最广为人知的动态规划问题之一,拥有很多变形.尽管在理解之后并不难写出程序,但初学者往 ...

  2. 算法竞赛——动态规划详解

    1.简单线性dp问题 题目1:01背包问题 有 N 件物品和一个容量是 V 的背包.每件物品只能使用一次. 第 i 件物品的体积是 vi,价值是 wi. 求解将哪些物品装入背包,可使这些物品的总体积不 ...

  3. python如何调用文件进行换位加密_python 换位密码算法的实例详解

    python 换位密码算法的实例详解 一前言: 换位密码基本原理:先把明文按照固定长度进行分组,然后对每一组的字符进行换位操作,从而实现加密.例如,字符串"Error should neve ...

  4. 数学建模——主成分分析算法详解Python代码

    数学建模--主成分分析算法详解Python代码 import matplotlib.pyplot as plt #加载matplotlib用于数据的可视化 from sklearn.decomposi ...

  5. 一文速学-时间序列分析算法之移动平均模型(MA)详解+Python实例代码

    目录 前言 一.移动平均模型(MA) 模型原理 自回归 移动平均模型 自相关系数 常用的 MA 模型的自相关系数 通用: MA(1)模型: MA(2)模型: 自协方差函数 二.Python案例实现 平 ...

  6. 文件不能断点 webstorm_详解python使用金山词霸的翻译功能(调试工具断点的使用)...

    这篇文章主要介绍了详解python使用金山词霸的翻译功能(调试工具断点的使用),本文给大家介绍得非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 今天试着用python获取金山 ...

  7. 详解python实现FP-TREE进行关联规则挖掘(带有FP树显示功能)附源代码下载(3)

    详解python实现FP-TREE进行关联规则挖掘(带有FP树显示功能)附源代码下载(3) 上一节简单讲了下FP树的生成,在这一节我将描述FP树的挖掘过程. 首先我们回顾一下要挖掘的特征项及样本空间: ...

  8. 数学建模_随机森林分类模型详解Python代码

    数学建模_随机森林分类模型详解Python代码 随机森林需要调整的参数有: (1) 决策树的个数 (2) 特征属性的个数 (3) 递归次数(即决策树的深度)''' from numpy import ...

  9. python从random生成列表_详解Python利用random生成一个列表内的随机数

    详解Python利用random生成一个列表内的随机数 首先,需要导入random模块: import random 随机取1-33之间的1个随机数,可能重复: random.choice(range ...

  10. python策略模式包含角色_详解Python设计模式之策略模式

    虽然设计模式与语言无关,但这并不意味着每一个模式都能在每一门语言中使用.<设计模式:可复用面向对象软件的基础>一书中有 23 个模式,其中有 16 个在动态语言中"不见了,或者简 ...

最新文章

  1. 20. matlab 中的gtext 函数
  2. 项目经理的超越(一)你超越了吗?
  3. # 对象json互相转换_推荐一款 Java 对象映射神器
  4. linux通过yum安装vim,linux/centos系统如何使用yum安装vi/vim?
  5. 低代码实现传统装饰企业的管理跃迁
  6. php cms 选择哪个好?
  7. 在window10上安装miniconda
  8. sql server 2008如何导入mdf,ldf文件
  9. 【Beta】Scrum Meeting 3
  10. win10sas安装教程_Android Studio详细安装教程
  11. mpchart点击_在MPAndroidChart中,如何为Barchart中的每个Bar添加click事件?
  12. 将无线鼠标改造成有线鼠标
  13. 开始写博客之学习编程的重要性
  14. 高并发累加器 Striped64
  15. 树莓派开机启动python文件_树莓派开机自启动Py文件
  16. PLM与ERP集成,这个头疼的问题,可以这样解决!
  17. 《R语言数据分析》期末试题
  18. Ubuntu编译ijkplayer so库并播放本地raw/assets文件
  19. 【嵌入式基础】串口中断通信VS串口DMA通信
  20. 用canvas,javascript制作“坦克大战“小游戏

热门文章

  1. 数据类型之字节与字符的区别
  2. Java模拟停车场管理
  3. 【hd水题】hd2018 母牛的故事
  4. 唯一让盖茨晕倒的中国人
  5. 注册表处理之(写入DWORD类型或者字符串类型的键项值)
  6. equals 和 equalsIgnoreCase
  7. php服务器连接错误,运行PHP文件时出现内部服务器错误的解决方法
  8. 学习大数据的第46天(Hadoop篇)——Hadoop框架的认识以及基础命令的认识
  9. Metricbeat部署指南
  10. python中math模块