力扣(leetcode)题目总结——动态规划篇

leetcode 经典题分类

  • 链表
  • 数组
  • 字符串
  • 哈希表
  • 二分法
  • 双指针
  • 滑动窗口
  • 递归/回溯
  • 动态规划
  • 二叉树
  • 辅助栈

本系列专栏:点击进入 leetcode题目分类 关注走一波


前言:本系列文章初衷是为了按类别整理出力扣(leetcode)最经典题目,一共100多道题,每道题都给出了非常详细的解题思路算法步骤代码实现。很多同学刚开始刷题都是按照力扣顺序刷题,其实这样对新手不太适用,刷题效果也很不好。因为力扣题目顺序是随机的,并没有按照算法分类,导致同一类型的算法强化训练不够,最后刷完也是迷迷糊糊的。所以本系列文章就是来帮你完成算法分类,针对每种算法做强化训练,保证让你以后遇到题目直接秒杀!

文章目录

  • leetcode 经典题分类
    • 最长回文子串
    • 正则表达式匹配
    • 括号生成
    • 外观数列
    • 接雨水
    • 通配符匹配
    • 跳跃游戏II
    • 最大子数组和
    • 不同路径
    • 不同路径II
    • 最小路径和
    • 爬楼梯
    • 编辑距离
    • 柱状图中最大的矩形
    • 扰乱字符串
    • 解码方法
    • 不同的二叉搜索树
    • 交错字符串


最长回文子串

【题目描述】

给你一个字符串 s,找到 s 中最长的回文子串。(回文子串:从正序读取子串和倒序的结果一样)

【输入输出实例】

示例 1:

输入:s = “babad”
输出:“bab” (“aba” 同样是符合题意的答案)

示例 2:

输入:s = “cbbd”
输出:“bb”

【算法思路】

对于一个子串而言,如果它是回文串,并且长度大于2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道“bab”是回文串,那么“ababa”一定是回文串,这是因为它的首尾两个字母都是“a”。

根据上述思路,我们就可以用动态规划的方法解决本题。我们用二维数组P(i, j)表示字符串s的第i到j个字母组成的串是否为回文串:

在这里插入图片描述

其它情况包含两种可能性:

(1)s[i, j]本身不是一个回文串;

(2)i > j,此时s[i, j]本身不合法;

那么我们就可以写出动态规划的状态转移方程:

P(i, j) = P(i+1, j−1) ∧ (Si == Sj)

也就是说,只有s[i+1, j−1]是回文串,并且s的第i和j个字母相同时,s[i, j]才会是回文串。

上文的所有讨论是建立在子串长度大于2的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为1或2。对于长度为1的子串,它显然是个回文串;对于长度为2的子串,只要它的两个字母相同,它就是一个回文串。

因此我们就可以写出动态规划的边界条件:

在这里插入图片描述

根据这个思路,我们就可以完成动态规划,最终的答案即为所有P(i, j) == true中j−i+1(即子串长度)的最大值。

注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的(即每次循环按照 i 和 j 相差k-1个字符进行,k起始为2,逐渐增加至s.size()),因此一定要注意动态规划的循环顺序。

【算法描述】

/**************** 动态规划 ****************/
string longestPalindrome(string s) 
{int length = s.size();int maxlength = 1;  //记录最长回文子串的长度int begin = 0;  //记录最长回文子串的起始位置vector<vector<bool>> n(length, vector<bool>(length));  //容器里放容器,相当于二维数组for(int i = 0; i < length; i++){n[i][i] = true;  //一个字符即为最短的回文子串}for(int k = 2; k <= length; k++){for(int i = 0; i < length; i++){int j = i + k - 1;  //每次按照i和j相差k-1个字符进行循环,k逐渐增加,直到k为lengthif(j >= length)  //超出右边界,退出循环{break;}if(s[i] != s[j])  //表示从第i位置到第j位置的子串不是回文子串{n[i][j] = false;}else{if(i+1 <= j-1)  //表示第i位置到第j位置的字符中间至少有一个字符{n[i][j] = n[i+1][j-1];}else  //表示第i位置字符和第j位置字符为相邻字符{n[i][j] = true;}}//找最长回文子串的起始下标和子串长度if(n[i][j] && maxlength < j - i + 1){maxlength = j - i + 1;begin = i;}}}return s.substr(begin, maxlength);  //返回由第begin位置开始的maxlength个字符组成的字符串
}

正则表达式匹配

【题目描述】

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

【输入输出实例】

示例 1:

输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。

示例 2:

输入:s = “aa”, p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

示例 3:

输入:s = “ab”, p = “."
输出:true
解释:".
” 表示可匹配零个或多个(‘*’)任意字符(‘.’)。

【算法思路】

设 s 的长度为 n , p的长度为 m ;将 s 的第 i 个字符记为 si,p 的第 j 个字符记为 pj ,将 s 的前 i 个字符组成的子字符串记为 s[:i] , 同理将 p 的前 j 个字符组成的子字符串记为 p[:j]。本题可转化为求 s[:n] 是否能和 p[:m] 匹配。

总体思路是从 s[:1] 和 p[:1] 开始判断是否能匹配,每轮添加一个字符并判断是否能匹配,直至添加完整个字符串 s 和 p 。展开来看,假设 s[:i] 与 p[:j] 可以匹配,那么下一状态有两种:

  • 添加一个字符 si+1 后是否能匹配?
  • 添加一个字符 pj+1 后是否能匹配?

因此,本题的状态共有 m×n 种,定义状态矩阵 dp ,dp[i][j] 代表 s[:i] 与 p[:j] 是否可以匹配。

  • 状态定义: 设动态规划矩阵 dp , dp[i][j] 代表字符串 s 的前 i 个字符和 p 的前 j 个字符能否匹配。
  • 转移方程: 需要注意,由于 dp[0][0] 代表的是空字符的状态, 因此 dp[i][j] 对应的添加字符是 s[i - 1] 和 p[j - 1] 。
    • p[j-1] == '*' 时,dp[i][j] 在当以下任一情况成立时都等于 true:
      • dp[i][j-2]:即将字符组合 p[j-2] * 看作是 p[j-2] 出现 0 次时,能否匹配;
      • dp[i-1][j] 且 s[i-1] == p[j-2]:即让字符 p[j - 2] 多出现 1 次去匹配 s[i - 1] 时,能否匹配;
      • dp[i-1][j] 且 p[j-2] == '.':即让字符 ‘.’ 多出现 1 次去匹配 s[i - 1] 时,能否匹配。
    • p[j-1] != '*' 时,dp[i][j] 在当以下任一情况成立时都等于 true:
      • dp[i-1][j-1] 且 s[i-1] == p[j-1]:即在 s[:i-1] 和 p[:j-1] 字符匹配的情况下,新加入的字符 s[i - 1] 和 p[j - 1] 相等时,会匹配。
      • dp[i-1][j-1] 且 p[j-1] == '.':即在 s[:i-1] 和 p[:j-1] 字符匹配的情况下,新加入的字符 p[i - 1] 为 ‘.’ 时,会匹配。
  • 初始化: 需要先初始化 dp 矩阵首行,以避免状态转移时索引越界。
    • dp[0][0] = true: 代表两个空字符串能够匹配;
    • dp[0][j] = dp[0][j-2] 且 p[j-1] == '*':首行 s 为空字符串,因此当 p 的偶数位为 ‘*’ 时才能够匹配(即让 p 的奇数位出现 0 次,也就是 p 中 * 前一位出现 0 次,保持 p 是空字符串)。因此循环遍历字符串 p,步长为 2(即只看偶数位,奇数位默认为false,因为不可能匹配)。
  • 返回值: dp 矩阵最右下角值,代表字符串 s 和 p 能否匹配。

如下图举例说明:

在这里插入图片描述

【算法描述】

bool isMatch(string s, string p) {int sLen = s.size();int pLen = p.size();std::vector<std::vector<bool>> dp(sLen+1, std::vector<bool>(pLen+1, false));// 初始化dp[0][0] = true;for(int i = 2; i <= pLen; i += 2) {dp[0][i] = dp[0][i-2] && (p[i-1] == '*');}// 动态规划for(int i = 1; i <= sLen; ++i) {for(int j = 1; j <= pLen; ++j) {if(p[j-1] == '*') {dp[i][j] = dp[i][j-2] || (dp[i-1][j] && (s[i-1] == p[j-2]))|| (dp[i-1][j] && (p[j-2] == '.'));// 简化// dp[i][j] = dp[i][j-2] || (dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.'));}else {dp[i][j] = (dp[i-1][j-1] && (s[i-1] == p[j-1])) || (dp[i-1][j-1] && (p[j-1] == '.'));// 简化// dp[i][j] = (dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.'));}}}return dp[sLen][pLen];
}

括号生成

【题目描述】

数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

【输入输出实例】

示例 1:

输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

示例 2:

输入:n = 1
输出:[“()”]

【算法思路】

当我们知道所有 i < n 时括号的有效排列后,对于 i = n 的情况,必定是由一组完整的括号 “( )” 和 i = n - 1 时的有效排列组成。那么,剩下 n - 1 组括号有可能在哪呢?

剩下的括号要么在这一组新增的括号内部,要么在这一组新增括号的外部(右侧)。既然知道了 i < n 的情况,那我们就可以对所有情况进行遍历:

“(” +【i=p时所有括号的排列组合】+ “)” +【i=q时所有括号的排列组合】

其中 p + q = n - 1,且 p 和 q 均为非负整数。

事实上,当上述 p 从 0 取到 n - 1,q 从 n - 1取到 0 后,所有情况就遍历完了。

【算法描述】

//动态规划
vector<string> generateParenthesis(int n) {vector<vector<string>> dp(n+1);    //dp[i]表示i组括号的所有组合dp[0] = {""};    //初始化dp[1] = {"()"};for(int i = 2; i <= n; i++){for(int j = 0; j < i; j++){for(string p : dp[j])  //循环时依次将dp[j]中的每一项赋给p,即遍历dp[j]的所有有效组合{for(string q : dp[i-j-1])   //遍历dp[i-j-1]的所有有效组合{dp[i].push_back("(" + p + ")" + q);  //动态规划方程}}}}return dp[n];  //返回n组括号的所有有效组合
}

外观数列

【题目描述】

给定一个正整数 n ,输出外观数列的第 n 项。「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。你可以将其视作是由递归公式定义的数字字符串序列:

  • countAndSay(1) = "1"
  • countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。

前五项如下:

  1. 1                     第一项是数字 1 
    
  2. 11                    描述前一项,前一项是 1 ,即 “ 一 个 1 ”,记作 "11"
    
  3. 21                    描述前一项,前一项是 11 ,即 “ 二 个 1 ” ,记作 "21"
    
  4. 1211                  描述前一项,前一项是 21 ,即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
    
  5. 111221                描述前一项,前一项是 1211 ,即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221" 
    

要描述一个数字字符串,首先要将字符串分割为最小数量的组,每个组都由连续的最多相同字符组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。

例如,数字字符串 “3322251” 的描述如下图:

在这里插入图片描述

【输入输出实例】

示例 1:

输入:n = 1
输出:“1”

示例 2:

输入:n = 4
输出:“1211”
解释:
countAndSay(1) = “1”
countAndSay(2) = 读 “1” = 一 个 1 = “11”
countAndSay(3) = 读 “11” = 二 个 1 = “21”
countAndSay(4) = 读 “21” = 一 个 2 + 一 个 1 = “12” + “11” = “1211”

【算法思路】

使用动态规划:

(1)找子问题:dp[i]表示第 i+1 项外观数列。

(2)动态方程思想:每次求 dp[i] 时,通过遍历前一项数列 dp[i-1] ,记录前一项数列中每个数的连续出现次数。将描述字符的数量和描述字符不断地加到 dp[i] 中,即可得到第 i+1 项外观数列 dp[i]。

(3)找边界(初始化):第 1 项外观数列 dp[0] 就是 “1”。

(4)找输出:返回题目中所求的第 n 项外观数列,即为 dp[n-1]。

(5)空间优化:根据动态方程,求 dp[i] 只和 dp[i-1] 有关,则可以使用「滚动变量」将代码进行优化。

【算法描述】

//动态规划
string countAndSay(int n) {vector<string> dp(n);    //dp[i]表示第 i+1 项外观数列dp[0] = "1";    //初始化for(int i = 1; i < n; i++)    //算出每一项外观数列dp[i]{int count = 1;    //记录前一项数列中每个数的连续出现次数int len = dp[i-1].size();for(int j = 1; j < len; j++)    //遍历前一项数列{if(dp[i-1][j] != dp[i-1][j-1]){dp[i] += count + '0';    //描述字符的数量dp[i] += dp[i-1][j-1];    //描述字符count = 1;continue;}count++;}dp[i] += count + '0';    //记录最后一个字符数字dp[i] += dp[i-1][len-1];}return dp[n-1];    //返回第 n 项外观数列
}//动态规划——空间优化
string countAndSay(int n) {string prestr = "1";    //表示dp[i-1],第 i 项外观数列string currstr;    //表示dp[i],第 i+1 项外观数列for(int i = 1; i < n; i++)    //算出每一项外观数列{int count = 1;    //记录前一项数列中每个数的连续出现次数int len = prestr.size();for(int j = 1; j < len; j++)    //遍历前一项数列{if(prestr[j] != prestr[j-1]){currstr += count + '0';    //描述字符的数量currstr += prestr[j-1];    //描述字符count = 1;continue;}count++;}currstr += count + '0';    //记录最后一个字符数字currstr += prestr[len-1];prestr = currstr;    //滚动变量更新currstr.clear();}return prestr;    //返回第 n 项外观数列
}

接雨水

【题目描述】

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

【算法思路】

一、双指针法

按照列来计算雨水的体积,那么每一列雨水的宽度一定是1,我们再把每一列的雨水的高度求出来就可以了。

可以看出每一列雨水的高度,取决于该列左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度,同时再减去该列柱子的高度。

例如:求下图中第4列雨水的体积

在这里插入图片描述

列4左侧最高的柱子是列3,高度为2(以下用lHeight表示)。

列4右侧最高的柱子是列7,高度为3(以下用rHeight表示)。

列4 柱子的高度为1(以下用height表示)

那么列4的雨水高度为 列3和列7的高度最小值 减 列4高度,即:min(lHeight, rHeight) - height。列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积。

按照上面的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。(注意第一个柱子和最后一个柱子不接雨水)

二、动态规划

本质类似于双指针法,只是用动态规划来提前求出每个柱子的最大左边柱子和最大右边柱子。因为在上述双指针法中,每遍历一列,都要重新求该列的最大左边柱子和最大右边柱子,重复了很多不必要的工作。

所以用动态规划来求maxLeft[i] = max(maxLeft[i-1], height[i]),只需要将前一根柱子的maxLeft[i-1]和当前柱子高度进行比较,若当前柱子高度较大,说明当前柱子的最大左边柱子就是自己本身。

得出所有柱子的最大左边柱子和最大右边柱子后,再通过遍历每根柱子求出每一列雨水的体积,相加之后就是总雨水的体积了。

【算法描述】

//双指针法(超时)
int trap(vector<int>& height) 
{int len = height.size();  int sum = 0;  //记录接的雨水for(int i = 1; i < len - 1; i++)  //遍历每列(除第一列和最后一列,因为它们不会接雨水){int maxLeft = 0;  //左边最高柱子的高度int maxRight = 0;  //右边最高柱子的高度for(int left = i - 1; left >= 0; left--)  //找左边最高柱子{maxLeft = max(maxLeft, height[left]);}for(int right = i + 1; right < len; right++)  //找右边最高柱子{maxRight = max(maxRight, height[right]);if(maxRight > maxLeft)  //因为最后要取两者最小,所以只要超过左边最高柱子,直接退出{break;}}int rainHeight = min(maxLeft, maxRight) - height[i];  //计算雨水高度sum += (rainHeight > 0) ? rainHeight : 0;}return sum;
}//动态规划——相当于双指针法,只是用动态规划来提前求出每个柱子的最大左边柱子和最大右边柱子
int trap(vector<int>& height) 
{int len = height.size();  int sum = 0;  //记录接的雨水vector<int> maxLeft(len);  //记录第i列左边柱子最大高度vector<int> maxRight(len);  //记录第i列右边柱子最大高度maxLeft[0] = height[0];  //初始化maxRight[len-1] = height[len-1];//记录每个柱子左边柱子最大高度for(int i = 1; i < len-1; i++){maxLeft[i] = max(maxLeft[i-1], height[i]);}//记录每个柱子右边柱子最大高度for(int i = len-2; i > 0; i--){maxRight[i] = max(maxRight[i+1], height[i]);}//遍历每列算雨水量再求和for(int i = 1; i < len - 1; i++){int rainHeight = min(maxLeft[i-1], maxRight[i+1]) - height[i];sum += (rainHeight > 0) ? rainHeight : 0;}return sum;
}

通配符匹配

【题目描述】

给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 '?''*' 的通配符匹配。

  • '?' 可以匹配任何单个字符(不包括空字符串)。
  • '*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 '?''*'

【输入输出实例】

示例 1:

输入: s = “aa”,p = “a”
输出: false (“a” 无法匹配 “aa” 整个字符串)

示例 2:

输入: s = “aa”,p = “*”
输出: true ( '*' 可以匹配任意字符串)

示例 3:

输入: s = “cb”,p = “?a”
输出: false ('?' 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’)

示例 4:

输入: s = “adceb”,p = "*a*b"
输出: true (第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 “dce”)

示例 5:

输入: s = “acdcb”,p = “a*c?b”
输出: false

【算法思路】

我们来看这样一个表:

在这里插入图片描述

其中,横轴为string s,纵轴为pattern p,这个表第(m, n)个格子的意义是:【p从0位置到m位置】这一整段,是否能与【s从0位置到n位置】这一整段匹配。

也就是说,如果表格的下面这一个位置储存的是T(True),说明 “adcb” 和 “a*b” 这一段是可以匹配的,你不用管前面发生了什么,你只要知道,这两段是匹配的,这就是为什么要用动态规划的原因。

在这里插入图片描述

在给定的模式串 p 中,只会有三种类型的字符出现:

  • 小写字母 a~z,可以匹配对应的一个小写字母;
  • 问号 ‘?’,可以匹配任意一个小写字母;
  • 星号 ‘*’,可以匹配任意字符串,可以为空,也就是匹配零或任意多个小写字母。

其中「小写字母」和「问号」的匹配是确定的,而「星号」的匹配是不确定的,因此我们需要枚举所有的匹配情况。

(1)找子问题:我们用 dp[i][j] 表示字符串 s 的前 i个字符和模式串 p 的前 j 个字符是否能匹配。

(2)动态方程思想:在进行状态转移时,我们可以考虑模式串 p 的第 j 个字符p[j],与之对应的是字符串 s 中的第 i 个字符 s[i]:

  • 如果 p[j] 是小写字母 a~z,那么 s[i] 必须也为相同的小写字母,状态转移方程为:
dp[i][j] = (s[i] == p[j]) && dp[i-1][j-1]
  • 如果 p[j] 是问号 ‘?’,那么对 s[i] 没有任何要求,状态转移方程为:
dp[i][j] = dp[i-1][j-1]
  • 如果 p[j] 是星号 ‘*’,那么同样对 s[i] 没有任何要求,但是星号可以匹配零或任意多个小写字母,因此状态转移方程分为两种情况,即:使用或不使用这个星号。如果我们不使用这个星号('*'代表是空字符),那么就会从dp[i][j–1]转移而来;如果我们使用这个星号('*'代表非空任何字符),那么就会从dp[i-1][j]转移而来。因此状态转移方程为:
dp[i][j] = dp[i-1][j] || dp[i][j-1]

我们也可以将前两种转移进行归纳,最终的状态转移方程如下:

dp[i][j] = (s[i] == p[j] || p[j] == '?') && dp[i-1][j-1]

dp[i][j] = (p[j] == '*') && (dp[i-1][j] || dp[i][j-1])

(3)找边界(初始化):

  • dp[0][0]:表示 s 的第一个空字符和 p 的第一个空字符匹配,所以为true

  • 第一行dp[0][j],s 为空字符,与 p 匹配,所以只要 p 开始为*才为true

  • 第一列dp[i][0],当然全部为False

(4)找输出:要判定是否成功匹配,只看最右下角的格子,即为dp[m][n],其中 m 和 n 分别是字符串 s 和模式 p 的长度。

【算法描述】

bool isMatch(string s, string p) 
{int m = s.size();    //行为字符串sint n = p.size();    //列为字符串pvector<vector<bool>> dp(m+1, vector<bool>(n+1));  //dp[i][j]表示 字符串s到i位置 和 字符串p到j位置 是否匹配dp[0][0] = true;    //初始化dp[0][0],表示s的第一个空字符和p的第一个空字符匹配//初始化第一列dp[i][0],当然全部为false//初始化第一行dp[0][j],s为空字符,与p匹配,所以只要p开始为'*'才为truefor(int j = 1; j <= n; j++)   {dp[0][j] = (p[j-1] == '*') ? dp[0][j-1] : false;}//填充二维数组,判断s[0:i]和p[0:j]是否可匹配for(int i = 1; i <= m; i++){for(int j = 1; j <= n; j++){//因为dp[0][0]初始化为s的第一个空字符和p的第一个空字符匹配,所以下面s和p字符下标都要-1//动态规划方程:单个字符匹配,'?'可以匹配任何单个字符if(dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '?')){dp[i][j] = true;}//动态规划方程:遇到字符串p为'*'时,//dp[i-1][j]是从使用了'*'转移过来,'*'代表非空任何字符,例如abcd, ab*//dp[i][j-1]是从不使用'*'转移过来,'*'代表是空字符,例如ab, ab*if(p[j-1] == '*' && (dp[i-1][j] || dp[i][j-1])){dp[i][j] = true;}// 简化// dp[i][j] = (dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '?'))//         || (p[j-1] == '*' && (dp[i][j-1] || dp[i-1][j]));}}return dp[m][n];   //要判定是否成功匹配,只看最右下角的格子
}

跳跃游戏II

【题目描述】

给定一个长度为 n 的整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例均可以到达 nums[n - 1]。

【输入输出实例】

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

【算法思路】

方法一:贪心算法

  1. 如果某一个作为起跳点的格子可以跳跃的距离是 3,那么表示后面 3 个格子都可以作为下一次起跳点。在当前跳跃区间内,可以对每一个能作为起跳点的格子都尝试跳一次,不断更新能跳到的最远距离,这个能跳到的最远距离就是下一次跳跃区间的终点。

  2. 所以,当一次跳跃结束时,从下一个格子开始,到现在能跳到最远的距离,就是下一次的跳跃区间

  • 对每一次跳跃用 for 循环来模拟。
  • 跳完一次之后,更新下一次的跳跃区间跳跃次数
  • 在新的范围内跳,继续更新能跳到最远的距离
  1. 记录跳跃次数,如果跳到了终点,就得到了结果。

在这里插入图片描述

贪心算法优化:

  • 从上面代码观察发现,其实被 while 包含的 for 循环中,i 是从头跑到尾的。
  • 其实只需要在一次跳跃完成时,更新下一次能跳到最远的距离,并以此刻作为时机来更新跳跃次数
  • 则可以在一次 for 循环中完成。

方法二:动态规划

(1)找子问题:dp[i] 表示到达 nums[i] 的最小跳跃次数。

(2)动态方程思想:每次求 dp[i] 时,需要遍历 nums[i] 之前的所有项,找出能到达 nums[i] 的项,再在这些能到达 nums[i] 的项里面找出最小跳跃次数,即为 dp[i]。

for(int j = 0; j < i; j++)    //遍历nums[i]之前的所有项
{if(nums[j] + j >= i)   //找能到达nums[i]的最小跳跃次数{minJump = min(minJump, dp[j]+1);}
}

(3)找边界(初始化):到达 nums[0] 不需要跳跃,则最小跳跃次数为 0,即dp[0] = 0;

(4)找输出:返回到达 nums[n-1] 的最小跳跃次数,即为 dp[n-1]。

【算法描述】

//方法一:贪心算法
int jump(vector<int>& nums) {int minJump = 0;    //记录到达nums[n-1]的最小跳跃次数int start = 0;    //记录当前能跳跃到的起始位置(闭区间)int end = 1;    //记录当前能跳跃到的终点位置(开区间)while(end < nums.size()){int maxStep = 0;   //记录在当前区间内能跳到的最远距离for(int i = start; i < end; i++){maxStep = max(maxStep, nums[i]+i);}start = end;    //下一次起跳点范围开始位置end = maxStep + 1;    //下一次起跳点范围结束位置minJump++;    // 跳跃次数}return minJump;
}
//贪心算法——优化
int jump(vector<int>& nums) {int minJump = 0;    //记录到达nums[n-1]的最小跳跃次数int end = 0;    //记录当前能跳跃到的终点位置(闭区间)int maxStep = 0;   //记录在当前区间内能跳到的最远距离for(int i = 0; i < nums.size() - 1; i++){maxStep = max(maxStep, nums[i]+i);if(i == end)    //达到当前区间末尾{end = maxStep;    //下一次起跳点范围结束位置minJump++;    // 跳跃次数}}return minJump;
}//方法二:动态规划(时间消耗比贪心算法大得多)
int jump(vector<int>& nums) {int n = nums.size();vector<int> dp(n);    //dp[i]表示到达nums[i]的最小跳跃次数dp[0] = 0;    //初始化for(int i = 1; i < n; i++)    //遍历nums{int minJump = i;    //记录到nums[i]的最小跳跃次数for(int j = 0; j < i; j++)    //遍历i项之前{if(nums[j] + j >= i)   //找到nums[i]的最小跳跃次数{minJump = min(minJump, dp[j]+1);}}dp[i] = minJump;}return dp[n-1];    //返回到达nums[n-1]的最小跳跃次数
}

最大子数组和

【题目描述】

给定一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。

【输入输出实例】

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6 (连续子数组[4,-1,2,1]的和最大,为 6 )

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

【算法思路】

题目只要求返回结果,不要求得到最大的连续子数组是哪一个。这样的问题通常可以使用「动态规划」解决。

动态规划算法:

关键:如何定义子问题(如何定义状态)?

设计状态思路:把不确定的因素确定下来,进而把子问题定义清楚,把子问题定义得简单。动态规划的思想通过解决了一个一个简单的问题,进而把简单的问题的解组成了复杂的问题的解。

我们不知道和最大的连续子数组一定会选哪一个数,那么我们可以求出所有经过输入数组的某一个数的连续子数组的最大和:

例如,输入数组是[-2,1,-3,4,-1,2,1,-5,4],可以求出以下子问题:

  • 子问题 1:经过 -2 的连续子数组的最大和是多少;

  • 子问题 2:经过 1 的连续子数组的最大和是多少;

  • 子问题 3:经过 -3 的连续子数组的最大和是多少;

  • 子问题 4:经过 4 的连续子数组的最大和是多少;

  • 子问题 5:经过 -1 的连续子数组的最大和是多少;

  • 子问题 6:经过 2 的连续子数组的最大和是多少;

  • 子问题 7:经过 1 的连续子数组的最大和是多少;

  • 子问题 8:经过 -5 的连续子数组的最大和是多少;

  • 子问题 9:经过 4 的连续子数组的最大和是多少。

一共9个子问题,这些子问题之间的联系并没有那么好看出来,这是因为子问题的描述还有不确定的地方(有后效性)。

例如「子问题 3」:经过 -3 的连续子数组的最大和是多少。

「经过 -3 的连续子数组」我们任意举出几个:

  • [-2,1,-3,4] ,-3 是这个连续子数组的第 3 个元素;

  • [1,-3,4,-1] ,-3 是这个连续子数组的第 2 个元素;

  • ……

我们不确定的是:-3 是连续子数组的第几个元素。那么我们就把 -3 定义成连续子数组的最后一个元素。在新的定义下,我们列出子问题如下:

  • 子问题 1:以 -2 结尾的连续子数组的最大和是多少;

  • 子问题 2:以 1 结尾的连续子数组的最大和是多少;

  • 子问题 3:以 -3 结尾的连续子数组的最大和是多少;

  • 子问题 4:以 4 结尾的连续子数组的最大和是多少;

  • 子问题 5:以 -1 结尾的连续子数组的最大和是多少;

  • 子问题 6:以 2 结尾的连续子数组的最大和是多少;

  • 子问题 7:以 1 结尾的连续子数组的最大和是多少;

  • 子问题 8:以 -5 结尾的连续子数组的最大和是多少;

  • 子问题 9:以 4 结尾的连续子数组的最大和是多少。

我们加上了「结尾的」,这些子问题之间就有了联系。我们单独看子问题 1 和子问题 2:

子问题 1:以 -2 结尾的连续子数组的最大和是多少;

以 -2 结尾的连续子数组是 [-2],因此最大和就是 -2。

子问题 2:以 1 结尾的连续子数组的最大和是多少;

以 1 结尾的连续子数组有 [-2,1] 和 [1] ,其中 [-2,1] 就是在「子问题 1」的后面加上1得到。-2 + 1 = -1 < 1−2+1=−1<1 ,因此「子问题 2」的答案是 1。

根据子问题得到:如果编号为i的子问题的结果是负数或者0,那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果舍弃掉。否则编号为 i + 1 的子问题为编号为 i 的子问题 + nums[i]。

根据上面对子问题的分析,编写动态规划题解的步骤如下:

(1)定义状态(定义子问题)

dp[i]:表示以 nums[i] 结尾的连续子数组的最大和。

说明:「结尾」和「连续」是关键字。

(2)状态转移方程(描述子问题之间的联系)

  • 如果 dp[i - 1] > 0,那么可以把nums[i]直接接在dp[i - 1]表示的那个数组的后面,得到和更大的连续子数组;

  • 如果 dp[i - 1] <= 0,那么nums[i]加上前面的数dp[i - 1]以后值不会变大。于是dp[i]“另起炉灶”,此时单独的一个nums[i]的值,就是dp[i]。

根据以上两种情况,写出如下状态转移方程:

在这里插入图片描述

因为以上两种情况的最大值就是 dp[i] 的值,则状态转移方程也可以写成:

在这里插入图片描述

(3)初始化(思考初始值)

dp[0] 根据定义,只有1个数,以nums[0]结尾,则dp[0] = nums[0]。

(4)思考输出

这里状态的定义不是题目中的问题的定义,不能直接将最后一个状态返回回去;

这个问题的输出是把所有的dp[0]、dp[1]、……、dp[n - 1] 都遍历一遍,取最大值。

(5)是否可优化空间

根据「状态转移方程」,dp[i]的值只和i以前的值(dp[i-1]、nums[i])有关,因此可以使用「滚动变量」的方式将代码进行优化。

【算法描述】

/********************* 动态规划 *********************/
int maxSubArray(vector<int>& nums) 
{int len = nums.size();vector<int> dq(len);    //dp[i]表示:以nums[i]结尾的连续子数组的最大和dq[0] = nums[0];    //初始化int Max = dq[0];    //记录连续子数组的最大和for(int i = 1; i < len; i++)    //根据状态方程更新所有的dp[i]{//如果以nums[i-1]结尾的连续子数组的最大和 <= 0,以nums[i]结尾的连续子数组直接为当前元素nums[i]//否则为当前元素 + 以nums[i-1]为结尾的连续子数组的最大和if(dq[i - 1] <= 0){dq[i] = nums[i];}else{dq[i] = dq[i - 1] + nums[i];}}for(int i = 1; i < len; i++)   //遍历dq[i],找最大值{Max = max(Max, dq[i]);}return Max;
}/***************** 动态规划——空间优化*****************/
int maxSubArray(vector<int>& nums) 
{int dq = nums[0];   //表示以当前元素为结尾的连续子数组的最大和int Max = dq;    //记录连续子数组的最大和for(int i = 1; i < nums.size(); i++){dq = max(nums[i], dq + nums[i]);    //更新dqMax = max(Max, dq);    //每更新一次dq,则找一次最大值}return Max;
}/***************** 动态规划——空间优化*****************/
int maxSubArray(vector<int>& nums) 
{int Max = nums[0];    //存放最大和for(int i = 1; i < nums.size(); i++){//若nums数组第i个元素之前的连续元素和 <= 0,则舍去之前的元素,重新开始  //若nums数组第i个元素之前的连续元素和 > 0,则将和加到当前元素nums[i]上if(nums[i-1] > 0){nums[i] += nums[i-1];}Max = max(Max, nums[i]);   //每次更新nums[i]后,要找最大值}return Max;
}

不同路径

【题目描述】

一个机器人位于一个 m × n 网格的左上角(起始点在下图中标记为Start)。机器人每次只能向下或者向右移动一步,机器人试图达到网格的右下角(在下图中标记为Finish)。

求总共有多少条不同的路径?

【输入输出实例】

示例 1:

在这里插入图片描述

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
从左上角开始,总共有3条路径可以到达右下角。
(1)向右 -> 向下 -> 向下
(2)向下 -> 向下 -> 向右
(3)向下 -> 向右 -> 向下

【算法思路】

经典的动态规划解法:

(1)找子问题:设dp[i][j]存放到达(i, j)的所有路径数。

(2)动态方程思想:到达右下角的路径数 = 到达右下角左边一格的路径数 + 到达右下角上面一格的路径数。即可列出动态方程:

dp[i][j] = dp[i-1][j] + dp[i][j-1]

(3)找边界(初始化):对于第一行dp[0][j],或者第一列dp[i][0],由于都是在边界,只有一条路径,所以路径数只能为1。

(4)空间优化:根据动态方程,dp[i][j]的值只和dp[i-1][j]dp[i][j-1]有关,因此可以使用「滚动变量」的方式将代码进行优化。可以只使用一行的空间来完成,甚至可以只使用3个变量。

【算法描述】

//动态规划
int uniquePaths(int m, int n) {//边界:第一行和第一列的路径数都为1,则dp全部初始化为1vector<vector<int>> dp(m, vector<int>(n, 1));   //存放(i,j)的路径数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];    //返回最右下角的路径数
}//动态规划————空间优化
int uniquePaths(int m, int n) {vector<int> dp(n, 1);    //初始化边界条件for(int i = 1; i < m; i++){for(int j = 1; j < n; j++){dp[j] = dp[j] + dp[j-1];   //右边dp[j]表示上一格路径数,dp[j-1]表示左一格路径数}}return dp[n-1];   //返回最右下角的路径数
}

不同路径II

【题目描述】

一个机器人位于一个 m × n 网格的左上角(起始点在下图中标记为Start)。机器人每次只能向下或者向右移动一步,机器人试图达到网格的右下角(在下图中标记为Finish)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?(网格中的障碍物和空位置分别用1和0来表示)

【输入输出实例】

示例 1:

在这里插入图片描述

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
从左上角到右下角一共有2条不同的路径:
(1)向右 -> 向右 -> 向下 -> 向下
(2)向下 -> 向下 -> 向右 -> 向右

示例 2:

在这里插入图片描述

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

【算法思路】

经典的动态规划解法:

(1)找子问题:设dp[i][j]存放到达(i, j)的所有路径数。

(2)动态方程思想:若当前格(i, j)无障碍物,则到达(i, j)的路径数 = 到达(i-1, j)的路径数 + 到达(i, j-1)的路径数。若当前格(i, j)有障碍物,则到达(i, j)的路径数为0。即可列出动态方程:

在这里插入图片描述

(3)找边界(初始化):对于第一行dp[0][j],或者第一列dp[i][0],由于都是在边界,只要没有障碍物则只有一条路径,所以路径数只能为1。若边界上出现障碍,则边界上后面的路都不通,路径数为0。

(4)空间优化:根据动态方程,dp[i][j]的值只和dp[i-1][j]dp[i][j-1]有关,因此可以使用「滚动变量」的方式将代码进行优化。

【算法描述】

//动态规划
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {int m = obstacleGrid.size();  //行数int n = obstacleGrid[0].size();  //列数vector<vector<int>> dp(m, vector<int>(n, 0));  //存放(i,j)的路径数for(int i = 0; i < m; i++)  //边界:初始化第一行{if(obstacleGrid[i][0])  //若出现障碍,则后面路都不通{break;}dp[i][0] = 1;  //边界点上路径为1}for(int j = 0; j < n; j++)  //边界:初始化第一列{if(obstacleGrid[0][j])  //若出现障碍,则后面路都不通{break;}dp[0][j] = 1;  //边界点上路径为1}for(int i = 1; i < m; i++){for(int j = 1; j < n; j++){//若当前位置无障碍物,则将该位置的上一位置和左一位置的路径数相加,即为该位置路径数if(!obstacleGrid[i][j])  {dp[i][j] = dp[i-1][j] + dp[i][j-1];  //动态方程}else  //若当前位置有障碍物,路径数为0{dp[i][j] = 0;}}}return dp[m-1][n-1];  //返回最右下角的路径数
}//动态规划————空间优化
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {int m = obstacleGrid.size();   //行数int n = obstacleGrid[0].size();   //列数vector<int> dp(n);   //全部初始化为0dp[0] = (obstacleGrid[0][0] == 0);for(int i = 0; i < m; i++){for(int j = 0; j < n; j++){//若当前位置有障碍物,路径数为0if(obstacleGrid[i][j]){dp[j] = 0;continue;}//若当前位置无障碍物且j > 0,路径数为上一位置和左一位置的路径数之和if(j > 0){dp[j] = dp[j] + dp[j-1];  //右边dp[j]表示上一格路径数,dp[j-1]表示左一格路径数}}}return dp[n-1];  //返回最右下角的路径数
}

【知识点】

问题:怎么想到用动态规划来解决这个问题呢?我们需要从问题本身出发,寻找一些有用的信息,例如本题中:

  • (i, j)位置只能从(i-1, j)和(i, j-1)走到,这样的条件就是在告诉我们这里转移是「无后效性」的,即 f(i, j) 和任何的 f(i’, j’) (i’ > i, j’ > j) 无关。

  • 动态规划的题目分为两大类:一种是求最优解类,典型问题是背包问题;另一种就是计数类,比如这里的统计方案数的问题,它们都存在一定的递推性质。前者的递推性质还有一个名字,叫做「最优子结构」——即当前问题的最优解取决于子问题的最优解;后者类似,当前问题的方案数取决于子问题的方案数。所以在遇到求方案数的问题时,我们可以往动态规划的方向考虑。

有了这两点要素,这个问题八成可以用动态规划来解决。

最小路径和

【题目描述】

给定一个包含非负整数的 m×n 网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

【算法思路】

经典的动态规划解法:

(1)找子问题:设dp[i][j]存放到达(i, j)的最小路径数字总和。

(2)动态方程思想:到达右下角的最小路径数字总和 = 右下角的路径数字 + 到达右下角左边一格和到达右下角上面一格中的较小路径数字和。即可列出动态方程:

dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])

(3)找边界(初始化):对于第一行dp[0][j],或者第一列dp[i][0],由于都是在边界,只有一条路径,所以路径数只能为1。

(4)空间优化:根据动态方程,dp[i][j]的值只和dp[i-1][j]dp[i][j-1]有关,因此可以使用「滚动变量」的方式将代码进行优化。

【算法描述】

//动态规划
int minPathSum(vector<vector<int>>& grid) {int m = grid.size();    //行数int n = grid[0].size();    //列数vector<vector<int>> dp(m, vector<int>(n));   //存放到(i,j)的最小路径dp[0][0] = grid[0][0];for(int i = 1; i < m; i++)  //边界:初始化第一列{dp[i][0] = grid[i][0] + dp[i-1][0];   //边界上只有一条路径 }for(int j = 1; j < n; j++)   //边界:初始化第一行{dp[0][j] = grid[0][j] + dp[0][j-1];    //边界上只有一条路径 }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];   //返回右下角最小路径
}//动态规划——空间优化
int minPathSum(vector<vector<int>>& grid) {int m = grid.size();    //行数int n = grid[0].size();    //列数vector<int> dp(n);dp[0] = grid[0][0];for(int j = 1; j < n; j++)  //边界:初始化第一行{dp[j] = dp[j-1] + grid[0][j];   //第一行只有一条路径}for(int i = 1; i < m; i++){for(int j = 0; j < n; j++){if(j > 0){dp[j] = grid[i][j] + min(dp[j], dp[j-1]);  //动态方程continue;}dp[j] += grid[i][j];  //j==0时,相当于第一列的边界}}return dp[n-1];   //返回右下角最小路径
}

爬楼梯

【题目描述】

假设你正在爬楼梯。需要 n 阶才能到达楼顶。每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢?

【输入输出实例】

示例 1:

输入:n = 2
输出:2 (1阶+1阶、2阶)

示例 2:

输入:n = 3
输出:3 (1阶+1阶+1阶、1阶+2阶、2阶+1阶)

【算法思路】

动态规划:

(1)常规解法将本问题划分成多个子问题,爬第n阶楼梯的方法数量等于下面两部分之和:

  • 爬上第n-1阶楼梯的方法数量(因为再爬1阶就能到第n阶)

  • 爬上第n-2阶楼梯的方法数量(因为再爬2阶就能到第n阶)

(2)所以我们得到动态规划方程:dp[n] = dp[n-1] + dp[n-2]

(3)同时需要初始化dp[0] = 1和dp[1] = 1。

(4)利用「滚动变量」进行空间优化。

【算法描述】

//动态规划
int climbStairs(int n) {vector<int> dp(n + 2);   //dp[i]表示爬到第i阶楼梯的所有方法dp[1] = 1;   //边界:初始dp[1]dp[2] = 2;   //边界:初始dp[2]for(int i = 3; i <= n; i++){//动态规划方程:爬到第i阶楼梯的所有方法 = 爬到第i-1阶楼梯的所有方法 + 爬到第i-2阶楼梯的所有方法//因为只用一步从第i-1阶到第i阶只有一种方法,只用一步从第i-2阶到第i阶也只有一种方法dp[i] = dp[i-1] + dp[i-2];}return dp[n];  //返回爬到第n阶楼梯的所有方法
}//动态规划——空间优化
int climbStairs(int n) 
{int p = 0;   //表示dp[i-2]int q = 0;   //表示dp[i-1]int r = 1;   //表示dp[i]for(int i = 1; i <= n; i++){//滚动变量,每次循环向后滑动一位p = q;q = r;r = p + q;  //dp[i] = dp[i-1] + dp[i-2];}return r;
}

编辑距离

【题目描述】

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

【输入输出实例】

示例 1:

输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

示例 2:

输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

【算法思路】

  • dp[i][j]表示的含义是 s1[0:i-1] 转换成 s2[0:j-1] 的最少操作数。

  • 动态规划方程:

    • 如果 s1[i] == s2[j] ,则dp[i][j] = dp[i-1][j-1]

    • 如果 s1[i] != s2[j] ,则有三种情况:

      • i后插入一个和s2[j]一样的字符,则dp[i][j] = dp[i][j-1] + 1

        在这里插入图片描述

      • s1[i]删掉,则dp[i][j] = dp[i-1][j] + 1

        在这里插入图片描述

      • 替换s1[i],换成和s2[j]一样的字符,则dp[i][j] = dp[i-1][j-1] + 1

        在这里插入图片描述

      • 上述三种情况对应的就是插入/删除/替换三种操作,因为求的是最少操作数,所以上述三个结果要求最小。

    上述流程用下图表示:

在这里插入图片描述

【算法描述】

int minDistance(string word1, string word2) {int m = word1.size();int n = word2.size();// dp[i][j] 表示 word1[0:i-1] 转换成 word2[0:j-1] 的最少操作数vector<vector<int>> dp(m+1, vector<int>(n+1, INT_MAX));// 初始化,word1和word2可以是空字符串dp[0][0] = 0;for(int i = 1; i <= m; ++i) {dp[i][0] = i;}for(int j = 1; j <= n; ++j) {dp[0][j] = j;}for(int i = 1; i <= m; ++i) {for(int j = 1; j <= n; ++j) {// 动态规划方程if(word1[i-1] == word2[j-1]) {dp[i][j] = dp[i-1][j-1];}else {dp[i][j] = min(min(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1;}}}return dp[m][n];
}

【知识点】

C/C++中的 <limits.h> 头文件中定义:

#define INT_MAX   2147483647
#define INT_MIN    (-INT_MAX - 1)

INT_MAX为 2^31-1 ,即 2147483647 ;

INT_MIN为 -2^31 , 即 2147483648 ;

除此之外还有INT16_MAXINT32_MAXINT64_MAX

溢出问题

在c/c++中,int 类型的取值范围为 [ -2147483648, 2147483647] ,超过这个范围则会产生溢出问题。

  • 当发生上溢时,即 INT_MAX + N (N为任意正整数),先进行 INT_MAX + 1 = INT_MIN ,这时上溢问题就解决了,之后便是正常的加减法,即 INT_MIN 再加上剩余的数,其结果为 INT_MIN + N -1

  • 当发生下溢时,即 INT_MIN - N (N为任意正整数),先进行 INT_MIN - 1 = INT_MAX ,这时下溢问题就解决了,之后便是正常的加减法,即 INT_MAX 再减去剩余的数,其结果为 INT_MAX - N +1

柱状图中最大的矩形

【题目描述】

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为1。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:heights = [2,1,5,6,2,3]
输出:10

示例 2:

在这里插入图片描述

输入:heights = [2,4]
输出:4

【算法思路】

本题目与42、接雨水非常相似,也是有三种方法,如下:

(1)双指针法和动态规划

接雨水是找每列中左侧最高的柱子和右侧最高的柱子。找到后该列的雨水高度可得到,宽度为1,就可得到该列的雨水量,再把每列的雨水累加起来即可;

本题是找每个柱子左右两侧最后一个大于等于该柱子高度的柱子。找到后勾勒出来的矩形的宽度为左右两侧这两列的下标差,高度就是本列柱子的高度,则可计算出本列勾勒出来最大矩形的面积,再把每列勾勒出来的矩形面积求出来,再找最大。

(2)单调栈

接雨水是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。所以从栈头到栈底的顺序应该是从大到小的顺序。

在这里插入图片描述

如上图,可知只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。

其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度。其中栈顶元素为矩形高度,栈顶的下一个元素以及要入栈的元素的下标构成了矩阵的宽度。

其实可以把这个过程想象成锯木板,如果木板都是递增的那我很开心不用管。如果突然遇到一块木板 i 矮了一截,那我就先找之前最戳出来的一块(其实就是第 i-1 块),计算一下这个木板单独的面积,然后把它锯成次高的,这是因为我之后的计算都再也用不着这块木板本身的高度了。再然后如果发现次高的仍然比现在这个 i 木板高,那我继续单独计算这个次高木板的面积(应该是第 i-1 和 i-2 块组成的木板),再把它俩锯短。直到发现不需要锯就比第 i 块矮了,那我继续开开心心往右找更高的。当然为了避免到了最后一直都是递增的,所以可以在最后加一块高度为0的木板。

这个算法的关键点是把那些戳出来的木板早点单独拎出来计算,然后就用不着这个值了。

【算法描述】

//双指针法——超时
int largestRectangleArea(vector<int>& heights) {int n = heights.size();int maxArea = 0;  //记录矩形的最大面积for(int i = 0; i < n; i++)  //遍历每根柱子{int left = i - 1;int right = i + 1;//找左边最后一个大于等于heights[i]的下标for(; left >= 0; left--){if(heights[left] < heights[i]){break;}}//找右边最后一个大于等于heights[i]的下标for(; right < n; right++){if(heights[right] < heights[i]){break;}}int height = heights[i];  //矩形高度int width = right - left - 1;  //矩形宽度maxArea = max(maxArea, height*width);  //计算最大面积}return maxArea;
}//动态规划
int largestRectangleArea(vector<int>& heights) 
{int n = heights.size();int maxArea = 0;   //记录矩形的最大面积vector<int> left(n);vector<int> right(n);left[0] = -1;    //初始化right[n-1] = n;//记录每个柱子左边第一个高度小于该柱子的下标for(int i = 1; i < n; i++){int index = i - 1;while(index >= 0 && heights[i] <= heights[index]){index = left[index];   //动态规划更新,上一次已经求过,不用重复求}left[i] = index;}//记录每个柱子右边第一个高度小于该柱子的下标for(int i = n - 2; i >= 0; i--){int index = i + 1;while(index < n && heights[i] <= heights[index]){index = right[index];  //动态规划更新,上一次已经求过,不用重复求}right[i] = index;}//计算矩形的最大面积for(int i = 0; i < n; i++){int height = heights[i];  //矩形高度int width = right[i] - left[i] - 1;   //矩形宽度maxArea = max(maxArea, height*width);   //计算最大面积}return maxArea;
}//单调栈
int largestRectangleArea(vector<int>& heights) {heights.insert(heights.begin(), 0);   //数组头部加入元素0heights.push_back(0);   //数组尾部加入元素0int maxArea = 0;   //记录矩形的最大面积stack<int> s;   //单调栈(递减)s.push(0);for(int i = 1; i < heights.size(); i++)  //遍历各柱子{while(heights[i] < heights[s.top()]){int height = heights[s.top()];   //矩形高度//int width = i - s.top();  //这时得到的矩形宽度没有把之前出栈的柱子宽度算上,因为之前出栈的柱子高度肯定大于它后面的柱子高度,所以才被出栈,那么要把前面已出栈的柱子宽度也要算上s.pop();int width = i - s.top() - 1;   //矩形宽度:把前面已出栈的宽度也要算上            maxArea = max(maxArea, height*width);  //计算最大面积}s.push(i);}return maxArea;
}

扰乱字符串

【题目描述】

使用下面描述的算法可以扰乱字符串 s 得到字符串 t

  1. 如果字符串的长度为 1 ,算法停止
  2. 如果字符串的长度 > 1 ,执行下述步骤:
    • 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串 s ,则可以将其分成两个子字符串 xy ,且满足 s = x + y
    • 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,s 可能是 s = x + y 或者 s = y + x
    • xy 这两个子字符串上继续从步骤 1 开始递归执行此算法。

给你两个 长度相等 的字符串 s1s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回 false

【输入输出实例】

示例 1:

输入:s1 = “great”, s2 = “rgeat”
输出:true
解释:s1 上可能发生的一种情形是:
“great” --> “gr/eat” // 在一个随机下标处分割得到两个子字符串
“gr/eat” --> “gr/eat” // 随机决定:「保持这两个子字符串的顺序不变」
“gr/eat” --> “g/r / e/at” // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割
“g/r / e/at” --> “r/g / e/at” // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」
“r/g / e/at” --> “r/g / e/ a/t” // 继续递归执行此算法,将 “at” 分割得到 “a/t”
“r/g / e/ a/t” --> “r/g / e/ a/t” // 随机决定:「保持这两个子字符串的顺序不变」
算法终止,结果字符串和 s2 相同,都是 “rgeat”
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true

示例 2:

输入:s1 = “abcde”, s2 = “caebd”
输出:false

示例 3:

输入:s1 = “a”, s2 = “a”
输出:true

【算法思路】

动态规划

给定两个字符串 T和 S,假设 T 是由 S 变换而来:

在这里插入图片描述

子问题就是分别讨论两种情况,T1 是否由 S1 变来,T2 是否由 S2 变来,或 T1 是否由 S2 变来,T2 是否由 S1 变来。

dp[i][j][k][h] 表示 T[k..h] 是否由 S[i..j] 变来。由于变换必须长度是一样的,因此这边有个关系 j − i = h − k,可以把四维数组降成三维。dp[i][j][k] 表示从字符串 S 中 i 开始长度为 k 的字符串是否能变换为从字符串 T 中 j 开始长度为 k 的字符串。

状态转移方程如下:

在这里插入图片描述

①和②中只要有一个为true,即可说明以s[i]s[j]开始的k个字符匹配,可以相互转换。

初始化:k == 1时,dp[i][j][k] = (s1[i] == s2[j]),只有一个字符时,直接判断是否相等即可。

最后返回结果:dp[0][0][n]表示从字符串S[0:n)是否能变换为从字符串T[0:n)

【算法描述】

bool isScramble(string s1, string s2) {int n = s1.size();vector<vector<vector<bool>>> dp(n, vector<vector<bool>>(n, vector<bool>(n+1)));for(int k = 1; k <= n; ++k) {for(int i = 0; i + k - 1 < n; ++i) {for(int j = 0; j + k - 1 < n; ++j) {// 初始化if(k == 1) {dp[i][j][k] = (s1[i] == s2[j]);continue;}// 枚举字符串长度u(取值为1 ~ k-1),因为要划分for(int u = 1; u <= k-1; ++u) {// 划分结果中,只要有一组满足,即可说明s[i]和s[j]开始的k个字符匹配bool reg1 = dp[i][j][u] && dp[i+u][j+u][k-u];bool reg2 = dp[i][j+k-u][u] && dp[i+u][j][k-u];if(reg1 || reg2) {dp[i][j][k] = true;break;}}}}}return dp[0][0][n];
}

解码方法

【题目描述】

一条包含字母 A~Z 的消息通过以下映射进行了编码:‘A’ -> “1”、‘B’ -> “2”、…、‘Z’ -> “26”。要解码已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母。

例如,“11106” 可以映射为:

  • “AAJF” ,将消息分组为(1 1 10 6)

  • “KJF” ,将消息分组为(11 10 6)

注意,消息不能分组为 (1 11 06),因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。

给定一个只含数字的****非空*字符串s,计算并返回*解码*方法的*总数****。

【输入输出实例】

示例 1:

输入:s = “12”
输出:2 (解释:“AB”(1 2)或者"L"(12))

示例 2:

输入:s = “226”
输出:3 (解释:“BZ”(2 26),“VF”(22 6)或者"BBF"(2 2 6))

示例 3:

输入:s = “0”
输出:0 (解释:没有字符映射到以 0 开头的数字)

【算法思路】

经典的动态规划解法:

(1)找子问题:dp[i]表示以s[i]为结尾的解码方法总数。

(2)动态方程思想:在进行状态转移时,我们可以考虑最后一次解码使用了s中的哪些字符,那么会有下面的两种情况:

  • 第一种情况是我们使用了一个字符,即 s[i] 进行解码,那么只要s[i] ≠ 0,它就可以被解码成 A~I 中的某个字母。由于剩余的前 i-1个字符的解码方法数为dp[i-1],因此我们可以写出状态转移方程:dp[i] = dp[i-1] 当s[i] ≠ 0

  • 第二种情况是我们使用了两个字符,即 s[i-1] 和 s[i] 进行编码。与第一种情况类似,s[i-1] 不能等于0,并且 s[i-1] 和 s[i] 组成的整数必须小于等于26,这样它们就可以被解码成 J~Z 中的某个字母。由于剩余的前 i−2 个字符的解码方法数为dp[i - 2],因此我们可以写出状态转移方程:dp[i] = dp[i-2] 当s[i-1] ≠ 0且s[i] + s[i-1] ≤ 26

每次找dp[i]时,只有上面两种情况,所以将上面的两种状态转移方程在对应的条件满足时进行累加,即可得到dp[i]的值。

(3)找边界(初始化):当s只有一个数字字符时,则最多只有一个解码方法。如果该字符数字是’0’,则解码方法为0;若为其它数字字符,解码方法为1,则初始条件为dp[0] = (s[0] != ‘0’);

(4)找输出:返回以最后一位字符为结尾的解码方法总数dp[len-1]。

(5)空间优化:根据动态方程,使用「滚动变量」将代码进行优化。

【算法描述】

//动态规划
int numDecodings(string s) {int len = s.size();vector<int> dp(len);   //dp[i]表示以s[i]为结尾的解码方法总数dp[0] = (s[0] != '0');   //初始化第一位字符的编码方法总数for(int i = 1; i < len; i++){//动态规划方程分两种情况:以s[i]为最后一位进行编码,以s[i-1]和s[i]为最后两位进行编码//将两种情况的dp[i]相加,即得到以s[i]为结尾解码方法总数if(s[i] != '0')   //以s[i]为最后一位进行编码:A~I{dp[i] += dp[i-1];}if(s[i-1] != '0' && 10*(s[i-1]-'0') + (s[i]-'0') <= 26)  //以s[i-1]和s[i]为最后两位进行编码:J~Z{if(i > 1)   //求dp[2]以后{dp[i] += dp[i-2];}else{dp[i] += 1;  //dp[1]的初始化}}}return dp[len-1];  //返回以最后一位字符为结尾的解码方法总数
}//动态规划——空间优化
int numDecodings(string s) {int len = s.size();int first = 1;   //表示dp[i-2]int second = (s[0] != '0');    //表示dp[i-1]for(int i = 1; i < len; i++){int third = 0;   //表示dp[i],起始均为0//动态规划方程分两种情况:以s[i]为最后一位进行编码,以s[i-1]和s[i]为最后两位进行编码//将两种情况的dp[i]相加,即得到以s[i]为结尾解码方法总数if(s[i] != '0')  //以s[i]为最后一位进行编码:A~I{third += second;}if(s[i-1] != '0' && 10*(s[i-1]-'0') + (s[i]-'0') <= 26)  //以s[i-1]和s[i]为最后两位进行编码:J~Z{third += first;}first = second;   //更新first和second,third下次重置为0second = third;}return second;  //返回以最后一位字符为结尾的解码方法总数
}

不同的二叉搜索树

【题目描述】

给你一个整数n,求恰由n个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

二叉搜索树:其左子树上所有结点的值均小于根结点的值;其右子树上所有结点的值均大于等于根结点的值;且其左右子树本身又各是一棵二叉搜索树。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

【算法思路】

在这里插入图片描述

(1)找子问题:设dp[i]表示由i个节点组成互不相同的二叉搜索树的数量。

(2)动态规划方程:n个节点存在二叉排序树的个数是dp[n],令 f(i) 为以 i 为根的二叉搜索树的个数,则dp[n] = f(1) + f(2) + f(3) + … + f(n)。当 i 为根节点时,其左子树节点个数为 i-1 个,右子树节点为 n-i,则f(i) = dp[i-1] * dp[n-i],综合两个公式可以得到卡特兰数公式:

dp[n] = dp[0]*dp[n-1] + dp[1]*dp[n-2] +...+ dp[n-1]*dp[0]

(3)找边界(初始化):当结点为空时,空节点也是一棵二叉树,所以数目为1,即dp[0] = 1;当只有一个结点时,二叉搜索树的数量也为1,即dp[1] = 1。

(4)找输出:dp[n]即为由n个节点组成互不相同的二叉搜索树的数量。

【算法描述】

int numTrees(int n) {vector<int> dp(n+1);   //dp[i]表示由i个节点组成互不相同的二叉搜索树的数量dp[0] = 1;   //初始化dp[1] = 1;for(int i = 2; i <= n; i++){//动态规划方程:dp[n] = dp[0]*dp[n-1] + dp[1]*dp[n-2] +...+ dp[n-1]*dp[0]for(int j = 0; j < i; j++){dp[i] += dp[j] * dp[i-j-1];}}return dp[n];
}

交错字符串

【题目描述】

给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错组成的。

两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干非空子字符串:

  • s = s1 + s2 + … + sn

  • t = t1 + t2 + … + tm

  • |n - m| <= 1

  • 交错是 s1 + t1 + s2 + t2 + s3 + t3 + … 或者 t1 + s1 + t2 + s2 + t3 + s3 + …

【输入输出实例】

示例 1:

在这里插入图片描述

输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true

示例 2:

输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出:false

示例 3:

输入:s1 = “”, s2 = “”, s3 = “”
输出:true

【算法思路】

(1)找子问题:我们使用dp[i][j]表示 s1 的前 i 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i+j 个元素。

(2)动态规划方程:从dp[1][1]开始遍历,有两种情况可以判断 s1 的前 i 个元素和 s2 的前 j 个元素能交错组成 s3 的前 i+j 个元素:

  • s1 的前 i−1 个字符和 s2 的前 j 个字符能构成 s3 的前 i+j−1 位,且 s1 的第 i 位(s1[i−1])等于 s3 的第 i+j 位(s3[i+j−1]),即:dp[i][j] = (dp[i-1][j] && s1[i-1] == s3[i+j-1])
  • s1 的前 i 个字符和 s2 的前 j−1 个字符能构成 s3 的前 i+j−1 位,且 s2 的第 j 位(s2[j−1])等于 s3 的第 i+j 位(s3[i+j−1]),即:dp[i][j] = (dp[i][j-1] && s2[j-1] == s3[i+j-1])
  • 综上两种情况,则dp[i][j] = (dp[i-1][j] && s1[i-1] == s3[i+j-1]) || (dp[i][j-1] && s2[j-1] == s3[i+j-1])

在这里插入图片描述

(3)初始化:初始化 dp 为 (len1+1) * (len2+1) 的 false 数组,dp 记录 s1 和 s2 的首字符为 ’ ’ ,则dp[0][0] = true

  • 初始化第一列 dp[i][0],遍历第一列,遍历区间 [1, n1],看 s1 的前 i 位是否能构成 s3 的前 i 位,即看 s1 的第 i 位是否等于 s3 的第 i 位,如果等于,则继续判断下一位;如果不等于,说明后续肯定也无法构成,即dp[i-1][0] && s1[i] == s3[i];
  • 初始化第一行 dp[0][j],遍历第一行,遍历区间 [1, n2],看 s2 的前 j 位是否能构成 s3 的前 j 位,即看 s2 的第 j 位是否等于 s3 的第 j 位,如果等于,则继续判断下一位;如果不等于,说明后续肯定也无法构成,即dp[0][j-1] && s2[j] == s3[j];

(4)找输出:返回dp[n1][n2],即为 s3 是否可由 s1s2 交错组成。

【算法描述】

//动态规划
bool isInterleave(string s1, string s2, string s3)
{int n1 = s1.size();int n2 = s2.size();int n3 = s3.size();if(n1 + n2 != n3)    //如s1和s2长度之和不等于s3,说明s3肯定不是由s1和s2组成的{return false;}vector<vector<bool>> dp(n1 + 1, vector<bool>(n2 + 1));   //dp[i][j]表示s1的前i个元素和s2的前j个元素是否能交错组成s3的前i+j个元素dp[0][0] = true;    //dp[0][0]:表示s1和s2都为空字符串,能交错组成空字符串s3for(int i = 0; i < n1; i++)    //初始化首列,s2为空字符串{if(s1[i] != s3[i]){break;}dp[i+1][0] = true;}for(int j = 0; j < n2; j++)    //初始化首行,s1为空字符串{if(s2[j] != s3[j]){break;}dp[0][j+1] = true;}for(int i = 1; i <= n1; i++)    //遍历 dp 数组{for(int j = 1; j <= n2; j++){//动态规划方程://s1 的前 i−1 个字符和 s2 的前 j 个字符能否构成 s3 的前 i+j−1 位,且 s1 的第 i 位(s1[i−1])是否等于 s3 的第 i+j 位(s3[i+j−1])//s1 的前 i 个字符和 s2 的前 j−1 个字符能否构成 s3 的前 i+j−1 位,且 s2 的第 j 位(s2[j−1])是否等于 s3 的第 i+j 位(s3[i+j−1])dp[i][j] = (dp[i-1][j] && s1[i-1] == s3[i+j-1]) || (dp[i][j-1] && s2[j-1] == s3[i+j-1]);   }}return dp[n1][n2];
}

恭喜你全部读完啦!古人云:温故而知新。赶紧收藏关注起来,用之前再翻一翻吧~


📣推荐阅读

C/C++后端开发面试总结:点击进入 后端开发面经 关注走一波

C++重点知识:点击进入 C++重点知识 关注走一波

力扣(leetcode)题目分类:点击进入 leetcode题目分类 关注走一波

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/61032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vscode/Code-server无网环境安装通义灵码

Date: 2024-11-18 参考材料&#xff1a;https://help.aliyun.com/zh/lingma/user-guide/individual-edition-login-tongyi-lingma?spma2c4g.11186623.0.i0 1. 首先在vscode/code-server插件市场中安装通义插件&#xff0c;这步就不细说了。如果服务器没网&#xff0c;会问你要…

fastadmin常用操作

数据库中遇到的操作 查询字段是json的某个值 //获取数据库中某个字段是json中得某个值&#xff0c;进行查询&#xff0c;goods是表中字段&#xff0c;brand_id是json中要查詢的字段。//数据类型一定要对应要不然查询不出来。$map[json_extract(goods, "$.brand_id")…

力扣周赛:第424场周赛

&#x1f468;‍&#x1f393;作者简介&#xff1a;爱好技术和算法的研究生 &#x1f30c;上期文章&#xff1a;力扣周赛&#xff1a;第422场周赛 &#x1f4da;订阅专栏&#xff1a;力扣周赛 希望文章对你们有所帮助 第一道题模拟题&#xff0c;第二道题经典拆分数组/线段树都…

STM32单片机设计防儿童人员误锁/滞留车内警报系统

目录 目录 前言 一、本设计主要实现哪些很“开门”功能&#xff1f; 二、电路设计原理图 1.电路图采用Altium Designer进行设计&#xff1a; 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 近年来在车辆逐渐普及的情况下&#xff0c;由于家长的疏忽&#xff0c;将…

Vue Canvas实现区域拉框选择

canvas.vue组件 <template><div class"all" ref"divideBox"><!-- 显示图片&#xff0c;如果 imgUrl 存在则显示 --><img id"img" v-if"imgUrl" :src"imgUrl" oncontextmenu"return false" …

React Hooks 深度解析与实战

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 React Hooks 深度解析与实战 React Hooks 深度解析与实战 React Hooks 深度解析与实战 引言 什么是 Hooks? 定义 为什么需要 Ho…

开源音乐分离器Audio Decomposition:可实现盲源音频分离,无需外部乐器分离库,从头开始制作。将音乐转换为五线谱的程序

今天给大家分析一个音频分解器&#xff0c;通过傅里叶变换和信封匹配分离音乐中的各个音符和乐器&#xff0c;实现音乐到乐谱的转换。将音乐开源分离为组成乐器。该方式是盲源分离&#xff0c;从头开始制作&#xff0c;无需外部乐器分离库。 相关链接 代码&#xff1a;https:…

Android 6年经验面试总结 2024.11.15

背景&#xff1a;深圳 面过12家中大厂、4家中小厂&#xff0c;通过4家中大厂&#xff0c;2家offer。 针对六年的求职面试总结&#xff1a;项目经验70%30%基础&#xff08;基础应该必会&#xff09; 对于上来就问八股文的公司&#xff0c;对于已经工作了5年以上的开发来说&…

智慧安防丨以科技之力,筑起防范人贩的铜墙铁壁

近日&#xff0c;贵州省贵阳市中级人民法院对余华英拐卖儿童案做出了一审宣判&#xff0c;判处其死刑&#xff0c;剥夺政治权利终身&#xff0c;并处没收个人全部财产。这一判决不仅彰显了法律的威严&#xff0c;也再次唤起了社会对拐卖儿童犯罪的深切关注。 余华英自1993年至2…

【原创】java+ssm+mysql房屋租赁管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

Linux高阶——1116—环形队列生产者消费者

目录 1、环形队列 2、生产者消费者 环形队列数组实现代码 成功截图 1、环形队列 相比于线性队列&#xff0c;环形队列可以有效避免访问越界问题&#xff0c;使用下标访问队列元素时&#xff0c;到达末尾后下标归0&#xff0c;返回起始位置&#xff0c;使用下标运算即可 a…

9.C++面向对象6(实现一个较为完善的日期类)

⭐本篇重点&#xff1a;const成员变量和函数&#xff0c;取地址重载 ⭐本篇代码&#xff1a;c学习/02.c面向对象-中/2.实现完善的日期类 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. 日期类分析 二. 代码实现 2.1 构造函数 2.2 拷贝构造 2.3 …

构建SSH僵尸网络

import argparse import paramiko# 定义一个名为Client的类&#xff0c;用于表示SSH客户端相关操作 class Client:# 类的初始化方法&#xff0c;接收主机地址、用户名和密码作为参数def __init__(self, host, user, password):self.host hostself.user userself.password pa…

springboot 文件高效上传

文件上传功能可以说对于后端服务是必须的&#xff0c;不同场景对文件上传的要求也各不相同&#xff0c;有的追求速度&#xff0c;有的注重稳定性&#xff0c;还有的需要考虑文件大小和安全性。所以便有了秒传、断点续传和分片上传等解决方案。 1、总述 秒传 秒传&#xff0c…

MySQL 中的数据排序是怎么实现的

MySQL 内部数据排序机制 1. 排序算法 MySQL 使用不同的算法来对数据进行排序&#xff0c;通常依据数据量和是否有索引来决定使用哪种排序算法。主要的排序算法包括&#xff1a; 文件排序 (File Sort)&#xff1a;这是 MySQL 默认的排序算法&#xff0c;用于无法利用索引或内…

199. 二叉树的右视图【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 199. 二叉树的右视图 一、题目描述 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 二…

22.useNavigatorOnLine

React useNavigatorOnLine 钩子:如何实时监测用户的在线状态? 在现代 Web 应用中,实时监测用户的在线状态对于提供良好的用户体验至关重要。无论是离线功能还是网络状态提示,都需要准确地知道用户的连接状态。useNavigatorOnLine 钩子提供了一种简单而有效的方式来在 Reac…

Mongo数据库集群搭建

目录 1、Mongo集群优势 1.1 高可用性 1.2 水平扩展性 1.3 高性能 1.4 灵活的架构设计 1.5 数据安全 1.6 管理与监控 2、下载指定操作系统版本包 3、部署和验证工作 3.1 准备配置文件及依赖 3.2 启动第一个节点 3.3 部署更多的节点 3.4 初始化副本集 3.5 设置管理…

DB Type

P位 p 1时段描述符有效&#xff0c;p 0时段描述符无效 Base Base被分成了三个部分&#xff0c;按照实际拼接即可 G位 如果G 0 说明描述符中Limit的单位是字节&#xff0c;如果是G 1 &#xff0c;那么limit的描述的单位是页也就是4kb S位 S 1 表示代码段或者数据段描…

大语言模型通用能力排行榜(2024年11月8日更新)

数据来源SuperCLUE 榜单数据为通用能力排行榜 排名 模型名称 机构 总分 理科 文科 Hard 使用方式 发布日期 - o1-preview OpenAI 75.85 86.07 76.6 64.89 API 2024年11月8日 - Claude 3.5 Sonnet&#xff08;20241022&#xff09; Anthropic 70.88 82.4…