目录

1. 引例

例1. 数字三角形最小路径和

代码1_ traverse法

代码2_ 分治法DC

代码3_动态规划DP_记忆化搜索(分治法DC + 记忆化)

代码4_动态规划DP_多重循环(本文重点,重点记忆)

动态规划方法对比:记忆化搜索 对比 多重循环

2. 动态规划的套路

2.1 动态规划的四点要素

2.2 什么情况下使用动态规划?

2.3 什么情况下不使用动态规划?

2.4 面试中常见的动态规划类型

3. 坐标型动态规划

例2. 二维矩形左上到右下最小路径和

例3. 二维矩阵从左上到右下统计共有多少路径

例4. 爬楼梯问题(青蛙跳台阶)

例5. 跳跃游戏 (Jump Game) (重点理解)

例6. 跳跃游戏II (Jump Game II)

例7. 最长上升子序列(重点例题)

4. 单序列动态规划

例8. 分割回文串(二)_将字符串全部分割成回文子串,最少分割几次

例9. 单词拆分(一)_字符串是否可以全部拆分成字典中的单词

5. 双序列动态规划

例10. 找两个字符串的最长公共子序列的长度(LCS)

例11. 编辑距离_将word1变成word2的最少操作(增删改)次数

例12. 不同的子序列_字符串S中找子序列T出现的方案个数

例13. 交叉字符串_判断字符串s3是否由字符串s1和s2交叉构成

6. 其他例题


参考:九章算法 lintocde leetcode 代码随想录carl

还有 算法笔记_面试题_18.动态规划_模板及示例十几道(下)

1. 引例

例1. 数字三角形最小路径和

描述:给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。如果只用额外空间复杂度O(n)O(n)完成,可以获得加分,其中n是数字三角形的总行数。

(来源:109 · 数字三角形leetcode 120. 三角形最小路径和 = lintcode 110 · 最小路径和)

输入:

triangle = [[2],[3,4],[6,5,7],[4,1,8,3]
]

输出:11

解释:从顶到底部的最小路径和为11 ( 2 + 3 + 5 + 1 = 11)。

代码1_ traverse法

(DFS深度优先遍历)在递归中,每个节点(x,y)都有2种情况,向“左子节点”走(x+1,y), 向“右子节点”(x+1, y+1)走,在走的时候,当前的和 加上 当前的节点的值求路径和sum, 在最后到达最后一层时,取所有到达最后一层路径和中的最小值。

当然,该方法,由于重复遍历了很多节点,所以可能超时。

class Solution {
public:/*** @param triangle: a list of lists of integers* @return: An integer, minimum path sum*/int result;int minimumTotal(vector<vector<int>> &triangle) {// write your code hereresult = INT_MAX;int sum = 0;traverse(0, 0, sum, triangle);return result;}void traverse(int x, int y, int sum, vector<vector<int>> & A) {if (x == A.size()){ //注意:退出条件,到达最后一层if (sum < result) {result = sum;}return;}traverse(x + 1, y    , sum + A[x][y], A); // 向下走(类似左子节点)traverse(x + 1, y + 1, sum + A[x][y], A); // 向下向右走(类似右子节点)}
};

traverse方法的时间复杂度:O(2^n),因为每次进入循环都有两条路径,又因为总共有n层,所以是2^n次。(n代表的是数字三角形的层数)

代码2_ 分治法DC

(DFS深度优先遍历)分治法有返回值。

返回值:在最下面的一层的下一层(类似于空节点)返回0。

单层的处理方法:在当前层 直接返回 “左右子树”中最小的值 与 当前节点的值 的和。即假设下面的每一层都计算好了(子问题的解有了,直接 加上 当前层的值即可。

class Solution {
public:int minimumTotal(vector<vector<int>> &triangle) {// write your code hereint result = divideConquer(0, 0, triangle);return result;}int divideConquer(int x, int y, vector<vector<int>> & A) {if (x == A.size()){ //注意:最后一层的下一层时(相当于空节点)的返回值return 0;}return A[x][y] + min(divideConquer(x + 1, y,     A), divideConquer(x + 1, y + 1, A));}
};

分治法的时间复杂度是O(n^2) , 因为每层n个节点,总共n层,每个点访问一次,访问了所有的节点,所以是 = n*( n-1)/2 = n^2 。(n代表的是数字三角形的层数)

代码3_动态规划DP_记忆化搜索(分治法DC + 记忆化

考虑到在下面每层计算时有重复计算的路径,所以,这种方法作了优化,定义一个dp数组dp[x][y], 表示从当前层节点(x,y)到最下面一层的某一节点的最短路径值,若该值已经计算过了(留意下面代码中的判断方式),则直接返回即可,不必重复计算动态规划和分治法的重要区别)。比如下面的e点对应路径e-h子路径,就被蓝色路径和青色路径计算多次。(递归:所以是自下而上)

class Solution {
public:int minimumTotal(vector<vector<int>> &triangle) {// write your code hereint N = triangle.size();vector<vector<int>> dp(N, vector<int>(N, INT_MAX)); //特别注意: 该dp[x][y]代表的含义:从当前节点(x,y)到最下面一层某节点的最小路径和,即子问题的解。int result = divideConquer(0, 0, triangle, dp);return result;}int divideConquer(int x, int y, vector<vector<int>> & A, vector<vector<int>>& dp) {if (x == A.size()){ //注意:最后一层的下一层时(相当于空节点)的返回值return 0;}if (dp[x][y] != INT_MAX) { //如果当前节点的最路径和计算过了,直接返回即可,避免重复计算,这里体现记忆化搜索。return dp[x][y];}dp[x][y] = A[x][y] + min(divideConquer(x + 1, y,     A, dp), //相当于“左子节点”divideConquer(x + 1, y + 1, A, dp));//相当于“右子节点”return dp[x][y];}
};

代码4_动态规划DP_多重循环(本文重点,重点记忆

那典型的动态规划是怎么做的呢?有2种方式,自上而下或者自下而上(两者差不多)。我们统一使用自上而下的方式。

动态规划解题思路:

递推公式:定义dp数组 f[x][y], 表示从最顶层到当前节点时的所有路径和中的最小者。则当前节点的取值 = 当前节点的值 + 上面两个节点中的较小者。即  f[x][y] = A[x][y] + min( f[x-1][y-1], f[x-1][y]

(比如h节点的dp值= h节点的值 + min(d节点的dp值,e节点的dp值))

初始化: 因为是自上而下计算dp数组,所有数字三角形的边缘节点没有2个节点中去选择,就是上一个节点的dp值 + 当前节点的值。所以要对边缘的节点进行初始化(图中红色节点)。

返回值: 自上而下,到达最底一层,则最底一层的dp就是所有的候选最小值集合,最小值即为最低一层中dp值中的最小者。

class Solution {
public:int minimumTotal(vector<vector<int>>& triangle) {int N = triangle.size();vector<vector<int>> dp(N, vector<int>(N,INT_MAX)); //定义动态规划数组:dp[x][y], 表示从最顶层到当前节点时的所有路径和中的最小者//初始化dp[0][0] = triangle[0][0];for(int i = 1; i < N; ++i) {dp[i][0] = triangle[i][0] + dp[i-1][0];dp[i][i] = triangle[i][i] + dp[i-1][i-1];}// 动态规划自上而下for(int i = 2; i < N; ++i) { //从 1开始也可以(dp[0][1]无对应的意义),不过从2开始更合适。因为初始化之后第一个待求dp是dp[2][1]for(int j = 1; j < i; ++j) {dp[i][j] = triangle[i][j] + min(dp[i-1][j-1], dp[i-1][j]);}}int result = INT_MAX;for (int i = 0; i < N; ++i) {result = min(result, dp[N-1][i]);}return result;}
};

动态规划方法对比:记忆化搜索 对比 多重循环

2. 动态规划的套路

2.1 动态规划的四点要素

  • 状态 State

    • 灵感,创造力,存储小规模问题的结果
  • 方程 Function
    • 状态之间的联系,怎么通过小的(或前一个)状态来算大的(后一个)状态
  • 初始化 Initialization
    • 最极限的小状态是什么, 起点
  • 答案 Answer
    • 最大的那个状态是什么,终点

动规要使用到 “状态”,状态必须要定义的清清楚楚明明白白;对于递归,函数的签名就是“状态”,对于动规,比如数字三角形,f[i][j]就是状态(接受什么参数,自身代表(返回)什么值)。

有了状态之后,再去研究“状态”和“状态”之间的关系,比如递推关系。。。(比如 访问根节点时的状态,跟访问左右子树时的状态之间时什么关系)

2.2 什么情况下使用动态规划?

满足下面三个条件之一:
  • 求最大值最小值
  • 判断是否可行
  • 统计方案个数
极有可能 是使用动态规划求解

2.3 什么情况下不使用动态规划?

  • 求出所有 具体 的方案而非方案 个数 http://www.lintcode.com/problem/palindrome-partitioning/
  • 输入数据是一个 集合 而不是 序列http://www.lintcode.com/problem/longest-consecutive-sequence/
极不可能 使用动态规划求解

2.4 面试中常见的动态规划类型

  • 坐标型动态规划 15%
  • 序列型动态规划 30%
  • 双序列动态规划 30%
  • 划分型动态规划 10%
  • 背包型动态规划 10%
  • 区间型动态规划 5%

3. 坐标型动态规划

思路模板:(记住,若能表示成有一个小人在坐标上跳时。)

f[x]  表示起点走到坐标x时的***(索引为x, 索引一般都是从0开始的)。

九章算法:初始化一个二维的动态规划时,就去初始化第0行和第0列。

例2. 二维矩形左上到右下最小路径和

描述:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:一个机器人每次只能向下或者向右移动一步。(来源:leetcode 剑指 Offer II 099. 最小路径之和 = lintcode 110 · 最小路径和)

示例:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

代码

同数字三角形一样。类似的,当前位置(x,y)的状态,是由正上方(x-1, y)的状态,和左侧位置(x, y-1)的状态决定的。只不过返回值只有一个候选值(右下角的dp值)。

状态的定义:运行到当前位置(x,y)时的最小路径和。

class Solution {
public:int minPathSum(vector<vector<int>>& grid) {if (grid.size() == 0) {return -1;}int m = grid.size();int n = grid[0].size();vector<vector<int>> dp(m, vector<int>(n));// initializationdp[0][0] = grid[0][0];for(int i = 1; i < m; ++i) {dp[i][0] = dp[i-1][0] + grid[i][0]; // 第一列的状态,只由上方状态决定}for(int i = 1; i < n; ++i) {dp[0][i] = dp[0][i-1] + grid[0][i]; // 第一行的状态,只由左侧状态决定}// dynamic programmingfor (int i = 1; i < m; ++i) {           // 其他位置的状态,由上方+左侧的状态共同决定for (int j = 1; j < n; ++j) {dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]);}}// get resultreturn dp[m-1][n-1];}
};

例3. 二维矩阵从左上到右下统计共有多少路径

描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?(来源: leetcode 62. 不同路径 = lintcode 114 · 不同的路径)

示例 :

输入:m = 3, n = 7
输出:28

代码

类似例2,dp数组(状态数组)含义一致。不同在于,当前状态(当前路径总数)等于 上侧与左侧状态之和。

class Solution {
public:int uniquePaths(int m, int n) {vector<vector<int>> dp(m, vector<int>(n)); //dp数组含义: 到达当前位置时,总共的路径条数for (int i = 0; i < m; ++i) {   // 初始化第一行第一列dp[i][0] = 1;}for (int i = 0; i < n; ++i) {dp[0][i] = 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];}
};

例4. 爬楼梯问题(青蛙跳台阶)

描述:假设你正在爬楼梯,需要n步(台阶总数)你才能到达顶部。但每次你只能爬一步或者两步,爬到顶部的方法有多少种?(来源:lintcode 111 · 爬楼梯 = leetcode剑指 Offer 10- II. 青蛙跳台阶问题)

样例1:

输入:3   输出:3

解释:1, 1, 1;   1, 2;   2, 1 共3种

样例2:

输入:1   输出:1

解释:只有一种方案

代码
该题满足用动规的两个条件:1.求最大值, 有坐标的概念 2.有序数组,元素不能调整位置。 (有个小人在跳桩子)所以采用动态规划解决。
很常见的经典的一道动态规划题(= 斐波那契数列)。定义dp数组,dp[n]表示跳到第n阶的含有的总共的路径条数。则当前阶(n阶)有两种情况可以到达,即第n-1阶跳1阶到达,以及 第n-2阶跳2阶到达。所以 状态转移方程是:dp[n] = dp[n-1] + dp[n-2]
class Solution {
public:int climbStairs(int n) {if (n <= 1) {return n; //有的地方要求n=0时返回1}vector<int> dp(n+1); //注意: n+1 //状态dp[n]表示跳到第n阶台阶含有的跳法总数。dp[1] = 1;dp[2] = 2;for (int i = 3; i < n + 1; ++i) { //注意: n+1dp[i] = dp[i-1] + dp[i-2];}return dp[n]; // 注意:这里返回的n}
};

例5. 跳跃游戏 (Jump Game) (重点理解)

描述: 给出一个非负整数数组,你最初定位在数组的第一个位置。数组中的每个元素代表你在那个位置可以跳跃的最大长度。判断你是否能到达数组的最后一个位置。(来源: lintcode 116 · 跳跃游戏 ~= leetcode 55. 跳跃游戏 (下面代码在lintcode通过,在leetcode超时))

样例:

输入:A = [2,3,1,1,4]         输出: true

解释:0 -> 1 -> 4(这里的数字为下标)是一种合理的方案。

代码

当前的位置是否能够达到,取决于前面的位置是否能够达到。若前面的位置能够达到,且 能够达到的位置的索引 + 所在位置的值 >= 当前位置的索引,则当前位置也能达到(即 dp[i] =  true, 当 dp[j] +j >= i时, j属于[0, i)之间)。为什么是大于等于?因为每个元素代表在那个位置可以跳跃的最大长度。(注意:找到一次true之后就可以且应该返回)。

注意:这里定义的dp数组是一个bool数组,dp[i]表示能否到达索引为i的位置。另外遍历的循环是2层,所以时间复杂度是O(n^2)。

class Solution {
public:bool canJump(vector<int> &A) {int N = A.size();if (N <= 1) {return true;}vector<bool> dp(N); //状态dp[i]表示是否能跳到索引为i的位置dp[0] = true;for (int i = 1; i < N; ++i) {for (int j = 0; j < i; ++j) {if (dp[j] && j + A[j] >= i) { //每个元素代表你在那个位置可以跳跃的最大长度,所以>=即可,不需要严格等于dp[i] = true;break;}}}return dp[N-1];}
};

例6. 跳跃游戏II (Jump Game II)

描述:给出一个非负整数数组,你最初定位在数组的第一个位置。数组中的每个元素代表你在那个位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。(来源: lintcode 117 · 跳跃游戏 II = leetcode 45. 跳跃游戏 II)

数组A的长度不超过20000,每个元素的大小不超过20000

样例

输入:A = [2,3,1,1,4]

输出:2

解释:到达最后位置的最小跳跃次数是2(从下标0到1跳跃1个距离长度,然后跳跃3个距离长度到最后位置)

代码

类似例5跳跃游戏。只不过,这里状态dp[i]表示的含义是,到达索引为i的位置所需要的最小跳跃次数。递推公式:当前位置的状态(最小跳跃次数)等于前面位置上的状态(前面的可以1步跳到当前状态的候选状态集合) + 1 中 的最小值。不要忘了前提:上一个状态可以到达

class Solution {
public:int jump(vector<int> &A) {int N = A.size();vector<int> dp(N, INT_MAX); //dp[i]表示跳到索引为i的位置需要的最小跳跃次数 //特别注意:取最小值,所以这里采用INT_MAX初始化dp[0] = 0;for (int i = 1; i < N; ++i) {for (int j = 0; j < i; ++j){if (dp[j] != INT_MAX && A[j] + j >= i){ //条件:索引为j的节点可以到达,且从j节点可以到达i时,dp[i] = min(dp[i], dp[j] + 1);  //取所有可达到方案中跳跃次数最小的}}}return dp[N-1];}
};

例7. 最长上升子序列(重点例题)

描述:给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。

最长上升子序列的定义:最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。

(来源:lintcode 76 · 最长上升子序列 =leecode 300. 最长递增子序列)

样例

输入:nums = [5,4,1,2,3]   输出:3     解释:LIS 是 [1,2,3]

代码

定义状态: dp[i] 表示索引为i的位置时(从起点走到索引为i的位置,以 i 结尾时的)的最长子序列的值

状态转移: dp[i] = max(dp[i], dp[j] + 1) 当 j < i, 且 nums[j] < nums[i] 时。

当前状态 等于 前面状态中可以连接到(上一个状态的nums小于当前状态的nums)当前状态的那些状态 + 1(连接到当前位置长度,所以加1)中取值最小的。

class Solution {
public:int longestIncreasingSubsequence(vector<int> &nums) {int N = nums.size();if (N == 0) {return 0;}vector<int> dp(N, 1);for(int i = 1; i < N; ++i) {for (int j = 0; j < i; ++j) {if (nums[j] < nums[i]) { //注意:对比对象dp[i] = max(dp[i], dp[j] + 1);}}}int result = INT_MIN;for(int i = 0; i < N; ++i) {result = max(result, dp[i]); //注意:这里 max中对比的是谁!!!}return result;}
};

时间复杂度: 因为两层循环,所以是O(n^2)。

其他题目:

lintcode :

4. 单序列动态规划

思路模板:

例8. 分割回文串(二)_将字符串全部分割成回文子串,最少分割几次

描述:给定字符串 s, 需要将它分割成一些子串, 使得每个子串都是回文串. 最少需要分割几次?(来源: leetcode 132. 分割回文串 II (级别:困难) = lintcode 108 · 分割回文串(二))

样例 1:

  • 输入:s = "a"
  • 输出:0
  • 解释:"a" 本身就是回文串, 无需分割

样例 2:

  • 输入:s = "aab"
  • 输出:1
  • 解释:将 "aab" 分割一次, 得到 "aa" 和 "b", 它们都是回文串.

代码

状态定义: dp[i]表示前i个字符串分割成回文串,最少需要分割的次数。

状态转移:对于前i个子串, 若之前的状态比如dp[j]的值, 以及 子串 [ j , i-1 ] 是否是回文串,决定了前i个子串的状态的值,即若子串[j, i-1]是回文,则dp[j] 再切一刀就保证了每个子串都是回文串,则dp[i] = dp[j]+1。 在这些状态中选最小的即可(单序列动态规划)。总而言之,

  • dp[i] = min(dp[i], dp[j] + 1), 在条件子串[j, i-1]是回文时。

判断一个string中的所有子串是否是回文串,也是动态规划(区间型动态规划)。使用一个二维bool数组记录所有子串是否是回文,状态dp[i][j]表示s的子串[i,j]是否是回文。状态转移方程:

  • dp[i][j] = dp[i+1][j-1] && (s[i] == s[j])

    • 即对于当前子串[i,j], 只有当两头相等,中间是true时,当前为true.

class Solution {
public:int minCut(string &s) {// write your code hereint N = s.size();if (N <= 1) {return 0;}vector<int> dp(N+1);// dp[i]表示 前i个字符串分割成回文串最少需要分割的次数vector<vector<bool>> isPalindrome = getIsPalindrome(s); //先判断所有的子串是否是回文,并以二维数组形式返回,第一维表示起始位置,第二维表示终止位置,闭区间[i,j]for (int i = 0; i < N + 1; ++i) {dp[i] = i - 1; //初始化,每个子串的最多分割次数。注意前0个子串,分割-1次(为了后面dp[j]+1方便:dp[1] = min(dp[1],dp[0]+1)=0表示前1个字符至少分割0次)}for (int i = 1; i < N + 1; ++i){for (int j = 0; j < i; ++j) {if (isPalindrome[j][i-1]) { //注意:这里是i-1,因为当前要计算的前i个字符的分割情况dp[i] = min(dp[i], dp[j] + 1); //状态转移:当substr[j,i-1]是回文,则前i个子串至少分割次数dp[i]等于所有(dp[j]+1)中取最小}                                   // +1的理解:当substr[j,i-1]是回文,说明dp[j]再切一次就达到了dp[i]的状态}}return dp[N];}//判断s的所有子串是否是回文,存入并返回一个二维bool数组。vector<vector<bool>> getIsPalindrome(const string &s) {int N = s.length();vector<vector<bool>> dp(N, vector<bool>(N)); //dp[i][j]表示s的子串[i,j]是否是回文// 初始化for(int i = 0; i <= N - 1; i++) {    // 子串只有1个字符时dp[i][i] = true;}for(int i = 0; i <= N - 2; i++) {    // 子串只有2个字符时dp[i][i+1] = (s[i] == s[i+1]);}// 状态转移for(int length = 3; length <= N; length++) {for(int i = 0; i <= N - length; i++) { //判断字符范围闭区间[i, i+length-1]dp[i][i+length-1] = dp[i+1][i+length-2] && (s[i] == s[i+length-1]); //只有当两头相等,中间是true时,当前为true}}return dp;}
};

独孤九剑:

例9. 单词拆分(一)_字符串是否可以全部拆分成字典中的单词

描述:给定字符串 s 和单词字典 dict,确定 s 是否可以分成一个或多个以空格分隔的子串,并且这些子串都在字典中存在。因为我们已经使用了更强大的数据,所以普通的DFS方法无法解决此题。(来源: lintcode 107 · 单词拆分(一) ≈ leetcode 139. 单词拆分)

样例 1:
输入:s = "lintcode"   dict = ["lint", "code"]
输出:true
解释:lintcode可以分成lint和code。

样例 2:
输入:s = "a"   dict = ["a"]
输出:true
解释:a在dict中。

代码:

状态定义:状态dp[i]表示字符串s前i个字符组成的字符串是否全部拆分成字典中的单词。

状态转移方程:若子串[j,i-1]在字典中,则当前状态dp[i] 取决于状态dp[j],即

  • dp[i] = dp[j] && (子串 [j,i-1] 是否在字典中);
class Solution {
public:/*** @param s: A string* @param wordSet: A dictionary of words dict* @return: A boolean*/bool wordBreak(string &s, unordered_set<string> &wordSet) {// write your code hereint N = s.size();vector<bool> dp(N + 1);//状态dp[i]表示字符串s前i个字符组成的字符串是否全部拆分成字典中的单词dp[0] = true;for (int i = 1; i <= N; ++i) {for (int j = 0; j < i; ++j) { //子串区间[j, i-1]dp[i] = dp[j] && (wordSet.find(s.substr(j, i-j)) != wordSet.end()); //j之前的满足条件 且 子串[j,i-1]存在字典中,则当前状态dp[i]为trueif (dp[i] == true) {break;      //省时间,只要有一种分割方法是true, 则当前状态就是true}}}return dp[N];}
};

上述代码会超时(lintcode下超时,leetcode下可以通过, 因为两层for循环,所以时间复杂度是O(N^2), N为字符串的长度),考虑到最后一个子串[j, i-1] 的长度若大于字典中最长单词的长度时,则一定不可将当前状态全部拆分成字典中的单词。所以不必让j从0开始遍历。而是定义最后一个单词的长度lastWordLength, 使其 <= 字典中的单词的最大长度。

(重点理解)

class Solution {
public:bool wordBreak(string &s, unordered_set<string> &wordSet) {// write your code here// write your code hereint N = s.length();vector<bool> dp(N + 1); //状态dp[i]表示前i个字符组成的字符串,是否全部拆分成字典中的单词。dp[0] = true; //理解:空字符串存在字典中// 优化:计算字典中单词的最大长度int maxSubLength = getMaxLengthInSet(wordSet);for (int i = 1; i < N + 1; i++) {for (int lastWordLength = 1; //注意:长度从1开始lastWordLength <= i && lastWordLength <= maxSubLength; lastWordLength++) {     //最后一个单词区间:[i-lastWordLength,i-1]dp[i] = dp[i-lastWordLength] && (wordSet.find(s.substr(i-lastWordLength, lastWordLength)) != wordSet.end());//当j<i中的所有子串[j,i-1]中,有一个j对应的子串在字典中,且dp[j]为true时,则dp[i]为trueif(dp[i] == true){break;}}}return dp[N];}int getMaxLengthInSet(unordered_set<string> &wordSet) {int maxLength = 0;for (auto word: wordSet) {maxLength = max(maxLength, (int)word.length()); //注意:不要少了size_t 转 int}return maxLength;}
};

上述代码在第二次for循环中减少了循环次数,第二个for循环最多只遍历字典中最长单词的长度(表示为L)次,而hash (即数据结构unordered_set)中的find方法的时间复杂度也是L, 所以时间复杂度相比第一种的O(N^2), 减小到O(N*L*L)

5. 双序列动态规划

例10. 找两个字符串的最长公共子序列的长度(LCS)

描述: 给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
最长公共子序列的定义:最长公共子序列问题是在一组序列(通常2个)中找到最长公共子序列(注意:不同于子串,LCS不需要是连续的子串)。该问题是典型的计算机科学问题,是文件差异比较程序的基础,在生物信息学中也有所应用。(来源:lintcode 77 · 最长公共子序列 = leectocde 剑指 Offer II 095. 最长公共子序列 = leetcode 1143. 最长公共子序列)

样例 1:

  • 输入:A = "ABCD"  B = "EDCA"      输出:1
  • 解释:LCS是 'A' 或 'D' 或 'C'

样例 2:

  • 输入:A = "ABCD"   B = "EACB"      输出:2
  • 解释:LCS 是 "AC"

代码

状态定义:二维数组dp[i][j]表示 A中前i个匹配上B中前j个字符的最长子串的长度值。

状态转移方程:当前状态(dp[i][j])可以由上一个状态dp[i-1][j-1]得到;

当上一个状态紧接着下一个状态的那一个字符相等时(A[i-1] == B[j-i])则上一个状态的值+1就是当前状态的一种值。或者A字符串中去掉一个字符A[i-1]的状态dp[i-1][j] 与 当前状态进行匹配,或者 B字符串去掉一个字符B[j-1]的状态dp[i][j-1] 与 当前状态进行匹配,三种“上一状态”中取最大即可 (这两种状态理论上小于dp[i-1][j-1] + 1,所以也可以忽略)。

当上一个状态紧接着下一个状态的那一个字符不相等时(A[i-1] != B[j-i]),当前状态的值等于 A去掉一个字符作为前一个状态,或者B去掉一个字符作为前一个状态中的较大者。

dp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + 1) 当 a[i-1] ==  b[j-1]时
           = max(dp[i-1][j], dp[i][j-1])                         当 a[i-1] !== b[j-1]时

初始化:dp[i][0] = 0 , dp[0][j] = 0 , 表示A(B)中前i个和B(A)中前0个子串的最长字符匹配个数,明显是0。

class Solution {
public:/*** @param A: A string* @param B: A string* @return: The length of longest common subsequence of A and B*/int longestCommonSubsequence(string &A, string &B) {// write your code hereint N1 = A.size();int N2 = B.size();if (N1 == 0 || N2 == 0) {return 0;}vector<vector<int>> dp(N1 + 1, vector<int>(N2 + 1)); //状态dp[i][j]含义:A中前i个匹配上B中前j个字符的最长子串的长度值for (int i = 0; i < N1 + 1; i++) {dp[i][0] = 0;     //表示A中前i个和B中前0个的最长字符匹配个数,明显是0}for (int i = 1; i < N2 + 1; i++) { // //最好从1开始,[0][0]已赋值 //不建议将这两个for循环直接在定义dp数组时初始化为0,那样含义不够直观突出dp[0][i] = 0;}for (int i = 1; i < N1 + 1; i++) {      //这里i j 表示的是前i个或者前j个子串for (int j = 1; j < N2 + 1; j++) {  //注意:这里j从1开始if (A[i-1] == B[j-1]) {  //特别注意:这里判断的是 i-1 和 j-1 对应的字符是否相等!即d[i-1][j-1]对应的下一个字符!dp[i][j] = max(dp[i-1][j-1] + 1, max(dp[i-1][j], dp[i][j-1])); // 或者 dp[i][j] = dp[i-1][j-1] + 1; 也可} else {dp[i][j] = max(dp[i-1][j], dp[i][j-1]);}}}return dp[N1][N2];}
};

例11. 编辑距离_将word1变成word2的最少操作(增删改)次数

描述: 给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你可进行三种操作:插入一个字符; 删除一个字符; 替换一个字符.(来源:lintcode 119 · 编辑距离 = leetcode 72. 编辑距离)

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

样例 2:
输入:word1 = "intention"   word2 = "execution"   输出:5
解释:intention -> inention (删除 't')   inention -> enention (替换 'i' 为 'e')   enention -> exention (替换 'n' 为 'x')   exention -> exection (替换 'n' 为 'c')   exection -> execution (插入 'u')

代码

状态定义:跟上一题 (例10) 基本上一样,换汤不换药。定义二维数组维度[N1+1, N2+1], dp[i][j]表示:word1的前i个字符变换到word2的前j个字符最少操作次数。则状态dp[i-1][j-1]到dp[i][j]中间需要判断的字符对是word1[i-1]word2[j-1] (注意这里是-1, i j在状态中表示前i或前j,当作索引使用时,表示索引位置时要留意下是否-1) 则:

状态转移:这两字符对相等时,三种情况:a.当前状态编辑距离等于上一状态的编辑距离(dp[i][j] = dp[i-1][j-1]),b. 或者当前状态word1中一个字符忽略该字符(word1[i-1])的状态dp[i-1][j]增加一个字符(即dp[i][j] = dp[i-1][j] + 1),c. 或者当前状态忽略word2[j-1] 删除一个字符(即dp[i][j] = dp[i][j-1] + 1),三者取最小。

这两字符对不相等时: a. 变换成相等的,即上一个状态加一次变换操作(dp[i][j] = dp[i-1][j-1] + 1); b. 同相等时的情况b; c.同相等时的情况c。

初始化:dp[i][0] 表示 word1中前i个字符变换到word2中前0个字符的最小变换次数,显然需要i次删除操作,所以是dp[i][0] = i; 同理 dp[0][j] = j。

class Solution {
public:int minDistance(string &word1, string &word2) {int N1 = word1.size();int N2 = word2.size();vector<vector<int>> dp(N1 + 1, vector<int>(N2 + 1));for (int i = 0; i < N1 + 1; i++) {dp[i][0] = i;}for (int i = 1; i < N2 + 1; i++) { //最好从1开始,[0][0]已赋值dp[0][i] = i;}for (int i = 1; i < N1 + 1; i++) {for (int j = 1; j < N2 + 1; j++) {if (word1[i-1] == word2[j-1]) { //特别注意:这里判断的是 i-1 和 j-1 对应的字符是否相等!dp[i][j] = min(dp[i-1][j-1],   min(dp[i-1][j]+1, dp[i][j-1]+1)); //注意:三种情况(不用动,删i-1,加j-1)} else {dp[i][j] = min(dp[i-1][j-1]+1, min(dp[i-1][j]+1, dp[i][j-1]+1)); //注意:三种情况(改,删i-1,加j-1)}}}return dp[N1][N2];}
};

例12. 不同的子序列_字符串S中找子序列T出现的方案个数

描述:给定字符串 S 和 T, 字符串S中有多少个子序列字符串和字符串T相同。 子序列字符串是原始字符串删除一些(或零个)字符之后得到的字符串, 并且不能改变剩下字符的相对位置。(比如 "ACE" 是 ABCDE 的一个子序列, 而 "AEC" 不是) (来源:lintcode 118 · 不同的子序列 (中等)= leetcode 115. 不同的子序列 (困难))
样例 1:

  • 输入:S = "rabbbit"  T = "rabbit"    输出:3
  • 解释:你可以删除 S 中的任意一个 'b', 所以一共有 3 种方式得到 T.

样例 2:

  • 输入:S = "abcd" T = ""   输出:1
  • 解释:只有删除 S 中的所有字符这一种方式得到 T

代码

状态定义:dp[i][j]表示从S中的前i个字符中选择(j个字符)等于T中前j个字符的方案有多少个。

状态转移:当前状态dp[i][j];上一个状态dp[i-1][j-1], 相关状态 dp[i-1][j];对应的要判断的字符对是S[i-1] 和 T[j-1]。

当这两个字符相等时,有两种情况,一种情况是使用这两个字符,且从S中i-1前面选择j-1个字符,匹配T中前j-1个字符,共同组成T中的前j字符串即状态 + dp[i-1][j-1];另一种情况是,虽然相等,但也可能使用S中的前i-1中(放弃选择S[i-1])直接选择j个字符,跟T中的前j个字符进行匹配, 即  + dp[i-1][j]. 所以 dp[i][j] = dp[i-1][j-1] + dp[i-1][j].

当两个字符不相等时,只有一种情况,只能从S中的前i-1中(不能选择S[i-1])直接选择j个字符跟T中的前j个字符进行匹配,即 dp[i][j] = dp[i-1][j].

初始化:初始化的是二维状态数组的第1列 和第1行,一定要明确好状态的定义。dp[i][0]表示S中前i个选择0个子串等于T中前0个子串有几种方案,显然是1。dp[0][j]根据状态定义显然是0。

class Solution {
public:int numDistinct(string &S, string &T) {int N1 = S.size();int N2 = T.size();vector<vector<int>> dp(N1 + 1, vector<int>(N2 + 1));// dp[i][j]表示从S中的前i个字符中选取等于T中前j个字符的方案有多少个for (int i = 0; i < N1 + 1; i++) {dp[i][0] = 1;   //从S前i个选择等于T中前0个字符串(即空串)的方案只有1种,即S前i个中选择0个。}for (int j = 1; j < N2 + 1; j++) {// 特别注意:这里从1开始,因为dp[0][0]=1, dp[0][j]=0dp[0][j] = 0;   //从S前0个选择等于T中前j个字符串的子串 的方案有0种。}for (int i = 1; i < N1 + 1; i++) {for (int j = 1; j < N2 + 1; j++) {if (S[i-1] == T[j-1]) {dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; } else {dp[i][j] = dp[i-1][j];}}}return dp[N1][N2];}
};

例13. 交叉字符串_判断字符串s3是否由字符串s1和s2交叉构成

描述:给出三个字符串s1、s2、s3,判断s3是否由s1和s2交叉构成。(来源:lintcode 29 · 交叉字符串 (困难) = leetcode 剑指 Offer II 096. 字符串交织 (中等))
样例 1:

  • 输入:
  • s1 = "aabcc"
  • s2 = "dbbca"
  • s3 = "aadbbcbcac"
  • 输出:true
  • 解释:s3 是由 s1 与 s2 交叉构成。

样例 2:

  • 输入:s1 = ""  s2 = ""  s3 = "1"
  • 输出:false
  • 解释:s3 不是由 s1 与 s2 交叉构成。

代码

状态定义:dp[i][j]表示从s1中选择前i个和从s2中选择前j个是否能组成s3中前i+j个字符。

状态转移:当前状态由两种情况转移过来,一种是s1中选i-1个,s2中选择j个的状态dp[i-1][j]转移过来的,这时需要判断s3中当前状态的最后一个字符(索引为i+j-1)是否来自于s1中的索引为i-1的字符(转移状态时的字符s1[i-1])。

另一种情况状态dp[i][j-1]同理(转移状态时的字符s2[j-1])。这两种情况一起决定了当前状态。

初始化:这个初始化稍微有点繁琐(请时刻牢记状态的定义)。初始化对象,依然还是二维状态数组的第一列和第一行,如第1列的初始化dp[i][0] 表示从s1中选择前i个,s2中选择前0个,组成s3中前i+0个的状态,所有该状态由s1前i个字符是否全都等于s3前i+0个字符决定。即 dp[i][0] = dp[i-1][0] && (s1[i-1] == s3[i-1]) 。所以这里先初始化了 dp[0][0]=true (0+0=0显然合理)。

class Solution {
public:bool isInterleave(string s1, string s2, string s3) {int N1 = s1.size();int N2 = s2.size();int N3 = s3.size();if (N1 + N2 != N3) {return false;}vector<vector<int>> dp(N1 + 1, vector<int>(N2 + 1)); //dp[i][j]表示从s1中选择前i个和从s2中选择前j个是否能组成s3中前i+j个字符。//初始化dp[0][0] = true;for (int i = 1; i < N1 + 1; i++) { //从s1中选择前i个,s2中选择前0个,组成s3中前i+0个的状态,由s1前i个字符是否全都等于s3前i+0个字符决定。dp[i][0] = dp[i-1][0] && (s1[i-1] == s3[i-1]);}for (int j = 1; j < N2 + 1; j++) {dp[0][j] = dp[0][j-1] && (s2[j-1] == s3[j-1]);}//状态转移for (int i = 1; i < N1 + 1; i++) {for (int j = 1; j < N2 + 1; j++) { //当前状态由两种情况转移过来,一种是s1中选i-1个,s2中选择j个的状态dp[i-1][j]转移过来的,这时需要判断s3中当前状态的最后一个字符(索引为i+j-1)是否来自于s1中的索引为i-1的字符(转移状态时的字符)。另一种情况同理。dp[i][j] = (dp[i-1][j] && (s1[i-1] == s3[i+j-1])) ||(dp[i][j-1] && (s2[j-1] == s3[i+j-1]));}}return dp[N1][N2];}
};

6. 其他例题

背包类:
http://www.lintcode.com/problem/backpack/
http://www.lintcode.com/problem/backpack-ii/
http://www.lintcode.com/problem/minimum-adjustment-cost/
http://www.lintcode.com/problem/k-sum/
区间类:
http://www.lintcode.com/problem/coins-in-a-line-iii/
http://www.lintcode.com/problem/scramble-string/ (hard)
划分类:
http://www.lintcode.com/problem/best-time-to-buy-and-sell-stock-iv/ (hard)
http://www.lintcode.com/problem/maximum-subarray-iii/

算法笔记_面试题_18.动态规划_模板及示例十几道(上)相关推荐

  1. 算法笔记_183:历届试题 九宫重排(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 如下面第一个图的九宫格中,放着 1~8 的数字卡片,还有一个格子空着.与空格子相邻的格子中的卡片可以移动到空格中.经过若干次移动,可以形成 ...

  2. 算法笔记_172:历届试题 波动数列(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 观察这个数列: 1 3 0 2 -1 1 -2 ... 这个数列中后一项总是比前一项增加2或者减少3. 栋栋对这种数列很好奇,他想知道长度 ...

  3. 算法笔记_188:历届试题 危险系数(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 抗日战争时期,冀中平原的地道战曾发挥重要作用. 地道的多个站点间有通道连接,形成了庞大的网络.但也有隐患,当敌人发现了某个站点后,其它站点 ...

  4. 【我的算法笔记】后序非递归遍历模板及其应用

    目录 前言 非递归后序遍历算法模板 求树的深度 打印值为x的节点的所有祖先 求节点t,s最近公共祖先节点 输出从每个叶子节点到根节点的逆路径 前言 本篇文章主要介绍非递归后序遍历的算法,并且在这个算法 ...

  5. paddle 图标注_标注外包业务预算评估模板(示例)

    RT, 此处采用纯文本描述,PPT/表格 更佳 评估基准: 以10月项目供应商报价作为基准 标注目标(周边车场采集的): 伪3D(1框+3点) 行人(1框) freespace(1多边) 轮档(1多边 ...

  6. codeup墓地目录(算法笔记习题刷题笔记)

    在线codeup contest 地址:http://codeup.cn/contest.php Contest100000575 - <算法笔记>3.1小节--入门模拟->简单模拟 ...

  7. 算法笔记CodeUp第一至第六章刷题记录

    文章目录 <算法笔记>2.2小节--C/C++快速入门->顺序结构 1.例题1-1-1 按要求输出信息(1) 2.例题1-1-2 按要求输出信息(2) 3.例题1-2-1 求两个整数 ...

  8. javascript进制转换_《算法笔记》3.5小节——入门模拟-gt;进制转换

    @[TOC] # Contest100000579 - <算法笔记>3.5小节--入门模拟->进制转换 ## 例题 ### PATB1022 PTA | 程序设计类实验辅助教学平台 ...

  9. 编写算法判别给定二叉树是否为完全二叉树_别找了,链表和二叉树相关面试题以及解答,我都给你们找好了...

    来源公众号:苦逼的码农 作者:帅地 无论是在面试还是在平时的做题中,链表相关算法题以及二叉树相关算法题,可以说是考的非常非常多的.我在 2019 的秋招面试中,也是遇到了好几次.为此,我总结了 9 道 ...

  10. c++ string 删除字符_算法笔记|(5)第二章C、C++的快速入门字符数组的存放方式string.h文件...

    字符数组的存放方式 由于字符数组是由若干个char类型的元素组成的,因此字符数组的每一位都是一个char字符,除此之外,在一维字符数组或者二维字符数组的第二维的末尾都有一个空字符\0表示存放的字符串的 ...

最新文章

  1. WinAPI: CopyFileEx
  2. Airbnb上市,又一批工程师财富自由:入职一年就能北京买房
  3. 用python画花瓣-Python:绘制樱花树
  4. Transformer开始往动态路由的方向前进了!厦大华为提出TRAR,在VQA、REC上性能SOTA!(ICCV 2021)...
  5. 运筹作业题:一个正三角形平面,在三个角的部分减去一部分,然后沿着剪开部分折叠起来,使折叠后的三棱台体积最大
  6. 期中HTML代码及技术博客
  7. matlab程序及C语言实现一维多元离散信源熵
  8. BigGAN论文解读
  9. 高速PCB设计的一些注意事项
  10. 【每日函数】每日函数(2021.08.22)
  11. MAC小技巧|Mac屏幕截图去除窗口阴影
  12. ffdshow 源代码分析 8: 视频解码器类(TvideoCodecDec)
  13. 蓝桥杯java打印菱形_Python基础练习实例22(抽签问题)
  14. 这几个画流程图的软件值得你们收藏
  15. 隔离,隔离,再隔离!
  16. 公司注册不满一年如何名称变更
  17. Jm 18.4 MVC 报告一
  18. java中fractions,[CF743C]Vladik and fractions
  19. 入门金融风控【贷款违约预测】
  20. 一个C语言的基本教程—IO篇

热门文章

  1. 关于BDC、SSP搜索相关解决方案 的数据结构
  2. .net core2 发送电子邮件封装
  3. webpack 使用教程
  4. 20170813xlVBA跨表筛选数据
  5. 洛谷P2347 砝码称重 [2017年4月计划 动态规划01]
  6. eclipse设置Tomcat超级详细
  7. 第一个scrum会议
  8. C#WinForm的TextBox 按TAB键让光标按照指定顺序走
  9. 你必须知道:localStorage、sessionStorage 和 Cookie 区别在什么地方
  10. JavaScript实现监听移动端上下左右滑动事件