告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我(万字长文)

动态规划难吗?说实话,我以为很难,特别是对于初学者来讲,我当时入门动态规划的时候,是看 0-1 背包问题,当时真的是一脸懵逼。后来,我遇到动态规划的题,看的懂答案,但就是本身不会作,不知道怎么下手。就像作递归的题,看的懂答案,但下不了手,关于递归的,我以前也写过一篇套路的文章,若是对递归不大懂的,强烈建议看一看:为何你学不会递归,告别递归,谈谈个人经验java

对于动态规划,春招秋招时好多题都会用到动态规划,一气之下,再 leetcode 连续刷了几十道题面试

在这里插入图片描述

以后,豁然开朗 ,感受动态规划也不是很难,今天,我就来跟你们讲一讲,我是怎么作动态规划的题的,以及从中学到的一些套路。相信你看完必定有所收获算法

若是你对动态规划感兴趣,或者你看的懂动态规划,但殊不知道怎么下手,那么我建议你好好看如下,这篇文章的写法,和以前那篇讲递归的写法,是差很少同样的,将会举大量的例子。若是一次性看不完,建议收藏,同时别忘了素质三连数组

为了兼顾初学者,我会从最简单的题讲起,后面会愈来愈难,最后面还会讲解,该如何优化。由于 80% 的动规都是能够进行优化的。不过我得说,若是你连动态规划是什么都没听过,可能这篇文章你也会压力山大。微信

1、动态规划的三大步骤

动态规划,无非就是利用历史记录,来避免咱们的重复计算。而这些历史记录,咱们得须要一些变量来保存,通常是用一维数组或者二维数组来保存。下面咱们先来说下作动态规划题很重要的三个步骤,数据结构

若是你听不懂,也不要紧,下面会有不少例题讲解,估计你就懂了。之因此不配合例题来说这些步骤,也是为了怕大家脑壳乱了学习

第一步骤:定义数组元素的含义,上面说了,咱们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个很是很是重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是表明什么意思?优化

第二步骤:找出数组元素之间的关系式,我以为动态规划,仍是有一点相似于咱们高中学习时的概括法的,当咱们要计算 dp[n] 时,是能够利用 dp[n-1],dp[n-2].....dp[1],来推出 dp[n] 的,也就是能够利用历史数据来推出新的元素值,因此咱们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来讲。3d

学过动态规划的可能都常常听到最优子结构,把大的问题拆分红小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计大家也听多了,因此这一次,我将换一种形式来说,再也不是各类子问题,各类最优子结构。因此大佬可别喷我再乱讲,由于我说了,这是我本身平时作题的套路。code

第三步骤:找出初始值。学过数学概括法的都知道,虽然咱们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],咱们能够经过 dp[n-1] 和 dp[n-2] 来计算 dp[n],可是,咱们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,因此咱们必需要可以直接得到 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值

由了初始值,而且有了数组元素之间的关系式,那么咱们就能够获得 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。

不懂?没事,咱们来看三四道例题,我讲严格按这个步骤来给你们讲解。

2、案例详解

案例1、简单的一维 DP

问题描述:一只青蛙一次能够跳上1级台阶,也能够跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

(1)、定义数组元素的含义

按我上面的步骤说的,首先咱们来定义 dp[i] 的含义,咱们的问题是要求青蛙跳上 n 级的台阶总共由多少种跳法,那咱们就定义 dp[i] 的含义为:跳上一个 i 级的台阶总共有 dp[i] 种跳法。这样,若是咱们可以算出 dp[n],不就是咱们要求的答案吗?因此第一步定义完成。

(2)、找出数组元素间的关系式

咱们的目的是要求 dp[n],动态规划的题,如大家常常据说的那样,就是把一个规模比较大的问题分红几个规模比较小的问题,而后由小的问题推导出大的问题。也就是说,dp[n] 的规模为 n,比它规模小的是 n-1, n-2, n-3.... 也就是说,dp[n] 必定会和 dp[n-1], dp[n-2]....存在某种关系的。咱们要找出他们的关系。

那么问题来了,怎么找?

这个怎么找,是最核心最难的一个,咱们必须回到问题自己来了,来寻找他们的关系式,dp[n] 究竟会等于什么呢?

对于这道题,因为状况能够选择跳一级,也能够选择跳两级,因此青蛙到达第 n 级的台阶有两种方式

一种是从第 n-1 级跳上来

一种是从第 n-2 级跳上来

因为咱们是要算全部可能的跳法的,因此有 dp[n] = dp[n-1] + dp[n-2]。

(3)、找出初始条件

当 n = 1 时,dp[1] = dp[0] + dp[-1],而咱们是数组是不容许下标为负数的,因此对于 dp[1],咱们必需要直接给出它的数值,至关于初始值,显然,dp[1] = 1。同样,dp[0] = 0.(由于 0 个台阶,那确定是 0 种跳法了)。因而得出初始值:

dp[0] = 0.
dp[1] = 1.
即 n <= 1 时,dp[n] = n.

三个步骤都作出来了,那么咱们就来写代码吧,代码会详细注释滴。

int f( int n ){
    if(n <= 1)
    return n;
    // 先建立一个数组来保存历史数据
    int[] dp = new int[n+1];
    // 给出初始值
    dp[0] = 0;
    dp[1] = 1;
    // 经过关系式来计算出 dp[n]
    for(int i = 2; i <= n; i++){
        dp[i] = dp[i-1] + dp[-2];
    }
    // 把最终结果返回
    return dp[n];
}
(4)、再说初始化

你们先想如下,你以为,上面的代码有没有问题?

答是有问题的,仍是错的,错在对初始值的寻找不够严谨,这也是我故意这样弄的,意在告诉大家,关于初始值的严谨性。例如对于上面的题,当 n = 2 时,dp[2] = dp[1] + dp[0] = 1。这显然是错误的,你能够模拟一下,应该是 dp[2] = 2。

也就是说,在寻找初始值的时候,必定要注意不要找漏了,dp[2] 也算是一个初始值,不能经过公式计算得出。有人可能会说,我想不到怎么办?这个很好办,多作几道题就能够了。

下面我再列举三道不一样的例题,而且,再在将来的文章中,我也会持续按照这个步骤,给你们找几道有难度且类型不一样的题。下面这几道例题,不会讲的特性详细哈。实际上 ,上面的一维数组是能够把空间优化成更小的,不过咱们如今先不讲优化的事,下面的题也是,不讲优化版本。

案例二:二维数组的 DP

我作了几十道 DP 的算法题,能够说,80% 的题,都是要用二维数组的,因此下面的题主要以二维数组为主,固然有人可能会说,要用一维仍是二维,我怎么知道?这个问题不大,接着往下看。

问题描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不一样的路径?

在这里插入图片描述

这是 leetcode 的 62 号题:https://leetcode-cn.com/problems/unique-paths/

仍是老样子,三个步骤来解决。

步骤1、定义数组元素的含义

因为咱们的目的是从左上角到右下角一共有多少种路径,那咱们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。那么,dp[m-1] [n-1] 就是咱们要的答案了。

注意,这个网格至关于一个二维数组,数组是从下标为 0 开始算起的,因此 右下角的位置是 (m-1, n - 1),因此 dp[m-1] [n-1] 就是咱们要找的答案。

步骤二:找出关系数组元素间的关系式

想象如下,机器人要怎么样才能到达 (i, j) 这个位置?因为机器人能够向下走或者向右走,因此有两种方式到达

一种是从 (i-1, j) 这个位置走一步到达

一种是从(i, j - 1) 这个位置走一步到达

由于是计算全部可能的步骤,因此是把全部可能走的路径都加起来,因此关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

步骤3、找出初始值

显然,当 dp[i] [j] 中,若是 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,由于这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,因此咱们的初始值是计算出全部的 dp[0] [0….n-1] 和全部的 dp[0….m-1] [0]。这个仍是很是容易计算的,至关于计算机图中的最上面一行和左边一列。所以初始值以下:

dp[0] [0….n-1] = 1; // 至关于最上面一行,机器人只能一直往左走

dp[0…m-1] [0] = 1; // 至关于最左面一列,机器人只能一直往下走

撸代码

三个步骤都写出来了,直接看代码

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推导出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空间复杂度能够优化成 O(min(n, m)) 的空间复杂度的,不过这里先不讲

案例3、二维数组 DP

写到这里,有点累了,,但仍是得写下去,因此看的小伙伴,大家可得继续看呀。下面这道题也不难,比上面的难一丢丢,不过也是很是相似

问题描述

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

举例:
输入:
arr = [
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 由于路径 1→3→1→1→1 的总和最小。

和上面的差很少,不过是算最优路径和,这是 leetcode 的第64题:https://leetcode-cn.com/problems/minimum-path-sum/

仍是老样子,可能有些人都看烦了,哈哈,但我仍是要按照步骤来写,让那些不大懂的加深理解。有人可能以为,这些题太简单了吧,别慌,小白先入门,这些属于 medium 级别的,后面在给几道 hard 级别的。

步骤1、定义数组元素的含义

因为咱们的目的是从左上角到右下角,最小路径和是多少,那咱们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,最下的路径和是 dp[i] [j]。那么,dp[m-1] [n-1] 就是咱们要的答案了。

注意,这个网格至关于一个二维数组,数组是从下标为 0 开始算起的,因此 由下角的位置是 (m-1, n - 1),因此 dp[m-1] [n-1] 就是咱们要走的答案。

步骤二:找出关系数组元素间的关系式

想象如下,机器人要怎么样才能到达 (i, j) 这个位置?因为机器人能够向下走或者向右走,因此有两种方式到达

一种是从 (i-1, j) 这个位置走一步到达

一种是从(i, j - 1) 这个位置走一步到达

不过此次不是计算全部可能路径,而是计算哪个路径和是最小的,那么咱们要从这两种方式中,选择一种,使得dp[i] [j] 的值是最小的,显然有

dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格种的值
步骤3、找出初始值

显然,当 dp[i] [j] 中,若是 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,由于这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,因此咱们的初始值是计算出全部的 dp[0] [0….n-1] 和全部的 dp[0….m-1] [0]。这个仍是很是容易计算的,至关于计算机图中的最上面一行和左边一列。所以初始值以下:

dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 至关于最上面一行,机器人只能一直往左走

dp[i] [0] = arr[i] [0] + dp[i] [0]; // 至关于最左面一列,机器人只能一直往下走

代码以下
public static int uniquePaths(int[][] arr) {
    int m = arr.length;
    int n = arr[0].length;
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    dp[0][0] = arr[0][0];
    // 初始化最左边的列
    for(int i = 1; i < m; i++){
      dp[i][0] = dp[i-1][0] + arr[i][0];
    }
    // 初始化最上边的行
    for(int i = 1; i < n; i++){
      dp[0][i] = dp[0][i-1] + arr[0][i];
    }
        // 推导出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + arr[i][j];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空间复杂度能够优化成 O(min(n, m)) 的空间复杂度的,不过这里先不讲

案例 4:编辑距离

此次给的这道题比上面的难一些,在 leetcdoe 的定位是 hard 级别。好像是 leetcode 的第 72 号题。

问题描述

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操做数 。

你能够对一个单词进行以下三种操做:

插入一个字符
删除一个字符
替换一个字符

示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释: 
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

解答

仍是老样子,按照上面三个步骤来,而且我这里能够告诉你,90% 的字符串问题均可以用动态规划解决,而且90%是采用二维数组。

步骤1、定义数组元素的含义

因为咱们的目的求将 word1 转换成 word2 所使用的最少操做数 。那咱们就定义 dp[i] [j]的含义为:当字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,将 word1 转化为 word2 所使用的最少操做次数为 dp[i] [j]

有时候,数组的含义并不容易找,因此仍是那句话,我给大家一个套路,剩下的还得看大家去领悟。

步骤二:找出关系数组元素间的关系式

接下来咱们就要找 dp[i] [j] 元素之间的关系了,比起其余题,这道题相对比较难找一点,可是,无论多难找,大部分状况下,dp[i] [j] 和 dp[i-1] [j]、dp[i] [j-1]、dp[i-1] [j-1] 确定存在某种关系。由于咱们的目标就是,**从规模小的,经过一些操做,推导出规模大的。对于这道题,咱们能够对 word1 进行三种操做

插入一个字符
删除一个字符
替换一个字符

因为咱们是要让操做的次数最小,因此咱们要寻找最佳操做。那么有以下关系式:

1、若是咱们 word1[i] 与 word2 [j] 相等,这个时候不须要进行任何操做,显然有 dp[i] [j] = dp[i-1] [j-1]。(别忘了 dp[i] [j] 的含义哈)。

2、若是咱们 word1[i] 与 word2 [j] 不相等,这个时候咱们就必须进行调整,而调整的操做有 3 种,咱们要选择一种。三种操做对应的关系试以下(注意字符串与字符的区别):

(1)、若是把字符 word1[i] 替换成与 word2[j] 相等,则有 dp[i] [j] = dp[i-1] [j-1] + 1;

(2)、若是在字符串 word1末尾插入一个与 word2[j] 相等的字符,则有 dp[i] [j] = dp[i] [j-1] + 1;

(3)、若是把字符 word1[i] 删除,则有 dp[i] [j] = dp[i-1] [j] + 1;

那么咱们应该选择一种操做,使得 dp[i] [j] 的值最小,显然有

dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1;

因而,咱们的关系式就推出来了,

步骤3、找出初始值

显然,当 dp[i] [j] 中,若是 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,由于这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,因此咱们的初始值是计算出全部的 dp[0] [0….n] 和全部的 dp[0….m] [0]。这个仍是很是容易计算的,由于当有一个字符串的长度为 0 时,转化为另一个字符串,那就只能一直进行插入或者删除操做了。

代码以下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 经过公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 若是 word1[i] 与 word2[j] 相等。第 i 个字符对应下标是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

最后说下,若是你要练习,能够去 leetcode,选择动态规划专题,而后连续刷几十道,保证你之后不再怕动态规划了。固然,遇到很难的,咱仍是得挂。

Leetcode 动态规划直达:https://leetcode-cn.com/tag/dynamic-programming/

3、如何优化?

前两天写一篇长达 8000 子的关于动态规划的文章告别动态规划,连刷40道动规算法题,我总结了动规的套路

这篇文章更多讲解我平时作题的套路,不过因为篇幅过长,举了 4 个案例以后,没有讲解优化,今天这篇文章就来说解下,对动态规划的优化如何下手,而且之前几天那篇文章的题做为例子直接讲优化,若是没看过的建议看一下(不看也行,我会直接给出题目以及没有优化前的代码):告别动态规划,连刷40道动规算法题,我总结了动规的套路

4、优化核心:画图!画图!画图

没错,80% 的动态规划题均可以画图,其中 80% 的题均可以经过画图一会儿知道怎么优化,固然,DP 也有一些很难的题,想优化可没那么容易,不过,今天我要讲的,是属于不怎么难,且最多见,面试笔试最常常考的难度的题。

下面咱们直接经过三道题目来说解优化,你会发现,这些题,优化事后,代码只有细微的改变,你只要会一两道,能够说是会了 80% 的题。

O(n*m) 空间复杂度优化成 O(n)

上次那个青蛙跳台阶的 dp 题是能够把空间复杂度 O( n) 优化成 O(1),原本打算从这道题讲起的,但想了下,想要学习 dp 优化的感受至少都是 小小大佬了,因此就不讲了,就从二维数组的 dp 讲起。

案例1:最多路径数

问题描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不一样的路径?

在这里插入图片描述

这是 leetcode 的 62 号题:https://leetcode-cn.com/problems/unique-paths/

这道题的 dp 转移公式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1],代码以下

不懂的看我以前文章:告别动态规划,连刷40道动规算法题,我总结了动规的套路

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推导出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

这种作法的空间复杂度是 O(n * m),下面咱们来说解如何优化成 O(n)。

dp[i] [j] 是一个二维矩阵,咱们来画个二维矩阵的图,对矩阵进行初始化

在这里插入图片描述
而后根据公式 dp[i][j] = dp[i-1][j] + dp[i][j-1] 来填充矩阵的其余值。下面咱们先填充第二行的值。
在这里插入图片描述
你们想一个问题,当咱们要填充第三行的值的时候,咱们须要用到第一行的值吗?答是不须要的,不行你试试,当你要填充第三,第四....第 n 行的时候,第一行的值永远不会用到,只要填充第二行的值时会用到。

根据公式 dp[i][j] = dp[i-1][j] + dp[i][j-1],咱们能够知道,当咱们要计算第 i 行的值时,除了会用到第 i - 1 行外,其余第 1 至 第 i-2 行的值咱们都是不须要用到的,也就是说,对于那部分用不到的值咱们还有必要保存他们吗?

答是不必,咱们只须要用一个一维的 dp[] 来保存一行的历史记录就能够了。而后在计算机的过程当中,不断着更新 dp[] 的值。单说估计你可能很差理解,下面我就手把手来演示下这个过程。

一、刚开始初始化第一行,此时 dp[0..n-1] 的值就是第一行的值。
在这里插入图片描述

二、接着咱们来一边填充第二行的值一边更新 dp[i] 的值,一边把第一行的值抛弃掉。

为了方便描述,下面咱们用arr (i,j)表示矩阵中第 i 行 第 j 列的值。从 0 开始哈,就是说有第 0 行。

(1)、显然,矩阵(1, 0) 的值至关于以往的初始化值,为 1。而后这个时候矩阵 (0,0)的值不在须要保存了,由于再也用不到了。
在这里插入图片描述
这个时候,咱们也要跟着更新 dp[0] 的值了,刚开始 dp[0] = (0, 0),如今更新为 dp[0] = (1, 0)。

(2)、接着继续更新 (1, 1) 的值,根据以前的公式 (i, j) = (i-1, j) + (i, j- 1)。即 (1,1)=(0,1)+(1,0)=2。
在这里插入图片描述
你们看图,以往的二维的时候, dp[i][j] = dp[i-1] [j]+ dp[i][j-1]。如今转化成一维,不就是 dp[i] = dp[i] + dp[i-1] 吗?

即 dp[1] = dp[1] + dp[0],并且还动态帮咱们更新了 dp[1] 的值。由于刚开始 dp[i] 的保存第一行的值的,如今更新为保存第二行的值。
在这里插入图片描述
(3)、一样的道理,按照这样的模式一直来计算第二行的值,顺便把第一行的值抛弃掉,结果以下
在这里插入图片描述
此时,dp[i] 将彻底保存着第二行的值,而且咱们能够推导出公式

dp[i] = dp[i-1] + dp[i]

dp[i-1] 至关于以前的 dp[i-1][j],dp[i] 至关于以前的 dp[i][j-1]。

因而按照这个公式不停着填充到最后一行,结果以下:
在这里插入图片描述
最后 dp[n-1] 就是咱们要求的结果了。因此优化以后,代码以下:

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[] dp = new int[n]; // 
    // 初始化
    for(int i = 0; i < n; i++){
      dp[i] = 1;
    }

        // 公式:dp[i] = dp[i-1] + dp[i]
    for (int i = 1; i < m; i++) {
        // 第 i 行第 0 列的初始值
        dp[0] = 1;
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j-1] + dp[j];
        }
    }
    return dp[n-1];
}

案例2:编辑距离

接着咱们来看昨天的另一道题,就是编辑矩阵,这道题的优化和这一道有一点点的不一样,上面这道 dp[i][j] 依赖于 dp[i-1][j] 和 dp[i][j-1]。而还有一种状况就是 dp[i][j] 依赖于 dp[i-1][j],dp[i-1][j-1] 和 dp[i][j-1]。

问题描述

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操做数 。

你能够对一个单词进行以下三种操做:

插入一个字符
删除一个字符
替换一个字符

示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释: 
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

解答

昨天的代码以下所示,不懂的记得看以前的文章哈:告别动态规划,连刷40道动规算法题,我总结了动规的套路

public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 经过公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 若是 word1[i] 与 word2[j] 相等。第 i 个字符对应下标是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

没有优化之间的空间复杂度为 O(n*m)

你们能够本身动手作下,按照上面的那个模式,你会优化吗?
在这里插入图片描述
对于这道题其实也是同样的,若是要计算 第 i 行的值,咱们最多只依赖第 i-1 行的值,不须要用到第 i-2 行及其之前的值,因此同样能够采用一维 dp 来处理的。

不过这个时候要注意,在上面的例子中,咱们每次更新完 (i, j) 的值以后,就会把 (i, j-1) 的值抛弃,也就是说以前是一边更新 dp[i] 的值,一边把 dp[i] 的旧值抛弃的,不过在这道题中则不能够,由于咱们还须要用到它。

哎呀,直接举例子看图吧,文字绕来绕去估计会绕晕大家。当咱们要计算图中 (i,j) 的值的时候,在案例1 中,咱们值须要用到 (i-1, j) 和 (i, j-1)。(看图中方格的颜色)
在这里插入图片描述
不过这道题中,咱们还须要用到 (i-1, j-1) 这个值(可是这个值在以往的案例1 中,它会被抛弃掉)
在这里插入图片描述
因此呢,对于这道题,咱们还须要一个额外的变量 pre 来时刻保存 (i-1,j-1) 的值。推导公式就能够从二维的

dp[i][j] = min(dp[i-1][j] , dp[i-1][j-1] , dp[i][j-1]) + 1

转化为一维的

dp[i] = min(dp[i-1], pre, dp[i]) + 1。

因此呢,案例2 其实和案例1 差异不大,就是多了个变量来临时保存。最终代码以下(可是初学者话,代码也没那么好写)

代码以下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[] dp = new int[n2 + 1];
    // dp[0...n2]的初始值
    for (int j = 0; j <= n2; j++) 
        dp[j] = j;
    // dp[j] = min(dp[j-1], pre, dp[j]) + 1
    for (int i = 1; i <= n1; i++) {
        int temp = dp[0];
        // 至关于初始化
        dp[0] = i;
        for (int j = 1; j <= n2; j++) {
            // pre 至关于以前的 dp[i-1][j-1]
            int pre = temp;
            temp = dp[j];
            // 若是 word1[i] 与 word2[j] 相等。第 i 个字符对应下标是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                dp[j] = pre;
            }else {
               dp[j] = Math.min(Math.min(dp[j - 1], pre), dp[j]) + 1;
            } 
            // 保存要被抛弃的值       
        }
    }
    return dp[n2]; 
}

总结

上面的这些题,基本都是不怎么难的入门题,除了最后一道相对难一点。而且基本 80% 的二维矩阵 dp 均可以像上面的方法同样优化成 一维矩阵的 dp,核心就是要画图,看他们的值依赖,固然,还有不少其余比较难的优化,可是,我遇到的题中,大部分都是我上面这种类型的优化。后面如何遇到其余的,我会做为案例来说,今天就先讲最广泛最通用的优化方案。记住,画二维 dp 的矩阵图,而后看元素之间的值依赖,而后就能够很清晰着知道该如何优化了。

在以后的文章中,我也会按照这个步骤,在给你们讲四五道动态规划 hard 级别的题,会放在天天推文的第二条给你们学习。若是以为有收获,不放三连走起来(点赞、感谢、分享),嘻嘻。

有收获?但愿老铁们来个三连击,给更多的人看到这篇文章

一、点赞,可让更多的人看到这篇文章
二、关注个人原创微信公众号『苦逼的码农』,第一时间阅读个人文章,已写了 150+ 的原创文章。

数据结构与算法文章截图

公众号后台回复『电子书』,还送你一份电子书大礼包哦。

做者简洁

做者:帅地,一位热爱、认真写做的小伙,目前维护原创公众号:『苦逼的码农』,已写了150多篇文章,专一于写 算法、计算机基础知识等提高你内功的文章,期待你的关注。
转载说明:务必注明来源(注明:来源于公众号:苦逼的码农, 做者:帅地)

相关文章
相关标签/搜索