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 一.概述 荀子说“君子性非异也,善假于物也”.当你会用英语,就可以与世界各国的人交流:当你会用编程语言,就可以与计算机交流:当你会用数学语言 ...
随机推荐
- Java数据库分表与多线程查询结果汇总
今天接到一个需求:要对一个物理分表的逻辑表进行查询统计.而数据库用的是公司自己研发的产品,考虑的到公司产品的特点以及业务的需求,该逻辑表是按年月进行分表的,而非分区.我们来看一下,在按时间段进行查询统 ...
- WEB安全新玩法 [8] 阻止订单重复提交
交易订单的重复提交虽然通常不会直接影响现金流和商品流,但依然会给网站运营方带来损害,如消耗系统资源.影响正常用户订单生成.制造恶意用户发起纠纷的机会等.倘若订单对象是虚拟商品,也有可能造成实际损失.订 ...
- 2021年BI软件排名,国内外BI软件功能对比
数据分析是帮助企业深入了解自身业务表现(例如正在做什么或哪块业务需要注意和改进)的重要元素.为了获得更直观的展现,数据分析BI软件可帮助公司通过报告.数据可视化.应用程序等从数据中获取决策数据支撑.没 ...
- Floyd弗洛伊德算法
先看懂如何使用 用Java实现一个地铁票价计算程序 String station = "A1 A2 A3 A4 A5 A6 A7 A8 A9 T1 A10 A11 A12 A13 T2 A1 ...
- TCP协议系列之一一什么是TCP协议,TCP的三次握手,为什么不是2次或4次?
CP 为什么三次握手而不是两次握手(正解版) https://blog.csdn.net/lengxiao1993/article/details/82771768 自己理解说明一下: 比如说有一条管 ...
- atom之插件安装及相关
1. simplified-chinese-menu 汉化软件 2. file-icons 加上文件图标 3. language-vue 加上vue语言支持 4. platformio-ide-ter ...
- mongo-express 远程代码执行漏洞(CVE-2019-10758)
影响版本 mongo-express 0.53.0 POST /checkValid HTTP/1.1 Host: 192.168.49.2:8081 Accept-Encoding: gzip, d ...
- 数据库建模、面向对象建模>从零开始学java系列
目录 数据库建模 前置知识 使用PowerDesigner数据库建模设计 一对多CDM概念数据模型设计 多对多的PDM物理数据模型设计(针对mysql) PowerDesigner将不同的模型进行转换 ...
- CF201C Fragile Bridges TJ
本题解依旧发布于洛谷,如果您能点个赞的话--(逃 前言 题目链接 正解:动态规划 思路不是很好想,想出来了应该就没有多大问题了,但是需要处理的细节较多,再加上水水的样例,难度应该是偏难的.个人感觉应该 ...
- 对象转换工具 MapStruct 介绍
前言 在我们日常开发的分层结构的应用程序中,为了各层之间互相解耦,一般都会定义不同的对象用来在不同层之间传递数据,因此,就有了各种 XXXDTO.XXXVO.XXXBO 等基于数据库对象派生出来的对象 ...