分享丨【算法题单】动态规划(入门/背包/划分/状态机/区间/状压/数位/树形/优化) - 讨论 - 力扣(LeetCode)
对于一些二维 DP (例如背包、最长公共子序列),如果把 DP 矩阵画出来,其实状态转移可以视作在网格图上的移动。所以在学习相对更抽象的二维 DP 之前,做一些形象的网格图 DP 会让后续的学习更轻松(比如 0-1 背包的空间优化写法为什么要倒序遍历)。
1.套路
1.想好开几维dp数组,且遍历时要按顺序遍历所有维度,最后的答案才让每个维度变成固定值[[十九.动态规划-二.网格图DP#5. 3393.统计异或值为给定值的路径数目(中等,学习递推数组的第三个参数)]]
2.题目描述
3.学习经验
1. 64.最小路径和(中等)
思想
1.给定一个包含非负整数的 _m_ x _n_ 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明: 每次只能向下或者向右移动一步。
2.转态方程含义是到当前点的数字总和最小的路径的数字总和
代码
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<int>> f(n, vector<int>(m, 0));
// 初始化
f[0][0] = grid[0][0];
for (int i = 1; i < n; ++i)
f[i][0] = f[i - 1][0] + grid[i][0];
for (int j = 1; j < m; ++j)
f[0][j] = f[0][j - 1] + grid[0][j];
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
// 状态转移方程
f[i][j] = min(f[i - 1][j], f[i][j - 1]) + grid[i][j];
}
}
// 返回答案
return f[n - 1][m - 1];
}
};
2. 62.不同路径(中等)
思想
1.一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 "Start" )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 "Finish" )。
问总共有多少条不同的路径?
2.跟[[十九.动态规划-二.网格图DP#1. 64.最小路径和(中等)]]一样,只是状态方程含义是不同路径数量,状态转移方程变成相加
代码
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> f(m, vector<int>(n, 0));
// 初始化
f[0][0] = 1;
for (int i = 1; i < m; ++i)
f[i][0] = 1;
for (int j = 1; j < n; ++j)
f[0][j] = 1;
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
// 状态转移方程
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
// 返回答案
return f[m - 1][n - 1];
}
};
3. 63.不同路径II(中等)
思想
1.给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角 (即 grid[0][0])。机器人尝试移动到 右下角 (即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。
网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。
返回机器人能够到达右下角的不同路径数量。
测试用例保证答案小于等于 2 * 10^9。
2.跟[[十九.动态规划-二.网格图DP#2. 62.不同路径(中等)]]相比,增加了障碍物这一限制条件,即到障碍物位置路径数为0,则更新状态数组为0即可
代码
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> f(m, vector<int>(n, 0));
if (obstacleGrid[0][0] == 1)
return 0;
// 初始化
f[0][0] = 1;
for (int i = 1; i < m; ++i) {
if (obstacleGrid[i][0] == 1)
f[i][0] = 0; // 有障碍物说明无路径
else
f[i][0] = f[i - 1][0];
}
for (int j = 1; j < n; ++j) {
if (obstacleGrid[0][j] == 1)
f[0][j] = 0;
else
f[0][j] = f[0][j - 1];
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (obstacleGrid[i][j] == 1) {
f[i][j] = 0;
} else {
// 状态转移方程
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
}
// 返回答案
return f[m - 1][n - 1];
}
};
4. 120.三角形最小路径和(中等)
思想
1.给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
2.从长方形网格变成下三角矩阵,初始化条件变成了对角线,最终答案为最后一行的最小值
代码
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<vector<int>> f(n, vector<int>(n, 0));
// 初始化
f[0][0] = triangle[0][0];
for (int i = 1; i < n; ++i) {
f[i][0] = f[i - 1][0] + triangle[i][0]; // 第一列只能来自上方
f[i][i] = f[i - 1][i - 1] + triangle[i][i]; // 对角线只能来自左上角
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < i; ++j) { // 下三角矩阵(不含对角线和第一列)
// 状态转移方程
f[i][j] = min(f[i - 1][j], f[i - 1][j - 1]) + triangle[i][j];
}
}
// 答案
int res = INT_MAX;
for (int j = 0; j < n; ++j)
res = min(res, f[n - 1][j]);
return res;
}
};
5. 3393.统计异或值为给定值的路径数目(中等,学习递推数组的第三个参数)
3393. 统计异或值为给定值的路径数目 - 力扣(LeetCode)
思想
1.给你一个大小为 m x n 的二维整数数组 grid 和一个整数 k 。
你的任务是统计满足以下 条件 且从左上格子 (0, 0) 出发到达右下格子 (m - 1, n - 1) 的路径数目:
- 每一步你可以向右或者向下走,也就是如果格子存在的话,可以从格子
(i, j)走到格子(i, j + 1)或者格子(i + 1, j)。 - 路径上经过的所有数字
XOR异或值必须 等于k。
请你返回满足上述条件的路径总数。
由于答案可能很大,请你将答案对10^9 + 7取余 后返回。
2.把到当前结点的异或值作为dp数组的第三个参数 ,即dp[i][j][x](表示到当前点异或值为x的路径总数),假如从上方dp[i-1][j][y]转移而来,有y^grid[i][j]=x,根据异或性质可知,y=x^grid[i][j](因为递推时要遍历第三个参数,已知当前位置异或值需要反推转移处的异或值 ),而最终求答案时第三维才取k
3.dp数组初始化时最大异或值的选取规则 :
XOR 运算本质是按位异或(不可能进位1) ,所以多个数异或结果一定小于(数组最大数的二进制位数所表示的最大数),公式化为
设二维数组中的所有元素满足
0 ≤ a i < 2 b 0 \le a_i < 2^b 0≤ai<2b
其中
b = b i t _ w i d t h ( max ( grid ) ) b = \mathrm{bit\_width}(\max(\text{grid})) b=bit_width(max(grid))
则对于任意有限个这样的数
x = a 1 ⊕ a 2 ⊕ ⋯ ⊕ a t x = a_1 \oplus a_2 \oplus \dots \oplus a_t x=a1⊕a2⊕⋯⊕at
恒有
0 ≤ x < 2 b 0 \le x < 2^b 0≤x<2b
证明
由
0 ≤ a i < 2 b 0 \le a_i < 2^b 0≤ai<2b
可知每个 a i a_i ai 的二进制表示最多只有 b b b 位,即
a i = ∑ k = 0 b − 1 c i , k 2 k a_i = \sum_{k=0}^{b-1} c_{i,k} 2^k ai=k=0∑b−1ci,k2k
其中
c i , k ∈ { 0 , 1 } c_{i,k} \in \{0,1\} ci,k∈{0,1}
并且对于所有 k ≥ b k \ge b k≥b,
c i , k = 0 c_{i,k} = 0 ci,k=0
按位异或运算定义为
(a_1 \\oplus \\dots \\oplus a_t)_k c_{1,k} \\oplus \\dots \\oplus c_{t,k}
对于所有 k ≥ b k \ge b k≥b,由于
c 1 , k = c 2 , k = ⋯ = c t , k = 0 c_{1,k} = c_{2,k} = \dots = c_{t,k} = 0 c1,k=c2,k=⋯=ct,k=0
因此
c 1 , k ⊕ ⋯ ⊕ c t , k = 0 c_{1,k} \oplus \dots \oplus c_{t,k} = 0 c1,k⊕⋯⊕ct,k=0
故异或结果在第 b b b 位及以上仍为 0 0 0,从而
x < 2 b x < 2^b x<2b
证毕。
代码
1.暴力方法
class Solution {
public:
const int mod = 1e9 + 7;
typedef long long ll;
int countPathsWithXorValue(vector<vector<int>>& grid, int k) {
int n = grid.size(), m = grid[0].size();
vector<vector<map<int, ll>>> f(
n, vector<map<int, ll>>(m)); // 异或值-路径数量
f[0][0][grid[0][0]] = 1;
for (int i = 1; i < n; ++i) {
for (auto it = f[i - 1][0].begin(); it != f[i - 1][0].end(); ++it) {
int curXor = (it->first) ^ grid[i][0];
f[i][0][curXor] = it->second;
}
}
for (int j = 1; j < m; ++j) {
for (auto it = f[0][j - 1].begin(); it != f[0][j - 1].end(); ++it) {
int curXor = (it->first) ^ grid[0][j];
f[0][j][curXor] = it->second;
}
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
// 更新当前点的所有异或值-路径数
for (auto it = f[i - 1][j].begin(); it != f[i - 1][j].end();
++it) {
int curXor = (it->first) ^ grid[i][j];
f[i][j][curXor] = (f[i][j][curXor] + it->second) % mod;
}
for (auto it = f[i][j - 1].begin(); it != f[i][j - 1].end();
++it) {
int curXor = (it->first) ^ grid[i][j];
f[i][j][curXor] = (f[i][j][curXor] + it->second) % mod;
}
}
}
// 只判断终点满足异或值为k的路径总数
ll res = 0;
for (auto it = f[n - 1][m - 1].begin(); it != f[n - 1][m - 1].end();
++it) {
if (it->first == k) {
res = (res + it->second) % mod;
}
}
return res;
}
};
2.三维dp数组方法:
class Solution {
public:
const int mod = 1e9 + 7;
int get_bit_wid(int x) { // 获取二进制位数
int res = 0;
while (x) {
++res;
x >>= 1;
}
return res;
}
int countPathsWithXorValue(vector<vector<int>>& grid, int k) {
int n = grid.size(), m = grid[0].size();
int maxn = INT_MIN;
for (int i = 0; i < n; ++i) {
for (auto& x : grid[i]) {
maxn = max(maxn, x);
}
}
int maxXor = (1 << get_bit_wid(maxn));
if (k >= maxXor)
return 0;
vector<vector<vector<int>>> f(
n, vector<vector<int>>(m, vector<int>(maxXor, 0)));
f[0][0][grid[0][0]] = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (i == 0 && j == 0)
continue;
int val = grid[i][j];
for (int u = 0; u < maxXor; ++u) { // 遍历第三个参数
if (i >= 1)
f[i][j][u] = (f[i][j][u] + f[i - 1][j][u ^ val]) % mod;
if (j >= 1)
f[i][j][u] = (f[i][j][u] + f[i][j - 1][u ^ val]) % mod;
}
}
}
return f[n - 1][m - 1][k];
}
};
6. 931.下降路径最小和(中等)
思想
1.给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
2.跟[[十九.动态规划-二.网格图DP#4. 120.三角形最小路径和(中等)]]差不多
代码
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int n = matrix.size();
vector<vector<int>> f(n, vector<int>(n, 0));
for (int j = 0; j < n; ++j)
f[0][j] = matrix[0][j];
for (int i = 1; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int minn = f[i - 1][j];
if (j >= 1)
minn = min(minn, f[i - 1][j - 1]);
if (j + 1 < n)
minn = min(minn, f[i - 1][j + 1]);
f[i][j] = minn + matrix[i][j];
}
}
int res = INT_MAX;
for (int j = 0; j < n; ++j)
res = min(res, f[n - 1][j]);
return res;
}
};
7. 2304.网格中的最小路径代价(中等)
2304. 网格中的最小路径代价 - 力扣(LeetCode)
思想
1.给你一个下标从 0 开始的整数矩阵 grid ,矩阵大小为 m x n ,由从 0 到 m * n - 1 的不同整数组成。你可以在此矩阵中,从一个单元格移动到 下一行 的任何其他单元格。如果你位于单元格 (x, y) ,且满足 x < m - 1 ,你可以移动到 (x + 1, 0), (x + 1, 1), ..., (x + 1, n - 1) 中的任何一个单元格。注意: 在最后一行中的单元格不能触发移动。
每次可能的移动都需要付出对应的代价,代价用一个下标从 0 开始的二维数组 moveCost 表示,该数组大小为 (m * n) x n ,其中 moveCost[i][j] 是从值为 i 的单元格移动到下一行第 j 列单元格的代价。从 grid 最后一行的单元格移动的代价可以忽略。
grid 一条路径的代价是:所有路径经过的单元格的 值之和 加上 所有移动的 代价之和 。从 第一行 任意单元格出发,返回到达 最后一行 任意单元格的最小路径代价。
2.跟[[十九.动态规划-二.网格图DP#6. 931.下降路径最小和(中等)]]差不多,只是转移位置从上方三处变成了上方一整行
代码
class Solution {
public:
int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> f(m, vector<int>(n, 0));
for (int j = 0; j < n; ++j)
f[0][j] = grid[0][j];
for (int i = 1; i < m; ++i) {
for (int j = 0; j < n; ++j) {
int minn = INT_MAX;
for (int k = 0; k < n; ++k) {
int val = f[i - 1][k] + moveCost[grid[i - 1][k]][j];
minn = min(minn, val);
}
f[i][j] = minn + grid[i][j];
}
}
int res = INT_MAX;
for (int j = 0; j < n; ++j)
res = min(res, f[m - 1][j]);
return res;
}
};
8. 2684.矩阵中移动的最大次数(中等)
2684. 矩阵中移动的最大次数 - 力扣(LeetCode)
思想
1.给你一个下标从 0 开始、大小为 m x n 的矩阵 grid ,矩阵由若干 正 整数组成。
你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid :
- 从单元格
(row, col)可以移动到(row - 1, col + 1)、(row, col + 1)和(row + 1, col + 1)三个单元格中任一满足值 严格 大于当前单元格的单元格。
返回你在矩阵中能够 移动 的 最大 次数。
2.此题要从第一列到某一列的最大移动次数,所以不能出现起始路径不满足条件,但中间路径满足条件的情况,所以要将不能移动到此处的信息传递给后方,用dp数组为-1来传递
代码
class Solution {
public:
int maxMoves(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> f(m, vector<int>(n, 0));
int res = 0;
for (int j = 1; j < n; ++j) {
bool tag = false;
for (int i = 0; i < m; ++i) {
bool flag = false;
if (grid[i][j] > grid[i][j - 1] && f[i][j - 1] != -1) {
f[i][j] = max(f[i][j], f[i][j - 1] + 1);
flag = true;
tag = true;
}
if (i >= 1 && grid[i][j] > grid[i - 1][j - 1] &&
f[i - 1][j - 1] != -1) {
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
flag = true;
tag = true;
}
if (i + 1 < m && grid[i][j] > grid[i + 1][j - 1] &&
f[i + 1][j - 1] != -1) {
f[i][j] = max(f[i][j], f[i + 1][j - 1] + 1);
flag = true;
tag = true;
}
if (flag)
res = max(res, f[i][j]);
else
f[i][j] = -1;
}
if (!tag)
break; // 整列都没有
}
return res;
}
};