本文将持续更新~~
hello hello~ ,这里是绝命Coding——老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
💥个人主页:绝命Coding-CSDN博客
💥 所属专栏:后端技术分享
这里将会不定期更新有关后端、前端的内容,希望大家多多点赞关注收藏💖
动态规划,是一种常用的解决优化问题的方法,它将问题分解为若干子问题,并通过存储子问题的解来避免重复计算,从而提高算法的效率。
动态规划的核心思想是利用已经求解过的子问题的解来求解当前问题的解。它通常适用于具有最优子结构的问题,即问题的最优解可以通过一系列子问题的最优解推导得出。通过将问题划分为更小的子问题,并适当定义状态和状态转移方程,动态规划可以高效地解决各类问题。
动态规划的一般步骤如下:
- 定义问题的状态:将原问题划分为若干个子问题,定义状态表示子问题的解。
- 定义状态转移方程:根据子问题之间的关系,定义状态之间的转移方程。
- 初始化边界条件:确定最小的子问题的解,作为边界条件进行初始化。
- 通过状态转移方程求解问题:根据已经求解的子问题的解,通过状态转移方程递推求解当前问题的解。
- 求解原问题的解:根据子问题的解,计算并返回原问题的解。
动态规划可以应用于各种计算机科学和数学问题,例如最短路径问题、背包问题、序列比对问题等。它在解决这些问题时,往往能够显著提高算法的效率,并且能够得到问题的最优解。
在实际应用中,动态规划通常需要使用一个数组或矩阵来存储子问题的解,以避免重复计算。通过组织和存储子问题的解,动态规划能够以较小的空间复杂度换取较高的时间效率。
动态规划
面试题 17.16. 按摩师
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
示例 3:
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
C语言
迭代法——滚动数组
//执行用时: **4 ms**
//内存消耗: **5.8 MB**
int massage(int* nums, int numsSize){
int ppre=0,pre=0,now=0;
for(int i=1;i<=numsSize;i++){
ppre=pre;
pre=now;
now=pre>ppre+nums[i-1]?pre:ppre+nums[i-1];
}
return now;
}
剑指 Offer 42. 连续子数组的最大和
53. 最大子序和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
- 1 <= arr.length <= 10^5
- -100 <= arr[i] <= 100
注意:本题与主站 53 题相同:. - 力扣(LeetCode)
C语言
//执行用时: **40 ms**
//内存消耗: **9.4 MB**
int maxSubArray(int* nums, int numsSize){
int m[numsSize];
m[0]=nums[0];
for(int i=1;i<numsSize;i++){
m[i]=m[i-1]+nums[i]>nums[i]?m[i-1]+nums[i]:nums[i];
}
int temp=m[0];
for(int i=0;i<numsSize;i++){
temp=temp>m[i]?temp:m[i];
}
return temp;
}
在C语言中可以使用#define max(A,B) A>B?A:B //优化替代
C++
//执行用时: **28 ms**
//内存消耗: **22.8 MB**
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int size = nums.size();
int m[size];
m[0]=nums[0];
for(int i=1;i<size;i++){
m[i]=max(m[i-1]+nums[i],nums[i]); //与单独的自己和自己+之前最大的和比较
} //这样中间产生的值可能更大,所以下面再进行一下比较
int temp = m[0]; //设置一开始的最大值是m[0],然后不断比较出最大值
for(int i=0;i<size;i++){
temp = max(temp,m[i]);
}
return temp;
}
};
面试题 16.17. 连续数列
给定一个整数数组,找出总和最大的连续数列,并返回总和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
C语言
//执行用时: **8 ms**
//内存消耗: **6.2 MB**
int maxSubArray(int* nums, int numsSize){
int m[numsSize];
m[0]=nums[0];
for(int i=1;i<numsSize;i++){
m[i]=m[i-1]+nums[i]>nums[i]?m[i-1]+nums[i]:nums[i];
}
int temp=m[0];
for(int i=0;i<numsSize;i++){
temp=temp>m[i]?temp:m[i];
}
return temp;
}
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
分析:f(x) = f(x-1) + f(x-2)
C语言——滚动数组(动图:https://leetcode-cn.com/problems/climbing-stairs/solution/pa-lou-ti-by-leetcode-solution/)
迭代法
动态规划=递归+备忘录
//执行用时: **0 ms**
//内存消耗: **5.6 MB**
int climbStairs(int n){
int p=0,q=0,r=1;
for (int i = 1; i <=n ; ++i) {
p=q;
q=r;
r=p+q; //状态方程
}
return r;
}
2.
C++——动态规划的状态方程 f(x) = f(x-1) + f(x-2)
//执行用时: **0 ms**
//内存消耗: **5.8 MB**
class Solution {
public:
int climbStairs(int n) {
int f[100]={0};
f[0]=0,f[1]=1,f[2]=2; //f[3]=3
for(int i=3;i<=n;i++){
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
};
3.
记忆化搜索
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
//内存消耗:6.1 MB, 在所有 C++ 提交中击败了18.93% 的用户
class Solution {
public:
//记忆化搜索(可以解决递归超时的问题)
vector<int> f;
int dfs(int i){
if( i == 0 || i == 1 ) return 1;
if( i == 2 ) return 2;
if( f[i] == -1 ){
f[i] = dfs(i - 1) + dfs(i - 2); //记录下来
}
return f[i];
}
int climbStairs(int n) {
f = vector<int>(n+1,-1);
return dfs(n);
}
};
4.acwing
class Solution {
public:
int climbStairs(int n) {
int a = 1, b = 1;
while ( -- n) {
int c = a + b;
a = b, b = c;
}
return b;
}
};
剑指 Offer 10- II. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
示例 3:
输入:n = 0
输出:1
提示:
- 0 <= n <= 100
1.C++——动态规划dp
//执行用时: **0 ms**
//内存消耗: **5.7 MB**
//动态规划
//状态方程:f[n] = f[n-1] + f[n-2]
class Solution {
public:
int numWays(int n) {
int f[105];
f[0] = 1,f[1] = 1,f[2] = 2;
if( n <= 2 ) return f[n];
for(int i = 3;i<=n;i++){
f[i] = (f[i-1] + f[i-2])%1000000007;
}
return f[n];
}
};
2.记忆化搜索
剑指 Offer 63. 股票的最大利润
121. 买卖股票的最佳时机
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
C语言
//执行用时: **4 ms**
//内存消耗: **6.8 MB**
int maxProfit(int* prices, int pricesSize){
if( pricesSize == 0 || pricesSize == 1 ) return 0;
int min=prices[0],diff=prices[1]-prices[0];
for(int i=1;i<pricesSize;i++){
diff=diff>prices[i]-min?diff:prices[i]-min;
min=min<prices[i]?min:prices[i];
}
if( diff < 0 ){
return 0;
}
return diff;
}
C++——
1.用后面减前面的差来解决这个卖出价格需要大于买入价格
2.注意size为0和1的情况
//执行用时: **8 ms**
//内存消耗: **12.4 MB**
//动态规划
class Solution {
public:
int maxProfit(vector<int>& prices) {
if( prices.size() == 0 || prices.size() == 1 ){
return 0;
}
int amin=prices[0],diff = prices[1]-prices[0];
for(int i=1;i<prices.size();i++){
amin = min(amin,prices[i]);
diff = max(diff,prices[i]-amin);
}
return diff;
}
};
64. 最小路径和
给定一个包含非负整数的 _m_ x _n_ 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 200
- 0 <= grid[i][j] <= 100
//执行用时: **12 ms**
//内存消耗: **9.6 MB**
//动态规划(数字三角形模型)
//f[i][j]表示的到i,j的数字最小值
class Solution {
public:
const int INF = 1e9;
int f[210][210] = {0};
int minPathSum(vector<vector<int>>& grid) {
int n = grid.size(),m = grid[0].size();
f[1][1] = grid[0][0];
for(int i = 1; i <= n ; i++ ){
for(int j = 1 ; j <= m ;j++ ){
if( i == 1 && j == 1 ){
f[1][1] = grid[0][0];
}else{
f[i][j] = INF;
if( i > 1 ){
f[i][j] = min(f[i][j],f[i-1][j]+grid[i-1][j-1]);
}
if( j > 1 ){
f[i][j] = min(f[i][j],f[i][j-1]+grid[i-1][j-1]);
}
}
}
}
return f[n][m];
}
};
//执行用时:4 ms, 在所有 C++ 提交中击败了98.24% 的用户
//内存消耗:9.7 MB, 在所有 C++ 提交中击败了60.73% 的用户
class Solution {
public:
//状态转移方程 f[i][j] = max(f[i-1][j],f[i][j-1]) + grid[i][j]
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size(),n=grid[0].size();
vector<vector<int>> f = vector<vector<int>>(m+2,vector<int>(n+2,0));
f[0][0] = grid[0][0];
for(int i = 1 ; i < m ; i ++ ) f[i][0] = f[i-1][0] + grid[i][0];
for(int j = 1 ; j < n ; j ++ ) f[0][j] = f[0][j-1] + grid[0][j];
for( int i = 1 ; i < m ; i ++ ){
for( int j = 1 ; j < n ; j++ ){
f[i][j] = min(f[i-1][j],f[i][j-1]) + grid[i][j];
}
}
return f[m-1][n-1];
}
};
3.acwing
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int n = grid.size();
if (!n) return 0;
int m = grid[0].size();
vector<vector<int>> f(n, vector<int>(m, INT_MAX));
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ ) {
if (!i && !j) f[i][j] = grid[i][j];
else {
if (i) f[i][j] = min(f[i][j], f[i - 1][j] + grid[i][j]);
if (j) f[i][j] = min(f[i][j], f[i][j - 1] + grid[i][j]);
}
}
return f[n - 1][m - 1];
}
};
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
- 0 <= nums.length <= 100
- 0 <= nums[i] <= 400
一
//执行用时: **0 ms**
//内存消耗: **5.8 MB**
/*
int rob(int* nums, int numsSize){
int ppre=0,pre=0,now=0;
for(int i=1;i<=numsSize;i++){
ppre=pre;
pre=now;
now=pre>ppre+nums[i-1]?pre:ppre+nums[i-1];
}
return now;
}
*/
#define max(A,B) (A>B?A:B)
int rob(int* nums, int numsSize){
int pre=0,now=0,temp;
for(int i=1;i<=numsSize;i++){
temp=now;
now=max(pre+nums[i-1],now);
pre=temp; //更新
}
return now;
}
二.
//执行用时: **4 ms**
//内存消耗: **5.9 MB**
int rob(int* nums, int numsSize){
int ppre=0,pre=0,now=0;
for(int i=1;i<=numsSize;i++){
ppre=pre;
pre=now;
now=pre>ppre+nums[i-1]?pre:ppre+nums[i-1];
}
return now;
}
三.总结:
f[i]表示是否包含自己:一种包含自己,一种包含自己
注意最后返回的时候比较两个末尾数的值
//执行用时: **4 ms**
//内存消耗: **7.4 MB**
class Solution {
public:
int rob(vector<int>& nums) {
if( nums.size() == 0 ) return 0;
else if( nums.size() == 1 ) return nums[0];
else if( nums.size() == 2 ) return max(nums[0],nums[1]);
int f[105] = {0}; //表示是否包含f[i]的最大值
f[0] = nums[0];
f[1] = max(nums[0],nums[1]);
for(int i = 2 ; i < nums.size() ; i++ ){
f[i] = max(f[i-1],f[i-2] + nums[i]);
}
return max(f[nums.size()-1],f[nums.size()-2]);
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [0]
输出:0
一.
/状态方程:max(n[i]+nums[i+1],n[i]+nums[i+2])
//https://leetcode-cn.com/problems/house-robber-ii/solution/213-da-jia-jie-she-ii-zhu-yao-shi-yao-neng-xiang-d/
//https://leetcode-cn.com/problems/house-robber-ii/solution/da-jia-jie-she-2-fen-lei-dp-by-dadaluoyu/
//关键:分成集合:含第一个和含最后一个的
#define max(A,B) (A>B?A:B)
int rob(int* nums, int numsSize){
if( numsSize == 1 ) return nums[0];
//含第一个不含最后一个
int pre1=0,now1=0,temp;
for(int i=1;i<=numsSize-1;i++){
temp=now1;
now1=max(pre1+nums[i-1],now1);
pre1=temp; //更新
}
int pre2=0,now2=0;
for(int i=2;i<=numsSize;i++){
temp=now2;
now2=max(pre2+nums[i-1],now2);
pre2=temp; //更新
}
return max(now1,now2);
}
二.C++ 思路是分为两种:偷窃第一个没有最后一个,没偷窃第一个有最后一个
总结:1.vector容器清除头部nums.erase(nums.begin());
//执行用时: **0 ms**
//内存消耗: **7.6 MB**
class Solution {
public:
int rob1(vector<int>& nums){
if( nums.size() == 0 ) return 0;
else if( nums.size() == 1 ) return nums[0];
else if( nums.size() == 2 ) return max(nums[0],nums[1]);
int f[105] = {0}; //表示是否包含f[i]的最大值
f[0] = nums[0];
f[1] = max(nums[0],nums[1]);
for(int i = 2 ; i < nums.size() ; i++ ){
f[i] = max(f[i-1],f[i-2] + nums[i]);
}
return max(f[nums.size()-1],f[nums.size()-2]);
}
int rob(vector<int>& nums) {
if( nums.size() == 0 ) return 0;
else if( nums.size() == 1 ) return nums[0];
else if( nums.size() == 2 ) return max(nums[0],nums[1]);
//偷窃第一个,没有尾部
int tail = nums.back();
nums.pop_back();
int res = rob1(nums);
//偷窃第二个,有尾部
nums.push_back(tail);
nums.erase(nums.begin());
return max(res,rob1(nums));
}
};
y总:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size(), INF = 1e8;
if (n == 1) return nums[0];
vector<vector<int>> f(n, vector<int>(2));
f[0][0] = -INF, f[0][1] = nums[0];
for (int i = 1; i < n; i ++ ) {
f[i][0] = max(f[i - 1][0], f[i - 1][1]);
f[i][1] = f[i - 1][0] + nums[i];
}
int res = f[n - 1][0];
f[0][0] = 0, f[0][1] = -INF;
for (int i = 1; i < n; i ++ ) {
f[i][0] = max(f[i - 1][0], f[i - 1][1]);
f[i][1] = f[i - 1][0] + nums[i];
}
return max(res, max(f[n - 1][0], f[n - 1][1]));
}
};
122. 买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
- 1 <= prices.length <= 3 * 10 ^ 4
- 0 <= prices[i] <= 10 ^ 4
一.
class Solution {
public:
int maxProfit(vector<int>& prices) {
if( prices.size() < 1 ) return 0;
int ans = 0 ;
for(int i = 1 ; i < prices.size() ; i++ ){
ans += max(0,prices[i]-prices[i-1]);
}
return ans;
}
};
二.y总
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int p[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &p[i]);
int res = 0;
for (int i = 0; i < n - 1; i ++ )
res += max(0, p[i + 1] - p[i]);
printf("%d\n", res);
return 0;
}
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 1000
- nums 中的所有元素 互不相同
- 1 <= target <= 1000
进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件
总结:vector<unsigned> f(m + 1);
//动态规划问题
class Solution {
public:
int combinationSum4(vector<int>& nums, int m) {
vector<unsigned> f(m + 1);
f[0] = 1;
for (int i = 1; i <= m; i ++ )
for (auto j: nums)
if (i >= j)
f[i] += f[i - j];
return f[m];
}
};
剑指 Offer 47. 礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[ [1,3,1], [1,5,1], [4,2,1] ]
输出:12解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:
- 0 < grid.length <= 200
- 0 < grid[0].length <= 200
1.动态规划
注意要初始化第一排和第一列,且注意是从左边开始拿的,所以得加上
//执行用时: **12 ms**
//消耗: **9 MB**
//动态规划
//状态转移方程
// f[i][j] = max(f[i-1][j],f[i][j-1])+grid[i][j];
// f[0][i] = grid[0][i] + f[0][i-1];
// f[i][0] = grid[i][0] + f[i-1][0];
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int f[205][205];
f[0][0] = grid[0][0];
int m = grid.size();
int n = grid[0].size();
for(int i = 1 ; i < m ; i++ ){ //初始化第一排
f[i][0] = grid[i][0] + f[i-1][0];
}
for(int i = 1 ; i < n ; i++ ){ //初始化第一列
f[0][i] = grid[0][i] + f[0][i-1];
}
for(int i = 1 ; i < m ; i++ ){
for(int j = 1 ; j < n ; j++ ){
f[i][j] = max(f[i-1][j],f[i][j-1])+grid[i][j];
}
}
return f[m-1][n-1];
}
};
2.优化
由1的状态转移方程可得f[i][j]只与f[i-1][…]有关,与f[i-2][…]无关,所以不用存储。
可以换成一维数组。
当我们在计算位置 (i,j)时,f[j+1]到 f[i]已经是第 i 行的值,而 f[0] 到 f[j] 仍然是第 i−1行的值。此时我们直接通过
f[j]=min(f[j-1],f[j])+c[i][j]
空间复杂度仍然为 O(n),但我们只使用了 n 的空间存储状态,减少了一半的空间消耗。
120. 三角形最小路径和
剑指 Offer II 100. 三角形中最小路径之和
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
提示:
- 1 <= triangle.length <= 200
- triangle[0].length == 1
- triangle[i].length == triangle[i - 1].length + 1
- -104 <= triangle[i][j] <= 104
进阶:
- 你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?
注意边界的处理
//动态规划
class Solution {
public:
int f[205][205] = {0};
int minimumTotal(vector<vector<int>>& triangle) {
f[1][1] = triangle[0][0];
for(int i = 2 ; i <= triangle.size() ; i++ ){
f[i][1] = f[i-1][1] + triangle[i-1][0];
for( int j = 2 ; j < i ; j++ ){
f[i][j] = min(f[i-1][j-1],f[i-1][j])+triangle[i-1][j-1];
}
f[i][i] = f[i-1][i-1] + triangle[i-1][i-1];
}
int res = 10e5;
for(int i = 1 ; i <= triangle.size() ; i++ ){ //看最后一行的结果
res = min(res,f[triangle.size()][i]);
}
return res;
}
};
931. 下降路径最小和
给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
示例 1:
输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:下面是两条和最小的下降路径,用加粗标注:
[[2,1,3], [[2,1,3],
[6,5,4], [6,5,4],
[7,8,9]] [7,8,9]]
示例 2:
输入:matrix = [[-19,57],[-40,-5]]
输出:-59
解释:下面是一条和最小的下降路径,用加粗标注:
[[-19,57],
[-40,-5]]
示例 3:
输入:matrix = [[-48]]
输出:-48
提示:
- n == matrix.length
- n == matrix[i].length
- 1 <= n <= 100
- -100 <= matrix[i][j] <= 100
//动态规划
class Solution {
public:
int f[101][101] = {0};
int minFallingPathSum(vector<vector<int>>& matrix) {
int n = matrix.size();
for(int i = 0 ; i < n ; i ++ ) f[0][i] = matrix[0][i];
for(int i = 1 ; i < n ; i ++ ){
f[i][0] = min(f[i-1][0],f[i-1][1]) + matrix[i][0];
for(int j = 1; j < n ; j++ ){
f[i][j] = min({f[i-1][j-1],f[i-1][j],f[i-1][j+1]}) + matrix[i][j];
}
f[i][n-1] = min(f[i-1][n-1],f[i-1][n-2]) + matrix[i][n-1];
}
int res = 10e5;
for( int i = 0 ; i < n ; i++ ){
res = min(res,f[n-1][i]);
}
return res;
}
};
44. 通配符匹配
给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
- 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 = "ab"
输出: true
解释: 第一个 '' 可以匹配空字符串, 第二个 '' 可以匹配字符串 "dce".
示例 5:
输入:
s = "acdcb"
p = "a*c?b"
输出: false
//执行用时:88 ms, 在所有 C++ 提交中击败了23.19% 的用户
//内存消耗:11.1 MB, 在所有 C++ 提交中击败了57.23% 的用户
class Solution {
public:
bool isMatch(string s, string p) {
//定义状态:dp[i][j] 表示字符串 p 的前 i 位与字符串 s 的前 j 位是否匹配。
/*
状态转移:
考虑三种情况:
p[i] 与 s[j] 相等:取决于 dp[i-1][j-1],如果字符串 p 前 i-1 位与字符串 s 的前 j 位匹配,则字符串 p 的前 i 位与字符串 s 的前 j 位也会匹配。状态转移方程为:
dp[i][j] = dp[i-1][j-1]。
p[i] 为 '?':p[i] 能匹配任意一个 s[j]。与上面的情况类似,p[i] 与 s[j] 已经匹配,如果字符串 p 的前 i-1 位与字符串 s 的前 j-1 位匹配,则字符串 p 的前 i 位与字符串 s 的前 j 位也会匹配。状态转移方程:
dp[i][j] = dp[i-1][j-1]。
p[i] 为 '*':p[i] 能匹配任何字符串。分为三种情况:
字符串 p 的前 i-1 位与字符串 s 的前 j-1 位匹配,则 * 匹配 s[j] 这一位,也就是:
dp[i][j] = dp[i-1][j-1]。
字符串 p 的前 i-1 位与字符串 s 的前 j 位匹配,则 * 匹配空字符串 '',也就是:
dp[i][j] = dp[i-1][j]。
最后一种是 * 匹配 s 的多个字符,这种可以根据前面的状态推过来。如果 dp[i][j-1] 为 true,也就是 p 的前 i 位可以匹配 s 的前 j-1 位,因为这里 p[i] 为 *,我可以让 * 再多匹配一位,即 p 的前 i 位匹配 s 的前 j 位,所以状态转移为:
dp[i][j] = dp[i][j-1]。
*/
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<vector<bool>> f(n+1,vector<bool>(m+1));
f[0][0] = true;
for(int i = 0 ; i <= n ; i ++ ){
for( int j = 1 ; j <= m ; j++ ){
if( p[j] == '*' ){ //如果是*号
f[i][j] = f[i][j-1] || i && f[i-1][j];
}else{
f[i][j] = (s[i]==p[j] || p[j]=='?') && i && f[i-1][j-1];
}
}
}
return f[n][m];
}
};
62. 不同路径
剑指 Offer II 098. 路径的数目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
- 1 <= m, n <= 100
- 题目数据保证答案小于等于 2 * 109
注意:本题与主站 62 题相同: . - 力扣(LeetCode)
1.数学知识
//执行用时: **0 ms**
//内存消耗: **5.8 MB**
class Solution {
public:
int uniquePaths(int m, int n) {
//数学知识(我们需要移动 m+n−2m+n-2m+n−2 次,其中有 m−1次向下移动,n−1次向右移动)
long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return (int) ans;
}
};
2.深度优先搜索
使用dfs+记忆化搜索来解决超时问题
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
//内存消耗:6.4 MB, 在所有 C++ 提交中击败了32.96% 的用户
class Solution {
public:
int m,n;
int res = 0;
vector<vector<int>> visit;
int dfs(int d,int r){
if( visit[d][r] != -1 ) return visit[d][r];
if( d > m ) return 0;
if( r > n ) return 0;
if( d == m && r == n ){
//res++;
return 1;
}
int len = dfs(d+1,r) + dfs(d,r+1);
visit[d][r] = len;
return len;
}
int uniquePaths(int _m, int _n) {
m = _m;
n = _n;
//注意这里+2
visit = vector<vector<int>>(m+2,vector<int>(n+2,-1));
return dfs(1,1);;
}
};
超时的代码
class Solution {
public:
int m,n;
int res = 0;
void dfs(int d,int r){
if( d > m ) return;
if( r > n ) return;
if( d == m && r == n ){
res++;
return;
}
dfs(d+1,r);
dfs(d,r+1);
}
int uniquePaths(int _m, int _n) {
m = _m;
n = _n;
dfs(1,1);
return res;
}
};
3.动态规划
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
//内存消耗:6.5 MB, 在所有 C++ 提交中击败了5.10% 的用户
class Solution {
public:
int m,n;
int res = 0;
//状态转移方程 f[i][j] = f[i-1][j] + f[i][j-1]
vector<vector<int>> f;
int uniquePaths(int _m, int _n) {
m = _m;
n = _n;
f = vector<vector<int>>(m+2,vector<int>(n+2,0));
//注意写=
for(int i = 1 ; i <= m ; i++) f[i][1] = 1;
for(int i = 1 ; i <= n ; i++) f[1][i] = 1;
for(int i = 2 ; i <= m ; i ++ ){
for(int j = 2 ; j <= n ; j++ ){
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
return f[m][n];
}
};
4.acwing动态规划
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> f(n, vector<int>(m));
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (!i && !j) f[i][j] = 1;
else {
if (i) f[i][j] += f[i - 1][j];
if (j) f[i][j] += f[i][j - 1];
}
return f[n - 1][m - 1];
}
};
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
- m == obstacleGrid.length
- n == obstacleGrid[i].length
- 1 <= m, n <= 100
- obstacleGrid[i][j] 为 0 或 1
//执行用时:4 ms, 在所有 C++ 提交中击败了55.80% 的用户
//内存消耗:7.8 MB, 在所有 C++ 提交中击败了5.55% 的用户
class Solution {
public:
int m,n;
//状态转移方程 f[i][j] = f[i-1][j] + f[i][j-1]
vector<vector<int>> f;
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
m = obstacleGrid.size();
n = obstacleGrid[0].size();
f = vector<vector<int>>(m+1,vector<int>(n+1,0));
if( obstacleGrid[0][0] != 1 ) f[0][0] = 1;
else f[0][0] = 0;
for(int i = 1 ; i < m ; i++)
if( obstacleGrid[i][0] != 1 ) f[i][0] = f[i-1][0];
for(int i = 1 ; i < n ; i++)
if( obstacleGrid[0][i] != 1 ) f[0][i] = f[0][i-1];
for(int i = 1 ; i < m ; i ++ ){
for(int j = 1 ; j < n ; j++ ){
if( obstacleGrid[i][j] != 1 )
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
return f[m-1][n-1];
}
};
2.acwing
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& o) {
int n = o.size();
if (!n) return 0;
int m = o[0].size();
vector<vector<int>> f(n, vector<int>(m));
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (!o[i][j]) {
if (!i && !j) f[i][j] = 1;
else {
if (i) f[i][j] += f[i - 1][j];
if (j) f[i][j] += f[i][j - 1];
}
}
return f[n - 1][m - 1];
}
};
646. 最长数对链
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例:
输入:[[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4]
提示:
- 给出数对的个数在 [1, 1000] 范围内。
超时代码
class Solution {
public:
int res = 0;
vector<vector<int>> tmp;
void dfs(int u,vector<vector<int>>& pairs){
if( u == pairs.size() ){
res = res > tmp.size() ? res : tmp.size();
return;
}
auto a = tmp.back();
//插入
if( a[1] < pairs[u][0] ){
tmp.push_back(pairs[u]);
dfs(u+1,pairs);
tmp.pop_back();
}
dfs(u+1,pairs);
}
int findLongestChain(vector<vector<int>>& pairs) {
dfs(0,pairs);
return res;
}
};
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
- 1 <= n <= 104
//执行用时:160 ms, 在所有 C++ 提交中击败了65.34% 的用户
//内存消耗:8.8 MB, 在所有 C++ 提交中击败了60.44% 的用户
class Solution {
public:
//f[n] = f[n-i^2] + 1 代表最少由几个平方数组成
int numSquares(int n) {
vector<int> dp = vector<int>(n+1);
for (int i = 1; i <= n; i++) {
dp[i] = i; // 最坏的情况就是每次+1
for (int j = 1; i - j * j >= 0; j++) {
dp[i] = min(dp[i], dp[i - j * j] + 1); // 动态转移方程
}
}
return dp[n];
}
};
300. 最长递增子序列
给你一个整数数组 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 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- 1 <= nums.length <= 2500
- -104 <= nums[i] <= 104
1.
//f[i]表示从第一个数字开始算,以nums[i]结尾的最大上升子序列
//f[i]=max(f[i],f[j]+1) w[i]>w[j]
#define max(A,B) ((A)>(B)?(A):(B))
int lengthOfLIS(int* nums, int numsSize){
int f[numsSize],mx=1;
for(int i=0;i<numsSize;i++){
f[i]=1; //f[i]默认为1,找不到前面数字小于自己的时候,只有自己就为1,从头开始找
for(int j=0;j<i;j++){
if( nums[i] > nums[j] ){ //前面有数字小于自身
f[i]=max(f[i],f[j]+1);//和之前找到相比,找到最大的长度
}
}
mx=max(mx,f[i]);
}
return mx;
}
//执行用时:264 ms, 在所有 C++ 提交中击败了50.69% 的用户
//内存消耗:10.2 MB, 在所有 C++ 提交中击败了68.63% 的用户
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
//f[n] 表示最大长度
//递推方程 f[i] = max(f[i],f[k]) k = 0 …… i-1 ( a[k] < a[i] )
vector<int> f = vector<int>(n+2);
0for( int i = 0 ; i < n ; i ++ ){
f[i] = 1;
for( int k = 0 ; k < i ; k++ ){
if( nums[k] < nums[i] ){
f[i] = max(f[i],f[k]+1 );
}
}
}
return *max_element(f.begin(),f.end());
}
};
673. 最长递增子序列的个数
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
提示:
- 1 <= nums.length <= 2000
- -106 <= nums[i] <= 106
//执行用时:152 ms, 在所有 C++ 提交中击败了19.09% 的用户
//内存消耗:12.8 MB, 在所有 C++ 提交中击败了87.07% 的用户
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
//f[n] 第一个表示长度 第二个表示可以由几个得到
//递推方程 f[i] = max(f[i],f[k]) k = 0 …… i-1 ( a[k] < a[i] )
if( n == 1 ) return 1;
vector<int> f = vector<int>(n+2,1);
vector<int> gs = vector<int>(n+2,1);
int amax = 0;
for( int i = 0 ; i < n ; i ++ ){
f[i] = 1;
for( int k = 0 ; k < i ; k++ ){
if( nums[k] < nums[i] ){
//第一次找到
if( f[i] < f[k]+1 ){
f[i] = f[k]+1;
gs[i] = gs[k];
}else if( f[i] == f[k]+1 ){ //再次找到
gs[i] += gs[k];
}
}
amax = max(amax,f[i]);
}
}
// 最后的返回值应该是所有最大长度的所有count的总和
int res = 0;
for(int i=0; i<n; i++) {
if(f[i] == amax){
res += gs[i];
}
}
return res;
}
};
超时 pair
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
//f[n] 第一个表示长度 第二个表示可以由几个得到
//递推方程 f[i] = max(f[i],f[k]) k = 0 …… i-1 ( a[k] < a[i] )
if( n == 1 ) return 1;
vector<pair<int,int>> f = vector<pair<int,int>>(n+2,{1,1});
int amax = 0;
for( int i = 0 ; i < n ; i ++ ){
f[i].first = 1;
for( int k = 0 ; k < i ; k++ ){
cout << f[i].first << " " << f[i].second << endl;
if( nums[k] < nums[i] ){
//f[i].first = max(f[i].first,f[k].first+1 );
//第一次找到
if( f[i].first < f[k].first+1 ){
f[i].first = f[k].first+1;
f[i].second = f[k].second;
}else if( f[i].first == f[k].first+1 ){ //再次找到
f[i].second += f[k].second;
}
}
amax = max(amax,f[i].first);
}
}
// 最后的返回值应该是所有最大长度的所有count的总和
int res = 0;
for(int i=0; i<n; i++) {
if(f[i].first == amax){
res += f[i].second;
}
}
return res;
}
};
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 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 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
- 1 <= nums.length <= 104
- -109 <= nums[i] <= 109
//执行用时:8 ms, 在所有 C++ 提交中击败了76.99% 的用户
//内存消耗:10.9 MB, 在所有 C++ 提交中击败了40.25% 的用户
class Solution {
public:
//递推方程式:f[i] = max(f[i-1]+1,1);
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
if( n == 1 ) return 1;
vector<int> f = vector<int>(n+1,1);
for( int i = 1 ; i < n; i ++ ){\
if( nums[i-1] < nums[i] ){
f[i] = f[i-1] + 1;
}
}
return *max_element(f.begin(),f.end());
}
};
518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
提示:
- 1 <= coins.length <= 300
- 1 <= coins[i] <= 5000
- coins 中的所有值 互不相同
- 0 <= amount <= 5000
//执行用时:648 ms, 在所有 C++ 提交中击败了5.00% 的用户
//内存消耗:18.9 MB, 在所有 C++ 提交中击败了5.24% 的用户
class Solution {
public:
int change(int amount, vector<int>& coins) {
//完全背包问题
//f[i][j] = max(f[i][j],f[i][j-k*v[i]] + w[i]*k )
//这里i->n j -> amount
int n = coins.size();
vector<vector<int>> f(n+5,vector<int>(amount+5,0));
f[0][0] = 1; //初始化条件:f[0][0] = 1`,使用`0`种硬币币,凑`0`元钱,也是一种方案。
for(int i = 1 ; i <= n; i ++ ){
for(int j = 0 ; j <= amount ; j++ ){
for( int k = 0 ; k*coins[i-1] <= j ; k ++ ){
f[i][j] += f[i-1][j-k*coins[i-1]] ;
}
}
}
return f[n][amount];
}
};
优化
class Solution {
public:
int change(int amount, vector<int>& coins) {
//完全背包问题
//f[i][j] = max(f[i][j],f[i][j-k*v[i]] + w[i]*k )
//这里i->n j -> amount
//优化成一维
int n = coins.size();
vector<int> f(amount+5,0);
f[0] = 1;
for(int i = 1 ; i <= n; i ++ ){
int v = coins[i-1];
for(int j = 0 ; j <= amount ; j++ ){
if( j >= v )
f[j] += f[j-v] ;
}
}
return f[amount];
}
};
//执行用时:4 ms, 在所有 C++ 提交中击败了99.41% 的用户
//内存消耗:6.8 MB, 在所有 C++ 提交中击败了95.60% 的用户
class Solution {
public:
int change(int amount, vector<int>& coins) {
//完全背包问题
//f[i][j] = max(f[i][j],f[i][j-k*v[i]] + w[i]*k )
//这里i->n j -> amount
//优化成一维
int n = coins.size();
vector<int> f(amount+5,0);
f[0] = 1;
for(int i = 1 ; i <= n; i ++ ){
int v = coins[i-1];
for(int j = v ; j <= amount ; j++ ){
f[j] += f[j-v] ;
}
}
return f[amount];
}
};
343. 整数拆分
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
- 2 <= n <= 58
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
//内存消耗:6.1 MB, 在所有 C++ 提交中击败了43.49% 的用户
class Solution {
public:
//f[n] //表示分割成的最大乘积
//f[i] = max(f[i-k]*k,f[i])
int integerBreak(int n) {
vector<int> f = vector<int>(n+2,0);
f[1] = f[0] = 1;
//0 不是正整数,1 是最小的正整数,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0
for( int i = 1 ; i <= n ; i ++ ){
for(int k = 1 ; k < i ; k ++ ){
//将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j×(i−j)
//将 i 拆分成 j和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j]
f[i] = max({f[i],f[i-k]*k,(i-k)*k});
}
}
return f[n];
}
};
887. 鸡蛋掉落
难度困难844收藏分享切换为英文接收动态反馈
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 1:
输入:k = 1, n = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。
如果它没碎,那么肯定能得出 f = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
示例 2:
输入:k = 2, n = 6
输出:3
示例 3:
输入:k = 3, n = 14
输出:4
提示:
- 1 <= k <= 100
- 1 <= n <= 104
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
//内存消耗:5.8 MB, 在所有 C++ 提交中击败了95.80%的用户
int f[10010][110];
class Solution {
public:
//f(i,j) 表示使用 i 次移动, j 个鸡蛋 ,最多可以检查的楼层高度是多少
//f(n,k)
//初始状态 f(1,0)=0,f(1,j)=1
//假设 n1=f(i−1,j−1),n2=f(i−1,j),我们在第 i 次移动时测试第 n1+1 层,即 x 层
//如果测试时鸡蛋碎掉了,则我们可以通过 i−1 次移动和 j−1 个鸡蛋来找到最高不会碎掉楼层,因为楼层不会超过 n1 了;如果鸡蛋没有碎掉,则在此基础上,我们可以使用 i−1 次移动和 j 个鸡蛋,再继续向上检查 n2 层,故只要是答案在 [0,n1+n2+1] 范围内,都可以通过 i 步和 j 个鸡蛋来找到。
//f(i,j)=f(i−1,j−1)+f(i−1,j)+1
int superEggDrop(int k, int n) {
for( int i = 1 ; i <= n ; i++ ){
for( int j = 1 ; j <= k ; j++ ){
f[i][j] = f[i-1][j-1] + 1 + f[i-1][j];
}
if( f[i][k] >= n ) return i;
}
return -1;
}
};
1884. 鸡蛋掉落-两枚鸡蛋
难度中等55收藏分享切换为英文接收动态反馈
给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。
每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 1:
输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。
示例 2:
输入:n = 100
输出:14
解释:
一种最优的策略是:
- 将第一枚鸡蛋从 9 楼扔下。如果碎了,那么 f 在 0 和 8 之间。将第二枚从 1 楼扔下,然后每扔一次上一层楼,在 8 次内找到 f 。总操作次数 = 1 + 8 = 9 。
- 如果第一枚鸡蛋没有碎,那么再把第一枚鸡蛋从 22 层扔下。如果碎了,那么 f 在 9 和 21 之间。将第二枚鸡蛋从 10 楼扔下,然后每扔一次上一层楼,在 12 次内找到 f 。总操作次数 = 2 + 12 = 14 。
- 如果第一枚鸡蛋没有再次碎掉,则按照类似的方法从 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99 和 100 楼分别扔下第一枚鸡蛋。
不管结果如何,最多需要扔 14 次来确定 f 。
提示:
- 1 <= n <= 1000
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
//内存消耗:5.7 MB, 在所有 C++ 提交中击败了98.66%的用户
int f[1010][3];
class Solution {
public:
//f[i,j] 表示的 j 个 鸡蛋 i 次移动 的 最多 检查楼层
//f[n,2]
//n1 = f(i-1,j-1) x n2 = f(i-1,j) //x下面是n1,上面是n2
// x = n1 + 1
//如果 在 x 下面 摔坏 了 ,则 说明 是 0 ~ n1 f(i-1,j-1)
//如果 没有摔坏 , 则说明 是 在 x + 0 ~ x + n2
//检查的楼层 为 x + n2 = n1 + 1 + n2 = f[i-1][j-1] + 1 + f[i-1][j]
int twoEggDrop(int n) {
for(int i = 1 ; i <= n ; i ++ ){
for( int j = 1 ; j <= 2 ; j ++ ){
f[i][j] = f[i-1][j-1] + 1 + f[i-1][j];
}
if( f[i][2] >= n ) return i;
}
return -1;
}
};
55. 跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
- 1 <= nums.length <= 3 * 104
- 0 <= nums[i] <= 105
//执行用时:48 ms, 在所有 C++ 提交中击败了74.79% 的用户
//内存消耗:47.2 MB, 在所有 C++ 提交中击败了33.82% 的用户
class Solution {
public:
bool canJump(vector<int>& nums) {
for( int i = 0 , j = 0 ; i < nums.size() ; i ++ ){
if( j < i ) return false;
j = max( j , i + nums[i] );
}
return true;
}
};
45. 跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
- 1 <= nums.length <= 104
- 0 <= nums[i] <= 1000
//执行用时:20 ms, 在所有 C++ 提交中击败了26.74% 的用户
//内存消耗:16.7 MB, 在所有 C++ 提交中击败了17.37% 的用户
class Solution {
public:
//f(i) 表示到达 i 所需要的最少步数
//定义辅助指针 j 为第一次到达 i 时上一步的位置,j 从 0 开始
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> f(n);
//因为一次移动,可以从 x 到 [x+1,x+nums(x)] 的任意位置
for( int i = 1 , j = 0 ; i <n ; i ++ ){
// 依次求 f[i] 的值。
while( j + nums[j] < i ) j++; // 根据 i 来更新 j。
f[i] = f[j] + 1; // 根据 f[j] 更新 f[i]。
}
return f[n-1];
}
};
96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
- 1 <= n <= 19
//执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
//内存消耗:5.9 MB, 在所有 C++ 提交中击败了39.45% 的用户
class Solution {
public:
//动态规划
/*
G(n):长度为n的序列能构成的不同二叉搜索树的个数
F(i,n):以i为根、序列长度为n的不同二叉搜索树个数
1 2 3 …… i-1 i i+1 …… n
左子序列G(i-1) 以i为根节点 右子序列G(n-i)
F(i,n) = G(i-1)*G(n-i)
不同的二叉搜索树的总数 G(n),是对遍历所有 i 的 F(i,n)之和,即G(n) = ∑F(i,n)
递归为G(n) = ∑G(i-1)*G(n-i)
对于边界情况,当序列长度为 1(只有根)或为 0(空树)时,只有一种情况
*/
int numTrees(int n) {
vector<int> G(n+1,0);
G[0] = 1;
G[1] = 1;
for(int i = 2 ; i <= n ; i++ ){
for( int j = 1 ; j <= i ; j++ ){
G[i] += G[j-1] * G[i-j];
}
}
return G[n];
}
};
2.acwing
class Solution {
public:
int numTrees(int n) {
vector<int> f(n + 1);
f[0] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
f[i] += f[j - 1] * f[i - j];
return f[n];
}
};
3.卡特 数
~动态规划
更多历史精彩文章(篇幅过多,不一一列出):
(简历相关)
求职经验分享(1):一份合格的简历应该如何写?-CSDN博客(推荐)
求职经验分享(2):简历如何优化以及如何应对面试【后端篇】-CSDN博客
(项目亮点相关)
大厂面试官赞不绝口的后端技术亮点【后端项目亮点合集(1):Redis篇】-CSDN博客
大厂面试官赞不绝口的后端技术亮点【后端项目亮点合集(2)】-CSDN博客
(八股文)
大厂面试官问我:Redis处理点赞,如果瞬时涌入大量用户点赞(千万级),应当如何进行处理?【后端八股文一:Redis点赞八股文合集】_java中redis如何实现点赞-CSDN博客大厂面试官问我:布隆过滤器有不能扩容和删除的缺陷,有没有可以替代的数据结构呢?【后端八股文二:布隆过滤器八股文合集】_布隆过滤器不能扩容-CSDN博客
………
感兴趣的小伙伴可以给个三连~