1 题目
给定一个包含非负整数的 m xn 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 20
2 代码实现
c++
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp (m , vector<int>(n));
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 j = 1 ; j < n ; j++){
dp[0][j] = dp[0][j -1 ] + grid[0][j];
}
for (int i = 1 ; i < m ; i ++){
for (int j =1 ; j < n ; j ++){
dp[i][j] = min (dp[i - 1][j] , dp [i][j -1 ]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
};
js
javascript
/**
* @param {number[][]} grid
* @return {number}
*/
var minPathSum = function(grid) {
const m = grid.length ;
const n = grid[0].length ;
const dp = new Array(m);
for (let i = 0 ; i < m ; i ++){
dp[i] = new Array(n);
}
dp[0][0] = grid[0][0];
for (let i = 1 ; i < m ; i ++){
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (let j = 1 ; j < n ; j ++){
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (let i = 1 ; i < m ; i++){
for (let 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];
};
思考
蛤?这为何多维动态规划,这该怎么做,我以为是DFS呢,不过按照已有的情况,应该是在维护一个dp数组,每次走的时候看看是下还是右比较大,不对,并不是每次都挑最小的走就行了,万一是在99和98里面挑呢,前面的都是1,这该怎么做??!不会啊!
我理解题解了,只不过是图的形式,二维数组我可以的。
题解
我完全懂你的困惑!你一开始想每次选最小的走 (贪心),但这绝对是错的 !贪心只看眼前一步,DP 看的是全局最优,这就是这道题必须用动态规划的原因。
我用最简单、最直白、最容易理解的 DP 数组思路,一步一步带你吃透。
一、先搞懂核心:为什么不能贪心?
看示例 1:
1 3 1
1 5 1
4 2 1
如果贪心:起点 1 → 右边 3(小)→ 右边 1 → 下边 1 → 下边 1,总和 7(刚好对)。
换个例子:
1 100
99 1
贪心:1 → 右 100(比下 99 大!)→ 下 1,总和 102 ❌正确:1 → 下 99 → 右 1,总和 101 ✅
结论 :不能只看当前一步选最小,必须看走到当前位置的所有路径里,总和最小的那个 → 这就是 DP!
二、动态规划核心三要素(死记硬背都能做)
1. 定义 DP 数组(最关键)
dp[i][j]:从左上角 (0,0) 走到 位置 (i,j) 的最小路径和
这就是我们要维护的数组,最终答案就是 dp[m-1][n-1](右下角)。
2. 状态转移方程(怎么算)
题目规定:只能向右 或 向下走 所以,走到 (i,j) 只有 2 种来源:
- 从上边
(i-1,j)走下来 - 从左边
(i,j-1)走过来
我们要最小和,所以选两个来源里更小的那个,加上当前格子的值:
cpp
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
3. 初始化(边界条件)
- 第一行:只能从左边一直走过来,没有上边
- 第一列:只能从上边一直走下来,没有左边
三、完整解题步骤(手绘逻辑)
以示例 1:grid = [[1,3,1],[1,5,1],[4,2,1]]
第一步:创建和 grid 一样大的 dp 数组
3 行 3 列,初始全空。
第二步:初始化起点
dp[0][0] = grid[0][0] = 1
第三步:初始化第一行
只能从左来:dp[0][1] = dp[0][0] + 3 = 4``dp[0][2] = dp[0][1] + 1 = 5
第四步:初始化第一列
只能从上来:dp[1][0] = dp[0][0] + 1 = 2``dp[2][0] = dp[1][0] + 4 = 6
第五步:填充剩余所有位置
dp[1][1] = min(dp[0][1], dp[1][0]) +5 = min(4,2)+5=7dp[1][2] = min(dp[0][2], dp[1][1]) +1 = min(5,7)+1=6dp[2][1] = min(dp[1][1], dp[2][0]) +2 = min(7,6)+2=8dp[2][2] = min(dp[1][2], dp[2][1]) +1 = min(6,8)+1=7
✅ 最终答案:7,完美匹配示例!
四、C++ 代码实现(最简版,直接抄)
我写了最容易理解的版本,没有任何优化,纯 DP 数组,注释拉满:
cpp
#include <iostream>
#include <vector>
#include <algorithm> // 用 min 函数
using namespace std;
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
// 1. 获取网格的行数和列数
int m = grid.size();
int n = grid[0].size();
// 2. 创建 dp 数组,大小和 grid 完全一样
vector<vector<int>> dp(m, vector<int>(n));
// 3. 初始化起点:左上角
dp[0][0] = grid[0][0];
// 4. 初始化第一行:只能从左边来
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j-1] + grid[0][j];
}
// 5. 初始化第一列:只能从上边来
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0] + grid[i][0];
}
// 6. 填充剩下的所有位置(核心状态转移)
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 选 上边 / 左边 最小的 + 当前值
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
// 7. 返回右下角的值,就是答案
return dp[m-1][n-1];
}
};
// 测试代码
int main() {
vector<vector<int>> grid = {{1,3,1},{1,5,1},{4,2,1}};
Solution s;
cout << s.minPathSum(grid) << endl; // 输出 7
return 0;
}
五、代码逐行解释(小白必看)
- 获取行列 :
m是行数,n是列数 - 创建 dp 数组 :和输入网格一样大,专门存最小路径和
- 起点初始化:起点的最小和就是它自己
- 第一行初始化:只能向右走,累加左边的值
- 第一列初始化:只能向下走,累加上边的值
- 核心循环 :
- 遍历每一个格子
- 看上边 和左边哪个和更小
- 加上当前格子的值,就是当前位置的最小和
- 返回结果:右下角就是从起点到终点的最小和
六、这个思路为什么是对的?
- 我们没有做任何贪心选择,没有只看当前一步
- 每个位置都存了走到这里的全局最优解
- 后面的位置只依赖前面已经算好的最优解
- 时间复杂度:
O(m*n)(遍历一遍网格,最快) - 空间复杂度:
O(m*n)(用了一个 dp 数组,最简单)
总结
- DP 数组定义 :
dp[i][j]= 走到 (i,j) 的最小路径和 - 转移方程 :
dp[i][j] = min(上边, 左边) + 当前值 - 初始化:第一行、第一列只能单向走,直接累加
- 结果:dp 数组右下角的值
3 题目
给你一个字符串 s,找到 s 中最长的 回文 子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
1 <= s.length <= 1000s仅由数字和英文字母组成
4 代码实现
c++
cpp
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n <= 1 ) return s ;
vector<vector<bool>> dp (n , vector<bool>(n,false));
int maxLen = 1 ;
int start = 0 ;
for (int i = 0 ; i < n ; i++){
dp[i][i] = true ;
}
for (int l = 2 ; l <= n ; l++){
for (int i = 0 ; i < n ; i++){
int j = i + l - 1;
if ( j >= n ) break ;
if (s[i] == s[j]){
if (j == i + 1){
dp[i][j] = true ;
}else {
dp[i][j] = dp[i + 1][j - 1];
}
}else{
dp[i][j] = false ;
}
if (dp[i][j] && l > maxLen){
maxLen = l ;
start = i ;
}
}
}
return s.substr(start,maxLen);
}
};
js
javascript
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
const len = s.length ;
if (len <= 1 ) return s ;
const dp = new Array(len);
for (let i = 0 ; i < len ; i++){
dp[i] = new Array(len).fill(false);
}
for (let i = 0 ; i < len ; i++){
dp[i][i] = true ;
}
let maxLen = 1 ;
let start = 0 ;
for (let l = 2 ; l <= len ; l ++ ){
for (let i = 0 ; i < len ; i++){
const j = i + l - 1 ;
if (j >= len) break ;
if (s[i] === s[j]){
if (j - i === 1){
dp[i][j] = true ;
}else {
dp[i][j] = dp [i + 1][j -1 ];
}
}
if (dp[i][j] && l > maxLen ){
maxLen = l ;
start = i ;
}
}
}
return s.substring(start , start + maxLen);
};
思考
我尝试冷静下来思考。。。
这应该如何做,我首先想到的是双指针,啊,这是啥意思哦。。。
dp数组,首先,回文的基础上是如何维护的,如何维护又一个,呃,这怎么办,不知道,感觉不是之前很简单判断向哪个方向走就可以了,而且这里要维护长度。
不会做呃没想到多为动规的思路。
题解
一、先搞懂:什么是回文?
回文:正读和反读一模一样 比如:aba、bb、a、ccacc
核心规律:如果一个字符串首尾两个字符相等 ,并且中间的子串也是回文 ,那它整体就是回文!例:abba
- 首尾
a == a - 中间
bb是回文→ 所以abba是回文
二、DP 动态规划 三要素(死记就能做)
1. DP 数组定义(最关键)
dp[i][j]:布尔值 ,表示字符串 s 从下标 i 到 j 的子串,是不是回文
true= 是回文false= 不是回文
2. 状态转移方程(核心公式)
-
如果 s [i] == s [j](首尾相等)
- 情况 1:子串长度 ≤2(
i和j重合 / 相邻)→ 一定是回文 例:a、bb - 情况 2:子串长度 >2 → 看中间的子串是不是回文 即:
dp[i][j] = dp[i+1][j-1]
- 情况 1:子串长度 ≤2(
-
如果 s [i] != s [j] → 直接不是回文:
dp[i][j] = false
最终公式:
js
if (s[i] === s[j]) {
// 长度<=2 必然是回文;长度>2 看中间
dp[i][j] = (j - i <= 2) || dp[i+1][j-1];
}
3. 初始化
所有单个字符都是回文:dp[i][i] = true
三、解题思路(超直白)
- 建一个二维 DP 数组,记录所有子串是否为回文
- 从小到大遍历子串长度,判断每个子串是不是回文
- 记录最长的那个回文子串
四、完整 JS 代码(最简 DP 版,直接抄)
javascript
运行
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
const len = s.length;
// 特殊情况:长度为1直接返回
if (len <= 1) return s;
// 1. 创建 dp 二维数组,len x len 大小
const dp = new Array(len);
for (let i = 0; i < len; i++) {
dp[i] = new Array(len).fill(false);
}
// 2. 初始化:单个字符一定是回文
for (let i = 0; i < len; i++) {
dp[i][i] = true;
}
// 记录最长回文的起始下标 和 最大长度
let maxLen = 1;
let start = 0;
// 3. 核心:遍历所有子串(从长度2开始)
// l 代表子串长度
for (let l = 2; l <= len; l++) {
// i 是子串起始下标
for (let i = 0; i < len; i++) {
// j 是子串结束下标
const j = i + l - 1;
// 越界直接退出
if (j >= len) break;
if (s[i] === s[j]) {
// 长度2:必然是回文
if (j - i === 1) {
dp[i][j] = true;
}
// 长度>2:看中间子串
else {
dp[i][j] = dp[i+1][j-1];
}
}
// 4. 如果当前是回文,更新最长回文
if (dp[i][j] && l > maxLen) {
maxLen = l;
start = i;
}
}
}
// 5. 截取最长回文并返回
return s.substring(start, start + maxLen);
};
五、代码逐行解释(小白必看)
- 特殊判断:字符串长度为 1,本身就是最长回文
- 创建 DP 数组 :
len x len的布尔数组,存是否为回文 - 初始化 :所有单个字符
dp[i][i]=true - 遍历子串 :
l:子串长度(从 2 开始,因为 1 已经初始化)i:子串起点,j:子串终点
- 判断回文 :
- 首尾相等 + 中间是回文 → 当前子串是回文
- 更新最长:发现更长的回文,记录起点和长度
- 返回结果:截取最长回文子串
六、测试示例
javascript
运行
// 示例1
console.log(longestPalindrome("babad")); // 输出 "bab" 或 "aba"
// 示例2
console.log(longestPalindrome("cbbd")); // 输出 "bb"
七、和上一题(最小路径和)的区别
- 最小路径和:
dp[i][j]存数字(最小和) - 最长回文子串:
dp[i][j]存布尔值(是否回文) - 核心逻辑:用已知的小问题,推导未知的大问题
- DP 数组定义 :
dp[i][j]= 子串 s [i~j] 是否为回文 - 核心公式:首尾相等 + 中间是回文 → 整体是回文
- 遍历方式:从短子串到长子串
- 结果:记录最长的回文子串
dp[i][j] = dp[i + 1][j - 1]
dp[i][j] 表示:从 i 到 j 的子串是不是回文
如果 s[i] == s[j] (两头相等)那 中间那一段必须是回文,整个串才是回文!
中间那一段就是:i+1 到 j-1
所以:
cpp
dp[i][j] = dp[i+1][j-1]
看字符串:a b c b a下标: 0 1 2 3 4
我们想判断:i=0,j=4 → 子串 a b c b a 是不是回文?
步骤:
- 看两头:
s[0] = a,s[4] = a→ 相等 - 那中间是什么?中间是
i+1=1到j-1=3→b c b - 如果 中间是回文,那整个串就是回文!
所以:
cpp
dp[0][4] = dp[1][3]
再看一个更简单的:a b b a
下标:0 1 2 3
判断 i=0,j=3
s[0]=a,s[3]=a→ 相等- 中间是
i+1=1到j-1=2→b b bb是回文 → 所以整个abba是回文
cpp
dp[0][3] = dp[1][2]
dp[i][j] = dp[i+1][j-1]
翻译成人话:
两头一样 → 那我只看中间中间是回文 → 我就是回文中间不是 → 我也不是
因为回文的核心规则 就是:一个字符串是回文 = 两头相等 + 中间也是回文
这就是动态规划的核心:用小问题的答案,推出大问题的答案!
cpp
子串:a [ b c b ] a
i i+1 j-1 j
dp[i][j] = 是不是回文?
= 看 dp[i+1][j-1]
dp[i][j]= i 到 j 是不是回文- 两头一样 → 看中间
- 中间是
i+1 ~ j-1→ 所以dp[i][j] = dp[i+1][j-1]
5 小结
好蠢好蠢,多学多练,主要是细节,还有思路上都是类似的,有点高中数列的味道,靠小的推大的。
加油!!还有二维数组的js写法!!要非常注意!!