,最小路径和可以说是动态规划的基础问题了 ,往后延伸有例如“三角路径和”与“最大正方形”等等矩阵相关的问题,基本可以做到一解通解,还是一样,先给出题目和题解(题解是多样化的),后面对动态规划进行学习
给定一个包含非负整数的
m x n
网格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.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 200
解题基本思想
因为路径只能向下或向右走,所以:
- 第一行的每个格子只能从左边的格子向右走到达,第一列的每个格子只能从上面的格子向下走到达。在这种情况下,路径是唯一的,所以每个格子的最小路径和就是从起点到这个格子的所有数字加起来的总和。
- 对于既不在第一行也不在第一列的其他格子,可以从上方的格子向下走一步到达,也可以从左边的格子向右走一步到达。这种情况下,当前格子的最小路径和等于上方格子和左边格子的最小路径和中较小的那个,再加上当前格子的数值。
由于每个格子的最小路径和都依赖于其相邻格子的最小路径和,所以可以用动态规划的方法来解决这个问题。
C++写法(基础解法与巧解)
通用解法1
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size(), columns = grid[0].size();
auto dp = vector < vector <int> > (rows, vector <int> (columns));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1];
}
};
通用解法2
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<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] = min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[m-1][n-1];
}
};
巧解
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
for (int i = 0; i < grid.size(); i++)
for (int j = !i; j < grid[0].size(); j++)
grid[i][j] += min(i ? grid[i-1][j] : INT_MAX,
j ? grid[i][j-1] : INT_MAX);
return grid.back().back();
}
};
Java解法
class Solution {
public int minPathSum(int[][] grid) {
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[0].length; j++) {
if(i == 0 && j == 0) continue;
else if(i == 0) grid[i][j] = grid[i][j - 1] + grid[i][j];
else if(j == 0) grid[i][j] = grid[i - 1][j] + grid[i][j];
else grid[i][j] = Math.min(grid[i - 1][j],
grid[i][j - 1]) + grid[i][j];
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
}
Python解法
class Solution:
def minPathSum(self, grid: [[int]]) -> int:
for i in range(len(grid)):
for j in range(len(grid[0])):
if i == j == 0: continue
elif i == 0: grid[i][j] = grid[i][j - 1] + grid[i][j]
elif j == 0: grid[i][j] = grid[i - 1][j] + grid[i][j]
else: grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j]
return grid[-1][-1]
动态规划问题
动态规划(简称DP)是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。它通常用于求解具有重叠子问题和最优子结构性质的问题,即可以通过合并子问题的解来构建原问题的解。
动态规划的核心要素:
- 最优子结构:问题的最优解包含其子问题的最优解。
- 重叠子问题:在求解过程中,很多子问题的解会被多次使用,因此可以通过缓存这些子问题的解来避免重复计算,从而提高效率。
- 无后效性:一旦某个阶段的状态确定后,这个状态不会受到后续决策的影响。
动态规划的解题步骤:
- 定义状态:确定问题的子结构,定义一个状态来表示子问题的解。
- 确定状态转移方程:找出状态之间的关系,即如何从前一个状态转移到下一个状态。
- 确定初始状态:确定最简单情况下的解,通常是边界条件。
- 执行状态转移:根据状态转移方程,从初始状态开始逐步计算出最终状态的解。
- 计算最终的解:通过状态转移得到最终问题的解。
动态规划的应用实例:
- 斐波那契数列:计算第n个斐波那契数,使用动态规划可以避免重复计算,提高效率。
- 背包问题:给定一系列物品,每个物品有自己的重量和价值,在不超过背包容量的前提下,选择物品使得总价值最大化。
- 最长递增子序列:在一个数字序列中,找到最长的子序列,使得子序列中的元素是严格递增的。
- 编辑距离:计算两个字符串之间的最小编辑操作次数,使得一个字符串可以转换成另一个字符串。
动态规划的实现方法:
- 自顶向下(Top-down):使用递归加备忘录的方法,先尝试解决较大的问题,当需要时才解决子问题。
- 自底向上(Bottom-up):从最简单的问题开始,逐步构建解决方案,直到解决原始问题。
动态规划是一种强大的工具,适用于解决许多优化问题,但它也有局限性,例如当状态空间过大时,可能会导致“维数灾难”。
维数灾难
“维数灾难” 是指随着问题维度(即状态变量的数量)的增加,问题的复杂度呈指数级增长,这会导致动态规划算法变得非常低效甚至不可行。为了避免或减轻“维数灾难”,可以采取以下几种策略:
1. 状态压缩
- 位运算:利用位运算来表示状态,可以大大减少状态的数量。例如,在某些图论问题中,可以用一个整数的二进制位来表示节点的访问状态。
- 状态合并:将多个相关的状态合并为一个复合状态,以减少状态空间的大小。
2. 状态剪枝
- 预处理:在动态规划之前,预先处理一些信息,排除不可能的状态或路径。
- 剪枝:在状态转移过程中,及时排除那些不可能成为最优解的状态,减少不必要的计算。
3. 使用近似方法
- 启发式搜索:使用启发式搜索算法(如A*算法)来寻找近似最优解,而不是精确解。
- 贪心算法:在某些情况下,可以使用贪心算法来快速找到一个较好的解,虽然不一定是全局最优解。
4. 降维技术
- 特征选择:在处理高维数据时,通过特征选择减少状态空间的维度。
- 主成分分析(PCA):利用主成分分析等降维技术,将高维数据映射到低维空间。
5. 分治法
- 分而治之:将大问题分解为若干个小问题,分别求解后再合并结果。这种方法在某些情况下可以显著减少状态空间的大小。
6. 动态规划的变种
- 记忆化搜索:结合自顶向下的递归和自底向上的动态规划,只计算必要的状态,避免冗余计算。
- 滚动数组:在某些动态规划问题中,只需要保存最近几层的状态,可以使用滚动数组来节省空间。
7. 并行计算
- 并行化:利用多核处理器或分布式计算资源,将状态转移过程并行化,加速计算。
8. 模型简化
- 简化模型:在不影响解的质量的前提下,简化模型,减少状态空间的复杂度。
9. 预先计算和存储
- 预计算:对于某些子问题,可以在程序运行前预先计算并存储结果,以便在实际计算中直接使用。
实例应用
假设你在解决一个背包问题,有100个物品,每个物品有100种可能的状态。如果直接使用动态规划,状态空间会非常庞大。可以通过以下方法来优化:
- 状态压缩:用位运算表示哪些物品已经被放入背包。
- 状态剪枝:在状态转移过程中,排除那些重量超过背包容量的状态。
- 预处理:预先计算每个物品的价值与重量比,优先考虑价值较高的物品。
通过这些方法,可以有效地减少状态空间的大小,提高动态规划算法的效率。
0-1背包问题
01背包问题是一个经典的动态规划问题,其核心在于如何在有限的背包容量下选择物品,使得所选物品的总价值最大。下面将详细介绍01背包问题的定义、解题思路、状态转移方程以及代码实现。
1. 问题定义
给定 n
个物品和一个容量为 W
的背包,每个物品有一个重量 w[i]
和一个价值 v[i]
。目标是在不超过背包容量的情况下,选择物品使得所选物品的总价值最大。
2. 解题思路
01背包问题的关键在于每个物品只能选择一次(0或1),因此可以使用动态规划来解决这个问题。
3. 动态规划解法
3.1 定义状态
用 dp[i][j]
表示前 i
个物品中选择总重量不超过 j
的最大价值。
3.2 状态转移方程
对于每个物品 i
,有两种选择:
- 不选择第
i
个物品:dp[i][j] = dp[i-1][j]
- 选择第
i
个物品:dp[i][j] = dp[i-1][j - w[i]] + v[i]
,前提是j >= w[i]
因此,状态转移方程为:dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])
3.3 初始状态
dp[0][j] = 0
:没有物品时,最大价值为0dp[i][0] = 0
:背包容量为0时,最大价值为0
3.4 计算顺序
从 i = 1
到 n
,从 j = 0
到 W
,依次计算 dp[i][j]
。
4. 代码实现
4.1 二维数组实现
Java
public class Knapsack2D {
public static int knapsack(int n, int W, int[] w, int[] v) {
// 初始化 dp 数组
int[][] dp = new int[n + 1][W + 1];
// 填充 dp 数组
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i - 1]) { // 当前背包容量不足以放下第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j],
dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
}
}
// 返回最大价值
return dp[n][W];
}
public static void main(String[] args) {
int n = 3;
int W = 5;
int[] w = {2, 1, 3};
int[] v = {4, 2, 3};
System.out.println(knapsack(n, W, w, v)); // 输出 7
}
}
C语言
#include <stdio.h>
int knapsack(int n, int W, int w[], int v[]) {
int dp[n + 1][W + 1];
// 初始化 dp 数组
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= W; j++) {
dp[0][j] = 0;
}
// 填充 dp 数组
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i - 1]) { // 当前背包容量不足以放下第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = (dp[i - 1][j] >
dp[i - 1][j - w[i - 1]] + v[i - 1]) ?dp[i - 1][j] :
dp[i - 1][j - w[i - 1]] + v[i - 1];
}
}
}
// 返回最大价值
return dp[n][W];
}
int main() {
int n = 3;
int W = 5;
int w[] = {2, 1, 3};
int v[] = {4, 2, 3};
printf("%d\n", knapsack(n, W, w, v)); // 输出 7
return 0;
}
Python
def knapsack(n, W, w, v):
# 初始化 dp 数组
dp = [[0] * (W + 1) for _ in range(n + 1)]
# 填充 dp 数组
for i in range(1, n + 1):
for j in range(W + 1):
if j < w[i-1]: # 当前背包容量不足以放下第 i 个物品
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i-1]] + v[i-1])
# 返回最大价值
return dp[n][W]
# 示例
n = 3
W = 5
w = [2, 1, 3]
v = [4, 2, 3]
print(knapsack(n, W, w, v)) # 输出 7
4.2 一维数组实现(优化空间复杂度)
可以通过滚动数组的方式将空间复杂度从 O(n * W)
优化到 O(W)
。
Java
public class Knapsack1D {
public static int knapsackOptimized(int n, int W, int[] w, int[] v) {
// 初始化 dp 数组
int[] dp = new int[W + 1];
// 填充 dp 数组
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
// 返回最大价值
return dp[W];
}
public static void main(String[] args) {
int n = 3;
int W = 5;
int[] w = {2, 1, 3};
int[] v = {4, 2, 3};
System.out.println(knapsackOptimized(n, W, w, v)); // 输出 7
}
}
C语言
#include <stdio.h>
int knapsackOptimized(int n, int W, int w[], int v[]) {
int dp[W + 1];
// 初始化 dp 数组
for (int j = 0; j <= W; j++) {
dp[j] = 0;
}
// 填充 dp 数组
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = (dp[j] > dp[j - w[i]] + v[i]) ?
dp[j] : dp[j - w[i]] + v[i];
}
}
// 返回最大价值
return dp[W];
}
int main() {
int n = 3;
int W = 5;
int w[] = {2, 1, 3};
int v[] = {4, 2, 3};
printf("%d\n", knapsackOptimized(n, W, w, v)); // 输出 7
return 0;
}
Python
def knapsack_optimized(n, W, w, v):
# 初始化 dp 数组
dp = [0] * (W + 1)
# 填充 dp 数组
for i in range(n):
for j in range(W, w[i] - 1, -1):
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
# 返回最大价值
return dp[W]
# 示例
n = 3
W = 5
w = [2, 1, 3]
v = [4, 2, 3]
print(knapsack_optimized(n, W, w, v)) # 输出 7
5. 详细解释
5.1 二维数组实现
- 初始化:
dp[0][j]
和dp[i][0]
都初始化为0,表示没有物品或背包容量为0时的最大价值为0。 - 状态转移:对于每个物品
i
,如果当前背包容量j
小于物品i
的重量w[i]
,则不选择该物品,dp[i][j] = dp[i-1][j]
;否则选择该物品,dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])
。 - 结果:最终结果保存在
dp[n][W]
中,表示前n
个物品在背包容量为W
时的最大价值。
5.2 一维数组实现
- 初始化:
dp
数组初始化为0,表示背包容量为0时的最大价值为0。 - 状态转移:对于每个物品
i
,从后向前遍历背包容量j
,确保每次更新dp[j]
时使用的是上一轮的结果。这样可以避免重复计算。 - 结果:最终结果保存在
dp[W]
中,表示在背包容量为W
时的最大价值。
6. 总结
01背包问题通过动态规划可以高效地解决。二维数组实现直观易懂,但空间复杂度较高;一维数组实现通过滚动数组优化了空间复杂度,适用于大规模问题。