10 动态规划
动态规划
01背包+装满背包+求排列
01背包+装满背包+求组合
01背包+求最大最小值
完全背包+装满背包+求排列
完全背包+装满背包+求组合
完全背包+求最大最小值
1 动态规划基础
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,
动态规划五步曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
2 斐波那契数
题目:
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
思路:
- 确定dp数组
dp[i]的定义为第i个数的斐波那契数列值为dp[i] - 确定递推公式
dp[i]=dp[i-1]+dp[i-2] - dp数组如何初始化
根据斐波那契数列定义,dp[0]=0,dp[1]=1 - 确定遍历顺序
根据递推公式可以看出,从前往后遍历 - 举例推导dp数组
举例推导,比如0 1 1 2 3 5 8 13 21 34 55
代码:
int fib(int n) {
if(n <= 1) return n;
int dp[2];
dp[0] = 0;
dp[1] = 1;
for(int i=2;i<=n;i++){
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
3 爬楼梯
题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
思路:
- 确定dp数组
dp[i]为爬到第n阶楼梯时的总方法数 - 确定递推公式
正向说,每一步有两种选择当在第i阶楼梯时,可以选择走一步或者两步,但是转换思维,第i阶楼梯可以由第i-2阶走两步和第i-1阶走一步,也就是dp[i]=dp[i-1]+dp[i-2] - dp数组如何初始化
根据定义第一阶一种方法,第二阶两种方法 - 确定遍历顺序
根据递推公式,从前往后 - 举例推导dp数组
1 1 2 3 5 8 13 21 34 55
代码:
int climbStairs(int n) {
if(n <= 2) return n;
int dp[2];
dp[0] = 1;
dp[1] = 2;
for(int i=3;i<=n;i++){
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
4 使用最小花费爬楼梯
题目:
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
例子:
输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
思路:
- 确定dp数组
dp[i]为爬到第i阶时的最小代价总和 - 确定递推公式
可以爬一步或者两步,那么同样是dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]) - dp数组如何初始化
dp[0] = 0,dp[1]= 0 - 确定遍历顺序
总前往后遍历 - 举例推导dp数组
代码:
int climbStairs(vector<int>& cost) {
int dp0 = 0;
int dp1 = 0;
for (int i = 2; i <= cost.size(); i++) {
int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
dp0 = dp1;
dp1 = dpi;
}
return dp1;
}
5 不同路径
题目:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
思路:
- 确定dp数组
dp[i][j]为从(0,0)到(i,j)时的路径数量 - 确定递推公式
dp[i][j]只能从两个方向来,要么dp[i-1][j],要么dp[i][j-1],那么dp[i][j]=dp[i-1][j]+dp[i][j-1] - dp数组如何初始化
在第一行和第一列都是1,因为都只有一条路
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1; - 确定遍历顺序
传递都是从左或者从上,那么从左到右一层一层遍历 - 举例推导dp数组
代码:
int uniquePaths(int m, int n) {
if(m == 1 || n == 1) return 1;
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0 ; i<m; i++) dp[i][0]=1;
for(int j = 0 ; j<n; j++) dp[0][j]=1;
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];
}
6 不同路径Ⅱ
题目:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
思路:
-
确定dp数组
dp[i][j]为从(0,0)到(i,j)时的路径数量 -
确定递推公式
dp[i][j]只能从两个方向来,要么dp[i-1][j],要么dp[i][j-1],那么dp[i][j]=dp[i-1][j]+dp[i][j-1],但是如果该点为障碍点,则continue跳过,因为到该点的路径数量为0 -
dp数组如何初始化
同样第一行,第一列值为1,但是如果遇到第一行或者第一列有障碍,则后序的所有点值为0,跳出循环 -
确定遍历顺序
-
举例推导dp数组
代码:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1)
return 0;
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
7 整数拆分
题目:
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
思路:
-
确定dp数组
dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。 -
确定递推公式
两种方式获得dp[i],一是\(j\times(i-j)\),一个是\(j\times dp[i-j]\),这两种情况分别对应着不拆分(i-j)和拆分(i-j),也即dp[i]=max(dp[i],max((i-j)j),dp[i-j]j),此处为什么是三者比较是因为会重复比较dp[i],取最大值 -
dp数组如何初始化
根据定义初始化dp[2] = 1 -
确定遍历顺序
从前往后遍历即可 -
举例推导dp数组
代码:
int integerBreak(int n) {
vector<int> dp(n+1);
dp[2] = 1;
for(int i = 3; i <= n;i++){
for(int j = 1; j < i - 1 ;j++){
dp[i] = max(dp[i],max(dp[i-j]*j,(i-j)*j));
}
}
return dp[n];
}
8 不同的二叉搜索树
题目:
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
思路:
-
确定dp数组
dp[i] : 由i个不同元素节点组成的二叉搜索树的个数为dp[i] -
确定递推公式
从1到i二叉搜索树一共个数为dp[i],dp[i]的所有情况可分为,具体某一个节点j当头节点,而每一个节点i都是左子树和右子树构成,左子树就是dp[i-1]右子树就是dp[n-i],而 dp[j]=dp[j-1]*dp[j-i] -
dp数组如何初始化
初始化,dp[1]=1,dp[2]=2 -
确定遍历顺序
从前到后的遍历顺序 -
举例推导dp数组
代码:
int numTrees(int n) {
vector<int> dp(n+1);
dp[0] = 1;
for(int i = 1;i <= n; i++){
for(int j = 1;j <= i; j++){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
9 0-1背包理论基础(一)
分类
01背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是\(o(2^n)\),这里的n表示物品数量。
二维dp数组01背包
例如背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
- 确定dp数组
是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 确定递推公式
那么可以有两个方向推出来dp[i][j],
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- dp数组如何初始化
首先当背包重量为0时,也即j=0,dp[i][0]=0;当i=0,也即遍历背包重量时,若j < wight[0]为0,大于等于wight[0]为value[0]。
-
确定遍历顺序
从左上到右下的顺序遍历,外层循环遍历物品,内层循环遍历重量 -
举例推导dp数组
void bagproblem1(){
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for(int j = weight[0]; j < value.size();j++){
dp[0][j] = value[0];
}
for(int i = 1;i < weight.size();i++){
for(int j = 0; j <= bagweight; j++){
if(wight[i]>j)dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
}
}
cout<<dp[weight.size()-1][bagweight]<<endl;
}
10 0-1背包理论基础(二)
题目:
将二维数组dp[i][j]压缩成一位数组dp[i]
思路:
-
确定dp数组
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 -
确定递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); -
dp数组如何初始化
-
确定遍历顺序
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
倒序是为了保证物品i仅仅背放置一次 -
举例推导dp数组
代码:
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
11 分割等和子集
题目:
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
思路:
转化为01背包:
- 背包的体积为sum / 2
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
-
确定dp数组
体积为i的背包最多装元素值的和为dp[i] -
确定递推公式
dp[j] = max(dp[j],dp[j-num[i]]+num[i])
-
dp数组如何初始化
初始都为0,因为会累加的 -
确定遍历顺序
外层循环,i从0到nums.size(),内层循环,从后往前遍历遍历到weight[i],本题中重量也就是nums[i] -
举例推导dp数组
代码:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int num : nums) sum += num;
if(sum % 2 == 1) return false;
int target = sum / 2;
vector<int> dp( target + 1, 0);
for(int i = 0;i < nums.size();i++){
for(int j = target;j >= nums[i];j--){
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[target] == target) return true;
return false;
}
12 后一块石头的重量Ⅱ
题目:
题目难度:中等
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
输入:[2,7,4,1,8,1]
输出:1
思路:
转换为尽量找到两堆重量相同的石头即跟11题相同了
与11题相同,只是在处理输出方面改变,target一定小于等于sum/2,所以最终结果为sum-dp[target]-dp[target]
代码:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int stone : stones) sum += stone;
int target = sum / 2;
vector<int> dp( target + 1, 0);
for(int i = 0;i < stones.size();i++){
for(int j = target;j >= stones[i];j--){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
13 目标和
题目:
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
思路:
假设加法和为x,那么减法和就是sum-x
所以x -(sum-x)=target
也就是x=(target+sum)/2
问题转化为装满容量为x的背包有几种方法?
这里的x,就是bagSize,也就是我们后面要求的背包的容量。
(target + sum)/2不为整数时无解
同时如果 target的绝对值已经大于sum,那么也是没有方案的。
-
确定dp数组
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法 -
确定递推公式
填满背包容量为j的方法等于填满从0到j
dp[j] += dp[j - nums[i]] -
dp数组如何初始化
初始化 dp[0] 为 1,因为方法都是从dp[0]累计上来的,如果dp[0]=0,则无论什么情况都是0了 -
确定遍历顺序
依旧nums放在外循环,target在内循环,且内循环倒序。 -
举例推导dp数组
举例推导
代码:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int num :nums) sum += num;
if((sum + target) % 2 == 1 || abs(target) > sum) return 0;
int plus = (sum + target) / 2;
vector<int> dp(plus+1,0);
dp[0] = 1;
for(int i = 0; i < nums.size();i++){
for(int j = plus; j >= nums[i] ; j--){
dp[j] += dp[j - nums[i]];
}
}
return dp[plus];
}
14 一和零
题目:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3
思路:
strs中的元素看做物品,价值为字符串的个数,字符串中0的数量和1的数量为重量的两个维度。所以依旧是01背包。
- 确定dp数组
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。 - 确定递推公式
dp[i][j]=max(dp[i][j],dp[i-zeroNum][j-oneNum]+1) - dp数组如何初始化
初始都为0就可以了,因为这个是有+1的过程,不是直接比较继承最大值。 - 确定遍历顺序
外层遍历物品,内层遍历重量,也就是说外层遍历元素str,内岑个遍历两个维度的重量,遍历重量的顺序无所谓,但是记得需要从后往前遍历,因为这是01背包。 - 举例推导dp数组
代码:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(string str : strs){
int zeroNum = 0;
int oneNum = 0;
for(char c : str){
if(c == '0')zeroNum++;
else oneNum++;
}
for(int i = m; i >= zeroNum; i--){
for(int j = n; j >= oneNum; j--){
dp[i][j] = max(dp[i-zeroNum][j-oneNum]+1,dp[i][j]);
}
}
}
return dp[m][n];
}
15 完全背包理论基础
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。
在下面的讲解中,我依然举这个例子:
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
每件商品都有无限个
问背包能背的物品最大价值是多少
01背包和完全背包唯一不同就是体现在遍历顺序上,所以本文就不去做动规五部曲了,我们直接针对遍历顺序经行分析
只需要在内层的循环中,将倒序修改为正序就能够重复选择物品了
16 零钱兑换
题目:
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
思路:
求的是组合数,物品为硬币,重量为coins的面额,价值为种类数量
- 确定dp数组
dp[j]为总金额为i时有dp[j]种方式凑成总金额 - 确定递推公式
装满背包的情况都是:
dp[j]+=dp[j-coins[i]]
- dp数组如何初始化
dp[0]一定为一,因为递归公式是迭代的, - 确定遍历顺序
外层遍历物品,内层遍历重量,但是外层的边界条件需要改变 - 举例推导dp数组
代码:
int change(int amount, vector<int>& coins) {
vector<int>dp(amount + 1, 0);
dp[0] = 1;
for(int i = 0; i < coins.size() ; i++){
for(int j = coins[i];j <= amount; j++){
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
注意如果外层遍历重量,内层遍历物品,那么就是排列数
如果外层遍历物品,内层遍历重量就是组合数
17 组合总和IV
题目:
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
思路:
只要求排列个数,所以可以用dp,如果要保存所有的排列,那么只能用回溯算法了。
背包容量为target,物品为num,重量为nums[i]
- 确定dp数组
dp[j]为背包容量为j时的组合总数 - 确定递推公式
又是一个装满背包的问题,递推公式依旧如下:
dp[j]+=dp[j-nums[i]]
- dp数组如何初始化
递推叠加,那么初始化一定为1,否则永远是0 - 确定遍历顺序
要求的是组合数,那么就外层遍历背包容量,内层遍历物品,注意内层判断j是否大于nums[i],否则会出现越界的行为 - 举例推导dp数组
代码:
int combinationSum4(vector<int>& nums, int target) {
vector<int>dp(target+1,0);
dp[0] = 1;
for(int j = 0; j <= target ; j++){
for( int i = 0; i < nums.size() ; i++){
if(j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]]){
dp[j] += dp[j - nums[i]];
}
}
}
return dp[target];
}
dp[j] < INT_MAX - dp[j - nums[i]]
是因为有超过INT的数,换成long long也可以解决问题
18 爬楼梯(进阶版)
题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
思路:
完全背包+装满背包+求排列问题,那么应该先遍历背包,再遍历物品
-
确定dp数组
dp数组dp[i]表示爬到第i阶楼梯有dp[i]种方法 -
确定递推公式
因为是装满背包,所以一般为dp[i] += dp[i - nums[j]],本题中有来源dp[i-1],dp[i-2],dp[i-3]...等,所以dp[i]+=dp[i-j] -
dp数组如何初始化
那么dp[0] 一定为1,其余为0 -
确定遍历顺序
需将target放在外循环,将nums放在内循环。
每一步可以走多次,这是完全背包,内循环需要从前向后遍历。 -
举例推导dp数组
代码:
int climbStairs(int n) {
vector<int>
}
19 零钱兑换
题目:
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
思路:
题目求最少硬币个数,则分解为完全背包+装满背包+求最小值
- 确定dp数组
dp[j]凑足总额为j的钱币最少需要dp[j]个硬币 - 确定递推公式
当前dp[j]
可以由dp[j-coins[i]]
推来,如果j-coins[i]大于0并且小于dp[j]的值,那么选择较小的那个就行了 - dp数组如何初始化
由于是要求选择最小值,所以初始化为最大值,并且是从dp[0]叠加有+1,所以dp[0]为0 - 确定遍历顺序
完全背包,从前往后遍历,求组合数,所以外层遍历物品,内层遍历背包容量 - 举例推导dp数组
代码:
int coinChange(vector<int>& coins, int amount) {
vector<int>dp(amount+1,INT32_MAX);
dp[0] = 0;
for(int i = 0; i < coins.size() ;i++){
for(int j = coins[i]; j <= amount ; j++){
if(dp[j - coins[i]] != INT32_MAX){
dp[j] = min(dp[j],dp[j - coins[i]] + 1);
}
}
}
if(dp[amount] == INT32_MAX) return -1;
return dp[amount];
}
20 完全平方数
题目:
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
思路:
分析题目为求最小值+完全背包+求排列
- 确定dp数组
dp[i]
为正整数n的最少的由完全平方数组成的和的完全平方数个数 - 确定递推公式
本题跟求硬币题类似,把完全平方数看成硬币就好了,只是我们需要求出平方数的集合数组,假设小于整数n的平方数的集合为squareNums,则dp[j] = min(dp[j],dp[j-squareNums[i]])
- dp数组如何初始化
除了dp[0] = 0外,都初始化为INT32_MAX - 确定遍历顺序
是求组合+完全背包,所以外层遍历物品(求组合),内层遍历背包,并且内层从前往后遍历(完全背包)。
这里内层外层遍历都无所谓,因为每次都在比较获得最小值,所以内外层没有影响,但是组合数本身就小于排列数,事实上我们要求的是组合数的。 - 举例推导dp数组
代码:
int numSquares(int n) {
vector<int>dp(n+1,INT32_MAX);
dp[0] = 0;
for(int i=1;i*i <= n;i++){
for(int j = i*i;j<=n;j++){
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
助理可以不用求出平方数的集合数,直接在循环中用平方数做比较就好了,可以节省一些复杂度
21 单词拆分
题目:
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
思路:
这道题可以用回溯算法来做,但是这样就复杂度就是指数级的,所以不能用,但是可以记忆化搜索,这样复杂度也能通过。这里还是使用背包来分析。
分析可以知道,这道题是完全背包,求是否能够装满背包,那么只需要判断条件传递true或者false即可
- 确定dp数组
string s中从0到第i个的子串是否能装满背包 - 确定递推公式
当前状态可以由dp[i-word[j]]
推出来,所以如果dp[i-word[j]] == true
,则dp[i]为true - dp数组如何初始化
由递推公式可以看出来,所有状态都是从dp[0]推出来,所以dp[0]一定为true,否则无法进行递推了 - 确定遍历顺序
本道题是求排列,因为单词是有顺序的,所以一定是外层遍历背包容量,内层遍历物品,并且完全背包,所以是从前到后遍历。 - 举例推导dp数组
代码:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool>dp(s.size()+1,false);
dp[0] = true;
for(int i = 1;i <= s.size();i++){
for(int j = 0;j < wordDict.size(); j++){
if( i < wordDict[j].size()) continue;
if(dp[i-wordDict[j].size()] == true && wordDict[j] == s.substr(i-wordDict[j].size(),wordDict[j].size())) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
22 打家劫舍
题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
思路:
- 确定dp数组
dp[i]为到第i家时获得的最大的收益 - 确定递推公式
dp[i]可以由偷第i家,不偷第i-1家和偷第i-1家不偷第i家推出来,所以dp[i] = max(dp[i-2]+cash[i],dp[i-1])
- dp数组如何初始化
dp[0] = 0, dp[1] = cash[1]; - 确定遍历顺序
从前往后遍历 - 举例推导dp数组
比如[1,2,3,1],dp[0]=0,dp[1]=1,dp[2]=2,dp[3]=4,dp[4]=4
代码:
int rob(vector<int>& nums) {
vector<int> dp(nums.size()+1,0);
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2;i<=nums.size();i++){
dp[i] = max(dp[i-2]+nums[i-1],dp[i-1]);
}
return dp[nums.size()];
}
23 打家劫舍Ⅱ
题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
思路:
本题跟打家劫舍Ⅰ唯一区别在于变成了环,环可以拆分为两部分:
- 从第一个到倒数第二个
- 从第二个到倒数第一个
这样就把所有情况遍历了,然后比较这两种情况下的结果,取最大值即可。
代码:
int robProcess(vector<int>& nums,int start,int end){
if(end - start == 1) return nums[start];
vector<int> dp(end,0);
dp[start] = nums[start];
dp[start+1] = max(nums[start+1],dp[start]);
for(int i = start + 2;i < end;i++){
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[end - 1];
}
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
int result1 = robProcess(nums,0,nums.size()-1);
int result2 = robProcess(nums,1,nums.size());
result1 = max(result1,result2);
return result1;
}
24 打家劫舍Ⅲ
题目:
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
也即在二叉树上进行动态规划
思路:
-
确定dp数组
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 -
确定终止条件
当遇到空节点是,偷不偷都是0,所以直接返回
if (cur == NULL) return vector<int>{0, 0};
- 遍历顺序
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
- 确定单层递归逻辑
-
偷当前节点:偷了当前节点后,就不能偷左右孩子,所以
val1=cur->val + left[0] + right[0]
-
不偷当前节点:此时需要判断得到的最大金钱,也就是左孩子的最大和右孩子的最大
val2=max(left[0],left[1])+max(right[0],right[1])
- 举例推导dp数组
代码:
vector<int> traversal(TreeNode*cur){
if(cur == nullptr) return{0,0};
vector<int> left = traversal(cur->left);
vector<int> right = traversal(cur->right);
int val1 = cur->val + left[0] + right[0];
int val2 = max(left[0],left[1]) + max(right[0],right[1]);
return {val2,val1};
}
int rob(TreeNode* root) {
vector<int> result = traversal(root);
return max(result[0],result[1]);
}
25 买卖股票的最佳时机
题目:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思路:
- 确定dp数组
dp[i][0]
表示第i天持有股票得到的最多现金
dp[i][1]
表示第i天不持有股票得到的最多现金 - 确定递推公式
dp[i][0]
可以由两种情况推出来:取两者中最大值- i-1天就持有了股票,第i天保持现状,则
dp[i][0] = dp[i-1][0]
- i-1天不持有股票,第i天买入股票,则
dp[i][0] = -prices[i]
(注意这里不是dp[i-1][1]-prices[i],因为股票只能买卖一次)
- i-1天就持有了股票,第i天保持现状,则
dp[i][1]
同样可以由两种情况推出来:取两者中最大值- i-1天不持有股票,第i天保持现状,则
dp[i][1] = dp[i-1][1]
- i-1天持有股票,第i天卖出入股票,则
dp[i][1] = dp[i-1][0]+prices[i]
- i-1天不持有股票,第i天保持现状,则
- dp数组如何初始化
dp[0][0] = -prices[0]
dp[0][1] = 0
- 确定遍历顺序
从前往后遍历 - 举例推导dp数组
代码:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1;i<prices.size();i++){
dp[i][0] = max(dp[i-1][0],-prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0] + prices[i]);
}
return dp[prices.size()-1][1];
}
26 买卖股票的最佳时机Ⅱ
题目:
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
思路:
- 确定dp数组
dp[i][0]
表示第i天持有股票得到的最多现金
dp[i][1]
表示第i天不持有股票得到的最多现金 - 确定递推公式
dp[i][0]
可以由两种情况推出来:取两者中最大值- i-1天就持有了股票,第i天保持现状,则
dp[i][0] = dp[i-1][0]
- i-1天不持有股票,第i天买入股票,则
dp[i][0] = dp[i-1][1]-prices[i]
- i-1天就持有了股票,第i天保持现状,则
dp[i][1]
同样可以由两种情况推出来:取两者中最大值- i-1天不持有股票,第i天保持现状,则
dp[i][1] = dp[i-1][1]
- i-1天持有股票,第i天卖出入股票,则
dp[i][1] = dp[i-1][0]+prices[i]
- i-1天不持有股票,第i天保持现状,则
- dp数组如何初始化
dp[0][0] = -prices[0]
dp[0][1] = 0
- 确定遍历顺序
从前往后遍历 - 举例推导dp数组
代码:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1;i<prices.size();i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1] - prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0] + prices[i]);
}
return dp[prices.size()-1][1];
}
27 买卖股票的最佳时机Ⅲ
题目:
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。
思路:
跟之前类似,但是限定只能最多买卖两次,那么,我们将两次分别拆分开来为两种另外的不同情况
- 确定dp数组
dp[i][0]
:第i天第一次持有股票所剩最多现金
dp[i][1]
:第i天第一次不持有股票所剩最多现金
dp[i][2]
:第i天第二次持有股票所剩最多现金
dp[i][3]
:第i天第二次不持有股票所剩最多现金 - 确定递推公式
dp[i][0]
可以由两种情况推出来:取两者中最大值- i-1天就第1次持有了股票,第i天保持现状,则
dp[i][0] = dp[i-1][0]
- i-1天第0次不持有股票,第i天买入股票,则
dp[i][0] =-prices[i]
- i-1天就第1次持有了股票,第i天保持现状,则
dp[i][1]
同样可以由两种情况推出来:取两者中最大值- i-1天第1次不持有股票,第i天保持现状,则
dp[i][1] = dp[i-1][1]
- i-1天第1次持有股票,第i天卖出入股票,则
dp[i][1] = dp[i-1][0]+prices[i]
- i-1天第1次不持有股票,第i天保持现状,则
dp[i][2]
可以由两种情况推出来:取两者中最大值- i-1天就第2次持有了股票,第i天保持现状,则
dp[i][2] = dp[i-1][2]
- i-1天第1次不持有股票,第i天买入股票,则
dp[i][2] = dp[i-1][1]-prices[i]
(注意这里不是dp[i-1][1]-prices[i],因为股票只能买卖一次)
- i-1天就第2次持有了股票,第i天保持现状,则
dp[i][3]
同样可以由两种情况推出来:取两者中最大值- i-1天第2次不持有股票,第i天保持现状,则
dp[i][3] = dp[i-1][3]
- i-1天第2次持有股票,第i天卖出入股票,则
dp[i][3] = dp[i-1][2]+prices[i]
- i-1天第2次不持有股票,第i天保持现状,则
- dp数组如何初始化
dp[0][0] = -prices[0]
dp[0][1] = 0
dp[0][2] = -prices[0]
相当于第一天买了又卖再买
dp[0][3] = 0
- 确定遍历顺序
从前往后,因为dp[i]依靠dp[i-1]推出来的 - 举例推导dp数组
代码:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(4,0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = -prices[0];
dp[0][3] = 0;
for(int i =1 ;i<dp.size();i++){
dp[i][0] = max(dp[i-1][0],-prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]+prices[i]);
dp[i][2] = max(dp[i-1][2],dp[i-1][1]-prices[i]);
dp[i][3] = max(dp[i-1][3],dp[i-1][2]+prices[i]);
}
return dp[dp.size()-1][3];
}
28 买卖股票的最佳时机Ⅳ
题目:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。
思路:
k次和2次区别就是将内部的写成一个循环
代码:
int maxProfit(int k, vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(2*k,0));
for(int i = 0 ; i < k;i++){
dp[0][2 * i] = -prices[0];
dp[0][2 * i + 1] = 0;
}
for(int i =1 ;i<dp.size();i++){
for(int j = 0; j < k; j++){
if(j == 0) dp[i][0] = max(dp[i-1][2*j],-prices[i]);
else dp[i][2*j] = max(dp[i-1][2*j],dp[i-1][2*j-1]-prices[i]);
dp[i][2*j+1] = max(dp[i-1][2*j+1],dp[i-1][2*j]+prices[i]);
}
}
return dp[dp.size()-1][2*k-1];
}
29 最佳买卖股票时机含冷冻期
题目:
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
思路:
加上冷冻期也就是再状态传递时发生改变而已
- 确定dp数组
dp[i][0]
表示第i天持有股票得到的最多现金
dp[i][1]
表示第i天卖出股票得到的最多现金
dp[i][2]
表示第i天冷冻期得到的最多现金
dp[i][3]
表示第i天冷冻期得到的最多现金 - 确定递推公式
dp[i][0]
可以由两种情况推出来:取两者中最大值- i-1天就持有了股票,第i天保持现状,则
dp[i][0] = dp[i-1][0]
- i-1天为冷冻期,第i天买入股票,则
dp[i][0] = dp[i-1][2]-prices[i]
- i-1天就持有了股票,第i天保持现状,则
dp[i][1]
同样可以由两种情况推出来:取两者中最大值- i-1天持有股票,第i天卖出入股票,则
dp[i][1] = dp[i-1][0]+prices[i]
- i-1天持有股票,第i天卖出入股票,则
dp[i][2]
- i-1天卖出股票
dp[i][2] = dp[i-1][1]
- i-1天为冷冻期
dp[i][2] = dp[i-1][2]
- i-1天卖出股票
- dp数组如何初始化
dp[0][0] = -prices[0]
dp[0][1] = 0
dp[0][2] = 0
- 确定遍历顺序
依旧是从前往后遍历,因为dp[i]都是由dp[i-1]推出来的 - 举例推导dp数组
代码:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector(3,0));
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=0;
for(int i = 1;i < prices.size();i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][2]-prices[i]);
dp[i][1] = dp[i-1][0]+prices[i];
dp[i][2] = max(dp[i-1][1],dp[i-1][2]);
}
return max(dp[prices.size()-1][2],dp[prices.size()-1][1]);
}
30 卖股票的最佳时机含手续费
题目:
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
思路:
在无限次购买股票的基础上对状态改变进行修改即可
- 确定dp数组
dp[i][0]
表示第i天持有股票得到的最多现金
dp[i][1]
表示第i天不持有股票得到的最多现金 - 确定递推公式
dp[i][0]
可以由两种情况推出来:取两者中最大值- i-1天就持有了股票,第i天保持现状,则
dp[i][0] = dp[i-1][0]
- i-1天不持有股票,第i天买入股票,则
dp[i][0] = dp[i-1][1]-prices[i]
- i-1天就持有了股票,第i天保持现状,则
dp[i][1]
同样可以由两种情况推出来:取两者中最大值- i-1天不持有股票,第i天保持现状,则
dp[i][1] = dp[i-1][1]
- i-1天持有股票,第i天卖出股票,则
dp[i][1] = dp[i-1][0]+prices[i]-fee
- i-1天不持有股票,第i天保持现状,则
- dp数组如何初始化
dp[0][0] = -prices[0];
dp[0][1] = 0;
- 确定遍历顺序
从前往后遍历 - 举例推导dp数组
代码:
int maxProfit(vector<int>& prices, int fee) {
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i =1;i<prices.size();i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
return max(dp[dp.size()-1][0],dp[dp.size()-1][1]);
}
31 最长递增子序列
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
思路:
- 确定dp数组
dp[i]为i之前包括i的以nums[i]为结尾的最长递增子序列的长度 - 确定递推公式
递推时,一定是因为因为当前数字nums[i]大于迁移数字nums[j]时,进行递推,因为此时才满足自增子序列的可能。所以dp[i] = max(dp[i],dp[j]+1)
- dp数组如何初始化
初始化都为1,因为把每一个数都当做子序列时,此时长度也为1 - 确定遍历顺序
从前往后 - 举例推导dp数组
代码:
32 长连续递增序列
题目:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
思路:
看到连续子序列,一定是双指针法了
其余都相同,除了递推公式中
- 不连续子序列 :
dp[i] = max(dp[i],dp[j]+1)
- 连续子序列:
dp[i] = dp[i - 1] + 1;
此时不用从0到i进行遍历了,只需要看i-1的状态,因为要求是连续
代码:
// 双指针
int findLengthOfLCIS(vector<int>& nums) {
int fast =1;
int slow = 0;
int result = 1;
while(fast < nums.size()){
int tmp = 0;
while(fast < nums.size() && nums[fast]-nums[fast-1] > 0){
++fast;
}
result = max(result,fast - slow);//能改进
slow = fast;
++fast;
}
return result;
}
//动态规划
int findLengthOfLCIS(vector<int>& nums) {
if (nums.size() == 0) return 0;
int result = 1;
vector<int> dp(nums.size() ,1);
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i - 1]) { // 连续记录
dp[i] = dp[i - 1] + 1;
}
if (dp[i] > result) result = dp[i];
}
return result;
}
33 最长重复子序列
题目:
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3, 2, 1] 。
提示:
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100
思路:
二维dp数组可以保存所有的比较的信息
- 确定dp数组
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 ) - 确定递推公式
根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。
即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始 - dp数组如何初始化
因为dp[i][0] 和dp[0][j]初始化为0,因为dp[1][1] = dp[0][0] + 1 - 确定遍历顺序
外层遍历A,内层遍历B,同时记录最大值result - 举例推导dp数组
代码:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector(nums2.size()+1,0));
int result = 0;
for(int i =1;i<=nums1.size();i++){
for(int j =1;j<=nums2.size();j++){
if(nums1[i-1] == nums2[j-1])dp[i][j]=dp[i-1][j-1]+1;
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
34 最长公共子序列
题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
思路:
与33题不同的是子序列,而非子数组(连续子序列)
- 确定dp数组
dp[i][j] :以下标i - 1为结尾的text1,和以下标j - 1为结尾的text2,最长重复子数组长度为dp[i][j]。 - 确定递推公式
两种情况:
- text[i-1]与text[j-1]相同时,dp[i][j] = dp[i-1][j-1] + 1
- text[i-1]与text[j-1]不同时,dp[i][j] = max(dp[i][j-1],dp[i-1][j])
- dp数组如何初始化
初始化统一为0,这也就是为什么dp[i][j]定义为下标i-1和下标j-1了 - 确定遍历顺序
那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。 - 举例推导dp数组
代码:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>>dp(text1.size()+1,vector<int>(text2.size()+1,0));
for(int i = 1;i<=text1.size();i++){
for(int j = 1;j <= text2.size(); j++){
if(text1[i-1] == text2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[text1.size()][text2.size()];
}
35 不相交的线
题目:
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
思路:
定义dp[i][j]为数字nums1[i-1]和nums2[j-1]获得的符合条件的最多的线,那么当nums1[i-1]=nums2[j-1]时,此时连成一条线,所以dp[i][j]+1,不相等时,此时取max(dp[i][j-1],dp[i-1][j])。此时分析发现,这不就是最长公共子序列吗
代码:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>>dp(nums1.size()+1 , vector<int>(nums2.size()+1,0));
for(int i = 1;i <= nums1.size();i++){
for(int j= 1;j<=nums2.size();j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
}
}
}
return dp[nums1.size()][nums2.size()];
}
36 最大子序和
题目:
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
思路:
-
确定dp数组
dp[i]为以下标i结尾的包括i的连续子数组的最大序列和 -
确定递推公式
dp[i]可以从两个方向来
- dp[i-1] + nums[i]
- nums[i]
- dp数组如何初始化
dp[0]初始化为nums[0] - 确定遍历顺序
从前往后遍历 - 举例推导dp数组
代码:
int maxSubArray(vector<int>& nums) {
vector<int>dp(nums.size(),0);
dp[0] = nums[0];
int result = dp[0];
for(int i = 1;i<nums.size();i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>result) result = dp[i];
}
return result;
}
37 判断子序列
题目:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
思路:
如果最长公共子序列等于字符串s的长度,那么就是子序列,返回true
代码:
bool isSubsequence(string s, string t) {
vector<vector<int>>dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
if (dp[s.size()][t.size()] == s.size()) return true;
return false;
}
38 不同的子序列
题目:
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
思路:
- 确定dp数组
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 - 确定递推公式
如果s[i-1] == t[j-1]则,dp[i][j] = dp[i-1][j-1]+dp[i-1][j];如果不相等时,此时dp[i][j] = dp[i-1][j],也即将s中的当前字母“删除”,继承上一个字母之前的结果 - dp数组如何初始化
通过递归公式可以看出来,dp数组中的值都是从dp[0][0]等中推出来的。
- dp[0][0]:两个空字符串,定义为1
- dp[0][j]:空字符串中出现以j-1结尾字符串,一定为0
- dp[i][0]:i-1结尾字符串出现空字符串,则删除所有元素,值为1,所以为1
- 确定遍历顺序
根据递推公式从上往下,从左往右 - 举例推导dp数组
代码:
int numDistinct(string s, string t) {
vector<vector<uint64_t>>dp(s.size()+1,vector<uint64_t>(t.size()+1,0));
for(int i = 0; i<=s.size();i++) dp[i][0] = 1;
for(int i =1;i<=s.size();i++){
for(int j =1;j<=t.size();j++){
if(s[i-1]==t[j-1]) dp[i][j] = dp[i-1][j-1]+dp[i-1][j];
else dp[i][j] = dp[i-1][j];
}
}
return dp[s.size()][t.size()];
}
39 两个字符串的删除操作
题目:
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
思路:
求出最长公共子序列,然后word1长度加上word2长度减去2倍最长公共子序列长度即可
代码:
int minDistance(string s, string t) {
vector<vector<int>>dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); i++) {
for (int j = 1; j <= t.size(); j++) {
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j - 1],dp[i-1][j]);
}
}
return s.size()+t.size()-2*dp[s.size()][t.size()];
}
40 编辑距离
题目:
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
思路:
- 确定dp数组
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。 - 确定递推公式
主要有四种情况
-
word1[i - 1] == word2[j - 1]
此时不操作,dp[i][j] = dp[i-1][j-1] -
word1[i - 1] != word2[j - 1]
- 增:增等于删,因为增加word1等于删除word2
- 删:此时分两种情况,删除word1和删除word2:
- 删除word1:
dp[i][j]=dp[i-1][j]+1
- 删除word2:
dp[i][j]=dp[i][j-1]+1
- 删除word1:
- 换:只需要替换一次,就可以使得word1[i - 1] == word2[j - 1],所以
dp[i][j]=dp[i-1][j-1]+1
综上:
if(word1[i - 1] == word2[j - 1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j]=min({dp[i-1][j-1],dp[i][j-1],dp[i-1][j]})+1;
}
- dp数组如何初始化
- dp[0][0]也就是两个都为空,所以为0
- dp[i][0]word1不为空,word2空,所以最小距离为word1的长度
- dp[0][j]word2不为空,word1空,所以最小距离为word2的长度
- 确定遍历顺序
从左上角遍历到右下角 - 举例推导dp数组
代码:
int minDistance(string word1, string word2) {
vector<vector<int>>dp(word1.size()+1,vector<int>(word2.size()+1,0));
for(int i = 0 ; i<=word1.size();i++) dp[i][0] = i;
for(int j = 0 ; j<=word2.size();j++) dp[0][j] = j;
for(int i = 1 ; i<=word1.size();i++){
for(int j = 1 ; j<=word2.size();j++){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = min({dp[i-1][j-1],dp[i][j-1],dp[i-1][j]})+1;
}
}
}
return dp[word1.size()][word2.size()];
}
41 回文子串
题目:
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:"abc"
输出:3
解释:三个回文子串: "a", "b", "c"
思路:
- 确定dp数组
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 - 确定递推公式
- 首先如果s[i]!=s[j],那么一定不是回文子串
- 如果s[i] == s[j]分三种情况,首先是
- j-i = 0,也就是单个字符,true
- j-i = 1,true
- j-i > 1,此时dp[i][j] = dp[i+1][j-1]
- dp数组如何初始化
初始都为false - 确定遍历顺序
由于当前值是依靠左下角值也就是dp[i][j] = dp[i+1][j-1],所以遍历顺序应该是从下到上,从左到右。 - 举例推导dp数组
代码:
int countSubstrings(string s) {
vector<vector<bool>>dp(s.size(),vector<bool>(s.size(),false));
int result = 0;
for(int i = s.size()-1;i>=0;i--){
for(int j = i;j < s.size();j++){
if(s[i]==s[j]){
if(j-i <= 1) {
dp[i][j] = true;
result++;
}else if(dp[i+1][j-1]){
dp[i][j] = true;
result++;
}
}
}
}
return result;
}
42 最长回文子序列
题目:
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。
示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。
提示:
1 <= s.length <= 1000
s 只包含小写英文字母
思路:
- 确定dp数组
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。 - 确定递推公式
当s[i]==s[j]时:dp[i][j] = dp[i+1][j-1] + 2
当s[i]!=s[j]时:dp[i][j] = max(dp[i][j-1],dp[i+1][j]) - dp数组如何初始化
由递推公式可以看出,i不能等于j,dp[i][j] = dp[i+1][j-1] + 2 计算不出i,j相同的情况。此时手动初始化dp[i][i]为1 - 确定遍历顺序
从递推公式可以看出,从左下角递归到右上角的 - 举例推导dp数组
代码:
int longestPalindromeSubseq(string s) {
vector<vector<int>>dp(s.size(),vector<int>(s.size(),0));
for(int i = 0;i<s.size();i++) dp[i][i] = 1;
for(int i = s.size()-1;i>=0;i--){
for(int j = i + 1;j < s.size();j++){
if(s[i]==s[j])dp[i][j] = dp[i+1][j-1] + 2;
else dp[i][j] = max(dp[i][j-1],dp[i+1][j]);
}
}
return dp[0][s.size()-1];
}
本文来自博客园,作者:mobbu,转载请注明原文链接:https://www.cnblogs.com/mobbu/p/17664424.html