动态规划
| 题号 | 题目名称 | 核心思路 | 时间复杂度 | 空间复杂度 | 代码亮点 | 牛客原题链接 |
|---|---|---|---|---|---|---|
| BM62 | 斐波那契数列 | 动态规划(迭代) / 递归 | O(n) / O(2ⁿ) | O(1) / O(n) | 迭代版空间 O(1) 优化 | 🔗 直达 |
| BM63 | 跳台阶 | DP:前两项和 | O(n) | O(1) | 与斐波那契同模型 | 🔗 直达 |
| BM64 | 最小花费爬楼梯 | DP:min(前两项花费+成本) | O(n) | O(1) | 边界 dp[0]=dp[1]=0 技巧 | 🔗 直达 |
| BM65 | 最长公共子序列 | 二维 DP + 回溯构造子序列 | O(n·m) | O(n·m) | 方向矩阵 b[][] 回溯输出 LCS | 🔗 直达 |
| BM66 | 最长公共子串 | 二维 DP + 记录 max 长度位置 | O(n·m) | O(n·m) | 子串必须连续 | 🔗 直达 |
| BM67 | 不同路径的数目(一) | DP 累加 / 组合数 C(m+n-2,n-1) | O(n·m) | O(min(n,m)) | 组合数写法更简洁 | 🔗 直达 |
| BM68 | 矩阵的最小路径和 | 二维 DP:min(上,左)+当前值 | O(n·m) | O(n·m) | 从左至右、从上至下 | 🔗 直达 |
| BM69 | 数字翻译字符串 | 线性 DP:单双数字解码 | O(n) | O(1) | 用于存储每个位置的解码方法数 | 🔗 直达 |
| BM70 | 兑换零钱(一) | 完全背包求最小硬币数 | O(n·aim) | O(aim) | INF=0x3f3f3f3f 初始化技巧 | 🔗 直达 |
| BM71 | 最长上升子序列 | 线性 DP + 贪心优化 | O(n²) | O(n) | 初始化动态规划数组,每个位置的初始值为 1,因为每个元素自身可以看作长度为 1 的递增子序列 | 🔗 直达 |
| BM72 | 连续子数组最大和 | Kadane:max(前和+当前, 当前) | O(n) | O(1) | 经典最大子段和 | 🔗 直达 |
| BM73 | 最长回文子串 | 中心扩展 / Manacher | O(n²) | O(1) | 奇偶中心双指针 | 🔗 直达 |
| BM74 | 复原 IP 地址 | 三重循环枚举三段分割 | O(1) | O(1) | 剪枝提前结束 | 🔗 直达 |
| BM75 | 编辑距离(一) | 二维 DP 经典 Levenshtein | O(n·m) | O(n·m) | 可滚动数组优化 | 🔗 直达 |
| BM76 | 正则表达式匹配 | 二维 DP:处理 * 和 . | O(n·m) | O(n·m) | 状态转移分三种情况 | 🔗 直达 |
| BM77 | 最长括号子串 | 栈 / 线性 DP | O(n) | O(n) / O(1) | 栈存索引或 DP 分段累加 | 🔗 直达 |
| BM78 | 打家劫舍(一) | 线性 DP:不相邻取数 | O(n) | O(1) | dp[i]=max(dp[i-1],dp[i-2]+nums[i-1]) | 🔗 直达 |
| BM79 | 打家劫舍(二) | 环形 DP:拆成两段线性 DP | O(n) | O(1) | 两次 DP 取 max | 🔗 直达 |
| BM80 | 买卖股票(一) | 一次交易 DP / 贪心 | O(n) | O(1) | minPrice 贪心写法更简洁 | 🔗 直达 |
| BM81 | 买卖股票(二) | 无限交易贪心 / DP | O(n) | O(1) | 贪心:累加所有上升段 | 🔗 直达 |
| BM82 | 买卖股票(三) | 最多两次交易状态机 DP | O(n) | O(1) | 5 个状态滚动更新 | 🔗 直达 |
✅ 复习顺序建议(由易到难)
BM62 → BM63 → BM64 → BM69 → BM67 → BM68 → BM70 → BM71 → BM72 → BM77 → BM78 → BM79 → BM80 → BM81 → BM82 → BM65 → BM66 → BM75 → BM76
BM62 斐波那契数列
斐波那契数列_牛客题霸_牛客网
https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
- 动态规划
import java.util.*;
public class Solution {
// 1. 计算斐波那契数列的第 n 项
public int Fibonacci(int n) {
List<Integer> fib = new ArrayList<Integer>(); // 2. 创建一个列表,用于存储斐波那契数列
fib.add(0); // 3. 添加第 0 项,值为 0
fib.add(1); // 4. 添加第 1 项,值为 1
for (int i = 2; i <= n; i++) { // 5. 从第 2 项开始计算,直到第 n 项
fib.add(fib.get(i - 1) + fib.get(i - 2)); // 6. 计算当前项,值为前两项之和
}
return fib.get(n); // 7. 返回第 n 项的值
}
}
- 空间优化 动态规划
import java.util.*;
public class Solution {
public int Fibonacci(int n) {
if (n <= 1) return n; // 1. 基准情况:F(0)=0, F(1)=1
int a = 0, b = 1, c = 0; // 2. 初始化 F(0), F(1), 临时变量
for (int i = 2; i <= n; i++) { // 3. 从 F(2) 递推到 F(n)
c = a + b; // 4. 计算当前项
a = b; // 5. 更新前两项
b = c;
}
return c; // 6. 返回 F(n)
}
}
- 递归
public class Solution {
// 1. 计算斐波那契数列的第 n 项
public int Fibonacci(int n) {
// 2. 如果 n 小于等于 1,直接返回 n(第 0 项是 0,第 1 项是 1)
if (n <= 1) return n;
else {
// 3. 根据斐波那契数列的定义,递归计算第 n 项
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
}
BM63 跳台阶
跳台阶_牛客题霸_牛客网
https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
// 1. 计算青蛙跳到第 number 级台阶的方法数
public int jumpFloor(int number) {
List<Integer> dp = new ArrayList<>(); // 2. 创建一个列表,用于存储每级台阶的方法数
dp.add(1); // 3. 第 0 级台阶有 1 种方法(即站在地上)
dp.add(1); // 4. 第 1 级台阶有 1 种方法(跳1级)
for (int i = 2; i <= 40; i++) { // 5. 从第 2 级台阶开始计算,直到第 40 级
dp.add(dp.get(i - 1) + dp.get(i - 2)); // 6. 当前级的方法数等于前两级方法数之和
}
return dp.get(number); // 7. 返回第 number 级台阶的方法数
}
}
BM64 最小花费爬楼梯
最小花费爬楼梯_牛客题霸_牛客网
https://www.nowcoder.com/practice/6fe0302a058a4e4a834ee44af88435c7?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
2025年8月23日20:51:18 2025年8月23日20:57:18
import java.util.*;
public class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length; // 1. 获取楼梯的阶数
int[] dp = new int[n + 1]; // 2. 创建一个数组,用于存储到达每一阶的最小成本
// 3. 初始化前两阶的成本,因为可以从地面或第一阶开始,所以初始成本为0
dp[0] = 0;
dp[1] = 0;
// 4. 从第2阶开始计算,直到第n阶
for (int i = 2; i <= n; i++) {
// 5. 到达第i阶的最小成本等于到达前两阶的最小成本加上对应的楼梯成本
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n]; // 6. 返回到达最后一阶的最小成本
}
}
BM65 最长公共子序列(二)
最长公共子序列(二)_牛客题霸_牛客网
https://www.nowcoder.com/practice/6d29638c85bb4ffd80c020fe244baf11?tpId=295&tqId=991075&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
String x = ""; // 1. 存储第一个字符串
String y = ""; // 2. 存储第二个字符串
// 3. 递归函数,用于根据方向矩阵 b 构造 LCS
String ans(int l1, int l2, int[][] b) {
String res = ""; // 4. 初始化结果字符串
if (l1 == 0 || l2 == 0) { // 5. 如果某个字符串的长度为 0,直接返回空字符串
return res;
}
if (b[l1][l2] == 1) { // 6. 如果方向矩阵 b[l1][l2] 为 1,表示当前字符是 LCS 的一部分
res += ans(l1 - 1, l2 - 1, b); // 7. 递归构造 LCS,并添加当前字符
res += x.charAt(l1 - 1); // 8. 添加当前字符
} else if (b[l1][l2] == 2) { // 9. 如果方向矩阵 b[l1][l2] 为 2,表示从上一个状态转移过来
res += ans(l1 - 1, l2, b); // 10. 递归构造 LCS
} else { // 11. 如果方向矩阵 b[l1][l2] 为 3,表示从左边的状态转移过来
res += ans(l1, l2 - 1, b); // 12. 递归构造 LCS
}
return res; // 13. 返回构造的 LCS
}
// 14. 主函数,计算两个字符串的 LCS
public String LCS(String s1, String s2) {
int l1 = s1.length(); // 15. 获取第一个字符串的长度
int l2 = s2.length(); // 16. 获取第二个字符串的长度
if (l1 == 0 || l2 == 0) { // 17. 如果某个字符串为空,直接返回 "-1"
return "-1";
}
x = s1; // 18. 存储第一个字符串
y = s2; // 19. 存储第二个字符串
int[][] dp = new int[l1 + 1][l2 + 1]; // 20. 创建动态规划表,用于存储 LCS 的长度
int[][] b = new int[l1 + 1][l2 + 1]; // 21. 创建方向矩阵,用于记录构造 LCS 的路径
// 22. 动态规划计算 LCS 的长度,并填充方向矩阵
for (int i = 1; i <= l1; i++) {
for (int j = 1; j <= l2; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) { // 23. 如果当前字符匹配
dp[i][j] = dp[i - 1][j - 1] + 1; // 24. LCS 长度加 1
b[i][j] = 1; // 25. 标记当前字符是 LCS 的一部分
} else {
if (dp[i - 1][j] > dp[i][j - 1]) { // 26. 如果从上一个状态转移过来的 LCS 更长
dp[i][j] = dp[i - 1][j]; // 27. 更新 LCS 长度
b[i][j] = 2; // 28. 标记从上一个状态转移过来
} else { // 29. 如果从左边的状态转移过来的 LCS 更长
dp[i][j] = dp[i][j - 1]; // 30. 更新 LCS 长度
b[i][j] = 3; // 31. 标记从左边的状态转移过来
}
}
}
}
String res = ans(l1, l2, b); // 32. 调用递归函数构造 LCS
if (res.isEmpty()) { // 33. 如果结果为空,返回 "-1"
return "-1";
}
return res; // 34. 返回构造的 LCS
}
}
BM66 最长公共子串
最长公共子串_牛客题霸_牛客网
https://www.nowcoder.com/practice/f33f5adc55f444baa0e0ca87ad8a6aac?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
public String LCS(String str1, String str2) {
int[][] dp = new int[str1.length() + 1][str2.length() + 1]; // 1. 创建动态规划表,用于存储最长公共子串的长度
int max = 0; // 2. 初始化最长公共子串的长度
int pos = 0; // 3. 初始化最长公共子串的结束位置
// 4. 动态规划计算最长公共子串的长度
for (int i = 1; i <= str1.length(); i++) {
for (int j = 1; j <= str2.length(); j++) {
if (str1.charAt(i - 1) == str2.charAt(j - 1)) { // 5. 如果当前字符匹配
dp[i][j] = dp[i - 1][j - 1] + 1; // 6. 当前位置的最长公共子串长度等于前一个位置的长度加1
if (dp[i][j] > max) { // 7. 更新最长公共子串的长度和结束位置
max = dp[i][j];
pos = i - 1;
}
} else {
dp[i][j] = 0; // 8. 如果当前字符不匹配,当前位置的最长公共子串长度为0
}
}
}
// 9. 返回最长公共子串
return str1.substring(pos - max + 1, pos + 1);
}
}
BM67 不同路径的数目(一)
不同路径的数目(一)_牛客题霸_牛客网
https://www.nowcoder.com/practice/166eaff8439d4cd898e3ba933fbc6358?tpId=295&tqId=685&sourceUrl=%2Fexam%2Foj
- 动态规划
import java.util.*;
public class Solution {
// 1. 计算从网格的左上角到右下角的唯一路径数
public int uniquePaths(int m, int n) {
int[][] dp = new int[m + 1][n + 1]; // 2. 创建一个动态规划表,用于存储到达每个位置的路径数
dp[0][0] = 1; // 3. 初始化起点的路径数为1
// 4. 初始化第一列的路径数,只能从上方来
for (int i = 1; i < m; i++) {
dp[i][0] = 1;
}
// 5. 初始化第一行的路径数,只能从左方来
for (int i = 1; i < n; i++) {
dp[0][i] = 1;
}
// 6. 动态规划计算每个位置的路径数
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]; // 7. 到达当前位置的路径数等于上方和左方路径数之和
}
}
return dp[m - 1][n - 1]; // 8. 返回到达右下角的路径数
}
}
- 组合数学
import java.util.*;
public class Solution {
// 1. 计算从网格的左上角到右下角的唯一路径数
public int uniquePaths(int m, int n) {
long res = 1; // 2. 初始化结果为 1
int x = n, y = 1; // 3. 初始化变量 x 和 y,x 表示分子的起始值,y 表示分母的起始值
while (y < m) { // 4. 当 y 小于 m 时,继续计算
res = res * x / y; // 5. 更新结果,每次乘以 x 并除以 y
x++; // 6. 分子 x 增加 1
y++; // 7. 分母 y 增加 1
}
return (int) res; // 8. 返回结果,强制转换为 int 类型
}
}
BM68 矩阵的最小路径和
矩阵的最小路径和_牛客题霸_牛客网
https://www.nowcoder.com/practice/7d21b6be4c6b429bb92d219341c4f8bb?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
- 动态规划
import java.util.*;
public class Solution {
public int minPathSum(int[][] matrix) {
int m = matrix.length; // 1. 获取矩阵的行数
int n = matrix[0].length; // 2. 获取矩阵的列数
int[][] dp = new int[m][n]; // 3. 创建一个动态规划表,用于存储到达每个位置的最小路径和
for (int i = 0; i < m; i++) { // 4. 遍历矩阵的每一行
for (int j = 0; j < n; j++) { // 5. 遍历矩阵的每一列
if (i == 0 && j == 0) { // 6. 如果是左上角的起点
dp[0][0] = matrix[0][0]; // 7. 起点的最小路径和就是它本身的值
} else if (i == 0) { // 8. 如果在第一行,只能从左边来
dp[i][j] = matrix[i][j] + dp[i][j - 1]; // 9. 当前位置的最小路径和等于左边的最小路径和加上当前值
} else if (j == 0) { // 10. 如果在第一列,只能从上边来
dp[i][j] = matrix[i][j] + dp[i - 1][j]; // 11. 当前位置的最小路径和等于上边的最小路径和加上当前值
} else { // 12. 其他位置可以从左边或上边来
dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + matrix[i][j]; // 13. 当前位置的最小路径和等于左边和上边的最小路径和中较小的一个加上当前值
}
}
}
return dp[m - 1][n - 1]; // 14. 返回到达右下角的最小路径和
}
}
- 直接在原数组上修改
import java.util.*;
public class Solution {
public int minPathSum(int[][] matrix) {
int m = matrix.length; // 1. 获取矩阵的行数
int n = matrix[0].length; // 2. 获取矩阵的列数
for (int i = 0; i < m; i++) { // 3. 遍历矩阵的每一行
for (int j = 0; j < n; j++) { // 4. 遍历矩阵的每一列
if (i == 0 && j == 0) { // 5. 如果是左上角的起点
continue; // 6. 跳过,因为起点的值不需要更新
} else if (i == 0) { // 7. 如果在第一行
matrix[i][j] += matrix[i][j - 1]; // 8. 当前位置的值加上左边的值
} else if (j == 0) { // 9. 如果在第一列
matrix[i][j] += matrix[i - 1][j]; // 10. 当前位置的值加上上边的值
} else { // 11. 其他位置
matrix[i][j] += Math.min(matrix[i][j - 1], matrix[i - 1][j]); // 12. 当前位置的值加上左边和上边的较小值
}
}
}
return matrix[m - 1][n - 1]; // 13. 返回右下角的值,即最小路径和
}
}
BM69 把数字翻译成字符串
把数字翻译成字符串_牛客题霸_牛客网
https://www.nowcoder.com/practice/046a55e6cd274cffb88fc32dba695668?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
public int solve(String nums) {
int n = nums.length(); // 1. 获取字符串的长度
if (nums.charAt(0) == '0' || n == 0) return 0; // 2. 如果字符串以 '0' 开头或为空,返回 0
int[] dp = new int[n + 1]; // 3. 创建一个动态规划数组,用于存储每个位置的解码方法数
dp[0] = 1; // 4. 初始化 dp[0] 为 1,表示空字符串有一种解码方法
for (int i = 1; i < n; i++) { // 5. 遍历字符串
if (nums.charAt(i) != '0') dp[i] = dp[i - 1]; // 6. 如果当前字符不是 '0',继承前一个位置的解码方法数
int t = (nums.charAt(i - 1) - '0') * 10 + (nums.charAt(i) - '0'); // 7. 计算当前字符和前一个字符组成的两位数
if (t >= 10 && t <= 26) { // 8. 如果这个两位数在 10 到 26 之间
if (i == 1) { // 9. 如果是第二个字符
dp[i] += 1; // 10. 增加一种解码方法
} else {
dp[i] += dp[i - 2]; // 11. 增加前两个位置的解码方法数
}
}
}
return dp[n - 1]; // 12. 返回最后一个位置的解码方法数
}
}
BM70 兑换零钱(一)
兑换零钱(一)_牛客题霸_牛客网
https://www.nowcoder.com/practice/3911a20b3f8743058214ceaa099eeb45?tpId=295&tqId=988994&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
public int minMoney(int[] arr, int aim) {
if (aim < 1) return 0; // 1. 如果目标金额小于1,返回0
int[] dp = new int[aim + 1]; // 2. 创建一个动态规划数组,用于存储每个金额的最少硬币数量
Arrays.fill(dp, 0x3f3f3f); // 3. 初始化数组为一个很大的数(表示无穷大)
dp[0] = 0; // 4. 初始化dp[0]为0,表示金额为0时不需要任何硬币
for (int i = 1; i <= aim; i++) { // 5. 遍历所有金额,从1到目标金额
for (int j = 0; j < arr.length; j++) { // 6. 遍历所有硬币面值
if (i - arr[j] >= 0) // 7. 如果当前金额减去硬币面值不小于0
dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1); // 8. 更新当前金额的最少硬币数量
}
}
return dp[aim] == 0x3f3f3f ? -1 : dp[aim]; // 9. 如果dp[aim]仍为无穷大,返回-1,否则返回dp[aim]
}
}
BM71 最长上升子序列(一)
最长上升子序列(一)_牛客题霸_牛客网
https://www.nowcoder.com/practice/5164f38b67f846fb8699e9352695cd2f?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
import java.util.*;
public class Solution {
public int LIS(int[] arr) {
int n = arr.length; // 1. 获取数组的长度
if (n == 0) return 0; // 2. 如果数组为空,返回 0
int[] dp = new int[n]; // 3. 创建一个动态规划数组,用于存储每个位置的最长递增子序列长度
Arrays.fill(dp, 1); // 4. 初始化动态规划数组,每个位置的初始值为 1,因为每个元素自身可以看作长度为 1 的递增子序列
int maxl = 1; // 5. 初始化最长递增子序列的长度为 1
for (int i = 1; i < n; i++) { // 6. 遍历数组,从第二个元素开始
for (int j = 0; j < i; j++) { // 7. 遍历当前元素之前的所有元素
if (arr[i] > arr[j] && dp[i] < dp[j] + 1) { // 8. 如果当前元素大于之前的某个元素,并且可以形成更长的递增子序列
dp[i] = dp[j] + 1; // 9. 更新当前元素的最长递增子序列长度
maxl = Math.max(maxl, dp[i]); // 10. 更新最长递增子序列的长度
}
}
}
return maxl; // 11. 返回最长递增子序列的长度
}
}
BM72 连续子数组的最大和
连续子数组的最大和_牛客题霸_牛客网
https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=295&tags=&title=&difficulty=0&judgeStatus=0&rp=0&sourceUrl=%2Fexam%2Foj
- 动态规划
import java.util.*;
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int n = array.length; // 1. 获取数组的长度
int[] dp = new int[n]; // 2. 创建一个动态规划数组,用于存储每个位置的最大子数组和
dp[0] = array[0]; // 3. 初始化第一个位置的最大子数组和为数组的第一个元素
int maxsum = array[0]; // 4. 初始化最大子数组和为数组的第一个元素
for (int i = 1; i < n; i++) { // 5. 遍历数组,从第二个元素开始
dp[i] = Math.max(dp[i - 1] + array[i], array[i]); // 6. 更新当前位置的最大子数组和
maxsum = Math.max(maxsum, dp[i]); // 7. 更新全局最大子数组和
}
return maxsum; // 8. 返回全局最大子数组和
}
}
- 优化空间动态规划
import java.util.*;
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int n = array.length; // 1. 获取数组的长度
int sum = 0; // 2. 初始化当前子数组的和为 0
int maxsum = array[0]; // 3. 初始化最大子数组和为数组的第一个元素
for (int i = 0; i < n; i++) { // 4. 遍历数组
sum = Math.max(sum, 0) + array[i]; // 5. 更新当前子数组的和,如果当前和小于 0,则重置为 0
maxsum = Math.max(maxsum, sum); // 6. 更新全局最大子数组和
}
return maxsum; // 7. 返回全局最大子数组和
}
}

2450

被折叠的 条评论
为什么被折叠?



