Medium
题目描述
给你一个 m x n 的网格。机器人从网格左上角 (0, 0) 开始,想要到达右下角 (m - 1, n - 1)。机器人在任意时刻只能向右或向下移动。
网格中每个单元格包含一个值 coins[i][j]:
- 如果 coins[i][j] >= 0,机器人获得相应数量的金币。
- 如果 coins[i][j] < 0,机器人遇到强盗,强盗会偷走 coins[i][j] 绝对值数量的金币。
机器人有一个特殊能力,可以在路径上最多 2 个单元格中使强盗失效,防止他们在这些单元格中偷取金币。
注意:机器人的总金币数可以为负数。
返回机器人在路线上能获得的最大利润。
示例 1:
输入:coins = [[0,1,-1],[1,-2,3],[2,-3,4]] 输出:8
解释: 获得最大金币的最佳路径是:
- 从 (0, 0) 开始,获得 0 金币(总金币 = 0)。
- 移动到 (0, 1),获得 1 金币(总金币 = 0 + 1 = 1)。
- 移动到 (1, 1),这里有强盗偷 2 金币。机器人在这里使用一次中和能力,避免被抢劫(总金币 = 1)。
- 移动到 (1, 2),获得 3 金币(总金币 = 1 + 3 = 4)。
- 移动到 (2, 2),获得 4 金币(总金币 = 4 + 4 = 8)。
示例 2:
输入:coins = [[10,10,10],[10,10,10]] 输出:40
解释: 获得最大金币的最佳路径是:
- 从 (0, 0) 开始,获得 10 金币(总金币 = 10)。
- 移动到 (0, 1),获得 10 金币(总金币 = 10 + 10 = 20)。
- 移动到 (0, 2),获得 10 金币(总金币 = 20 + 10 = 30)。
- 移动到 (1, 2),获得最后 10 金币(总金币 = 30 + 10 = 40)。
约束条件:
- m == coins.length
- n == coins[i].length
- 1 <= m, n <= 500
- -1000 <= coins[i][j] <= 1000
解题思路
这是一道典型的三维动态规划问题。关键在于状态的定义和转移。
状态定义:设 dp[i][j][k] 表示机器人到达位置 (i, j) 时,已经使用了 k 次中和能力的最大金币收益。这里 k 的取值范围是 0 到 2。
状态转移:对于每个位置 (i, j),机器人可以从上方 (i-1, j) 或左方 (i, j-1) 到达。对于每种到达方式和每种中和能力使用次数,我们需要考虑两种选择:
- 不使用中和能力:直接获得/失去
coins[i][j]的金币 - 使用中和能力(如果
coins[i][j] < 0且还有剩余次数):将负数位置中和为 0,避免损失
初始化:起始位置 (0, 0) 需要特殊处理,同样考虑是否对起始位置使用中和能力。
优化考虑:由于只能向右或向下移动,可以使用滚动数组优化空间复杂度,但为了代码清晰度,这里使用完整的三维数组。
时间复杂度为 O(m×n×3),空间复杂度为 O(m×n×3)。
推荐解法:使用三维DP数组,状态转移时考虑所有可能的中和能力使用情况,确保找到全局最优解。
代码实现
class Solution {
public:
int maximumAmount(vector<vector<int>>& coins) {
int m = coins.size(), n = coins[0].size();
vector<vector<vector<int>>> dp(m, vector<vector<int>>(n, vector<int>(3, INT_MIN)));
// 初始化起始位置
dp[0][0][0] = coins[0][0];
if (coins[0][0] < 0) {
dp[0][0][1] = 0; // 使用一次中和能力
}
// 填充第一行
for (int j = 1; j < n; j++) {
for (int k = 0; k <= 2; k++) {
if (dp[0][j-1][k] != INT_MIN) {
// 不使用中和能力
dp[0][j][k] = max(dp[0][j][k], dp[0][j-1][k] + coins[0][j]);
// 使用中和能力(如果可能)
if (k < 2 && coins[0][j] < 0) {
dp[0][j][k+1] = max(dp[0][j][k+1], dp[0][j-1][k]);
}
}
}
}
// 填充第一列
for (int i = 1; i < m; i++) {
for (int k = 0; k <= 2; k++) {
if (dp[i-1][0][k] != INT_MIN) {
// 不使用中和能力
dp[i][0][k] = max(dp[i][0][k], dp[i-1][0][k] + coins[i][0]);
// 使用中和能力(如果可能)
if (k < 2 && coins[i][0] < 0) {
dp[i][0][k+1] = max(dp[i][0][k+1], dp[i-1][0][k]);
}
}
}
}
// 填充其余位置
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
for (int k = 0; k <= 2; k++) {
// 从上方来
if (dp[i-1][j][k] != INT_MIN) {
dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i][j][k+1] = max(dp[i][j][k+1], dp[i-1][j][k]);
}
}
// 从左方来
if (dp[i][j-1][k] != INT_MIN) {
dp[i][j][k] = max(dp[i][j][k], dp[i][j-1][k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i][j][k+1] = max(dp[i][j][k+1], dp[i][j-1][k]);
}
}
}
}
}
return max({dp[m-1][n-1][0], dp[m-1][n-1][1], dp[m-1][n-1][2]});
}
};
class Solution:
def maximumAmount(self, coins: List[List[int]]) -> int:
m, n = len(coins), len(coins[0])
dp = [[[float('-inf')] * 3 for _ in range(n)] for _ in range(m)]
# 初始化起始位置
dp[0][0][0] = coins[0][0]
if coins[0][0] < 0:
dp[0][0][1] = 0 # 使用一次中和能力
# 填充第一行
for j in range(1, n):
for k in range(3):
if dp[0][j-1][k] != float('-inf'):
# 不使用中和能力
dp[0][j][k] = max(dp[0][j][k], dp[0][j-1][k] + coins[0][j])
# 使用中和能力(如果可能)
if k < 2 and coins[0][j] < 0:
dp[0][j][k+1] = max(dp[0][j][k+1], dp[0][j-1][k])
# 填充第一列
for i in range(1, m):
for k in range(3):
if dp[i-1][0][k] != float('-inf'):
# 不使用中和能力
dp[i][0][k] = max(dp[i][0][k], dp[i-1][0][k] + coins[i][0])
# 使用中和能力(如果可能)
if k < 2 and coins[i][0] < 0:
dp[i][0][k+1] = max(dp[i][0][k+1], dp[i-1][0][k])
# 填充其余位置
for i in range(1, m):
for j in range(1, n):
for k in range(3):
# 从上方来
if dp[i-1][j][k] != float('-inf'):
dp[i][j][k] = max(dp[i][j][k], dp[i-1][j][k] + coins[i][j])
if k < 2 and coins[i][j] < 0:
dp[i][j][k+1] = max(dp[i][j][k+1], dp[i-1][j][k])
# 从左方来
if dp[i][j-1][k] != float('-inf'):
dp[i][j][k] = max(dp[i][j][k], dp[i][j-1][k] + coins[i][j])
if k < 2 and coins[i][j] < 0:
dp[i][j][k+1] = max(dp[i][j][k+1], dp[i][j-1][k])
return max(dp[m-1][n-1])
public class Solution {
public int MaximumAmount(int[][] coins) {
int m = coins.Length, n = coins[0].Length;
int[,,] dp = new int[m, n, 3];
// 初始化为极小值
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < 3; k++) {
dp[i, j, k] = int.MinValue;
}
}
}
// 初始化起始位置
dp[0, 0, 0] = coins[0][0];
if (coins[0][0] < 0) {
dp[0, 0, 1] = 0; // 使用一次中和能力
}
// 填充第一行
for (int j = 1; j < n; j++) {
for (int k = 0; k <= 2; k++) {
if (dp[0, j-1, k] != int.MinValue) {
// 不使用中和能力
dp[0, j, k] = Math.Max(dp[0, j, k], dp[0, j-1, k] + coins[0][j]);
// 使用中和能力(如果可能)
if (k < 2 && coins[0][j] < 0) {
dp[0, j, k+1] = Math.Max(dp[0, j, k+1], dp[0, j-1, k]);
}
}
}
}
// 填充第一列
for (int i = 1; i < m; i++) {
for (int k = 0; k <= 2; k++) {
if (dp[i-1, 0, k] != int.MinValue) {
// 不使用中和能力
dp[i, 0, k] = Math.Max(dp[i, 0, k], dp[i-1, 0, k] + coins[i][0]);
// 使用中和能力(如果可能)
if (k < 2 && coins[i][0] < 0) {
dp[i, 0, k+1] = Math.Max(dp[i, 0, k+1], dp[i-1, 0, k]);
}
}
}
}
// 填充其余位置
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
for (int k = 0; k <= 2; k++) {
// 从上方来
if (dp[i-1, j, k] != int.MinValue) {
dp[i, j, k] = Math.Max(dp[i, j, k], dp[i-1, j, k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i, j, k+1] = Math.Max(dp[i, j, k+1], dp[i-1, j, k]);
}
}
// 从左方来
if (dp[i, j-1, k] != int.MinValue) {
dp[i, j, k] = Math.Max(dp[i, j, k], dp[i, j-1, k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i, j, k+1] = Math.Max(dp[i, j, k+1], dp[i, j-1, k]);
}
}
}
}
}
return Math.Max(Math.Max(dp[m-1, n-1, 0], dp[m-1, n-1, 1]), dp[m-1, n-1, 2]);
}
}
var maximumAmount = function(coins) {
const m = coins.length, n = coins[0].length;
const dp = Array(m).fill().map(() =>
Array(n).fill().map(() => Array(3).fill(-Infinity))
);
// 初始化起始位置
dp[0][0][0] = coins[0][0];
if (coins[0][0] < 0) {
dp[0][0][1] = 0; // 使用一次中和能力
}
// 填充第一行
for (let j = 1; j < n; j++) {
for (let k = 0; k <= 2; k++) {
if (dp[0][j-1][k] !== -Infinity) {
// 不使用中和能力
dp[0][j][k] = Math.max(dp[0][j][k], dp[0][j-1][k] + coins[0][j]);
// 使用中和能力(如果可能)
if (k < 2 && coins[0][j] < 0) {
dp[0][j][k+1] = Math.max(dp[0][j][k+1], dp[0][j-1][k]);
}
}
}
}
// 填充第一列
for (let i = 1; i < m; i++) {
for (let k = 0; k <= 2; k++) {
if (dp[i-1][0][k] !== -Infinity) {
// 不使用中和能力
dp[i][0][k] = Math.max(dp[i][0][k], dp[i-1][0][k] + coins[i][0]);
// 使用中和能力(如果可能)
if (k < 2 && coins[i][0] < 0) {
dp[i][0][k+1] = Math.max(dp[i][0][k+1], dp[i-1][0][k]);
}
}
}
}
// 填充其余位置
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
for (let k = 0; k <= 2; k++) {
// 从上方来
if (dp[i-1][j][k] !== -Infinity) {
dp[i][j][k] = Math.max(dp[i][j][k], dp[i-1][j][k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i][j][k+1] = Math.max(dp[i][j][k+1], dp[i-1][j][k]);
}
}
// 从左方来
if (dp[i][j-1][k] !== -Infinity) {
dp[i][j][k] = Math.max(dp[i][j][k], dp[i][j-1][k] + coins[i][j]);
if (k < 2 && coins[i][j] < 0) {
dp[i][j][k+1] = Math.max(dp[i][j][k+1], dp[i][j-1][k]);
}
}
}
}
}
return Math.max(dp[m-1][n-1][0], dp[m-1][n-1][1], dp[m-1][n-1][2]);
};
复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 三维动态规划 | O(m × n × 3) | O(m × n × 3) |