文章目录
动态规划算法解释
动态规划(Dynamic Programming,DP)在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
动态规划与其他遍历算法(如DFS、BFS)都是将原问题拆成多个子问题然后求解,它们之间最本质的区别是:动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程。这样可以通过计算和储存子问题的解来求解最终问题。
也可以对动态规划进行空间压缩,起到节省空间消耗的效果。
某些情况下,动态规划可以看作是带有状态记录(memoization)的优先搜索。状态记录的意思为:如果一个子问题在优先搜索时已经计算过一次,我们可以将它的结果储存下来,之后遍历到该子问题的时候可以直接返回储存的结果。动态规划是自下而上的,即先解决子问题,再解决父问题;而用带有状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。如果题目需求的是最终状态,那么使用动态搜索比较方便,如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。
基本动态规划-一维
70.爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
思路:
经典的斐波那契数列题。
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
。
定义一个数组dp
,dp[i]
表示走到第i
阶的方法数。因为我们每次可以走一步或者两步,所以第i
阶可以从第i-1
或i-2
阶到达。
举例:假设一共有10级台阶,可以从第9级台阶走一步到达,也可以从第8级台阶走两步到达。
也就是说,走到第i
阶的方法数即为走到第i-1
阶的方法数加上走到第i-2
阶的方法数。
这样我们就得到了状态转移方程:dp[i] = dp[i-1] + dp[i-2]
。
需要注意边界条件的处理。
还可以对动态规划进行空间压缩。因为dp[i]
只与dp[i-1]
和dp[i-2]
有关,因此可以只用两个变量来存储dp[i-1]
和dp[i-2]
,使得原来O(n)
空间复杂度优化为O(1)
复杂度。
代码:
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) return n;//当台阶数为1时,只有一种上楼梯方法,当台阶数为2时,有1+1和2两种方法。
vector<int> dp(n+1,1);
for (int i = 2;i <= n;++i){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) return n;//当台阶数为1时,只有一种上楼梯方法,当台阶数为2时,有1+1和2两种方法。
int pre2 = 1, pre1 = 2, cur;
for (int i = 2;i < n;++i){
cur = pre2 + pre1;
pre2 = pre1;
pre1 = cur;
}
return cur;
}
};
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路:
定义一个数组dp
,dp[i]
表示抢劫到第i
个房子时,可以抢劫的最大数量。
考虑dp[i]
,此时可以抢劫的最大数量有两种可能:
- 我们选择不抢劫这个房子,此时累计的金额为
dp[i-1]
; - 我们选择抢劫这个房子,那么此前累计的最大金额只能是
dp[i-2]
,因为我们不能抢劫第i-1
个房子,否则会触发警报。
本题的状态转移方程为dp[i] = max(dp[i - 1), nums[i - 1]+dp[i - 2]
。此处的nums[i - 1]
是当前抢劫的房子。
错误代码:
class Solution {
//思路错误:以为是隔一个地偷,但是有可能隔两个
//举例:[1,2,3,1,4,6]
//1 + 3 + 4 = 8
//2 + 1 + 6 = 9
//最高金额情况:1 + 3 + 6 = 10
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1) return nums[0];
if (n == 2) return nums[0] >= nums[1] ? nums[0] : nums[1];
int sum1 = 0, sum2= 0;
if (n % 2 == 0){
for (int i = 0;i < (n / 2);i++){
sum1 += nums[2*i];
sum2 += nums[2*i+1];
}
}
if (n % 2 == 1){
for (int i = 0;i < (n / 2);i++){
sum1 += nums[2*i];
sum2 += nums[2*i+1];
}
sum1 += nums[n-1];
}
return sum1 >= sum2 ? sum1 : sum2;
}
};
正确代码:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
vector<int> dp(n + 1, 0);
dp[1] = nums[0];
for (int i = 2;i <= n;++i){
dp[i] = max(dp[i-1], nums[i-1] + dp[i-2]);
}
return dp[n];
}
};
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
if(n == 1) return nums[0];
int pre2 = 0, pre1 = 0, cur;
for (int i = 0;i < n;++i){
cur = max(pre1, nums[i] + pre2);
pre2 = pre1;
pre1 = cur;
}
return cur;
}
};
413… 等差数列划分
如果一个数列 至少有三个元素
,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums
,返回数组 nums
中所有为等差数组的 子数组
个数。
子数组
是数组中的一个连续
序列。
示例 1:
输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
示例 2:
输入:nums = [1]
输出:0
思路:
定义状态:dp[i]
表示从nums[0]
到nums[i]
且以nums[i]
为结尾的等差数列子数组的数量。
状态转移方程:dp[i] = dp[i-1]+1
if
nums[i]-nums[i-1]==nums[i-1]-nums[i-2]
else
0
解释:如果nums[i]
能和nums[i-1]
、nums[i-2]
组成等差数列,则以nums[i-1]
结尾的等差数列均可以nums[i]
结尾,且多了一个新等差数列[nums[i],nums[i-1],nums[i-2]]
。
举例:[1, 2, 3, 4, 5, 19, 20, 30, 40]
,答案为7
。分别为[1,2,3] [2,3,4] [3,4,5] [20,30,40] [1,2,3,4] [2,3,4,5] [1,2,3,4,5]
。
从nums[0] = 1
到nums[2] = 3
且以nums[2] = 3
为结尾的等差数列子数组包括[1,2,3]
,数量为1,dp[2] = dp[1] + 1 = 1
;
从nums[0] = 1
到nums[3] = 4
且以nums[3] = 4
为结尾的等差数列子数组包括[2,3,4] [1,2,3,4]
,数量为2,dp[3] = dp[2] + 1 = 2
;
从nums[0] = 1
到nums[4] = 5
且以nums[4] = 5
为结尾的等差数列子数组包括[3,4,5] [2,3,4,5] [1,2,3,4,5]
,数量为3,dp[4] = dp[3] + 1 = 3
;
从nums[0] = 1
到nums[5] = 19
且以nums[5] = 19
为结尾的等差数列子数组数量为0,因为19 - 5 != 5 - 4
;
从nums[0] = 1
到nums[6] = 20
且以nums[6] = 20
为结尾的等差数列子数组数量为0,因为20 - 19 != 19 - 5
;
从nums[0] = 1
到nums[7] = 30
且以nums[7] = 30
为结尾的等差数列子数组数量为0,因为30 - 20 != 20 - 19
;
从nums[0] = 1
到nums[8] = 40
且以nums[8] = 40
为结尾的等差数列子数组数量为1,因为40 - 30 == 30 - 20
,dp[8] = dp[7] + 1 = 1
;
所以,最终等差数列子数组的数量为dp数组的和。
代码:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n = nums.size();
if (n < 3) return 0; //如果数组中元素个数少于3,则不可能为等差序列。
vector<int> dp(n,0);
for (int i = 2;i < n;i++){
if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]){
dp[i] = dp[i-1] + 1;
}
}
return accumulate(dp.begin(),dp.end(),0); //accumulate(起始迭代器, 结束迭代器, 初始值, 自定义操作函数)
}
};
基本动态规划:二维
64. 最小路径和
给定一个包含非负整数的 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
思路:
定义状态:二维dp数组dp[i][j]
表示从左上角开始到(i,j)
位置的最优路径的数字和。
状态转移方程:每次只能向下或者向右移动,所以dp[i][j] = min(dp[i-1][j] + grid[i][j],dp[i][j-1] + grid[i][j])
。
在此基础上进行空间压缩:dp矩阵的值只和左边和上边的值相关,可以将其压缩为一维。
对于第i
行,在遍历到第j
列的时候,因为第j-1
列已经更新过了,所以 dp[j-1]
代表 dp[i][j-1]
的值;而dp[j]
待更新,当前存储的值是在第i-1
行的时候计算的,所以代表dp[i-1][j]
的值。(参考视频讲解)
还可以进一步地进行空间优化,即不开辟dp数组,直接在grid数组上进行修改。
代码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (i == 0 && j == 0){ //左上角格子
dp[i][j] = grid[i][j];
}
else if (i == 0){ //第一行格子 只可能是左边的格子向右走了一步 不可能是上边的格子向下走了一步
dp[i][j] = dp[i][j-1] + grid[i][j];
}
else if (j == 0){ //第一列格子 只可能是上边的格子向下走了一步 不可能是左边的格子向右走了一步
dp[i][j] = dp[i-1][j] + grid[i][j];
}
else {
dp[i][j] = min(dp[i-1][j] + grid[i][j],dp[i][j-1] + grid[i][j]); //普遍情况
}
}
}
return dp[m-1][n-1];
}
};
压缩代码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<int> dp(n,0);
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (i == 0 && j == 0){ //左上角格子
dp[j] = grid[i][j];
}
else if (i == 0){ //第一行格子 只可能是左边的格子向右走了一步 不可能是上边的格子向下走了一步
dp[j] = dp[j-1] + grid[i][j];
}
else if (j == 0){ //第一列格子 只可能是上边的格子向下走了一步 不可能是左边的格子向右走了一步
dp[j] = dp[j] + grid[i][j];
}
else {
dp[j] = min(dp[j] + grid[i][j],dp[j-1] + grid[i][j]); //普遍情况
}
}
}
return dp[n-1];
}
};
542. 01 矩阵
给定一个由 0
和 1
组成的矩阵 mat
,请输出一个大小相同的矩阵,其中每一个格子是 mat
中对应位置元素到最近的 0
的距离。
两个相邻元素间的距离为 1
。
示例 1:
输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]
示例 2:
输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]
思路-动态规划-四个方向:
考虑向四个方向遍历,分别为:
- 只能水平向左移动和竖直向上移动;
- 只能水平向左移动和竖直向下移动;
- 只能水平向右移动和竖直向上移动;
- 只能水平向右移动和竖直向下移动。
定义状态:二维dp
数组dp[i][j]
表示[i][j]
位置元素到最近的0
的距离。
状态转移方程:以只能水平向左移动和竖直向上移动为例,
d p [ i ] [ j ] = { 1 + m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) , i f m a t [ i ] [ j ] = = 1 0 , i f m a t [ i ] [ j ] = = 0 dp[i][j] =\left\{\begin{array}{l} 1 + min(dp[i-1][j],dp[i][j-1]),if \quad mat[i][j]==1\\ 0,if \quad mat[i][j]==0 \end{array}\right. dp[i][j]={1+min(dp[i−1][j],dp[i][j−1]),ifmat[i][j]==10,ifmat[i][j]==0
对于另外三种移动方法,也可以写出类似的状态转移方程,得到四个dp[i][j]
的值,其中最小的值就是所求的最短距离。
代码:
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m,vector<int>(n,INT_MAX/2)); //初始化动态规划数组,初始值均为很大的数
//如果mat中的元素为0,则对应dp数组的值也为0
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (mat[i][j] == 0){
dp[i][j] = 0;
}
}
}
//向左向上:证明左边和上边是遍历过的,即是从左上角向右下角遍历
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (i - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i-1][j] + 1); //此处比较最小值
}
if (j - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i][j-1] + 1); //此处比较最小值
}
}
}
//向左向下:证明左边和下边是遍历过的,即是从左下角向右上角遍历
for (int i = m - 1;i >= 0;i--){ //注意条件不同
for (int j = 0;j < n;j++){
if (i + 1 < m){
dp[i][j] = min(dp[i][j],dp[i+1][j] + 1); //此处比较最小值
}
if (j - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i][j-1] + 1); //此处比较最小值
}
}
}
//向右向上::证明右边和上边是遍历过的,即是从右上角向左下角遍历
for (int i = 0;i < m;i++){
for (int j = n - 1;j >= 0;j--){
if (i - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i-1][j] + 1); //此处比较最小值
}
if (j + 1 < n){
dp[i][j] = min(dp[i][j],dp[i][j+1] + 1); //此处比较最小值
}
}
}
//向右向下:证明右边和下边是遍历过的,即是从右下角向左上角遍历
for (int i = m - 1;i >= 0;i--){
for (int j = n - 1;j >= 0;j--){
if (i + 1 < m){
dp[i][j] = min(dp[i][j],dp[i+1][j] + 1); //此处比较最小值
}
if (j + 1 < n){
dp[i][j] = min(dp[i][j],dp[i][j+1] + 1); //此处比较最小值
}
}
}
return dp;
}
};
思路-动态规划-两个方向:
事实上,只需要从左上角遍历到右下角,以及从右下角遍历到左上角,即可得到最终dp数组。
证明:
关于动态规划为什么只考虑左上和右下就可以了。
_ _ _ 0 _
_ _ _ _ _
_ _ 1 _ _
_ _ _ _ _
如上图,我们假设某个位置(x,y)
离它最近的0
只有一个,且是在它的右上角,在它“右1上2”
的位置。这意味着距离中心1
距离为“3=1+2”
的范围内,所有的位置都是1
(或者出界,我们暂时不考虑出界的情况,不影响结论)。
所以这张图一定是(不考虑左下方的出界情况,不影响):
? ? ? ? 1 ? ? ? ?
? ? ? 1 1 0 ? ? ?
? ? 1 1 1 1 1 ? ?
? 1 1 1 "1" 1 1 1 ?
? ? 1 1 1 1 1 ? ?
? ? ? 1 1 1 ? ? ?
? ? ? ? 1 ? ? ? ?
所以我们来考虑它右一处标为X
的位置,离这个位置最近的0
,一定就是它“上二”
方向的0
:
? ? ? ? 1 ? ? ? ?
? ? ? 1 1 0 ? ? ?
? ? 1 1 1 1 1 ? ?
? 1 1 1 "1" X 1 1 ?
? ? 1 1 1 1 1 ? ?
? ? ? 1 1 1 ? ? ?
? ? ? ? 1 ? ? ? ?
所以在“考虑左上角的时候”
,X
处的位置可以记录到目标0
的信息,X
处的最短距离被正确记录了。
然后在“考虑右下角的时候”
,X
处的最短信息,正好又能被我们的中心“1”
观察到,所以中心1
间接地,记录到了目标0
的最短距离。
代码:
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m,vector<int>(n,INT_MAX/2)); //初始化动态规划数组,初始值均为很大的数
//如果mat中的元素为0,则对应dp数组的值也为0
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (mat[i][j] == 0){
dp[i][j] = 0;
}
}
}
//向左向上:证明左边和上边是遍历过的,即是从左上角向右下角遍历
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (i - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i-1][j] + 1); //此处比较最小值
}
if (j - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i][j-1] + 1); //此处比较最小值
}
}
}
//向右向下:证明右边和下边是遍历过的,即是从右下角向左上角遍历
for (int i = m - 1;i >= 0;i--){
for (int j = n - 1;j >= 0;j--){
if (i + 1 < m){
dp[i][j] = min(dp[i][j],dp[i+1][j] + 1); //此处比较最小值
}
if (j + 1 < n){
dp[i][j] = min(dp[i][j],dp[i][j+1] + 1); //此处比较最小值
}
}
}
return dp;
}
};
思路-BFS:
利用广度优先搜索,与声纹类似,对mat
矩阵中每一个0
进行上下左右四个方向的广播,同时将距离0
的长度更新为最短。
参考力扣题解。
代码:
class Solution {
public:
vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
int m = mat.size(), n = mat[0].size();
int dir[4][2] = {-1,0,1,0,0,-1,0,1};
vector<vector<int>> res(m,vector<int>(n,INT_MAX));
queue<pair<int,int>> que;
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (mat[i][j] == 0){
res[i][j] = 0;
que.push({i,j});
}
}
}
while(!que.empty()){
auto temp = que.front();
que.pop();
for (int i = 0;i < 4;i++){
int x = temp.first + dir[i][0];
int y = temp.second + dir[i][1];
if (x >= 0 && x < m && y >= 0 && y < n){
if (res[x][y] > res[temp.first][temp.second] + 1){
res[x][y] = res[temp.first][temp.second] + 1;
que.push({x,y});
}
}
}
}
return res;
}
};
1162. 地图分析
你现在手里有一份大小为 n x n
的 网格 grid
,上面的每个 单元格
都用 0
和 1
标记好了。其中 0
代表海洋,1
代表陆地。
请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的,并返回该距离。如果网格上只有陆地或者海洋,请返回 -1
。
我们这里说的距离是「曼哈顿距离」
( Manhattan Distance):(x0, y0)
和 (x1, y1)
这两个单元格之间的距离是 |x0 - x1| + |y0 - y1|
。
示例 1:
输入:grid = [[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释:
海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。
示例 2:
输入:grid = [[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释:
海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。
代码-对上一题简单的生搬硬套:
class Solution {
public:
int maxDistance(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
int dir[4][2] = {0,1,0,-1,1,0,-1,0};
vector<vector<int>> res(m,vector<int>(n,INT_MAX));
queue<pair<int,int>> que;
//如果网格上只有陆地或者海洋,请返回 -1。
int num0 = 0, num1 = 0;
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
if (grid[i][j] == 0){
num0 ++;
}
if (grid[i][j] == 1){
num1 ++;
res[i][j] = 0;
que.push({i,j});
}
}
}
if (num0 == m*n || num1 == m*n) return -1;
while(!que.empty()){
auto temp = que.front();
que.pop();
for (int i = 0;i < 4;i++){
int x = temp.first + dir[i][0];
int y = temp.second + dir[i][1];
if (x >= 0 && x < m && y >= 0 && y < n){
if (res[x][y] > res[temp.first][temp.second] + 1){
res[x][y] = res[temp.first][temp.second] + 1;
que.push({x,y});
}
}
}
}
int ans = 0;
for (int i = 0;i < m;i++){
for (int j = 0;j < n;j++){
ans = max(ans,res[i][j]);
}
}
return ans;
}
};
代码-稍作优化版本:
class Solution {
public:
int maxDistance(vector<vector<int>>& grid) {
//此题与上题的区别是一个需要过程中的所有结果 一个只需要层数
int n = grid.size();
int dir[4][2] = {0,1,0,-1,1,0,-1,0};
queue<pair<int,int>> que;
for (int i = 0;i < n;i++){
for (int j = 0;j < n;j++){
//先在队列中放入陆地位置
if (grid[i][j] == 1){
que.push({i,j});
}
}
}
if (que.size() == 0 || que.size() == n*n) return -1; //如果网格上只有陆地或者海洋,请返回 -1。
int dis = -1; //遍历层数
while(!que.empty()){
dis ++;
int size = que.size();
while (size--){
int x = que.front().first, y =que.front().second;
que.pop();
for (int i = 0;i < 4;i++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= n || grid[nextx][nexty] != 0) continue; //越界或者不是海洋的不管
grid[nextx][nexty] = 2;//标记已经来过的海洋
que.push({nextx,nexty});
}
}
}
return dis;
}
};
代码-动态规划:
class Solution {
public:
int maxDistance(vector<vector<int>>& grid) {
int n = grid.size();
vector<vector<int>> dp(n,vector<int>(n,INT_MAX/2));
for (int i = 0;i < n;i++){
for (int j = 0;j < n;j++){
if (grid[i][j] == 1){
dp[i][j] = 0;
}
}
}
for (int i = 0;i < n;i++){
for (int j = 0;j < n;j++){
if (i - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i-1][j] + 1);
}
if (j - 1 >= 0){
dp[i][j] = min(dp[i][j],dp[i][j-1] + 1);
}
}
}
for (int i = n - 1;i >= 0;i--){
for (int j = n - 1;j >= 0;j--){
if (i + 1 < n){
dp[i][j] = min(dp[i][j],dp[i+1][j] + 1);
}
if (j + 1 < n){
dp[i][j] = min(dp[i][j],dp[i][j+1] + 1);
}
}
}
int ans = -1;
for (int i = 0;i < n;i++){
for (int j = 0;j < n;j++){
if (!grid[i][j]){ //只遍历海洋格子对应的dp格子
ans = max(ans,dp[i][j]);
}
}
}
if (ans == INT_MAX/2){
return -1;
}
else{
return ans;
}
}
};
221. 最大正方形
在一个由 '0'
和 '1'
组成的二维矩阵内,找到只包含 '1'
的最大正方形,并返回其面积。
示例 1:
输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4
示例 2:
输入:matrix = [[“0”,“1”],[“1”,“0”]]
输出:1
示例 3:
输入:matrix = [[“0”]]
输出:0
思路:
对于矩阵内搜索正方形或者长方形的题型,一种常见的做法是:
定义一个二维dp数组,其中dp[i][j]
表示满足题目条件的,以[i,j]
为右下角的正方形或者长方形的属性。
对于本题,则表示以[i,j]
为右下角的全由1构成的最大正方形边长。
如果当前位置是0,那么dp[i][j]
即为0;如果当前位置为1,假设dp[i][j]
为k
,其充分条件为dp[i-1][j-1]
、dp[i][j-1]
和dp[i-1][j]
的值必须都不小于 k-1
,否则[i,j]
位置不可以构成一个面积为
k
2
k^2
k2 的正方形。同理,如果这三个值中的的最小值为k-1
,则[i,j]
位置一定且最大可以构成一个面积为
k
2
k^2
k2 的正方形。
注:充分条件和必要条件
若p,则q如果成立,那么p是q的充分条件,q是p的必要条件。
针对本题还有需要注意的地方:将dp[i+1][j+1]
定义为以mat的[i,j]
为右下角的正方形。之所以不是dp[i][j]
,是因为原数组的左边第一列和上边第一行的左上均没有格子,需要特殊处理。
因此为了代码简洁,我们假设补充了多一行全0,多一列全0,实际代码中先将dp数组全部置0。
注意放到VS里调试时,
vector<vector<char>> matrix = { {'1', '0', '1', '0', '0'} ,{'1', '0', '1', '1', '1'},{'1', '1', '1', '1', '1'},{'1', '0', '0', '1', '0'} };//char类型应该用单引号包裹,string类型才是双引号 第二点:vector类型应该用{}包裹,而不是用[]包裹
代码-不考虑边界情况:
class Solution {
public:
int maximalSquare(vector<vector<char>>& mat) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m, vector<int>(n, 1));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == '0') {//这里一定要注意是字符类型
dp[i][j] = 0;
}
//矩阵左边第一列和上边第一行的数字原来是多少还是多少 这里只需要考虑不要越界
if (mat[i][j] == '1' && i - 1 >= 0 && j - 1 >= 0) {//这里一定要注意是字符类型
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
}
}
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
res = max(res, dp[i][j]);
}
}
return res * res;
}
};
代码-考虑边界:
class Solution {
public:
int maximalSquare(vector<vector<char>>& mat) {
int m = mat.size(), n = mat[0].size(), max_side = 0;
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (mat[i-1][j-1] == '1') {//这里一定要注意是字符类型
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
}
max_side = max(max_side,dp[i][j]);
}
}
return max_side * max_side;
}
};
分割类型题
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
思路:
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。
定义状态:dp[i]
表示数字i
最少可以由几个完全平方数相加构成。
状态转移方程:位置i
只依赖于i - k^2
的位置,如i-1
,i - 4
,i - 9
等等,才能满足完全平方分割的条件。因此状态转移方程为dp[i] = 1 + min(dp[i-1], dp[i-4], dp[i-9], ...)
。
代码:
class Solution {
public:
int numSquares(int n) {
//最少数量
vector<int> dp(n+1,INT_MAX);
dp[0] = 0;
for (int i = 1;i <= n;i++){
//先找到小于等于i的k:对其进行开方 向下取整 sqrt(i)
for (int j = 1;j <= sqrt(i);j++){//j*j <= i
dp[i] = min(dp[i],dp[i-j*j] + 1);
}
}
return dp[n];
}
};
91.
343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数
的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积
。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
思路1-动态规划:
定义状态:dp[i]
表示正整数i
拆分以后可得到的最大乘积。
状态转移方程:
当 i≥2
时,假设对正整数 i
拆分出的第一个正整数是 j(1≤j<i)
,则有以下两种方案:
- 将
i
拆分成j
和i−j
的和,且i−j
不再拆分成多个正整数,此时的乘积是j×(i−j)
; - 将
i
拆分成j
和i−j
的和,且i−j
继续拆分成多个正整数,此时的乘积是j×dp[i−j]
。
因此,当 j
固定时,有 dp[i]=max(j×(i−j),j×dp[i−j])
。由于 j
的取值范围是 i−1
,需要遍历所有的 j
得到 dp[i]
。
初始化状态:
0 和 1 都不能拆分,因此 dp[0]=dp[1]=0
。
遍历顺序:
由状态转移方程知道 dp[i]
是从 j×(i−j)
和 j×dp[i−j]
且 j
的取值范围是 1
到 i−1
,需要遍历所有的 j
得到 dp[i]
所以从前往后遍历。
返回值:
最终得到 dp[n]
的值即为将正整数 n
拆分成至少两个正整数的和之后,这些正整数的最大乘积。
代码1:
class Solution {
public:
int integerBreak(int n) {
if (n == 0 || n == 1) return 0;
vector<int> dp(n + 1, 0);
for (int i = 2;i < n + 1;i++) {
for (int j = 1;j <= i - 1;j++) {
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));//注意三个数比较需要用两个max
}
}
return dp[n];
}
};
思路2-数学方法:
You’re making it pretty complicated.
If an optimal product contains a factor f >= 4, then you can replace it with factors 2 and f-2 without losing optimality, as 2*(f-2) = 2f-4 >= f. So you never need a factor greater than or equal to 4, meaning you only need factors 1, 2 and 3 (and 1 is of course wasteful and you’d only use it for n=2 and n=3, where it’s needed).
For the rest I agree, 33 is simply better than 22*2, so you’d never use 2 more than twice.
这里引用力扣官方题解下面的评论。大致意思是:
- 最大的拆分因子不可能大于等于4,假设该因子为
f (f >= 4)
,那么它可以继续拆分成2
与(f - 2)
,而2 * (f - 2) = 2f - 4 >= f (2f - 4 - f = f - 4 >= 0)
;因此拆分因子就在1,2,3中,而1只在n为2或3时有用; - 尽可能多地拆分3,因为
3 * 3 = 6 > 2 * 2 * 2 = 8
。(有点贪心算法的思想)
代码2:
class Solution {
public:
int integerBreak(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
int res = 1;
while(n > 4) {
res *= 3;
n -= 3;
}
res *= n;
return res;
}
};
在剑指系列中增加了大数据取余的要求,首先给出循环取余的代码框架:
循环求余代码框架:
// 求 (x^a) % p —— 循环求余法。固定搭配建议背诵
public long remainder(int x,int a,int p){ //x为底数,a为幂,p为要取的模
long rem = 1 ;
for (int i = 0; i < a; i++) {
rem = (rem * x) % p ;
}
return rem;
}
然后给出题解代码:
class Solution {
public:
int cuttingRope(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
long res = 1;
while (n > 4) {
res = res * 3 % 1000000007;
n -= 3;
}
return res * n % 1000000007;
}
};
这里有一个问题,上述取余的操作是正确的吗?给出解释:
已知 (a * b) % c = ((a % c) * (b % c)) % c
,在本题中,b = 3
,c = 1000000007
,b % c = 3 = b
,因此本题认为(a * b) % c = ((a % c) * b) % c
没有问题。
有关int和long型数据范围的分析:
(1) int型一共32位,有一位作为符号位,其数据范围是-2^31 ~ 2^31,即-2147483648 ~ 2147483647;
近似范围可以记为-2000000000 ~ 2000000000 即 - 2 × 10^9 ~ 2 × 10^9。
本题中给的模数为1 × 10^9 + 7(1000000007),若再乘以3,就超过了int型的范围,所以要使用long存储结果才不会溢出。
(2) long型一共64位,对应int型的方式,long型数据范围可以简单记为:-8 × 10^18 ~ 8 × 10^18。
本题的1000000007平方小于2 × 10^18,所以用long存储模数的平方也是没有问题的。
子序列
10. 正则表达式匹配
请实现一个函数用来匹配包含'. '
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = “a*”
输出: true
解释: 因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:
s = “ab”
p = “."
输出: true
解释: ".” 表示可匹配零个或多个(‘*’)任意字符(‘.’)。
示例 4:
输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = “misisp*.”
输出: false
思路: