推荐学习labuladong大佬的动态规划系列文章:先弄明白什么是动态规划即可,不必一次看完。接着尝试自己做,没有思路了再回过头看相应的文章。

动态规划一般可以由 递归 + 备忘录 一步步转换而来,不必被名字唬住。通常只要找到状态转移方程问题就解决了一大半,至于状态的选择只要题目做多了自然就会形成经验,通常是问什么就设什么为状态。

常见四种类型

  1. Matrix DP (10%)
  2. Sequence (40%)
  3. Two Sequences DP (40%)
  4. Backpack (10%)

注意:

  • 贪心算法大多题目靠背答案,所以如果能用动态规划就尽量用动规,不用贪心算法。一般可以先尝试用动态规划,如果超时再用贪心。

1、矩阵类型(10%)

120. 三角形最小路径和

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 ii + 1

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
class Solution {
public int minimumTotal(List<list<integer>> triangle) {
/**
* 状态:当前位置到底部最小路径和
* 状态转移方程:dp[i][j] = triangle[i][j] + min(dp[i + 1][j], dp[i + 1]j[j + 1]);i为行,j为列
* base case:最后一行dp[i][j] = triangle[i][j];
*/ int m = triangle.size();
int[][] dp = new int[m][m];
//base case
for (int i = 0; i < m; i++) {
dp[m - 1][i] = triangle.get(m - 1).get(i);
} //倒数第二行开始转移(递推)
for (int i = m - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[i][j] = triangle.get(i).get(j) + Math.min(dp[i + 1][j], dp[i + 1][j + 1]);
}
} return dp[0][0];
}
}

该解法空间复杂度为dp表的大小,为 O(N2) 。容易发现当前行dp的值只与下一行的相关,我们不必将所有dp值通过二维数组存下来,可以通过复用一个一维数组来实现。

class Solution {
public int minimumTotal(List<list<integer>> triangle) { int m = triangle.size();
int[] dp = new int[m];
//base case,先只存最后一行的dp值
for (int i = 0; i < m; i++) {
dp[i] = triangle.get(m - 1).get(i);
} //倒数第二行开始转移(递推)
for (int i = m - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[j] = triangle.get(i).get(j) + Math.min(dp[j], dp[j + 1]);
}
} return dp[0];
}
}

64. 最小路径和

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

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

示例 1:

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

思路:动态规划,和上一题相似。

  • 状态:起点到当前结点的最小路径和

  • 转移方程:起点到当前结点最小路径和dp[i][j]等于:min(起点到其相邻左结点最小路径和dp[i][j - 1],起点到其相邻上结点最小路径和dp[i - 1][j]) + 当前结点值grid[i][j]

  • base case: dp[0][0] = grid[0][0]; 第一行dp[0][x]都为其相邻左结点dp[0][x -1] + 自身结点值grid[0][x], x >= 1;第一列dp[x][0]都为其相邻上结点dp[x - 1][0] + 自身结点值grid[x][0], x >= 1

class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length; //从左上角到i, j的最短路径和
int[][] dp = new int[m][n]; //base case
dp[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];
} //转移方程
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]) + grid[i][j];
}
} return dp[m - 1][n - 1];
}
}

62. 不同路径

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

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

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

示例 1:

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

思路:动态规划

  • 状态:从起点到当前结点的不同路径数。

  • 转移方程:起点到当前点的不同路径数dp[i][j]等于:起点到当前结点相邻左结点dp[i][j - 1]和相邻上结点dp[i - 1][j]不同路径数之和。

  • base case:第0行dp[0][x]和0列dp[x][0]都为1,前者只能通过其相邻左节点到达,后者只能通过相邻上结点到达。

class Solution {
public int uniquePaths(int m, int n) {
//状态
int dp[][] = new int[m][n]; //base case
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
dp[i][0] = 1;
} //转移方程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
} return dp[m - 1][n - 1];
}
}

63. 不同路径 II

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

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

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[][] = new int[m][n]; // base case
if (obstacleGrid[0][0] != 1) { // 起点不是障碍
dp[0][0] = 1;
}
for (int i = 1; i < n; i++) {
if (obstacleGrid[0][i] != 1) {
dp[0][i] = dp[0][i - 1];
}
}
for (int i = 1; i < m; i++) {
if (obstacleGrid[i][0] != 1) {
dp[i][0] = dp[i - 1][0];
}
} for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] != 1) {
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
} return dp[m - 1][n - 1];
}
}

2、序列类型(40%)

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

思路:动态规划

  • 状态:从第0个台阶跳到当前台阶的不同方式

  • 转移方程:第0个台阶到当前台阶的不同方式dp[i]等于:第0个台阶到当前台阶下面两个台阶的不同方式之和(dp[i - 1] + dp[i - 2])

  • base case: dp[0] = dp[1] = 1

class Solution {
public int climbStairs(int n) {
//状态:从第0个台阶跳到当前台阶的不同方式
int[] dp = new int[n + 1]; //base case
dp[0] = 1;
dp[1] = 1; //转移方程
for (int i = 2; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
} return dp[n];
}
}

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

class Solution {
public boolean canJump(int[] nums) {
int len = nums.length;
//起始位置能否跳至当前位置
boolean[] dp = new boolean[len]; //base case
dp[0] = true; //转移方程:i前面所有的点只要有一个能跳到当前结点就说明当前结点可达
for (int i = 1; i < len; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && (j + nums[j] >= i)) {
dp[i] = true;
break;
}
}
} return dp[len - 1];
}
}

45. 跳跃游戏 II

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

说明:

假设你总是可以到达数组的最后一个位置。

class Solution {
//状态:从下标为0的位置跳到i所需的最小跳跃次数
//转移方程:从下标为0的位置跳到i所需的最小跳跃次数等于:i前面一次跳跃就能到达i的所有结点中的最小dp值 + 1
//base case:dp[0] = 0
public int jump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, n); //最多跳n - 1次,求最小值,先将其初始化为足够大 //base case
dp[0] = 0; for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] + j >= i) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
} return dp[n - 1];
}
}

132. 分割回文串 II

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。

返回符合要求的 最少分割次数

class Solution {
// 状态:从头字符到以当前字符结尾形成的字符串分割成回文子串需要的最少分割次数
// 转移方程:dp[i] = min(dp[i], dp[j] + 1), j < i 且 [j + 1, i]区间的子串为回文子串
// base case:dp[0] = 0
public int minCut(String s) {
int len = s.length(); // 先使用动态规划获得任意两个区间的字符串是否为回文字符串
boolean[][] isPalindrome = getPalindrome(s); // 求最小值,先初始化足够大(最多s最多分割 len - 1 次)
int[] dp = new int[len];
Arrays.fill(dp, len); for (int j = 0; j < len; j++) {
//无需分割
if (isPalindrome[0][j]) {
dp[j] = 0;
continue;
} for (int i = 1; i <= j; i++) {
if (isPalindrome[i][j]) {
dp[j] = Math.min(dp[j], dp[i - 1] + 1);
}
}
} return dp[len - 1];
} private boolean[][] getPalindrome(String s) {
int len = s.length(); // 区间i,j的字符串是否为回文字符串(左右都为闭区间)
boolean[][] dp = new boolean[len][len]; for (int j = 0; j < len; j++) {
for (int i = 0; i <= j; i++) {
if (s.charAt(i) == s.charAt(j) && (j - i <= 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
}
} return dp;
}
}

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

  • 思路:动态规划

  • 状态:以当前字符结尾的字符串中最长递增子序列的长度

  • 转移方程:dp[i] = max(dp[j] + 1, dp[i]),其中 j < inums[j] < nums[i]

  • base case:dp[i] = 1

class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length; // dp[i] 表示以当前字符结尾的字符串中最长递增子序列的长度
int[] dp = new int[len]; //base case, 最少长度为1
Arrays.fill(dp, 1); int maxLen = 0;
for (int i = 0; i < len; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
} maxLen = Math.max(maxLen, dp[i]);
} return maxLen;
}
}

139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

  • 拆分时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。
class Solution {
public boolean wordBreak(String s, List<string> wordDict) {
Set<string> set = new HashSet<>();
for (String str : wordDict) {
set.add(str);
} int len = s.length(); // 状态: s 中前 i 个字符能否拆分成功
boolean[] dp = new boolean[len + 1]; // base case
dp[0] = true; // 状态转移
// s[0, i]能否被分割取决于:区间[j, i]是否属于set和dp[j]的值(前j个字符 [0, j - 1] 能否被分割),j <= i
for (int i = 1; i < len + 1; i++) {
for (int j = 0; j < i; j++) {
if (set.contains(s.substring(j, i)) && dp[j]) {
dp[i] = true;
break;
}
}
} return dp[len];
}
}

推荐题解:「手画图解」剖析三种解法: DFS, BFS, 动态规划 |139.单词拆分

3、双序列(40%)

1143. 最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length(); //状态:text1前m个和text2前n个字符的最长公共子序列长度
int[][] dp = new int[m + 1][n + 1];
//base case, dp[x][0] = dp[0][x] = 0; 默认值即可 for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//如果当前两个字符相同
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
//不等说明有其中一个字符不在最长公共子序列中
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
} return dp[m][n];
}
}

72. 编辑距离

给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length(); //word1 前m个字符和 word2 前n个字符之间的编辑距离,注意下标对应关系
int[][] dp = new int[m + 1][n + 1]; //base case
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int i = 0; i <= n; i++) {
dp[0][i] = i;
} // 状态转移
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 最后一个字符相等
if (word1.charAt(i - 1) == word2.charAt(j -1)) {
dp[i][j] = dp[i - 1][j - 1];
// 不等则 在word1后增加word2的最后一个字符、删除word1中最后一个字符,或将word1最后一个字符修改成和word2最后一个字符相同;取代价最小的一个
} else {
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
} return dp[m][n];
}
}

4、零钱和背包(10%)

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

class Solution {
public int coinChange(int[] coins, int amount) {
// 状态:dp[i] 表示凑够i需要的最少硬币数
int[] dp = new int[amount + 1];
// 求最小值,先初始为足够大。(若能凑成,最多需要amount枚硬币)
Arrays.fill(dp, amount + 1); // base case
dp[0] = 0; for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
// 当前背包(总金额)若能装下物品(硬币面额)
if (i >= coins[j]) {
dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
}
}
} return dp[amount] >= amount + 1 ? -1 : dp[amount];
}
}

92 · 背包问题

在 n 个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为 m,每个物品的大小为 A[i]

public class Solution {
public int backPack(int m, int[] A) {
int n = A.length;
//背包容量为m,有前n个物品,能否将背包装满
boolean[][] dp = new boolean[m + 1][n + 1]; //base case, 背包容量为0时dp[0][x] = true; 背包容量大于0但没有物品时dp[x][0] = false,x > 0
for (int i = 0; i <= n; i++) {
dp[0][i] = true;
} for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//如果前 j - 1 个就可以装满 i
if (dp[i][j - 1]) {
dp[i][j] = true;
} else if (i >= A[j - 1] && dp[i - A[j - 1]][j - 1]) {
dp[i][j] = true;
}
}
} for (int i = m; i > 0; i--) {
if (dp[i][n]) {
return i;
}
} return 0;
}
}

125 · 背包问题 II

n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值. 问最多能装入背包的总价值是多大?

public class Solution {
public int backPackII(int m, int[] A, int[] V) {
int n = A.length;
//背包容量为m,有前n个物品时能装入的最大价值
int[][] dp = new int[m + 1][n + 1]; //base case, dp[x][0] = dp[0][x] = 0 for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
//当前背包能容纳
if (i >= A[j - 1]) {
dp[i][j] = Math.max(dp[i - A[j - 1]][j - 1] + V[j - 1], dp[i][j - 1]);
} else {
dp[i][j] = dp[i][j - 1];
}
}
} return dp[m][n];
}
}

</list</list

LeetCode入门指南 之 动态规划思想的更多相关文章

  1. LeetCode入门指南 之 回溯思想

    模板 result = {} void backtrack(选择列表, 路径) { if (满足结束条件) { result.add(路径) return } for 选择 in 选择列表 { 做选择 ...

  2. LeetCode入门指南 之 链表

    83. 删除排序链表中的重复元素 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 .返回同样按升序排列的结果链表. class Soluti ...

  3. LeetCode入门指南 之 栈和队列

    栈 155. 最小栈 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈. push(x) -- 将元素 x 推入栈中. pop() -- 删除栈顶的元素. top( ...

  4. LeetCode入门指南 之 排序

    912. 排序数组 给你一个整数数组 nums,请你将该数组升序排列. 归并排序 public class Sort { //归并排序 public static int[] MergeSort(in ...

  5. LeetCode入门指南 之 二叉树

    二叉树的遍历 递归: void traverse (TreeNode root) { if (root == null) { return null; } //前序遍历位置 traverse(root ...

  6. LeetCode入门指南 之 二分搜索

    上图表示常用的二分查找模板: 第一种是最基础的,查找区间左右都为闭区间,比较后若不等,剩余区间都不会再包含mid:一般在不需要确定目标值的边界时,用此法即可. 第二种查找区间为左闭右开,要确定targ ...

  7. Ext JS 6学习文档–第1章–ExtJS入门指南

    Ext JS 入门指南 前言 本来我是打算自己写一个系列的 ExtJS 6 学习笔记的,因为 ExtJS 6 目前的中文学习资料还很少.google 搜索资料时找到了一本国外牛人写的关于 ExtJS ...

  8. 【翻译Autofac的帮助文档】1.入门指南

    [写在前面]尝试做完一件工作之外自我觉得有意义的一件事,那就从翻译Autofac的帮助文档吧. 入门指南 将Autofac集成你的应用程序的步骤通常很简单,一般是: 时刻以IOC(控制反转)的思想来规 ...

  9. OpenCASCADE入门指南

    OpenCASCADE入门指南 eryar@163.com 一.概述 荀子说“君子性非异也,善假于物也”.当你会用英语,就可以与世界各国的人交流:当你会用编程语言,就可以与计算机交流:当你会用数学语言 ...

随机推荐

  1. 原来ReadWriteLock也能开发高性能缓存,看完我也能和面试官好好聊聊了!

    大家好,我是冰河~~ 在实际工作中,有一种非常普遍的并发场景:那就是读多写少的场景.在这种场景下,为了优化程序的性能,我们经常使用缓存来提高应用的访问性能.因为缓存非常适合使用在读多写少的场景中.而在 ...

  2. Vue框架主要内容学习总结

    Vue框架体系主要内容: 1. vue核心语法和用法: 2. vue-router--路由.路由相当于访问路径,将访问路径与vue组件映射起来.传统方式常采用超链接实现路径或页面之间的切换, 而在vu ...

  3. (JAVA2)写博客的好帮手:Typora

    (二)写博客的好帮手:Typora 推荐文本编辑器 :Typora 文件后缀 : xxx.md 安装步骤 打开浏览器搜索Typora 进入官网后,点击Download(下载) 选择自己的操作系统 选择 ...

  4. jvm源码解读--05 常量池 常量项的解析JVM_CONSTANT_Utf8

    当index=18的时候JVM_CONSTANT_Utf8 case JVM_CONSTANT_Utf8 : { cfs->guarantee_more(2, CHECK); // utf8_l ...

  5. 【阅读笔记】Java核心技术卷一 #5.Chapter7

    7 异常.断言和日志 在 Java 中,如果某个方法不能够采用正常的途径完整它的任务,就可以通过另外一个路径退出方法. 在这种情况下,将会立刻退出,并不返回任何值,而是抛出(throw)一个封装了错误 ...

  6. Spring Boot中使用时序数据库InfluxDB

    除了最常用的关系数据库和缓存之外,之前我们已经介绍了在Spring Boot中如何配置和使用MongoDB.LDAP这些存储的案例.接下来,我们继续介绍另一种特殊的数据库:时序数据库InfluxDB在 ...

  7. canal同步异常:当表结构变化时,同步失败

    场景 canal 同步Mysql一段时间后突然失败,报如如下错误: 2021-08-06 16:16:51.732 [MultiStageCoprocessor-Parser-Twt_instance ...

  8. 那些 Unix 命令替代品们「GitHub 热点速览 v.21.32」

    作者:HelloGitHub-小鱼干 好用的 Unix 命令替代工具能让你事半功倍,例如,bat 便是个带着高亮特性的加强版 cat,就像你用了 oh my zsh 之后便会感受到它的强大.同样好用的 ...

  9. http笔记随笔

    1.HTTP (HyperText Transfer Protocol)超文本传输协议(80端口) 1.规定浏览器和服务器之间相互通信的规则 2.万维网交换信息的基础 3.允许将HTML文档从Web服 ...

  10. selenium元素定位之 动态id, class元素定位

    1.直接进入正题 如下图, 有些元素每次进入都会刷新, 造成元素无法重复定位 怎么办? "xpath部分属性值"定位方法可以帮到我们 1.包含属性定位 driver.find_el ...