Leecode刷题心得和bug(动态规划)
一、动态规划
0 动态规划基础
-
求解动态规划题目的五部曲:
- 确定dp数组(dp table)以及下标的含义;
- 确定递推公式;
- dp数组如何初始化;
- 确定遍历顺序;
- 举例推导dp数组。
本章每道题的源码中我都将把这五步写到注释中。
509 斐波那契数
1.题目描述
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
2.AC源码及注释
//普通的做法
class Solution {
public:
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
};
//动规五部曲的做法
class Solution {
public:
//dp[i]的定义为:第i个数的斐波那契数值是dp[i];
//题目已给出状态转移方程:dp[i] = dp[i - 1] + dp[i - 2];
//题目也给出了初始化:dp[0] = 0;dp[1] = 1;
//遍历的顺序一定是从前到后遍历的;
//当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55;
int fib(int N) {
if (N <= 1) return N;
vector<int> dp(N + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= N; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[N];
}
};
3.总结
- 简单的题,理解了动规五部曲后可以写出动态规划的解法。
70 爬楼梯
1.题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
2.AC源码及注释
class Solution {
public:
int climbStairs(int n) {
//dp[i]是爬上i阶楼梯的不同方法数
//递推公式dp[i] = dp[i - 1] + dp[i - 2],因为每次都只能走1步或者2步
//所以要走到第i阶就是走到第i-1阶(再走一步)的方法数和走到第i-2阶(再走两步)的方法数
//初始化:dp[1] = 1; dp[2] = 2
//从前往后遍历
//把dp[0]设置为1。 1 1 2 3 5
if (n <= 1) return n;
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;//因为这里直接对dp[2]进行操作,所以如果输入1,dp的容量只有2,是没有dp[2]的
for (int i = 3; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
3.总结
- 题简单在做的时候de了好久才de出的bug:因为初始化需要对下标为1和2的赋值,所以必须保证vector有足够的空间,当n<=1的时候就可以直接返回即可。
746 使用最小花费爬楼梯
1.题目描述
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
2.AC源码及注释
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
//dp[i]表示到达第i个楼梯花费的最低开销
//dp[i] = min(dp[i - 1] , dp[i - 2]) + cost[i]
//dp[0] = cost[0]; dp[1] = cost[1]
//还是从前往后
//示例2:1 100 2 3 3 103 4 5 104 6 6
if (cost.size() == 2) {
return (cost[0] < cost[1] ? cost[0] : cost[1]);
}
int size = cost.size() + 1;
vector<int> dp(size);
dp[0] = cost[0];
dp[1] = cost[1];
for(int i = 2; i < size; i++) {
if (i == size - 1) {
dp[i] = (dp[i - 1] < dp[i - 2] ? dp[i - 1] : dp[i - 2]);
continue;
}
dp[i] = (dp[i - 1] < dp[i - 2] ? dp[i - 1] : dp[i - 2]) + cost[i];
}
return dp[size - 1];
}
};
3.总结
- 题目的动态规划关系还是挺明显的。注意dp的大小是比cost大1的,因为要计算达到最后一层的总计开销。其实cost是没有dp最后下标的元素值的,所以dp[dp.size - 1]的赋值逻辑与之前的元素不同,只是去取前两个元素最小值,而不会再加上cost[i];
- 也de了很久的bug:在赋值dp最后一个元素的if逻辑中,没有写continue导致没有执行完后没有退出循环,继续走if外的逻辑加上了一个不存在的cost[i]值,导致dp最后一个元素值很异常。
62 不同路径
1.题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
2.AC源码及注释
class Solution {
public:
int uniquePaths(int m, int n) {
//dp[i][j]表示达到i+1 j+1网格的路径条数
//递推公式:dp[i][j] = dp[i][j-1] + dp[i-1][j]
//初始化:表格中第一行和第一列其实都可以初始化为1,因为这些位置的网格只能直走。
//遍历顺序:从上往下从左往右
//就不举例啦
int dp[m][n];
dp[0][0] = 1;
for (int i = 1; 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][j - 1] + dp[i - 1][j];
}
}
return dp[m - 1][n - 1];
}
};
3.总结
- 心情不好题简单,不总结了。
63 不同路径II(有障碍物版)
1.题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
2.AC源码及注释
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
//逻辑跟上道题大致是一样的,只是需要增加当前位置上或者左存在障碍物的判断逻辑。
//dp[i][j]表示达到i+1 j+1网格的路径条数
//递推公式:dp[i][j] = dp[i][j-1] + dp[i-1][j]
//初始化:表格中第一行和第一列其实都可以初始化为1,因为这些位置的网格只能直走。
//遍历顺序:从上往下从左往右
//就不举例啦
// int m = obstacleGrid.size();
// int n = obstacleGrid[0].size();
// for (int i = 0; i < m; i++) {
// if (obstacleGrid[i][0] == 1) {
// obstacleGrid[i][0] = -1;
// break;
// } else {
// obstacleGrid[i][0] = 1;
// }
// }
// for (int j = 1; j < n; j++) {
// if (obstacleGrid[0][j] == 1) {
// obstacleGrid[0][j] = -1;
// break;
// } else if (obstacleGrid[0][j - 1] == -1) {
// obstacleGrid[0][j] = 0;
// break;
// } else {
// obstacleGrid[0][j] = 1;
// }
// }
// for (int i = 1; i < m; i++) {
// for (int j = 1; j < n; j++) {
// if (obstacleGrid[i][j] == 1) {
// obstacleGrid[i][j] = -1;
// }
// }
// }
// for (int i = 1; i < m; i++) {
// for (int j = 1; j < n; j++) {
// if (obstacleGrid[i][j] == -1) {
// continue;
// }
// if (obstacleGrid[i - 1][j] == -1 && obstacleGrid[i][j - 1] != -1) {
// obstacleGrid[i][j] = obstacleGrid[i][j - 1];
// } else if (obstacleGrid[i][j - 1] == -1 && obstacleGrid[i - 1][j] != -1) {
// obstacleGrid[i][j] = obstacleGrid[i - 1][j];
// } else if (obstacleGrid[i - 1][j] == -1 && obstacleGrid[i][j - 1] == -1) {
// obstacleGrid[i][j] == 0;
// } else {
// obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1];
// }
// }
// }
// return obstacleGrid[m - 1][n - 1] == -1 ? 0 : obstacleGrid[m - 1][n - 1];
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0
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];
}
};
3.总结
- 今日(2022.12.3)出现了我这辈子最恨的题。这道题看了半天,非要在原数组上进行操作,又要判断是否是障碍物,又要存储路径条数,然后在对原数组的第一行第一列进行处理的时候发现特别复杂:一旦一行或一列出现了障碍物(原值为1),需要将值变换为-1,来表征是障碍物,那么后面所有的无障碍物都保持原值0,并且需要把所有的1都换为-1.
- 可能也好做吧:判断那一条里面是否有1,没有的话把所有的0都换成1,表示有1条路径;存在1了,就把所有1换成-1。就是不想写了随便吧,不要舍不得那点空间。
343 整数拆分
1.题目描述
给定一个正整数
n
,将其拆分为k
个 正整数 的和(k >= 2
),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。
2.AC源码及注释
class Solution {
public:
int integerBreak(int n) {
//没想出转移方程
//dp[i]是分拆数字i,得到的最大乘积
//递推式:dp[i] = dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
//从前往后遍历
//不举例,但是dp[0]和dp[1]没有意义可不赋值。
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n; i++) {
//(i - j) * j, dp[i - j] * j 因为没有dp[1] 所以j是从1到 i - j > 1 即j < i - 1
for (int j = 1; j < i - 1; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
3.总结
-
-
有贪心算法,把n分拆成多个3,如果剩下的是4,就保留4。此时的拆分组合乘积就是最大的。
96 不同的二叉搜索树
1.题目描述
给你一个整数
n
,求恰由n
个节点组成且节点值从1
到n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
2.AC源码及注释
class Solution {
public:
int numTrees(int n) {
//看了题解又懂了,做动规题就是要举例简单的问题,然后发现问题间的关系
//dp[i]表示i个节点的二叉搜索树有多少种。
//从i个节点中取一个作为根节点,剩下i - 1个节点用作孩子。j从0开始遍历到i - 1得到二叉树的个数
//dp[i] += dp[j] * dp[i - 1 -j]
//从前往后遍历
//不举例了,特例dp[0] = 0
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - 1 - j];
}
}
return dp[n];
}
};
3.总结
- 卡哥的图,very clear
01背包(自己画图总结)
-
二维数组:需要对物品0那一行进行初始化,并且是从左往右,从上到下的遍历,即先遍历容量再遍历物品。
-
一维数组:
-
一般不对物品0进行初始化,直接从外层循环i = 0开始;若对物品0初始化,那么外层循环从1开始,跟一维一样;
-
外层循环i遍历物品;内层循环遍历容量。
-
416 分割等和子集
1.题目描述
给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
2.AC源码及注释
class Solution {
public:
bool canPartition(vector<int>& nums) {
//dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]
//dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
//如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
//物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历
//不举例
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
if (sum % 2 != 0) return false;
int halfSum = sum / 2;
vector<int> dp(halfSum + 1, 0);
//不用写这个初始化啊,因为下面的i是从0开始的,不用再去赋值了;从1开始倒是可以写这个
// for (int j = nums[0]; j <= halfSum; j++) {
// dp[j] = nums[0];
// }
for (int i = 0; i < nums.size(); i++) {
for (int j = halfSum; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[halfSum] == halfSum) return true;
return false;
}
};
3.总结
- 这个题的大概思路自己想到了:判断集合中是否存在加起来等于总和一半的子集。但是不是很能看懂一个问题的“背包”特性,看了题解说用背包才明白。
- 在这个题中物品的重量和价值都是一样的,所以想起来会比较绕。说白了就是单纯运用背包的能在一个最大值内找最大值的思想解的本题:在最大值为j的情况下,去看前i个数加起来的最大值。
1049 最后一块石头的重量II
1.题目描述
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
2.AC源码及注释
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
//dp[j]表示以重量j为最大值下的最大值
//dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
//遍历顺序从0到i循环,倒序
//不举例
int sum = 0;
for (int i = 0; i < stones.size(); i++) {
sum += stones[i];
}
int halfSum = sum / 2;
vector<int> dp(halfSum + 1, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = halfSum; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[halfSum] - dp[halfSum];
}
};
3.总结
- 牛逼,看了题解的第一句话:尽量分成重量相同的两堆,然后碰撞后就是最小重量。理解了卡哥编排题目顺序的良苦用心了,基本是就是一招解决一堆问题。类比昨天那道题,基本思路就是在halfsum的最大值下找到相加起来最大的值,所以核心逻辑基本是都是一毛一样的
- 出了一个小bug,把递推式中的最后stones[i]写成了stones[j]了,看了好一会没看出来。
494 目标和
1.题目描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
2.AC源码及注释
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if ((sum + target) % 2 != 0) return 0;
if (abs(target) > sum) return 0;
//dp[j]表示相加得到j的组合个数(填满j(包括j)这么大容积的包,有dp[j]种方法)
//dp[j] += dp[j - nums[i]]
//dp[0]初始化为1!
//仍然采用一维dp的遍历顺序
//举例在总结中
int leftSum = (sum + target) / 2;
vector<int> dp(leftSum + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = leftSum; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[leftSum];
}
};
3.总结
- 这道题属于背包问题中一类新的解决问题的领域。不再是之前的在某个给定最大值(背包容量)下去求解能得到的实际最大值,而是装满背包容量的方法数。
- 以下递推式是解决这类问题的较为通用的公式:
dp[j] += dp[j - nums[i]];
-
感觉背包问题都是给出一个特定的问题背景,你总能找到背包容量bagsize和需要放入的物品。很多时候物品的weight和value都是同一个,而不会专门分开给出。
-
此题的dp[0]初始化为1是关键,卡哥的解释如下:
-
dp数组举例如下:
474 一和零
1.题目描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
2.AC源码及注释
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
//dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
//遍历顺序就是外层一个for遍历物品,内层两个for循环遍历背包,顺序也是从后往前,总而言之就是一维dp遍历顺序的doubleplus版
//全部初始化为0
//举例在总结中
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0));
for (string str : strs) {
int zeroSum = 0, oneSum = 0;
for (char c : str) {
if (c == '0') {
++zeroSum;
}else {
++oneSum;
}
}
for (int i = m; i >= zeroSum; i--) {
for (int j = n; j >= oneSum; j--) {
dp[i][j] = max(dp[i][j], dp[i - zeroSum][j - oneSum] + 1);
}
}
}
return dp[m][n];
}
};
3.总结
- 被吓到了,两个条件,但是又看了一开始的题解,觉得一切又合理了起来。其实我一开始也明白本题就是在01背包的基础上将原来的一维背包容量扩展成了二维,但是没想到具体该怎么写。又看了一眼题解代码,觉得合理,完完全全就是套的一维dp的模板。
- 得自己比划比划一下一个例子的dp赋值过程。
完全背包
- 01背包vs完全背包:
- 01背包的物品每种只有一个,而完全背包的物品每种有无限个。
- 在解题上,仅在遍历顺序有所不同:以一维dp解题为例,01背包在内层遍历容量时,是从大到小遍历(bagsize->weight[i]);完全背包在内层遍历容量时,是从小到大遍历(weight[i]->bagsize)。目的是前者只添加一次物品,而后者可以添加多次物品。
- 关于dp数组的物品与容量循环遍历的先后顺序,就是先for i 还是先 for j :
- 01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
- **在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!**不同的循环遍历顺序代码不同,建议统一都是先物品后容量。
518 零钱兑换II
1.题目描述
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
2.AC源码及注释
class Solution {
public:
int change(int amount, vector<int>& coins) {
//dp[j]表示凑成j金额的组合数
//dp[j] += dp[j - coins[i]]
//跟之前的组合问题一样dp[0]初始化为1
//与01背包的一维数组解题模式类似的遍历顺序,但是完全背包的对于容量的遍历顺序是从小到大
//不举例
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];
}
};
3.总结
- 这道题放在完全背包讲解的后面,就是完全地套用模式来解题了,知道这是完全背包问题后,改变遍历顺序即可解题;
- 动规题感觉递推式是拍脑袋想出来的,自己去走一遍dp数组的赋值过程多多少少会加深理解。
377 组合总和 IV
1.题目描述
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
2.AC源码及注释
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) {
for (int j = 0; j < nums.size(); j++) { if (i >= nums[j] && dp[i] < INT_MAX - dp[i - nums[j]])
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
3.总结
- 开始以为跟上一道题一样,火速写出模板,然后没过……才发现例子下面写的“请注意,顺序不同的序列被视为不同的组合”,意识到这不是组合问题而是排列问题。
- 然后又去看了题解,又发现了排列的解法呢。因为之前组合的解法是先遍历物品,所以其实在递增方法数的时候,新增的方法其实就只是把物品加在组合的后面。在解排列问题时候,就先遍历容量再遍历物品就可,注意调换顺序时,容量和物品重量的关系没办法体现在for循环中了,需要再循环体中增加用于判断这两者的if逻辑。
- 本题卡哥说的测试用例有相加大于int最大值的,所以需要再if逻辑再加入该判断。
爬楼梯(进阶版)
1.题目描述
将每次至多爬两个台阶改成一步一个台阶,两个台阶,三个台阶,…,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
2.AC源码及注释
class Solution {
public:
int climbStairs(int n) {
//dp[i]表示爬到第i层楼梯的方法数
//排列问题与组合问题递推式相同:dp[i] += dp[i - j]
///组合和排列问题dp[0]初始化都要为0
//遍历顺序:排列问题必须要求先遍历背包容量再遍历物品;完全背包要求遍历背包时必须正序遍历
//不举例
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) { // 遍历背包
for (int j = 1; j <= m; j++) { // 遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
3.总结
- 该部分仅作为扩展部分,爬楼梯原题是一道十分简单的动规题,但是修改后的题目就可以作为完全背包和排列问题的结合题目;
- 每次选择爬的层数就是物品,总共的阶梯数就是背包。由于每次选择的层数都可以相同,即第一次可以选择爬一层每次都可以选择爬一层所以是完全背包。然后第一次爬一层第二次爬两层和第一次爬两层层第二次爬一层是两种不同的方案所以是排列问题;
- 题目修改后其实是一个通用问题,原爬楼梯问题其实就是这个背包问题中把物品数限制在2及以下,修改上面代码中的第二个循环中的 j <= m 为 j <=2。
322 零钱兑换
1.题目描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的
2.AC源码及注释
//自己写的
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//完全背包问题,用min函数
//dp[i]表示凑成总金额i所需的最小的硬币个数
//dp[i] = min(dp[i], dp[i - coins[i] + 1])
//感觉这里有点特殊了,dp[0]初始化为0
//正常的完全背包遍历顺序即可
//不举例
if (amount == 0) return 0;
vector<int> dp(amount + 1, 0);
for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j] == 0 && (j - coins[i] == 0 || dp[j - coins[i]] != 0)) {
dp[j] = dp[j - coins[i]] + 1;
} else {
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == 0 ? -1 : dp[amount];
}
};
//看了题解后改的
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//完全背包问题,用min函数
//dp[i]表示凑成总金额i所需的最小的硬币个数
//dp[i] = min(dp[i], dp[i - coins[i] + 1])
//感觉这里有点特殊了,dp[0]初始化为0
//正常的完全背包遍历顺序即可
//不举例
if (amount == 0) return 0;
vector<int> dp(amount + 1, INT_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]] != INT_MAX) {
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};
3.总结
- 这道题真的好可惜,思路什么的都是自己想的,没有看题解,结果还是没过,看了题解发现思路很正确,但是细节出了问题。我想的是dp[i]表示最少硬币数嘛就感觉应该都初始化为0,而且dp[0]初始化为0也是我自己想到的,但是后面的逻辑就非常有问题,就比如当dp[i]依赖dp[0]的时候且dp[i]为0,就应该直接赋值而不应该min得出值;dp[i]依赖的非下标0的dp值如果为0就不应该赋值等等,就非常地混乱;
- 然后看了题解才知道,初始化其他元素应该根据递推式啊,因为是min比较,就应该设置为最大值INT_MAX这样就能逻辑通顺,而不会需要要单独去判断上面的逻辑。
279 完全平方数
1.题目描述
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
2.AC源码及注释
class Solution {
public:
int numSquares(int n) {
//完全背包 最少数量
//if (n == 1) return 1;
//首先计算出物品数量,即itemNum的平方为恰好不大于n
int x;
for (x = 1; x <= n; ++x) {
if (x * x > n) {
break;
}
}
//假设itemNum = 3,表示数的范围是 1 2 3
int itemNum = x - 1;
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= itemNum; i++) {
for (int j = i * i; j <= n; j++) {
dp[j] = min(dp[j], dp[j - i * i] + 1);
}
}
return dp[n];
}
};
3.总结
- 很简单的套模版题,自己写出来了;
- 本题有一个特点就是需要自己去计算物品的集合,物品的价值和重量就是i的平方。
139 单词拆分
1.题目描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
2.AC源码及注释
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//dp[i]表示字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
//if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
//dp[0]初始化为true,其他初始化为false:就拿第一个例子来说dp[4]依赖dp[0],且dp[4]为true
//完全背包! 排列问题!->注重结果的顺序的就是排列问题
//举例在总结中
unordered_set<string> wordSet(wordDict.begin(), wordDict.end()); //为了方便判断子串是否在字典中出现,使用set转存字典
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) {
for (int j = 0; j < i; j++) {
string word = s.substr(j, i - j);
if (dp[j] && wordSet.find(word) != wordSet.end()) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
3.总结
- 本题特殊在物品并未直接给出,其实是j到i之间的子串;
- 再次说明一下为什么dp[i]是根据dp[j]和j到i之间的子串推导出的,难道前一个到j后一个从j开始,不会有重复吗?
其实dp[j]和j到i的子串是刚好错开的!
198 打家劫舍
1.题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
2.AC源码及注释
class Solution {
public:
int rob(vector<int>& nums) {
//dp[i]
//
//
//
//
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
//
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
//
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
3.总结
213 打家劫舍I
1.题目描述
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
2.AC源码及注释
class Solution {
public:
int rob(vector<int>& nums) {
//很牛的思路,把成环的过程分为包含首元素不包含尾元素和包含尾元素不包含首元素两种情况
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
int resule1 = robRange(nums, 0, nums.size() - 2);
int resule2 = robRange(nums, 1, nums.size() - 1);
return max(resule1,resule2);
}
int robRange(vector<int>& nums, int start, int end) {
if (start == end) return nums[start];
vector<int> dp(nums.size() - 1, 0);
dp[0] = nums[start];
dp[1] = max(nums[start], nums[start + 1]);
for (int i = 2; i < nums.size() - 1; i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[start + i]);
}
return dp[nums.size() - 2];
}
};
3.总结
337 打家劫舍 II
1.题目描述
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
2.AC源码及注释
class Solution {
public:
int rob(TreeNode* root) {
//树形dp,感觉还不是很理解
if (!root) return 0;
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
vector<int> robTree(TreeNode* cur) {
//dp[0]表示不偷当前节点的最大金额 dp[1]表示偷当前节点的最大金额
if (!cur) return vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
//不偷当前节点就可以去考虑左右孩子
int val1 = max(left[0], left[1]) + max(right[0], right[1]);
//偷当前节点就不能偷孩子
int val2 = cur->val + left[0] + right[0];
return {val1, val2};
}
};
3.总结
121 买卖股票的最佳时机
1.题目描述
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
2.AC源码及注释
class Solution {
public:
int maxProfit(vector<int>& prices) {
//dp[i][0]表示第i天持有股票的最大现金(i-1天持有第i天保持、第i天买入)
//dp[i][1]表示第i天不持有股票的最大现金(i-1天不持有、第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[0][0]第0天持有股票是-prices[0];dp[0][1]第0天不持有股票是0
if (prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(2));
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], prices[i] + dp[i - 1][0]);
}
return dp[prices.size() - 1][1];
}
};
3.总结
- 本题说白了就是在一个数组中求解一个最大差值,没想到意外滴还挺复杂,不看题解想不到动规怎么解的
122 买卖股票的最佳时机II
1.题目描述
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
2.AC源码及注释
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; 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[len - 1][1];
}
};
3.总结
- I是只能允许买卖一次,但是II这道题是允许多次买卖的;
- 唯一区别就是在第i天持有股票且情况是买入时,一定是要求昨天没有股票的且手上可能有买卖过的现金:dp[i] [0]=dp[i-1] [1] - prices[i]
123 买卖股票的最佳时机II
1.题目描述
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
2.AC源码及注释
class Solution {
public:
int maxProfit(vector<int>& prices) {
//困难题,但是还是可以从基础题上扩展
//根据题意每天都有五个状态:0无操作 1第一次买入状态 2第一次卖出状态 3第二次买入状态 4第二次卖出状态 dp[i][j] 表示在i天的状态j下的最大现金
//递推式放在总结中
//初始化:第一列的初始化即没有操作的时候都是0,第一行的初始化即第一天的时候分别为0 -prices[0] 0 -prices[0] 0 因为在同一天可以买卖两次
//举例看总结
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(5, 0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for (int i = 1; i < len; 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]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[len - 1][4];
}
};
3.总结
188 买卖股票的最佳时机II
1.题目描述
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
2.AC源码及注释
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
//难道跟III的差别就是有k个状态然后dp数组的列数多一点嘛?
//dp[i][j]表示第i天的j状态下的最大现金
//dp[i][0]=0
//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]) 偶数是不持有
//按照推导,j的数量2k+1,当j为奇数或偶数时有不同的递推式
//初始化全部为0,在第1天的时候持有股票都是-prices[0]
//不举例
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector(2 * k + 1, 0));
for (int j = 1; j <= 2 * k; j++) {
if (j % 2 != 0) dp[0][j] = -prices[0];
}
for (int i = 1; i < len; i++) {
for (int j = 1; j <=2 * k; j++) {
if (j % 2 != 0) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
}
}
}
return dp[len - 1][2 * k];
}
};
3.总结
309 买卖股票的最佳时机含冷冻期
1.题目描述
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
2.AC源码及注释
class Solution {
public:
int maxProfit(vector<int>& prices) {
//本题的解析放在总结中了
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(4, 0));
dp[0][0] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] -prices[i]));
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
}
return max(dp[len - 1][1], max(dp[len - 1][2], dp[len - 1][3]));
}
};
3.总结
714 买卖股票的最佳时机含手续费
1.题目描述
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
2.AC源码及注释
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] = -prices[0];
for (int i = 1; i < len; 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 dp[len - 1][1];
}
};
3.总结
- 在II的基础上加入每次卖出都要减去fee的逻辑,注意是完成一笔完整的订单后需要交手续费。
300 最长递增子序列
1.题目描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
2.AC源码及注释
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//dp[i]是包括nums[i]在内的最大子序列的长度,所以可能包含最后一个元素的最大子序列长度不是题目需要的,所以需要一个变量记录dp中最大的值
//dp[i]=max(dp[i], dp[j]+1),j是nums[i]比nums[j]大的所有元素集合
//初始化为1
//遍历顺序从前往后,且每次遍历需要嵌套一个子遍历
//不举例
int len = nums.size();
if (len <= 1) return len;
vector<int> dp(len, 1);
int result = 0;
for (int i = 1; i < len; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; shi
}
return result;
}
};
3.总结
- 一开始想的太简单了:觉得因为是子序列所以仅仅是看i的前一个i-1是否比i小来判断dp[i]的值。但其实是要根据i之前所有的dp值来决定。
674 最长连续递增子序列
1.题目描述
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
2.AC源码及注释
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
//dp[i]表示以nums[i]为结尾的最大连续子序列的值
//如果nums[i]大于nums[i-1],那么dp[i]=dp[i-1]+1,不然保持1,使用一个变量记录dp中的最大值
int len = nums.size();
if (len <= 1) return len;
vector<int> dp(len, 1);
int result = 0;
for (int i = 1; i < len; ++i) {
if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
if (dp[i] > result) result = dp[i];
}
return result;
}
};
3.总结
- 在前一道题的基础上,追加了连续的条件。前一道题因为是子序列的原因,所以元素之间可以不连续,而追加连续条件后,dp[i]的值仅由前一个元素值及其dp值决定,也就是上道题我最开始想错了的思路
718 最长重复子数组
1.题目描述
给两个整数数组
nums1
和nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
2.AC源码及注释
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
//dp[i][j]表示以nums1[i]为结尾和以nums2[j]为结尾的最大公共子数组的长度
//dp[i][j]=dp[i-1][j-1]+1 只有当nums1[i]=nums2[j]
//全部初始化为0
int len1 = nums1.size();
int len2 = nums2.size();
int result = 0;
if (len1 == 0 || len2 == 0) return 0;
vector<vector<int>> dp(len1, vector<int>(len2, 0));
for (int i = 0; i < len1; ++i) {
if (nums1[i] == nums2[0]) {
dp[i][0] = 1;
result = 1;
}
}
for (int j = 0; j < len2; ++j) {
if (nums1[0] == nums2[j]){
dp[0][j] = 1;
result = 1;
}
}
for (int i = 1; i < len1; ++i) {
for (int j = 1; j < len2; ++j) {
if (nums1[i] == nums2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
3.总结
- 这道题一开始我以为在算法课上讲过,但翻阅自己的上课笔记后发现,讲的是下面👇这道题哈哈。题目要求也是一个连续一个不连续。这道题要求连续,所以在下面这道题的递推式修改后得到这道题的答案;
- 最长公共子序列这道题是定义一个维度均比给定数组大1个的dp数组,然后对边框元素初始化为0,对非边框元素均有递推式推导得出,此时dp[i] [j]表示x的0-i的子串与y的0-j的子串的最长公共子序列长度,所以dp数组赋值完毕后,最后一个元素就是答案的值;
- 由于本题有连续的特性,实际上没必要在意不满足x[i] == y[j]条件的dp值了。当x[i] == y[j] 的时候,去在dp[i-1] [j-1]的基础上加一就是dp[i] [j]的值,表示的是以x[i]结尾的子串和以y[j]结尾的子串间的最长公共子数组的值,用一个变量去记录这个过程中产生的最大值即可。
1143 最长公共子序列
1.题目描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
2.AC源码及注释
class Solution {
public:
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()];
}
};
3.总结
1035 不相交的线
1.题目描述
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。
2.AC源码及注释
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
//想到了先自己写一下
//草!就是最长公共子序列fuckall!我这个解法有误!如果nums1很早就连线到nums2的末尾的话,可能会错失最好情况!
//对比一下,就发现其实就是很想最长公共子序列的逻辑,我加入了一个不能超过之前选定位置的逻辑就错了
// int len1 = nums1.size();
// int len2 = nums2.size();
// if (len1 == 0 || len2 == 0) return 0;
// vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
// int pre = 0;
// for (int i = 1; i <= len1; i++) {
// for (int j = 1; j <= len2; j++) {
// if (nums1[i - 1] == nums2[j - 1] && j > pre) {
// dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + 1;
// pre = j;
// for (int x = j + 1; x <= len2; x++) {
// dp[i][x] = dp[i][j];
// }
// break;
// } else {
// dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
// }
// }
// }
// return dp[len1][len2];
vector<vector<int>> dp(A.size() + 1, vector<int>(B.size() + 1, 0));
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[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[A.size()][B.size()];
}
};
3.总结
53 最大子数组和
1.题目描述
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
2.AC源码及注释
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//算法课上讲过的
//回忆一下递归式应该是:dp[i]=max(dp[i-1]+nums[i], nums[i]),由于dp[i]是包括nums[i]在内
//的最大子数组的和,所以需要最后一个dp值不是最终答案,需要一个变量记录过程中产生的dp最大值
int len = nums.size();
if (len == 0) return 0;
vector<int> dp(len, 0);
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < len; i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
if (dp[i] > result) result = dp[i];
}
return result;
}
};
3.总结
392 判断子序列
1.题目描述
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
2.AC源码及注释
class Solution {
public:
bool isSubsequence(string s, string t) {
//这道题很自然的思路就是复用最长公共子序列题目的代码
//求出两个字符串间的最长公共子序列的值,如果等于s的长度,说明s是t的子序列
//看了题解发现,答案思路一样又不一样,代码主要逻辑的不同就在于当两个字符串对应下标不相等的时候
//dp的值不再是看上面和左面的dp值了,而是只看左面的!
//因为题解的dp[i][j]是短字符串逐个匹配长字符串的相同的序列长度,所以匹配到字符不同的时候,
//可以把长字符串的对应字符删了,所以只看左面的,其实我也没理解
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 - 1][j], dp[i][j - 1]);
}
}
}
return (dp[s.size()][t.size()] == s.size() ? true : false);
}
};
3.总结
115 不同的子序列
1.题目描述
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
2.AC源码及注释
class Solution {
public:
int numDistinct(string s, string t) {
//感觉很多需要用二维dp的题目基本上都会dp数组的大小多声明一个,这样在进行dp数组的赋值时,
//dp的下标和题目所给数据的下标就不是一一对应,一般是dp[i] --- data[i-1]这样的关系
//这样做的好处,一是dp的递推式有时候会是从上面或者左面的dp推导出来,如果不这样做,边框dp元素就很难执行正确的推导
//二是,这样避免了对边界条件的单独处理,所有情况都包含在了dp数组里,比如说当数据大小为0呀,但是仍然可以声明一个大小为1的dp数组,不然就需要对为0的情况单独讨论
//dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
//递推式推导看总结
//初始化:dp[i][0]表示以i-1为结尾的s可以随便删除元素,出现空字符串的个数,把所有元素都删除就肯定能得到一个空字符串,所以dp[i][0]=1;dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数,一个空字符串无法包含一个不为空的串,所以dp[0][i]=0;最后特殊讨论dp[0][0]=1
vector<vector<uint64_t>> dp(s.size()+ 1, vector<uint64_t>(t.size() + 1));
for (int i = 0; i <= s.size(); ++i) {
dp[i][0] = 1;
}
for (int j = 1; j <= t.size(); ++j) {
dp[0][j] = 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] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.size()][t.size()];
}
};
3.总结
583 两个字符串的删除操作
1.题目描述
给定两个单词
word1
和word2
,返回使得word1
和word2
相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
2.AC源码及注释
class Solution {
public:
int minDistance(string word1, string word2) {
//yeyi自己做出来了开熏
//dp[i][j]表示word1[0-i]和word2[0-j]两个字符串之间相同的最小步数
//递推式还是看总结吧,
//初始化就讨论dp边框元素就行,拿dp[i][0]举例,此时表示的是word1[0-i]要到空字符串的最小步数
//那么其实就是要把i个字符都删完啊,dp[i][0]=i;dp[0][j]是一样的;dp[0][0]=0。
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for (int i = 1; i <= word1.size(); ++i) {
dp[i][0] = i;
}
for (int j = 1; 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][j - 1], dp[i - 1][j]) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};
3.总结
72 编辑距离
1.题目描述
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
2.AC源码及注释
class Solution {
public:
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 - 1][j], dp[i][j - 1]}) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};
3.总结
647 回文子串
1.题目描述
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串
2.AC源码及注释
class Solution {
public:
int countSubstrings(string s) {
//自己也想到dp的定义了,但是没仔细想递推式;
//dp[i][j]表示s(i-j)是否是一个回文子串,注意这里是 是否 ,而不是什么长度啊个数之类的,说明题目需要的个数需要另外一个变量记录,不再是直接从dp表中得出;
//递推式看总结
//没有举例了
int len = s.size();
if (len == 0) return 0;
vector<vector<bool>> dp(len, vector<bool>(len));
int count = 0;
for (int i = len - 1; i >= 0; --i) {
for (int j = i; j < len; ++j) {
if (s[i] == s[j]) {
if (i == j) {
dp[i][j] = true;
++count;
} else if (j - i == 1) {
dp[i][j] = true;
++count;
} else {
if (dp[i + 1][j - 1]) {
dp[i][j] = true;
++count;
} else {
dp[i][j] = false;
}
}
} else {
dp[i][j] = false;
}
}
}
return count;
}
};
3.总结
- 首先关于回文的动规题一个大的思路要想到的是:判断条件肯定都会涉及到首尾字符是否相等,然后分情况去讨论有关递推式的逻辑如何进行;
516 最长回文子序列
1.题目描述
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
2.AC源码及注释
class Solution {
public:
int longestPalindromeSubseq(string s) {
//思路和回文子串是基本一样的
int len = s.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(len, 0));
for (int i = len - 1; i >= 0; --i) {
for (int j = i; j < len; ++j) {
if (s[i] == s[j]) {
if (i == j) {
dp[i][j] = 1;
} else if (j - i == 1) {
dp[i][j] = 2;
} else {
dp[i][j] = dp[i + 1][j - 1] + 2;
}
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][len - 1];
}
};
3.总结
- 这道题虽说跟下一道题回文子串思路十分相近,但是细节上还是有很多差别的,可以看一下下一道题的总结,我把我对这道题一开始的想法写了一下,可以看到基本上就是完全对照着写出来的;
- 但是实际上按这种思路是错误的,当我提交后显示错误了自己去把dp数组跑了一遍发现:当s[i]!=s[j]时,dp值并不是由dp[i+1] [j-1]推出。比如:串“bba”如果按照我的逻辑,dp值等于去掉首尾的串的dp值,那么就等于1,但其实实际上,中间的串是可以与首或尾去组成回文的;
- 最后就是卡哥的题解写的很巧妙,把很多逻辑都合并了,但是展开写也没什么大问题。在最后的else逻辑中,为什么我把i和j相邻的逻辑单独拎出来会报错呢,虽然的确是没必要。
三、回溯
77 组合
1.题目描述
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合。你可以按 任何顺序 返回答案。
2.AC源码及注释
class Solution {
public:
//看题解的,画一下递归过程
//再看一下自己的回溯笔记,加深理解
vector<vector<int>> result;
vector<int> path;
void backTracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i<=n; ++i) {
path.push_back(i);
backTracking(n, k, i + 1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backTracking(n, k, 1);
return result;
}
};
3.总结
-
记得算法课讲回溯时有一个状态树一样
-
因为直接的回溯其实是等于暴力穷举的,只是可以让一些题能够暴力解出来(像这道题,暴力举都是做不出来的,因为要根据给定参数确定for循环的个数),与此同时,同时回溯可以有剪枝操作,让暴力不是那么的笨,不用把所有的分支都遍历。对于本题,如果当前能够选择的解的个数小于k-path变量中的个数其实就不用走那个for循环了,剪枝后的代码如下:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
}
216 组合总和III
1.题目描述
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
2.AC源码及注释
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
int pathSum = 0;
void backTracking(int k, int n, int startIndex) {
if (pathSum == n && path.size() == k) {
result.push_back(path);
return;
}
if (pathSum != n && path.size() == k) return;
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; ++i) {
path.push_back(i);
pathSum += i;
backTracking(k, n, i + 1);
pathSum -= i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backTracking(k, n, 1);
return result;
}
};
3.总结
- 卡哥编题有一手的,基本上都会跟上一道题有所关联的。这道题也是我想着上一道题的思路自己做出来的。不同的终止条件;由于将选择范围定在1-9中所以for中横向遍历是可以有确定的边界的;同样可以用上道题差不多的剪枝策略;每次横向遍历的过程中多了一个对sum变量的控制。
17 电话号码的字母组合
1.题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
2.AC源码及注释
class Solution {
public:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
string path;
vector<string> result;
void backTracking(string digits, int index) {
if (path.size() == digits.size()) {
result.push_back(path);
return;
}
char cDigit = digits[index];
int iDigit = cDigit - '0';
string letter = letterMap[iDigit];
int digitLoop = letter.size();
for (int i = 0; i < digitLoop; ++i) {
path.push_back(letter[i]);
backTracking(digits, index + 1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) {
return result;
}
backTracking(digits, 0);
return result;
}
};
3.总结
- 这道题也是自己写出来,当然是看了一点点题解,就看到题解里开头letterMap的声明,就懂了该怎么写了。换一句话说,这道题最难的地方就是数字和字母的映射,回溯的思路很好想到。