LeetCode入门指南 之 动态规划思想
推荐学习labuladong大佬的动态规划系列文章:先弄明白什么是动态规划即可,不必一次看完。接着尝试自己做,没有思路了再回过头看相应的文章。
动态规划一般可以由 递归 + 备忘录 一步步转换而来,不必被名字唬住。通常只要找到状态转移方程问题就解决了一大半,至于状态的选择只要题目做多了自然就会形成经验,通常是问什么就设什么为状态。
常见四种类型
- Matrix DP (10%)
- Sequence (40%)
- Two Sequences DP (40%)
- Backpack (10%)
注意:
- 贪心算法大多题目靠背答案,所以如果能用动态规划就尽量用动规,不用贪心算法。一般可以先尝试用动态规划,如果超时再用贪心。
1、矩阵类型(10%)
120. 三角形最小路径和
给定一个三角形
triangle
,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标
i
,那么下一步可以移动到下一行的下标i
或i + 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”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用
1
和0
来表示。示例 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 < i
且nums[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. 最长公共子序列
给定两个字符串
text1
和text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回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. 编辑距离
给你两个单词
word1
和word2
,请你计算出将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入门指南 之 动态规划思想的更多相关文章
- LeetCode入门指南 之 回溯思想
模板 result = {} void backtrack(选择列表, 路径) { if (满足结束条件) { result.add(路径) return } for 选择 in 选择列表 { 做选择 ...
- LeetCode入门指南 之 链表
83. 删除排序链表中的重复元素 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 .返回同样按升序排列的结果链表. class Soluti ...
- LeetCode入门指南 之 栈和队列
栈 155. 最小栈 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈. push(x) -- 将元素 x 推入栈中. pop() -- 删除栈顶的元素. top( ...
- LeetCode入门指南 之 排序
912. 排序数组 给你一个整数数组 nums,请你将该数组升序排列. 归并排序 public class Sort { //归并排序 public static int[] MergeSort(in ...
- LeetCode入门指南 之 二叉树
二叉树的遍历 递归: void traverse (TreeNode root) { if (root == null) { return null; } //前序遍历位置 traverse(root ...
- LeetCode入门指南 之 二分搜索
上图表示常用的二分查找模板: 第一种是最基础的,查找区间左右都为闭区间,比较后若不等,剩余区间都不会再包含mid:一般在不需要确定目标值的边界时,用此法即可. 第二种查找区间为左闭右开,要确定targ ...
- Ext JS 6学习文档–第1章–ExtJS入门指南
Ext JS 入门指南 前言 本来我是打算自己写一个系列的 ExtJS 6 学习笔记的,因为 ExtJS 6 目前的中文学习资料还很少.google 搜索资料时找到了一本国外牛人写的关于 ExtJS ...
- 【翻译Autofac的帮助文档】1.入门指南
[写在前面]尝试做完一件工作之外自我觉得有意义的一件事,那就从翻译Autofac的帮助文档吧. 入门指南 将Autofac集成你的应用程序的步骤通常很简单,一般是: 时刻以IOC(控制反转)的思想来规 ...
- OpenCASCADE入门指南
OpenCASCADE入门指南 eryar@163.com 一.概述 荀子说“君子性非异也,善假于物也”.当你会用英语,就可以与世界各国的人交流:当你会用编程语言,就可以与计算机交流:当你会用数学语言 ...
随机推荐
- SpringCloud学习之【Eureka实现服务注册与发现】
这段时间开始整理之前的SpringCloud实践笔记,这里感谢翟永超大佬的文章SpringCloud从入门到精通的指导. 项目结构 服务注册中心 注意: 1.SpringCloud与SpringBoo ...
- [考试总结]noip模拟12
菜 今天总体来说 菜爆了,打了 \(3\) 个暴力,没有一个是正解,并且每一个分数都低得要命... 主要还是太菜了... 第一题开题发现和昨天 \(T3\) 一样,然而因为还没学可持久化数据结构就咕掉 ...
- airtest前期准备(pocoSDK+unity打apk包+安装pocoui库)
只介绍unity的环境准备,cocos的可以参考官方文档 https://poco-chinese.readthedocs.io/zh_CN/latest/source/doc/integrat ...
- 深入刨析tomcat 之---第7篇 这个是链接,如果使用idea 创建servlet工程
recoded by 张艳涛 使用IDEA创建Servlet项目 使用IDEA创建Servlet项目
- tp3 联表查询
联表查询D("column") ->field("{$DbPrefix}column.pid,{$DbPrefix}news.*") ->where ...
- 大都市meg DFS序
题目描述 在经济全球化浪潮的影响下,习惯于漫步在清晨的乡间小路的邮递员Blue Mary也开始骑着摩托车传递邮件了.不过,她经常回忆起以前在乡间漫步的情景.昔日,乡下有依次编号为1..n的n个小村庄, ...
- vue的项目初始化
1.创建文件 blog 2.下载安装node mongoose 3.(1)vue创建后端项目文件 vue create admin (2)vue创建前端项目文件 vue create web (3)新 ...
- Java流程控制03——选择结构
选择结构 if单语句结构 我们很多时候要去判断一个东西是否可行,然后我们才去执行,这样一个过程我们用if语句来表示 语法 if(布尔表达式){ //如果布尔表达式结果为true将执行的语句 } if ...
- Centos配置网络和主机映射
目录 虚拟机网络的三种配置方式 配置虚拟机IP 主机映射问题 配置虚拟机的主机名 虚拟机远程登录 虚拟机网络的三种配置方式 桥接模式:当前虚拟机与主机在同一个局域网下,同一个局域网下的所有电脑都可以访 ...
- .Net Core with 微服务 - 分布式事务 - 2PC、3PC
最近比较忙,好久没更新了.这次我们来聊一聊分布式事务. 在微服务体系下,我们的应用被分割成多个服务,每个服务都配置一个数据库.如果我们的服务划分的不够完美,那么为了完成业务会出现非常多的跨库事务.即使 ...