一、不同路径
1、题目
力扣链接:https://leetcode.cn/problems/unique-paths/
2、解法
1)定义dp数组:我们定义dp[i][j]为到达第i行第j列的网格的路径种类数,注意i和j从0开始,与代码中数组的定义一致,那么最后到达右下角的路径数为dp[m-1][n-1];
2)状态转移方程:由于每次只能向下或向右移动一步,那么状态转移方程为dp[i][j] = dp[i-1][j] + dp[i][j-1];
3)确定初始值:dp[0][j]和dp[i][0]表示第一行和第一列的网格,只有一种方式到达,即向右和向下,因此都为1,注意后面遍历时,从初始值之后开始;
4)确定子问题的最优解如何组合为原问题的解:答案为dp[m-1][n-1]。
Python:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n] + [[1] + [0] * (n-1) for _ in range(m - 1)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m-1][n-1]
C++:
class Solution {
public:
int uniquePaths(int m, int n) {
int dp[m][n];
// 初始化
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i++){
dp[0][i] = 1;
}
// 推导出 dp[m-1][n-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];
}
};
二、最小路径和
1、题目
力扣链接:https://leetcode.cn/problems/minimum-path-sum/
2、解法
1)定义dp数组:我们定义dp[i][j]为到达第i行第j列的网格的最小路径和,注意i和j从0开始,与代码中数组的定义一致,那么最后到达右下角的最小路径和为dp[m-1][n-1];
2)状态转移方程:由于每次只能向下或向右移动一步,并且当前网格肯定要加上,因此只需要比较到达上一个网格和左面一个网格的最小路径和的大小,加上小的那个,就可以确保路径和一直为最小。那么状态转移方程为dp[i][j] = grid[i][j] + min( dp[i-1][j], dp[i][j-1] );
3)确定初始值:dp[0][j]和dp[i][0]表示第一行和第一列的网格,只有一种方式到达,即向右和向下,因此最小路径和一直加下去即可,注意后面遍历时,从初始值之后开始;
4)确定子问题的最优解如何组合为原问题的解:答案为dp[m-1][n-1]。
Python:
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [[0 for _ in range(n)] for _ in range(m)]
dp[0][0] = grid[0][0]
for i in range(1, n):
dp[0][i] = dp[0][i-1] + grid[0][i]
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
return dp[m-1][n-1]
C++:
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, 0));
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 i = 1; i < m; i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for (int i = 1; i < m; i++){
for (int j = 1; j < n; j++){
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m-1][n-1];
}
};
三、最长回文子串
1、题目
力扣链接:https://leetcode.cn/problems/longest-palindromic-substring
2、解法
一般动态规划求解字符串的题,尤其涉及到子串的题,很多都会使用二维数组作为dp来解决。
1)定义dp数组:设n = len(s),即字符串的长度。既然我们需要求出最长的回文子串,我们首先需要判断子串是否的回文的,那不如我们直接定义dp[i][j]为 s 从第i个位置到第j个位置之间子串是否为回文子串,即bool值。由于j肯定大于i,因此dp数组左上角就完全可以等价于字符串s的所有子串,也就满足了动态规划的最优子结构的要求;
2)状态转移方程:要判断一个子串是否为回文,可以等价于判断是 s[i]是否等于s[j]且s的第i+1的位置到第j-1的位置的子串是否为回文,即dp[i+1][j-1]是否为True。因此状态转移方程为dp[i][j] = s[i] == s[j] && dp[i+1][j-1];
3)确定初始值:确定初始值或者说是特殊情况,我们需要仔细考虑dp数组中i和j的取值。根据状态转移方程,当i = j时,该方程中的dp[i+1][j-1]不可用,因为子串只有一个字符,肯定为True;当j - i = 1时,我们也只需要判断s[i] == s[j];当j - i > 1时,就可以使用该状态转移方程。
此外遍历时很重要的一点是遍历顺序,对dp[i][j]通过状态转移方程赋值时,dp[i+1][j-1]必须是已经计算过的,否则没有意义。因此,i必须要从后往前遍历,j必须从前往后遍历,而且j必须大于等于i,这样dp[i+1][j-1]才是已经计算过的!
4)确定组合方式以求解原问题:定义三个变量left,right,和max_len分别记录最长回文子串的左位置,右位置和长度,遍历过程中不断更新,那么最后的结果为s[i, j+1]。
Python:
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n == 1:
return s
if n == 2 and s[0] == s[1]:
return s
dp = [[False] * n for _ in range(n)]
max_len = 0
left = 0
right = 0
for i in range(n-1, -1, -1):
for j in range(i, n):
if j - i <= 1:
dp[i][j] = s[i] == s[j]
else:
dp[i][j] = s[i] == s[j] and dp[i+1][j-1]
if dp[i][j] and j-i+1 > max_len:
max_len = j-i+1
left = i
right = j
return s[left: right+1]
C++:
知识点:用s.substr(n, m)返回从n开始长度为m的字符,返回子字符串,只填写一位数字n,就默认返回[n,s.size()),一些C++常用的函数可见该博客:这些c++的基础知识,你都知道吗
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
int maxlenth = 0;
int left = 0;
int right = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
}
}
return s.substr(left, maxlenth);
}
};