20.1-动态规划(算法基础篇)

176 阅读5分钟

前言

我们之前学习了递推算法和地推套路,也说过了递推算法与动态规划暧昧不清的关系,那么,今天就来初步的学习一下动态规划。

状态转移方程

我们之前学习递推算法时所使用的递推公式,在动态规划中叫做:状态转移方程

重点

  1. **状态:**一个数学符号加上语义描述
  2. **决策:**从所有可能产生最优解的答案当中找出一个最大值或最小值。(动态规划的重点就在于决策,如果一个算法,不需要决策就能得到最优解,这类算法称为贪心算法,至于什么是贪心算法不在我们今天的讨论范畴,我们暂且略过,之后再一起学习。)
  3. **阶段:**当前阶段仅仅依赖上一个阶段

递推问题的求解方向

  • **我(目标值)从哪里来:**求当前状态时查看前置依赖项(pull,拉取别人的更新,就像ReactAngular数据变化侦测的方式)

    如之前我们学过的数字三角形的递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + val[i][j]中,我们要求得dp[i][j]的话,必须先得到dp[i-1][j]dp[i-1][j-1]的值。这就是从哪里推导出目标值的求解方向。

  • **我到哪里去:**求得当前状态时主动更新下一个状态(push,主动推送自己的更新,就像Vue侦测数据变化的方式)

    还是数字三角形的题,当我们计算出来了dp[i-1][j]的值时,我就去更新一下dp[i][j]的值,当我计算出dp[i-1][j-1]的结果时,我们再更新一下dp[i][j]的值,通过两次更新,我们也可以保证dp[i][j]的正确性。这种实现方式与上一种在思维层面和实现层面都有所不同,但也是一种可行的求解方向。

那么,什么情况下应该选择我从哪里来的求解方向求解,什么情况下选择我到哪里去的求解方向求解呢?

当我们的状态和阶段很明确,并且状态不多时,我们可以使用我从哪里来这种求解方向,这也是绝大部分动态规划问题的求解方向。而当某些情况下,我们很难说清楚我从哪里来,也就是状态和阶段太过于抽象或隐晦,或者依赖的状态实在是太多了,此时,我们可以采用我到哪里去这个求解方向。通过当前状态值主动更新下一阶段状态的值。

任何一个动态规划的程序,都可以用上面的两种方式去实现

示例

之前我们已经使用过“我从哪里来”的求解方向解决过数字三角形的问题,那么,我们现在来使用“我到哪里去”这个求解方向来求解试试

120. 三角形最小路径和
解题思路

与“我从哪里来”通过上一行数据计算下一行的数据不同,“我到哪里去”是假设当前你已经求出来dp[i][j]了,那么,此时你可以利用这个已经求出来的结果去主动更新下一行的数据。

代码演示
function minimumTotal(triangle: number[][]): number {
    const n = triangle.length;
    let dp: number[][] = [];
    // 由于后续每次都要比较求解最小值,因此,我们初始时将dp数组的每一位都初始化为最大整型,方便后续进行大小比较
    for(let i=0;i<n;i++) dp.push([]);
    for(let i=0;i<n;i++) {
        for(let j=0;j<=i;j++) {
            dp[i][j] = Number.MAX_SAFE_INTEGER;
        }
    }
    // 边界条件,第0行第0列的值应该就是三角形顶点的值
    dp[0][0] = triangle[0][0];
    // 循环每一行的数据
    for(let i=0; i<n-1; i++) {
        // 循环每一列的数据
        for(let j=0;j<=i;j++) {
            // 我们此时假设dp[i][j]已经是一个合法的值了,我们通过dp[i][j]求得dp[i+1][j]和dp[i+1][j+1]的值
            dp[i+1][j] = Math.min(dp[i + 1][j], dp[i][j] + triangle[i+1][j]);
            dp[i+1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i][j] + triangle[i+1][j + 1]);
        }
    }
    // 最后在dp手足最后一行找找到一个最小值就是我们要找的答案了。
    return Math.min(...dp[n-1]);
};

底层结构

为何递推和动态规划会有上面说说的多种求解方向的奇特现象呢?其实,本质上,递推程序和动态规划,底层是个图结构。为什么这么说呢?我们来看一下我们动态规划的状态转移过程是怎样的:

top

拓扑序基础概念扫盲

拓扑序是有向图当中每个节点的一个顺序,对于每一个节点,当我们输出这个节点之前,他前面所有依赖的点,即前置节点都已经输出出来了。因此,所谓的拓扑序,就是有向图中节点的一维序列化的结果。

拓扑排序是一个图算法,由于图的含义极其广泛,因此应用非常广泛。拓扑排序就是求出一张图的其中一种拓扑序。对于一张而言,拓扑序是不唯一的,可能有很多种,一般我们只要求出其中一种即可。拓扑排序我们一般会借助队列辅助完成排序。当某一个节点没有任何前置节点,即入度为0时,就可以把这个节点放入队列中,然后依次弹出队列就是我们要求的其中一组拓扑序了。

递推(动态规划)的求解顺序,其实就是状态依赖图的拓扑序