leetcode-hot100-动态规划
作者:互联网
文章目录
- [5. 最长回文子串 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/longest-palindromic-substring/)
- [647. 回文子串 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/palindromic-substrings/)
- [10. 正则表达式匹配 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/regular-expression-matching/)
- [53. 最大子序和 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/maximum-subarray/)
- [62. 不同路径 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/unique-paths/)
- [64. 最小路径和 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/minimum-path-sum/)
- [70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/climbing-stairs/)
- [72. 编辑距离 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/edit-distance/)
- [96. 不同的二叉搜索树 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/unique-binary-search-trees/)
- [121. 买卖股票的最佳时机 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)
- [139. 单词拆分 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/word-break/)
- [152. 乘积最大子数组 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/maximum-product-subarray/)
- [198. 打家劫舍 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/house-robber/)
- [221. 最大正方形 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/maximal-square/)
- [279. 完全平方数 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/perfect-squares/)
- [300. 最长递增子序列 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
- [309. 最佳买卖股票时机含冷冻期 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)
- [322. 零钱兑换 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/coin-change/)
- [337. 打家劫舍 III - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/house-robber-iii/)
- [338. 比特位计数 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/counting-bits/)
- [416. 分割等和子集 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/partition-equal-subset-sum/)
- [494. 目标和 - 力扣(LeetCode) (leetcode-cn.com)](https://leetcode-cn.com/problems/target-sum/)
5. 最长回文子串 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]表示从[i,j]能的字符串是否能组成回文子串
- 子问题
dp[i][j]是否是回文子串取决于其子串是否是回文的。
即:如果dp[i][j]是回文子串,那么其子串必然是回文的。
既然是回文的,那么必然首尾相等。
- s[i] == s[j],首尾相等,此时只需要关注子串,即判断
dp[i + 1][j - 1]
是否是回文的。当j - 1 - (i + 1) + 1 < 2的时候,说明[i, j]的长度
为1,那么1个字符肯定是回文的
,所以此时还需要对这个区间的长度进行判断
。- 不相等,直接判定为false。
- 状态转移方程
dp[i][j] = dp[i + 1][j - 1] s[i] == s[j]
- 初始化和边界
dp[i][i] = true 即:单个字符也是回文的
初始化dp的大小为dp[n][n],因为在最后的时候只需要考虑到n-1位置即可,同样第二维的长度也是n,这样就凑齐了一个矩阵,这个矩阵中对角线肯定是true的。
为什么第二维不是2?
因为二维限定死了,要么是回文要么不是回文,这样就在对角线上无法得到一个回文的意思
- 代码:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if(n < 2){
return s;
}
// dp: dp[i][j]表示字符串s在范围i~j是否为一个回文串
boolean[][] dp = new boolean[n][n];
for(int i = 0; i < n; i++){
dp[i][i] = true;
}
int begin = 0;
int maxLen = 1;
String res = "";
char[] ss = s.toCharArray();
for(int j = 1; j < n; j++){
for(int i = 0; i < j; i++){
if(ss[i] == ss[j]){
if(j - i < 3){
dp[i][j] = true;
}else {
dp[i][j] = dp[i + 1][j - 1];
}
}else {
dp[i][j] = false;
}
if(dp[i][j] && j - i + 1 > maxLen){
begin = i;
maxLen = j - i + 1;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
647. 回文子串 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 思路基本上和上一题差不多。有一些区别。
- 在因为是求回文子串,本题求的是
个数
,所以,最左端的指针是可以移动到最右端,和最右端指针重合的,所以两指针都是从最左端移动得到的。
- 代码:
class Solution {
public int countSubstrings(String s) {
int n = s.length();
// dp定义: dp[i][j]表示字符串在i~j范围是否是一个回文子串
boolean[][] dp = new boolean[n][n];
int res = 0;
char[] ss = s.toCharArray();
//这里移动的初始位置就是区别。
for(int j = 0; j < n; j++){
for(int i = 0; i <= j; i++){
if(ss[i] == ss[j]){
if(j - i < 3){
dp[i][j] = true;
}else {
dp[i][j] = dp[i + 1][j - 1];
}
}else {
dp[i][j] = false;
}
}
}
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(dp[i][j]){
res++;
}
}
}
return res;
}
}
10. 正则表达式匹配 - 力扣(LeetCode) (leetcode-cn.com)
题解推荐:宫水三叶
- 三个匹配:
单字符
匹配:需要和s字符串的同一位置字符完全匹配。'.'
匹配:可以匹配s字符串同一位置的任意字符。'*'
匹配:不能单独的匹配,必须和前一个字符搭配使用。表示能匹配同一位置字符多次。
- 思路:
- 状态定义:
dp[i][j]
表示字符串s中以i结尾的子串能否与p中以j结尾的子串匹配。
- 子问题:
dp[i][j]
状态取决于s[i]
是否匹配p[j]
,若匹配,那么状态:dp[i][j] = dp[i - 1][j - 1]
,那么此时就涉及到了是三个匹配中的哪个匹配了。
1. 如果是字符
匹配,即:普通匹配,那么:dp[i][j]
= dp[i - 1][j - 1] && s[i] == p[j]
2. 如果是'.'
匹配,说明
:如果是符号匹配的话,那么只能是p字符串的j位置,因为是用p去匹配s字符串的,那么:dp[i][j] = dp[i - 1][j - 1] && p[j] == '.'
3. 如果p[j] == '*'
,表示可以匹配0个或者多个前一个位置的元素,此时就需要考虑p[j - 1]
与s[i]
的关系,只有当p[j - 1] == s[i]
才能继续向下匹配。
1. 匹配0个
:即:p[j - 1] != s[i]
,此时就需要再向前看一个字符,即:dp[i][j] = dp[i][j - 2]
2. 匹配1个
:即:p[j - 1] == s[i]
,此时p[j - 1]
可能是字符也可能是'.'
,所以,此时的dp[i][j] = dp[i - 1][j - 2] && ((s[i] == p[j - 1]) || p[j - 1] == '.')
3. 匹配2及以上
:说明p[j - 1] == s[i] && p[j - 1] == s[i - 1]
,此时dp[i][j] = dp[i - 2][j - 2] && ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j - 1] == '.')
- 转移方程:
p[j] == s[i]
,dp[i][j] = dp[i - 1][j - 1]
p[j] == '.'
,dp[i][j] = dp[i-1][j-1]
p[j] == '*'
,那么又需要分没有匹配以及匹配:
p[j - 1] != s[i]
,dp[i][j] = dp[i][j-2]
2.p[j - 1] == s[i]
或者p[j - 1] == '.'
:
1. 空匹配:dp[i][j] = dp[i][j-2]
2. 一个匹配:dp[i][j] = dp[i][j-1] && (s[i] == p[j - 1] || p[j - 1] == '.')
3. 多个匹配:dp[i][j] = dp[i-2][j - 2] ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j - 1] == '.'
4. …
5. 表达式展开:盗一波三叶姐的图,
- 边界以及初始化:
- 初始默认都没有一个可以进行匹配,并且使用空串对两个字符串进行拼接一下,这样有利于后面的滚动。
- 边界:
dp[0][0]
为true,表示s以0结尾的子串可以匹配p中以0结尾的子串,两个空串肯定能互相匹配。
- 代码:
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length();
int m = p.length();
s = " " + s;
p = " " + p;
char[] ss = s.toCharArray();
char[] pp = p.toCharArray();
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true;
//s可能是空串
for(int i = 0; i <= n; i++){
for(int j = 1; j <= m; j++){
// 如果是'*' 的话,不能是第一个
// 需要和前面字符进行配合,所以是j + 1
if(j + 1 <= m && pp[j + 1] == '*'){
continue;
}
if(i - 1 >= 0 && pp[j] != '*'){
//字符匹配以及'.'匹配
dp[i][j] = (dp[i - 1][j - 1] && ss[i] == pp[j])
|| (dp[i - 1][j - 1] && pp[j] == '.');
}else if(pp[j] == '*'){
// 这里不能直接写else
//前面是0匹配,后面是匹配
dp[i][j] = (j - 2 >= 0 && dp[i][j - 2]) ||
(i - 1 >= 0 && dp[i - 1][j] && (ss[i] == pp[j - 1] || pp[j - 1] == '.'));
}
}
}
return dp[n][m];
}
}
53. 最大子序和 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示数组到下标位置i的最大和。
- 子问题:
最终问题是由上一个问题决定是否加上当前数判定的。所以子问题也是最大和,只不过是到下标
i - 1
位置。
- 状态转移方程:
dp[i] = max(dp[i - 1] + num, num)
- 边界和初始化:
因为最后是要返回最大和,设置一个变量用来记录,同时为了防止只有一个数,所以初始值就是
nums[0]
。
dp的大小为n,因为最后只会计算到n-1位置的。
- 代码:
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
// dp[i]:表示数组i位置的最大和
int[] dp = new int[n];
dp[0] = nums[0];
int res = nums[0];
for(int i = 1; i < n; i++){
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
}
62. 不同路径 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示为走到数组(i,j)
位置,总共有多少种不同的路径。
- 子问题:
因为每次只能向下或者向右移动,所以要移动到
m * n
位置,要么是从它的右边移动过去,要么就是从它的上边移动过去。
- 转移方程:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
- 边界和初始化:
边界:如果只有一行或者一列的时候,那么只有一种走法。
初始化:dp数组大小为[m][n]
,因为最后实际上返回的是[m - 1][n - 1]
- 代码:
class Solution {
public int uniquePaths(int m, int n) {
// dp定义:dp[i][j]表示走到网格(i,j)位置总有有多少种不同的路径
int[][] dp = new int[m][n];
//在写只有一行或者一列的时候很容易写错,稍微想想就能想明白
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int j = 0; j < n; j++){
dp[0][j] = 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];
}
}
64. 最小路径和 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:与剑指 Offer 47. 礼物的最大价值思路大体一致,不过本题不能外加一行0和一列0,因为这样,本题求的是最小路径,加了0之后,会优先走0的。
- 代码:
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
for(int i = 1; i < n; i++){
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for(int j = 1; j < m; j++){
dp[j][0] = dp[j - 1][0] + grid[j][0];
}
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];
}
}
70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示跳到i阶楼梯有多少中方法
- 子问题:
因为每次要么爬
1个
阶梯,要么爬2个
阶梯所以爬到i阶梯的时候要么前面爬了i-1个阶梯,要么前面爬了i-2个阶梯所以到i阶梯的时候,是这两个之和。
- 状态转移方程:
dp[i] = dp[i - 1] + dp[i - 2]
- 边界和初始化:
- 边界:
dp[0] = 1
,在0阶梯,只需要1步就行;dp[1] = 1
,在1阶梯,爬1个阶梯也到顶。- 初始化:最后返回的时候返回
dp[n]
,因为需要计算爬到第n阶梯的位置,所以大小为n+1
。
- 代码:
class Solution {
public int climbStairs(int n) {
int[] f = new int[n + 1];
f[0] = 1;
f[1] = 1;
for(int i = 2; i <= n; i++){
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
}
72. 编辑距离 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示word1从i位置
转换到word2的j位置
需要的最少步数。
因为最后的时候需要将word1转换为word2
。
- 子问题:
- 如果
word1[i] = word2[j]
,那么只需要关注word(i - 1)与word(j - 1)
的关系,即:dp[i - 1][j - 1]
。- 如果word1[i] != word2[j],那么此时有三个操作:记住三个操作都是针对word1的,因为要将word1转换为word2,因为有这三次操作,不管是哪一个,
只要有这种操作,就要加1
。
1. 插入:因为插入之后,两个字符串最后一个字符肯定相等,但是此时word1的长度是原来的长度+1,即word1(原来长度 + 1) == word2(j),此时需要看word1(i)是否等于word2(j - 1)
。
2. 删除:因为删除之后,因为删除的是word1字符串的,所以删除了之后,要看word1(i-1)与word2(j)是否相等,但是此时word1的长度是原来的-1.即word1(原来长度 - 1) == word2(j),此时需要看word1(i - 1)是否等于word2(j)
。
3. 替换:因为替换之后,两个字符串最后一个字符肯定相等,长度也没有发生变化,但此时word1(i) == word2(j),所以此时需要看word1(i - 1)是否等于word2(j - 1)
。
- 状态转移方程:
word1[i] == word2[j]
,dp[i][j] = dp[i - 1][j - 1]
。
word1[i] != word2[j]
,dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1])
- 边界和初始化:
边界:为了方便滚动,默认给两个字符串首尾加上一个空格,但不用实际真正加。因此
dp[0][0] = 0
,当只有一行和一列的时候,每次只加1。
初始化:因为加上0,所以数组的大小为(word1.length() + 1, word2.length() + 1)
。
- 代码:
class Solution {
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
for(int i = 1; i <= n1; i++){
dp[i][0] = dp[i - 1][0] + 1;
}
for(int j = 1; j <= n2; j++){
dp[0][j] = dp[0][j - 1] + 1;
}
for(int i = 1; i <= n1; i++){
for(int j = 1; j <= n2; j++){
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[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];
}
}
96. 不同的二叉搜索树 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示以i为根节点
能生成的二叉搜索树的个数
- 子问题:
在[1,n]选取k作为
k
作为根节点
,那么使用[1,k-1]构建左子树
,使用[k+1,n]去构建右子树
。
此时总的数量就有左子树的个数 * 右子树的个数
- 状态转移方程:
dp[i] = ∑ \sum ∑dp[j - 1] * dp[i - j] , j∈[1,i]
左子树用了j - 1个,那么右子树只能是i-j个。
因为给定了是i,所以左+右+根= i,j - 1 + (i - j) + 1 = i
- 边界和初始化:
边界:n = 0/1的时候,dp[i] = 1,要么是空树,要么就是一个单节点。
初始化:因为要返回dp[n],所以初始化大小为dp[n + 1]
- 代码:
class Solution {
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n ;i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j - 1] * dp[i - j];//前面是以i为根节点,i-1个左子树,后面是n-j个右子树
}
}
return dp[n];
}
}
121. 买卖股票的最佳时机 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示到i天结束,持股的状态j,此时具有的最大利润。
dp[i][0]
表示第i + 1天不持股。
dp[i][1]
表示第i + 1天持股。
- 子问题:
dp[i][0]
表示到第i+1天没有持股:
1、第i天持股,到第i+1天给卖了
2、第i天不持股,到第i+1天也不持股dp[i][1]
表示到第i+1天持股
1、第i持股,到第i+1天继续持有
2、第i天不持股,到第i+1天持股
- 状态转移方程:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
dp[i][1] = max(-prices[i], dp[i - 1][1]); //因为可以多次买入
- 边界和初始化:
初始化:dp数组的大小为:
dp[n][2]
,因为有两个持股状态
边界:dp[0][0] = 0,dp[0][1] = -prices[0]
- 代码:
class Solution {
public int maxProfit(int[] prices) {
//定义:dp[i][j]表示第i + 1天持有状态j,此时具有的最大利润
int n = prices.length;
int[][] dp = new int[n + 1][2];
//dp[i][0]表示第i + 1天不持股
//dp[i][1]表示第i + 1天持股
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < n; i++){
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
return dp[n - 1][0];
}
}
139. 单词拆分 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示字符串s
到i
位置能否被拆分为wordDict中的字符串。
- 子问题:
想知道
i
位置能否被拆分,就需要知道前一个位置i-1
能否被拆分,如果i
位置可以被拆分,那么前一个位置必然可以被拆分。那么也就是说:
- 记
i
是从[0,n)
进行扫描,j
是单词尾部,从[i + 1, n + 1)
。dp[j]=true
,那么dp[i]
也必然为true
。- 同时
s
的子串[i,j]
组成的字符串是一定在wordDict
中的。
- 转移方程:
dp[j] = dp[i] && wordDict.contains(s.substring(i,j))
- 边界和初始化:
边界:记
dp[0] = true
。这是为了方便后面进行滚动,默认0位置是可以被拆分到wordDict
中的。
初始化:因为最后是需要返回整个字符串s的情况,即:返回dp[n]
,所以需要知道n
位置的情况,所以数组大小为[n+1]
。
- 代码:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n + 1; j++){
String subStr = s.substring(i,j);
if(dp[i] && wordDict.contains(subStr)){
dp[j] = true;
}
}
}
return dp[n];
}
}
152. 乘积最大子数组 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示到数组i
位置的最大值。为什么要设置j呢?因为通过案例发现是有负数
的。所以这个j就是来判断最大值和最小值的,j=0的时候代表最小值,j=1的时候代表最大值。
- 子问题:
需要知道
i
位置就需要知道i-1
位置的值。然后根据当前值的正负关系,去取前一个位置的最大值还是最小值。所以每一步都需要计算得到最大最小值
当nums[i] < 0
的时候,要想取得最大值,就需要乘上前一个位置的最小值和当前位置值最判断,以及最小值是当前位置的值和前一个位置的最大值*当前值。
当nums[i] > 0
,情况与上面相反。
- 转移方程:
nums[i] < 0
:
-dp[i][0] = Math.min(nums[i], dp[i - 1][1] * nums[i]);
-dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i]);
nums[i] > 0
:
-dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i]);
-dp[i][1] = Math.max(nums[i], dp[i - 1][1] * nums[i]);
- 边界和初始化:
边界:最后返回
dp[n - 1][1]
,就是最后一个位置的最大值,所以大小为[n][2]
。
初始化:最大最小值都是第一个数。最后结果也默认是第一个数,防止给定的数组只有一个数。
- 代码:
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int max = nums[0];
int[][] dp = new int[n][2];
dp[0][0] = nums[0];
dp[0][1] = nums[0];
for(int i = 1; i < n; i++){
if(nums[i] < 0){
dp[i][0] = Math.min(nums[i], dp[i - 1][1] * nums[i]);
dp[i][1] = Math.max(nums[i], dp[i - 1][0] * nums[i]);
}else {
dp[i][0] = Math.min(nums[i], dp[i - 1][0] * nums[i]);
dp[i][1] = Math.max(nums[i], dp[i - 1][1] * nums[i]);
}
max = Math.max(max,dp[i][1]);
}
return max;
}
}
198. 打家劫舍 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示到i号房间偷取到的最大价值。j同样有两个值,一个是偷,一个是不偷,那么dp[i][0]
表示到i号房间不偷能取得的最大价值,dp[i][1]
表示到i好房间偷能取得的最大价值。
- 子问题:
因为题目限制,不能连续偷两个相邻两个房间,所以:
- 偷了i号房间就不能偷前一个房间,至于后面的房间偷不偷,那不是后面i+1号房间该考虑的事。那么此时的金额就是前面不偷的基础上加上此时偷i好房间的金额。
- 不偷i号房间,就可以偷前面的值。那么此时金额就在前面房间的偷和不偷之间取最大值。
但是,本题要求是最大价值,所以不管怎么样金额一定要最大。也就是不管偷还是不偷i号房间,最终的结果一定的是最大的。
- 状态转移方程:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1])
dp[i][1] = dp[i - 1][0] + nums[i]
- 边界和初始化:
边界:
dp[0][0] = 0
、dp[0][1]=nums[0]
初始化:因为最后要知道给定数组的偷取最大金额,所以,最后返回的是dp[n - 1]
,至于偷还是不偷,看哪个金额大。数组大小为[n][2]
。
- 代码:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1){
return nums[0];
}
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i = 1; i < n; i++){
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
return Math.max(dp[n - 1][0], dp[n - 1][1]);
}
}
221. 最大正方形 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示以(i,j)
为右下角,并且只
包含1
的正方形的边长最大值。
为什么是右下角?
因为最后是需要判断完整个二维数组的,数组从(0,0)出发,到(row - 1, col - 1),所以是需要右下角进行判断。
- 子问题:
要判断是正方形,那么也就是四个方向必须全部为1, 那么只有当
左上、右上、左下
都为1时候,此时右下
是否为1,就很关键
如果该位置上值是0,那么dp[i][j] = 0,因为当前位置不可能在由1组成的正方形中。
如果该位置上是1,那么dp[i][j]的值是由左上、右上、左下
三个相邻位置的dp值决定的,当前位置的值是取决于三个相邻位置的最小值+1
- 转移方程:
dp[i][j] = min(dp(i-1)(j-1),dp(i-1)(j),dp(i)(j-1)) + 1
因为此时位置上的值是1,那么当前位置的值就要+1。也就是只包含1
的正方形的边长最大值+1。 - 边界和初始化:
边界:全部初始化为0,即:没有能够可以全部由1
组成的正方形。
初始化:最后需要[row] - 1[col - 1]
位置的值,所以,大小为[row][col]
。
- 代码:
class Solution {
public int maximalSquare(char[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return 0;
}
int row = matrix.length;
int col = matrix[0].length;
int maxSide = 0;
int[][] dp = new int[row + 1][col + 1];
for(int i = 0; i < row; i++){
for(int j = 0; j < col; j++){
if(matrix[i][j] == '1'){
if(i == 0 || j == 0){
dp[i][j] = 1;
}else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
return maxSide * maxSide;
}
}
279. 完全平方数 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示最少使用多少个数的平方就可以组成i。
- 子问题:
要知道最少使用多少个数的平方可以组成i,假设最坏的情况是一个都没有,那么最差的情况就是:
n个1
。如果有平方数可以组成,那么就要满足扫描数的平方≤i
,(一旦超过了,那么说明就凑不齐),即:dp[i - j * j]
表示如果正好差数j的平方就可以组成i,那么前一个位置所需要的最少个数,再加上这个j就可以组成dp[i]
所需的最少平方数。
- 转移方程:
min = min(i,dp[i - j * j])
dp[i] = min + 1
min表示数字i最少能够使用多少个平方数组成。其初值为i,最差的情况就是全是1组成的,但不管是由全1组成或者有平方数组成,每次计算完一个数之后,都要+1,这样让其最少的完全平方数+1。
- 边界和初始化:
边界:dp[0] = 0
。
初始化:因为最后需要计算n的情况,即:dp[n]
,所以dp数组大小为n+1
。
- 代码:
class Solution {
public int numSquares(int n) {
//dp[i]表示最少使用多少个平方数就可以组成i
int[] dp = new int[n + 1];
dp[0] = 0;
for(int i = 1; i < n + 1; i++){
int min = i;
for(int j = 1; j * j <= i; j++){
min = Math.min(min, dp[i - j * j]);
}
dp[i] = min + 1;
}
return dp[n];
}
}
300. 最长递增子序列 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示到数组i位置的递增子序列的长度。
- 子问题:
- 要想知道i位置递增子序列的长度,就需要知道前面一个位置的递增子序列的长度,如果前面也是,同时当前位置的值大于前一个位置,那么加1即可,同样是递增子序列。
- 转移方程:
dp[i] = max(dp[j] + 1, dp[i]);
j表示前一个位置,i表示当前位置。
j∈[0,i)、i∈[1,n-1]
- 边界和初始化:
边界:给出了数组大小之后,先默认没有递增的,即:每个位置上的递增子序列的长度都是1,就是自己。
初始化:因为需要算得最后的位置,即:下标n-1
,所以数组大小为n
。
- 代码:
class Solution {
public int lengthOfLIS(int[] nums) {
//定义:dp[i]表示到i位置递增子序列的长度
int n = nums.length;
int max = 1;
int[] dp = new int[n];
for(int i = 0; i < n; i++){
dp[i] = 1;
}
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
max = Math.max(max, dp[i]);
}
}
return max;
}
}
309. 最佳买卖股票时机含冷冻期 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示到第i天持有股票状态j能收获的最大收益
根据题意可知,可以多次的买卖股票,卖出股票之后有冷冻期,过了冷冻期才可以买入。所以:
j
有三种状态:0、持股;1、不持股但处于冷冻期;2、不持股且没有处于冷冻期。
注
:冷冻期为一天。最后返回的时候一定是不持股的状态能取得的最大值
- 子问题:
dp[i][0]
:表示第i天持有股票能取得的最大收益,此时要么
就是前一天继续持股,要么
就是前一天不持股且没有处于冷冻期今天买入。(如果前一天不持股且处于冷冻期,今天必然是无法买入的)
dp[i][1]
:表示第i天不持有股票且处于冷冻期的最大收益。此时因为处于冷冻期
,那么必然前一天是卖掉股票的且前一天不持股的,因为卖掉了所以不持股。
dp[i][1]
:表示处于第i天且没有处于冷冻期的最大收益。此时没有处于冷冻期
,要么
就是前一天处于冷冻期的,今天解冻了,要么
就是前一天也没有持股并且处于冷冻期的。
- 转移方程:
经过分析可知:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i])
dp[i][1] = dp[i - 1][0] + prices[i]
dp[i][2] = max(dp[i - 1][1], dp[i - 1][2])
- 边界和初始化:
边界:
dp[0][0] = -price[0]
dp[0][1] = 0
dp[0][2] = 0
初始化:因为要计算到最后一天,并且由三种状态,所以初始化的时候是dp[n][3]
。
- 代码:
class Solution {
public int maxProfit(int[] prices) {
//0表示持股,1表示不持股处于冷冻期,2表示不持股也不处于冷冻期
int n = prices.length;
int[][] dp = new int[n][3];
if(n == 0){
return 0;
}
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
for(int i = 1; i < n; i ++){
//要么前一天持股,要么前一天不持股也没有处于冷冻期今天买入
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
//前一天持股今天卖掉
dp[i][1] = dp[i - 1][0] + prices[i];
//前一天不持股也不处于冷冻期,要么前一天持股处于冷冻
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1]);
}
return Math.max(dp[n - 1][1], dp[n - 1][2]);
}
}
312. 戳气球 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示数组[i,j]
的积分值,i控制左边界,j控制右边界。
- 子问题:
此时的子问题是在
[i,j]
这个区间内,选择不同的气球扎破能得到的最大积分值,那么就将区间划分为了[i,k),k,(k,j]
。
如果最后只有三个气球了,那么此时只能扎破中间的,那么此时得到的金币数量就是i * k * j
,还要加上之前在区间[i,k)
以及(k,j]
位置扎破的金币
注:为什么要加dp[i][k]和dp[k][j]?
因为k
永远都是最后
一个被扎破的气球,(i,j)区间中的k两边的东西必然是先各自被戳破。两边是互不干扰
重点:
k永远都是最后一个被戳破的,并且始终都是找金币数最大的那个。
- 转移方程:
sum = i指向的气球拥有金币数 * j指向的气球拥有金币数 * k指向的气球拥有金币数
sum += dp[i][k] + dp[k][j]
dp[i][j] = max(dp[i][j], sum)
- 边界和初始化:
初始化:根据题意是可以在给定数组的两端加上数字1,这样大小就是n+2,此时dp数组的大小为:
[n+2][n+2]
,但是最后实际上是返回的dp[0][n+2]
。返回0~(n + 1)能扎破出的最大金币数
边界:dp数组都初始化为0
- 代码:
class Solution {
public int maxCoins(int[] nums) {
int n = nums.length;
int[][] dp = new int[n + 2][n + 2];
int[] temp = new int[n + 2];
temp[0] = 1;
temp[n + 1] = 1;
for(int i = 1; i <= n; i++){
temp[i] = nums[i - 1];
}
//倒着处理, 因为是(i,j)区间,让j先指向末尾,i比j小两个位置,k永远在这个区间进行移动,选择最大值
for(int i = n - 1; i >= 0; i--){
for(int j = i + 2; j <= n + 1; j++){
for(int k = i + 1; k < j; k++){
int sum = temp[i] * temp[j] * temp[k];
sum += dp[i][k] + dp[k][j];
dp[i][j] = Math.max(sum, dp[i][j]);
}
}
}
return dp[0][n + 1];
}
}
322. 零钱兑换 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示使用最少的硬币就可以总金额i。
- 子问题:
要想知道dp[i],首先看它的前一个问题:就是最少可以用多少硬币组成
dp[i - coins[j]],j∈[0,n)
。在不知道选择的面值大小的时候,假设选择使用面值为ak
,那么ak∈[coins[i]]
,那么要拼出金额i
,子问题就是用最少的硬币拼出i - ak
,那么最后一个问题的时候,可以选择或者不选面值为ak
的。选择
ak之后,就可以正好组成i
,硬币数+1
.不选择ak
,硬币数就是正无穷,就永远无法凑齐。
- 转移方程:
dp[i] = min(dp[i], dp[i - coins[j]] + 1),j∈[0,n)
- 边界和初始化
边界:dp[0] = 0;
初始化:因为最后要返回使用多少硬币组成amount,所以需要dp[amount],那么大小就为dp[amount+1]。
- 代码:
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
//定义:dp[i]表示最少使用多少硬币可以组成i
int[] dp = new int[amount + 1];
dp[0] = 0;
for(int i = 1; i < amount + 1; i++){
//先默认是没有能凑齐
dp[i] = Integer.MAX_VALUE;
//j代表硬币种类
for(int j = 0; j < n; j++){
//此时虽然有转移方程,但是还要满足下标大于等于0,并且前面一个位置是可以被凑齐的
if(i - coins[j] >= 0 && dp[i - coins[j]] != Integer.MAX_VALUE){
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
if(dp[amount] != Integer.MAX_VALUE){
return dp[amount];
}else {
//等于默认值就是无法被凑齐,返回-1即可。
return -1;
}
}
}
337. 打家劫舍 III - 力扣(LeetCode) (leetcode-cn.com)
- 题目意思:
如果偷了根节点,那么左右子树就不能偷;如果偷了根节点,那么就可以选择左右子树。
- 思路:
- 状态定义:
dp[i]
表示选择i节点偷与不偷的情况下能取得的最大值。
dp[0]表示不选择根节点
dp[1]表示选择根节点
- 子问题:
因为相邻节点不能一起偷,就是说不能偷根的时候继续偷左右子树结点。
当前结点偷了
,左右两个孩子节点就不能再偷了
,
金额 = 当前结点的钱 + 左孩子选择自己不偷的时候可以得到的最大的钱 + 右孩子选择不偷可以得到最大的钱数
当前根节点不偷
,两个孩子节点需要拿出最多的钱即可
金额= 左孩子能够偷到的钱 + 右孩子能偷到的钱
- 转移方程:
dp[1]
= root.val + left[0] + right[0] 只能选择根节点的,左右节点都不能选的情况下的最大节点和
dp[0]
= max(left[0], left[1]) + max(right[0], right[1]) 不选择根节点的情况下,偷左右节点可以偷的最大窃取
- 边界和初始化:
边界:如果dfs到了叶子结点,那么返回{0,0},向上级报告,从这里再向下能偷取到的是{0,0}。
初始化:dp只有两个状态,一个是偷当前结点,一个是不偷当前结点,所以dp[2]
。
- 代码:
class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root);
return Math.max(res[0], res[1]);
}
private int[] dfs(TreeNode root){
if(root == null){
return new int[]{0,0};
}
int[] left = dfs(root.left);
int[] right = dfs(root.right);
int[] dp = new int[2];
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
dp[1] = root.val + left[0] + right[0];
return dp;
}
}
338. 比特位计数 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i]
表示数字i的二进制数中1的个数。
- 子问题:
要想得知
i
的二进制数中1的个数,那么就需要知道它前一个数的二进制数中的1的个数,因为后一个数比大前一个数大1
,这个1,就是多出来的1的个数
。
为奇数
的时候,二进制最后一位肯定是1
;为偶数
,二进制数最后一位肯定是0
。
为奇数的时候,因为二进制最后一位是1,就比前一个数多了一个1.
为偶数
的时候,1的个数和自己除2之后的二进制数中的1的个数一样
(eg: 2(01) 4(10) 8(100))
- 转移方程:
dp[i] = dp[i - 1] + 1 i为奇数
= dp[i / 2] i为偶数
- 边界和初始化:
边界:
dp[0] = 0
初始化:因为返回的n数字的二进制1的情况,那么一定要计算到dp[n]
,初始化大小为n+1
。
- 代码:
class Solution {
public int[] countBits(int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
for(int i = 1; i < n + 1; i++){
if(i % 2 == 1){
dp[i] = dp[i - 1] + 1;
}else {
dp[i] = dp[i / 2];
}
}
return dp;
}
}
416. 分割等和子集 - 力扣(LeetCode) (leetcode-cn.com)
leetcode官方视频题解
本题就是一个0-1背包问题,有兴趣的朋友可以了解一下
- 思路:
- 状态定义:
dp[i][j]
表示数组从[0,i]
是否能有一种方案,使得选取正整数之和为j
- 子问题:
要想知道
[0,i]
是否能有一种方案使得选取的正整数之和为j
,那么就需要看dp[i-1][x]
,那么就是否选择nums[i]
,以及选择之后,剩余数的大小与nums[i]
的大小关系。
- 如果不选择
nums[i]
,那么dp[i][j] = dp[i - 1][j]
- 如果选择,那么又需要看
nums[i]
和j
的大小关系
- 如果正好相等,那么
dp[i][j]=true
- 如果当前数小于j,那么
dp[i][j]=dp[i-1][j-nums[i]]
- 如果大于j的话,就肯定不能选。
- 状态转移方程:
dp[i][j] = dp[i - 1][j] j < num[i]
= dp[i - 1][j] || dp[i - 1][j - nums[i]] j ≥ nums[i]
不选取nums[i] || 选取nums[i]
- 边界和初始化:
边界:
1.如果不选择任何正整数,那么表示被选取的正整数等于0.即:dp[i][0]=true;
2. 如果i为0,且只有一个正整数nums[0]可以被选取,那么dp[0][nums[0]]=true
初始化:因为最后需要判断最后一个数[n - 1]
,那么设置dp[n][target]
,这个target就是要求的数之和。target一定是数组之和的一半。
- 代码:
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n < 2){
return false;
}
int maxNum = 0, sum = 0;
for(int num : nums){
sum += num;
maxNum = Math.max(maxNum, num);
}
if(sum % 2 != 0){
return false;
}
int target = sum / 2;
if(maxNum > target){
return false;
}
//dp[i][j]表示从0-i是否能凑齐j
boolean[][] dp = new boolean[n][target + 1];
//都不选
for(int i = 0; i < n; i++){
dp[i][0] = true;
}
for(int i = 1; i < n; i++){
int num = nums[i];
for(int j = 1; j < target + 1; j++){
if(j >= num){
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
}else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
494. 目标和 - 力扣(LeetCode) (leetcode-cn.com)
- 思路:
- 状态定义:
dp[i][j]
表示从数组[0,i]的元素进行加减
可以在[-sum,sum]
得到值j
的方法数。
为什么这样定义可以看一下下面的初始化部分。
- 子问题:
要想知道
dp[i][j]
的情况,就要对dp[i - 1][x]
进行讨论,那么关键就是nums[i]
选取过后的加减问题。
选取nums[i]
,那么此时就需要讨论x是加上当前数凑齐j
,还是x减去当前数凑齐j
,分别为:[i - 1][j - nums[i]]、[i - 1][j + nums[i]]
。
- 转移方程:
dp[i][j]= dp[i - 1][j + nums[i]] + dp[i - 1][j - nums[i]]
(分别表示减或者加上当前数就能凑齐j)
- 边界和初始化:
初始化:
因为最后需要得到所以,应该每一行的长度为dp[n -1][target]
,所以在初始化的时候是dp[n][target]
。但经过打表之后就能分析出问题,因为本题是可以通过加、减得到的,2 * sum + 1
。(1就是中间0的位置)。所以真正
的dp大小为[len - 1][2 * sum + 1]
。最后的返回是dp[n - 1][sum + target]
。
边界定值:确定好了数组的大小之后,进行打表,如果给定数组第一个数为0,那么dp[0][sum] = 2(加减)
;否则dp[0][sum ± nums[0]] = 1(要么加,要么减)
。(可以看打表过程,也可以自己画,以[1,1]
和[0,1]
为例)
在下表中,因为数组下标不能为负,所以将负数和0进行了右移,所以下图dp[0][sum]
实际上是用黄色标出来的2。
- 代码:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int n = nums.length;
int sum = 0;
for(int num : nums){
sum += num;
}
//如果给定目标值都比和还大,肯定是凑不齐的,返回0
if(Math.abs(target) > Math.abs(sum)){
return 0;
}
//j的范围是[-sum, sum]之间,中间还有一个0。
int[][] dp = new int[n][2 * sum + 1];
//第一个值为0,那么得到最后和一定会有两个方案(加或者减)
//数组[0,1]可以证明,最后和为1,那么第一个位置可正可负
if(nums[0] == 0){
dp[0][sum] = 2;
}else {
//否则第一个数只能分别组成(+、-)nums[i]的情况
//[1,1]为例.
//dp[0][3]表示在数组在[0,0]可以在范围[-2,2]得到值为3-2的方案数为1
//dp[0][1]表示数组在[0,0]可以在范围[-2,2]得到值为1-2的方案数为1.
dp[0][sum + nums[0]] = 1;
dp[0][sum - nums[0]] = 1;
}
for(int i = 1; i < n; i++){
for(int j = 0; j < 2 * sum + 1; j++){
//负数部分要确保下标大于等于0
int left = (j - nums[i]) >= 0 ? j - nums[i] : 0;
//正数部分要确保不能超过上限
int right = (j + nums[i]) < 2 * sum + 1 ? j + nums[i] : 0;
dp[i][j] = dp[i - 1][left] + dp[i - 1][right];
}
}
// System.out.println(Arrays.deepToString(dp));
return dp[n - 1][sum + target];
}
}
- 此外本题还可以用dfs解决,因为题目说了要么+,要么-
- dfs比起dp更好理解一些,如果dp没想起来就用dfs更快的AC吧
- 代码:
class Solution {
private int count = 0;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums, target, 0, 0);
return count;
}
private void dfs(int[] nums, int target, int idx, int sum){
if(idx == nums.length){
if(target == sum){
count++;
}
}else {
dfs(nums, target, idx + 1, sum - nums[idx]);
dfs(nums, target, idx + 1, sum + nums[idx]);
}
}
}
标签:cn,nums,int,hot100,动态,com,leetcode,dp 来源: https://blog.csdn.net/qq_41784433/article/details/120918254