目录
- 爬楼梯
- 背包问题
- 热门题🔥
爬楼梯
一维爬楼梯
先从简单但最经典的开始!
70. 爬楼梯:假设你正在爬一个 n
阶的楼梯,每次可以爬 1
或 2
个台阶。有多少种不同的方法可以爬到楼顶呢?
思路:定义 dp[i]
表示爬 i+1
阶楼梯的方法总数。
- 状态转移方程:
dp[i] = dp[i-1] + dp[i-2]
。 - 初始值:
dp[0] = 1
,dp[1] = 2
。
public int climbStairs(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (n == 2) return 2;
int prev = 2;
int sum = 3;
for (int i = 3; i < n; i++) {
int tmp = sum;
sum += prev;
prev = tmp;
}
return sum;
int[] dp = new int[n];
dp[0] = 1;
dp[1] = 2;
for (int i = 2; i < n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n - 1];
}
空间优化:观察到一旦算出 dp[i]
,dp[i−2]
及其左边的状态就永远不会用到了。
public int climbStairs(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (n == 2) return 2;
int dp_n1 = 1;
int dp_n2 = 1;
for (int i = 1; i < n; i++) {
int dp_n = dp_n1 + dp_n2;
dp_n1 = dp_n2;
dp_n2 = dp_n;
}
return dp_n2;
}
二维爬楼梯
62. 不同路径:从 m x n
网格的左上角开始,每次只能向下或者向右移动 1
步,到达网格的右下角共有多少条不同的路径?
思路:定义 dp[i][j]
表示到达网格 (i, j)
的方法总数。
- 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 - 初始值:
i = 0
或j = 0
时,dp[i][j] = 0
。
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
// 第一次空间优化
int[] dp = new int[n]; // 取代 dp[i][j]
int[] dp_prev = new int[n]; // 取代 dp[i - 1][j]
Arrays.fill(dp_prev, 1); // 即 i = 0 时,dp[j] = 0
Arrays.fill(dp, 1); // 即 j = 0 时,dp[j] = 0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] = dp_prev[j] + dp[j - 1];
}
dp_prev = dp.clone();
}
return dp[n - 1];
// 第二次空间优化
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++){
for (int j = 1; j < n; j++){
dp[j] += dp[j - 1] ;
}
}
return dp[n - 1];
}
类似问题:
背包问题
问题描述:给定容量 target
,以及一组物品的大小数组 weights
,如何选取物品使得正好填满容量?
完全背包问题
每种物品可以无限次使用。
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int weight : weights) {
// 正序遍历,允许重复当前物品
for (int i = weight; i <= target; i++) {
dp[i] = dp[i] || dp[i - weight];
}
}
return dp[target];
0/1背包问题
每种物品最多只能被选取一次。
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int weight : weights) {
if (weight > target) {
break;
}
// 倒序遍历,确保每个物品只使用一次
// 如果正序遍历,dp[i] 的更新会影响后续的 dp[i + weight],导致物品被多次使用。
for (int i = target; i > 0; i--) {
if (i >= weight) {
dp[i] = dp[i] || dp[i - weight];
}
}
}
return dp[target];
例题:
- 分隔等和子集(0/1背包问题)
热门题🔥
32. 最长有效括号
问题描述:给定只包含 '('
和 ')'
的字符串,找出最长有效且连续括号子串的长度。
- 示例 1:输入:
"(()"
输出:2- 示例 2:输入:
")()())"
输出:4- 示例 3:输入:
"()(())"
输出:6
思路:定义 dp[i]
表示以下标 i
字符结尾的最长有效括号长度。以 '('
结尾的子串对应的 dp
值必定为 0
,因此我们只需讨论以 ')'
结尾的情况:
s[i] = ')'
且s[i−1] = '('
:此时字符串形如"……()"
。因此i > 1
时,有dp[i] = dp[i−2] + 2
;如果i = 1
,则dp[i]
直接等于2
。s[i] = ')'
且s[i−1] = ')'
:此时字符串形如"……))"
。因此去找i
前面对应字符prev
是否为'('
,其下标为i
减去前一个字符已匹配的长度(i - dp[i-1] - 1
);如果匹配,则dp[i]
在dp[i-1]
的基础上加2
;如果prev
前还有字符,则再加上prev
前已匹配的长度dp[i - dp[i-1] - 2]
。public int longestValidParentheses(String s) { int len = s.length(); if (len == 0) return 0; int max = 0; int[] dp = new int[len]; dp[0] = 0; for (int i = 1; i < len; i++) { if (s.charAt(i) == ')') { if (s.charAt(i - 1) == '(') { dp[i] = 2 + (i > 1 ? dp[i - 2] : 0); } else if (i - dp[i - 1] - 1 >= 0 && s.charAt(i - dp[i - 1] - 1) == '(') { int prev = i - dp[i - 1] - 2; dp[i] = dp[i - 1] + 2 + (prev > 0 ? dp[prev] : 0); } max = Math.max(max, dp[i]); } } return max; }
300. 最长递增子序列
问题描述:找出整数数组 nums
中最长严格递增子序列(可不连续但按序)的长度。
- 示例 1:输入:
[0,1,0,3,2,3]
输出:4
(即[0,1,2,3]
)- 示例 2:输入:
[7,7,7,7,7,7,7]
输出:1
(即[7]
)
思路:定义 dp[i]
表示以下标 i
的整数结尾的子序列最长长度。
- 状态转移方程:
dp[i] = max{dp[j]} + 1
,其中0 < j < i, nums[j] > nums[i]
;若所有nums[j]
都大于nums[i]
,则dp[i] = 1
。 - 初始值:
dp[0] = 1
。
public int lengthOfLIS(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
Arrays.fill(dp, 1);
for (int i = 1; i < len; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int max = 0;
for (int v : dp) {
max = Math.max(max, v);
}
return max;
}
文章标题:动态规划
文章作者:nek0peko
文章链接:https://nek0peko.com/index.php/archives/124/
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,未经站长允许不得对文章文字内容进行修改演绎。
本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证