如何理解动态规划

本文介绍了动态规划的基本概念,如最优子结构、重叠子问题和无后效性,以斐波那契数列为例详细阐述自底向上和自顶向下方法。通过爬楼梯、不同路径和最长公共子序列问题展示了动态规划设计步骤。重点讲解了如何利用动态规划解决实际问题并优化算法效率。
摘要由CSDN通过智能技术生成

1. 动态规划

动态规划(Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

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

动态规划常常适用于有重叠子问题最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法。

动态规划有自底向上自顶向下两种解决问题的方式。自顶向下即记忆化递归,自底向上就是递推。

使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量。

这是leetcode上面的概念,我们可以把他结构化一下,并理解一下具体的概念。

image-20210913104321193

1、什么是最优子结构?

当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。

可以反向理解,我们可以通过子问题的最优解,推导出问题的最优解,也就是子找父。如果我们把最优子结构,对应到我们前面定义的动态规划上,那我们也可以理解为,原问题可以通过子问题推导出来,如F(10) = F(9)+F(8),则F(9)F(8)F(10)的最优子结构。

2、什么是重叠子问题?

递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次,这种性质称为子问题的重叠性质。

3、什么是无后效性?

某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。

2. 斐波拉契数列

斐波那契数列指的是这样一个数列:

image-20210913112547375

这个数列数列从第 3 项开始,每一项都等于前两项之和。其递推公式为:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

2.1 暴力递归

我们通过递归的方式来实现一个斐波那契数列

public int fib(int n) {
   
    if (n == 0) {
   
        return 0;
    }
    if (n == 1) {
   
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}

可以看到在leetcode运行上并不是很优秀。

image-20210913113734097

我们来分析一下它的执行过程,假如我们要求n=6的值,则其递归树如下:

image-20210913144609538

可以看到,上面每个节点都会执行一次,而且存在重复执行的节点,这也就是重叠子问题性质的表现,如fib(2)被重复执行了 5 次。由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。

我们试着用动态规划的自底向上自顶向下方式来解决斐波拉契数列问题。

2.2 自顶向下备忘录

在递归方法中如果要计算原问题 f(20)的值,就得先计算出子问题 f(19)f(18),然后要计算 f(19),我就要先算出子问题 f(18)f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。

image-20210913145627020

因此,我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

一般使用一个数组充当这个「备忘录」,当然也可以使用哈希表。

private Map<Integer, Integer> memo = new HashMap<>();
//备忘录法
public int fib(int n) {
   
    if (n == 0) return 0;
    if (n == 1 || n == 2) return 1;
    if (memo.containsKey(n)) {
   
        return memo.get(n);
    } else {
   
        int value = fib(n - 1) + fib(n - 2);
        memo.put(n, value);
        return value;
    }
}

再次查看时间消耗,很明显耗时减少。

image-20210913143212839

「备忘录」到底做了什么?

当计算 f(20)的值,先计算出 f(19)f(18),而在计算f(19)的值时,已经把f(18)的值计算出来了。

image-20210913150539383

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数,即:

image-20210913151214786

2.3 自底向上动态规划

可以看到,备忘录法还是利用了递归,计算fib(20)的时候还是要计算出fib(19)fib(18)fib(17)…,如果我们先计算出fib(1)fib(2)fib(3)…呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。

因为我们已经知道fib(0)fib(1)的值,实际上,fib(2)的值也是知道的,即:

fib(2) = fib(0) + fib(1) = 1
fib(3) = fib(1) + fib(2) = 2
fib(4) = fib(2) + fib(3) = 3
fib(5) = fib(3) + fib(4) = 5
fib(6) = fib(4) + fib(5) = 8
......

我们根据备忘录的思想,用一张表来记录,即:

image-20210913161559976

public int fib(int n
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪了个王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值