动态规划算法

动态规划

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划题目的特点

动态规划解决的一般是求计数、求最大最小值和求存在性的题目。计数问题指的是像有多少中方法可以从棋盘的左上角走到右下角,或者是有多少种方法选出k个数使得和是sum;求最大最小值一般指的是从左上角走到右下角路径的最大数字和,或者是求最长上升子序列长度;求存在性指的是取石子游戏,先手是否必胜,或者是能不能选出k个数是的和是sum;当然以上问题不仅仅可以用动态规划解决,同时动态规划能解决的问题也不只是上述问题。动态规划的问题的规律可以用“一个模型三个特征”来总结。分别对应的就是多阶段决策最优解模型、最优子结构、无后效性和重复子问题。

所谓多阶段决策最优解模型,指的是动态规划适合解决的问题的模型。一般来说,动态规划最适合用来解决最优解问题。而在求解过程中,我们需要一步一步的进行决策,每次决策都对应着一种状态,我们的目的就是寻找一组能够产生最优解的决策序列。

最优子结构指的是,问题的最优解包含子问题的最优解。即我们可以通过求解子问题的最优解,推导出最终问题的最优解。

无后效性则有两层含义,一层含义指的是在我们推到后面的状态的时候,我们不关心这个值是怎么来的。另一层含义指的就是某个阶段的状态一旦确定,就不会再受后面的状态影响。

重复子问题指的是用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。

可能这说的有一点抽象,我们以棋盘问题为例,假设我们有一个 n 乘以 n 的矩阵 w[n][n]。矩阵存储的都是正整数。棋子起始位置在左上角,终止位置在右下角。我们将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。从左上角到右下角,会有很多不同的路径可以走。我们把每条路径经过的数字加起来看作路径的长度。那从左上角移动到右下角的最短路径长度是多少呢?

我们首先提出利用动态规划解决这个问题的大概思路,先申请一个二维数组用于保存状态,称之为状态数组。然后我们回归原棋盘数组中,我们开始从左上角开始移动,每次移动的时候就需要做向下或向左移动的操作,然后我们将到达某一位置时最小的路径和保存在状态数组中,那么当最终移动到最后的右下角的时候,状态数组中的最后的值就是最终的最优解。代码如下:

public int minDistDP(int[][] matrix, int n) {
    int[][] states = new int[n][n];
    int sum = 0;
    // 初始化states的第一行数据
    for (int i = 0; i < n; i++) {
        sum += matrix[0][i];
        states[0][i] = sum;
    }
    sum = 0;
    // 初始化states的第一列数据
    for (int i = 0; i < n; i++) {
        sum += matrix[i][0];
        states[i][0] = sum;
    }
    // 决策阶段
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < n; j++) {
            states[i][j] = matrix[i][j] + Math.min(states[i][j - 1], states[i - 1][j]);
        }
    }
    return states[n - 1][n - 1];
}

在梳理明白上述的思路之后,我们可以借此来理解一个模型和三个特征。一个模型可以理解为,我们每个阶段都要进行向下或向左的决策,然后通过多次决策以达到最优解。同时每一次移动过后,我们都会得到一个阶段的状态,而这个状态可以视为更小的一个棋盘的解,我们在进行比较来确定当前位置的最小值,而最终的最优解也是由此推导出来的,可以说我们是通过求解子棋盘的最优解,从而推到出最终棋盘的最优解,这就是最优子结构。而某一个位置的最短路径只取决于前一位置的状态,并且后续的移动是不会对该位置的最短路径造成影响,这就是无后效性。与此同时,如果我们采用回溯的方式进行求解的话,就会发现同一位置会经历很多次,这就是重复子问题。

动态规划解题思路

这个我们可以按照四个步骤来确定解题的大体过程。分别是确定状态、转移方程、初始条件和边界情况以及计算顺序。下面中为了方便理解,我们视为状态数组和棋盘中的位置下标是从零开始的。

首先是确定状态,简单来说,解动态规划问题是需要开一个数组的,数组中的每一个元素代表着什么,这就是所谓的状态。例如在棋盘问题中,我们就需要开一个n*n的数组,用于表示坐标对应的最小路径。在确定状态时,我们需要关注两个点:最后一步和子问题。最后一步即从棋盘的[n - 1][n]和[n][n - 1]位置移动到[n][n]位置,这时就能明确一件事情,若到达最后一步的结果是最优解的话,到达[n - 1][n]和[n][n - 1]也一定是最优的。所以原问题就可以拆解为求解到达[n - 1][n]和[n][n - 1]的最短路径,这也就是子问题,同样的道理,我们还可以继续拆解。通过这样的拆解,我们就可以将原问题拆解为规模较小的子问题。

其次是转移方程,我们可以通过子问题来递归求解,也就是所谓的最优子结构。根据最优子结构,就可以写出递归公式,也就是所谓的动态转移方程。上述确定状态时,我们就明确了,需要去从子问题的最优解来解决规模更大的同等问题,而规模更大的问题的状态是由子问题的状态来确定,例如棋盘问题中的最后一步,确定[n][n]的状态时,需要将[n - 1][n]和[n][n - 1]的最优解中的最小值加上[n][n]棋盘的整数值,即states[n][n] = Math.min(matrix[n - 1][n], matrix[n][n - 1]) + matrix[n][n],这时最后一步的操作,但是推导过程中并不是只有最后一步需要状态的转移,所以我们要写一个更为通用的状态转移方程,即:states[i][j] = matrix[i][j] + Math.min(states[i][j - 1], states[i - 1][j])

然后就是初始条件和边界情况,我们写出的状态转移方程虽然通用,但是一开始的状态以及一些特殊情况是需要我们额外考虑的。例如棋盘问题中的初始化第一行和第一列的操作就是要明确初始化条件。而边界情况则是要考虑类似于下标越界的情况。

最后就是要明确计算顺序,简单理解就是那些可以作为初始状态来推到最终结果,例如棋盘问题中就是通过第一行和第一列的初始状态进行向右下角的方向进行计算的。

总结

我们往往是通过“一个模型和三个特征”来判断是否可以利用动态规划来解决,解决动态规划问题时,我们则可以通过确定状态、状态方程、初始化条件和边界条件以及计算顺序来逐步梳理解题思路。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值