一、爬楼梯
1.套路
1.本质是爬楼梯问题:每次可以爬多种高度的楼梯,相互独立,最终从0层爬到target层,求不同方案个数 (调换一个方案中的两次楼梯就视作另一种方案)
2.代码:
typedef long long ll;
vector<ll> f; // 从0爬到i的不同方案个数
// 假设vector<int> nums记录可以爬的高度
f[0]=1; // 赋初值
for(int i=1;i<=target;++i){ // 递推
for(int& x:nums){
// 从i-x爬x到i
if(i-x>=0) f[i]+=f[i-x];
}
}
return f[target]; // 最终答案
2.题目描述
3.学习经验
1. 70.爬楼梯(简单,学习)
思想
1.假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
2.启发思考:寻找子问题
题目要求从0爬到n的方法数量(原问题),而n可以由n-1或n-2爬到,所以转化为求从0爬到n-2(子问题1)和从0爬到n-1(子问题2)的方法数量,这两种情况都把原问题转化为规模更小的子问题,所以可以用递归解决 。
注 :从大往小思考,主要是为了方便把递归翻译成递推。
3.递归写法 :
(1)状态定义 :dfs(i)表示从0爬到i的方法数量
(2)转态转移方程 :由2分析可知,dfs(i)=dfs(i-1)+dfs(i-2)(递归得到)
(3)状态初值 (递归终止条件):dfs(0)=dfs(1)=1(因为dfs(2)=dfs(0)+dfs(1)=2,所以逆向推出dfs(0)=1)
4.递归+记录返回值=记忆化搜索
对于3,dfs(i-2)会被dfs(i)和dfs(i-1)都调用,产生重叠子问题,所以拿一个数组记录dfs(i),若没调用过(数组值为初值),则调用并更新数组,否则在调用前返回数组值
5.1:1翻译成递推
把3/4的递归的递去掉,只保留归 ,则从自顶向下变成自底向上,状态定义和状态转移方程一致,但从递归终止条件(转态初值)向上归,f[i]=f[i-1]+f[i-2],答案为f[n](递归入口,归结束)
6.空间优化
根据f[i]=f[i-1]+f[i-2]得知,得到f[i]后,f[i-2]及其之前值不会再用,所以只要知道前一个值和前前一个值就能得到当前值(需判断开始情况),辗转更新
代码
3.递归写法:
class Solution {
public:
int dfs(int n) {
if (n <= 1)
return 1;
return dfs(n - 1) + dfs(n - 2);
}
int climbStairs(int n) { return dfs(n); }
};
4.递归+记录返回值=记忆化搜索
class Solution {
public:
vector<int> memory;
int dfs(int n) {
if (n <= 1)
return 1;
if (memory[n] != -1)
return memory[n];
return memory[n] = dfs(n - 1) + dfs(n - 2);
}
int climbStairs(int n) {
memory.resize(n + 1, -1); //[0,n]
return dfs(n);
}
};
5.1:1翻译成递推
class Solution {
public:
vector<int> f;
int climbStairs(int n) {
f.resize(n + 1); //[0,n]
f[0] = f[1] = 1;
for (int i = 2; i <= n; ++i) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
};
6.空间优化
class Solution {
public:
int climbStairs(int n) {
if(n<=1) return 1;
int preVal = 1, prePreVal = 1, newVal;
for (int i = 2; i <= n; ++i) {
newVal = preVal + prePreVal;
prePreVal = preVal;
preVal = newVal;
}
return newVal;
}
};
2. 746. 使用最小花费爬楼梯(简单)
思想
1.给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
2.f[i]表示从0爬到i的最低花费
代码
class Solution {
public:
// f[i]:从0爬到i的最小花费
int n;
int minCostClimbingStairs(vector<int>& cost) {
n = cost.size();
if (n == 1)
return cost[0];
if (n == 2)
return min(cost[0], cost[1]);
int prePreVal = cost[0], preVal = cost[1], newVal;
for (int i = 2; i <= n; ++i) {
newVal = min(preVal, prePreVal);
if (i < n) // 向上爬
newVal += cost[i];
prePreVal = preVal;
preVal = newVal;
}
return newVal;
}
};
3. 3693.爬楼梯II(中等)
https://leetcode.cn/problems/climbing-stairs-ii/description/
思想
1.你正在爬一个有 n + 1 级台阶的楼梯,台阶编号从 0 到 n。
你还得到了一个长度为 n 的 下标从 1 开始 的整数数组 costs,其中 costs[i] 是第 i 级台阶的成本。
从第 i 级台阶,你 只能 跳到第 i + 1、i + 2 或 i + 3 级台阶。从第 i 级台阶跳到第 j 级台阶的成本定义为: costs[j] + (j - i)2
你从第 0 级台阶开始,初始 cost = 0。
返回到达第 n 级台阶所需的 最小 总成本。
3.f[i]表示从0爬到i的最小总成本
代码
class Solution {
public:
vector<int> f;
int n;
int climbStairs(int n, vector<int>& costs) {
n = costs.size();
f.resize(n + 1);
f[0] = 0;
f[1] = costs[1 - 1] + 1;
if (n == 1)
return f[1];
f[2] = costs[2 - 1] + min(f[0] + 4, f[1] + 1);
for (int i = 3; i <= n; ++i) {
f[i] = min({f[i - 3] + 9, f[i - 2] + 4, f[i - 1] + 1});
f[i] += costs[i - 1];
}
return f[n];
}
};
4. 377.组合总和IV(中等,学习)
https://leetcode.cn/problems/combination-sum-iv/description/
思想
1.给你一个由 不同 整数组成的数组 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.此题本质是[[十八.动态规划-一.入门DP#1. 70.爬楼梯(简单,学习)]],只不过现在可以爬nums中的任意值 ,即题意是从0级开始,从nums中任意爬任意次各台阶,最终到达target的不同方案个数
代码
class Solution {
public:
typedef unsigned long long ull;
vector<ull> f;
int combinationSum4(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
f.resize(target + 1);
f[0] = 1;
for (int i = 1; i <= target; ++i) {
for (int& x : nums) {
if (i - x >= 0)
f[i] += f[i - x];
else
break;
}
}
return f[target];
}
};
5. 2466.统计构造好字符串的方案数(中等)
2466. 统计构造好字符串的方案数 - 力扣(LeetCode)
思想
1.给你整数 zero ,one ,low 和 high ,我们从空字符串开始构造一个字符串,每一步执行下面操作中的一种:
- 将
'0'在字符串末尾添加zero次。 - 将
'1'在字符串末尾添加one次。
以上操作可以执行任意次。
如果通过以上过程得到一个 长度 在low和high之间(包含上下边界)的字符串,那么这个字符串我们称为 好 字符串。
请你返回满足以上要求的 不同 好字符串数目。由于答案可能很大,请将结果对109 + 7取余 后返回。
2.还是爬楼梯,这次一次·可以爬zero或one层,最终到达[low,high]之间
代码
class Solution {
public:
typedef long long ll;
vector<ll> f; // 从0到长度i的字符串个数
const int mod = 1e9 + 7;
int countGoodStrings(int low, int high, int zero, int one) {
f.resize(high + 1);
f[0] = 1;
for (int i = 1; i <= high; ++i) {
// i-zero,i-one
if (i - zero >= 0)
f[i] = (f[i] + f[i - zero]) % mod;
if (i - one >= 0)
f[i] = (f[i] + f[i - one]) % mod;
}
int res = 0;
for (int i = low; i <= high; ++i) {
res = (res + f[i]) % mod;
}
return res;
}
};
二、打家劫舍
1.套路
1.相邻数字不能同时选想到打家劫舍问题,转换成相邻项([[十八.动态规划-一.入门DP#2. 213.打家劫舍II(中等,学习)]]对第0家分类讨论转变为原始打家劫舍,[[十八.动态规划-一.入门DP#4. 740.删除并获得点数(中等,学习)]]转化为值域数组转变为原始打家劫舍)
2.题目描述
3.学习经验
1. 198.打家劫舍(中等)
思想
1.你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
2.子问题:f[i]表示偷i家的最高金额
代码
class Solution {
public:
typedef long long ll;
vector<ll> f; // f[i]:从i家能偷到的最高金额
int n;
int rob(vector<int>& nums) {
n = nums.size();
f.resize(n + 1);
f[0] = 0;
f[1] = nums[0];
if (n == 1)
return f[1];
f[2] = max(f[1], 1LL * nums[1]);
for (int i = 2; i <= n; ++i) {
// 偷i:nums[i-1]+f[i-2],不偷i:f[i-1]
f[i] = max(f[i - 1], nums[i - 1] + f[i - 2]);
}
return f[n];
}
};
2. 213.打家劫舍II(中等,学习)
思想
1.你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
2.此题限制是第0家和第n-1家不能同时选,所以以选不选第0家进行分类讨论
- 选第0家,那么1和n-1不能选,可选范围
[2,n-2] - 不选第0家,可选范围
[1,n-1]
可选范围的选择变成[[十八.动态规划-一.入门DP#1. 198.打家劫舍(中等)]]题
代码
class Solution {
public:
int rob1(vector<int>& nums, int left, int right) {
// nums:[left,right]
int len = right - left + 1; // 共len家
vector<int> f(len + 1); // f[i]:偷i家
f[0] = 0;
f[1] = nums[left];
if (len == 1)
return f[1];
f[2] = max(nums[left], nums[left + 1]);
for (int i = 2; i <= len; ++i) {
f[i] = max(f[i - 2] + nums[left + i - 1], f[i - 1]);
}
return f[len];
}
int n;
int rob(vector<int>& nums) {
n = nums.size();
if (n == 1)
return nums[0];
if (n == 2)
return max(nums[0], nums[1]);
if (n == 3)
return max({nums[0], nums[1], nums[2]});
// 选nums[0],不能选nums[1]和nums[n-1],范围[2,n-2],不选nums[0],范围[1,n-1],要求n>=4
return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
}
};
3. 2320.统计放置房子的方式数(中等)
2320. 统计放置房子的方式数 - 力扣(LeetCode)
思想
1.一条街道上共有 n * 2 个 地块 ,街道的两侧各有 n 个地块。每一边的地块都按从 1 到 n 编号。每个地块上都可以放置一所房子。
现要求街道同一侧不能存在两所房子相邻的情况,请你计算并返回放置房屋的方式数目。由于答案可能很大,需要对 109 + 7 取余后再返回。
注意,如果一所房子放置在这条街某一侧上的第 i 个地块,不影响在另一侧的第 i 个地块放置房子。
代码
class Solution {
public:
typedef long long ll;
vector<ll> f; // f[i]:i块地块放置房屋方案数
const int mod = 1e9 + 7;
int countHousePlacements(int n) {
f.resize(n + 1);
f[0] = 1; // 不放也是1种方案
f[1] = 2;
if (n == 1)
return f[1] * f[1];
// 第i块放:f[i-2],不放:f[i-1]
for (int i = 2; i <= n; ++i) {
f[i] = (f[i - 2] + f[i - 1]) % mod;
}
return f[n] * f[n] % mod;
}
};
4. 740.删除并获得点数(中等,学习)
思想
1.给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
2.相邻数字不能同时选,打家劫舍问题。
但此题同一个值的数字为一个范围,所以转换成值域数组 :sum[i]表示nums种值为i的元素之和,然后对值域数组使用打家劫舍问题动态规划算法
代码
class Solution {
public:
typedef long long ll;
int n;
vector<ll> sum, f; // sum[x]:值为x的和, f[i]表示共i个数
int deleteAndEarn(vector<int>& nums) {
n = nums.size();
int maxn = INT_MIN;
for (int& x : nums)
maxn = max(maxn, x);
sum.resize(maxn + 1);
f.resize(maxn + 2); // 共maxn+1个数,[0,maxn+1]
for (int& x : nums)
sum[x] += x;
f[0] = 0;
f[1] = sum[0];
if (maxn == 0)
return f[1];
for (int i = 2; i <= maxn + 1; ++i) {
f[i] = max(f[i - 2] + sum[i - 1], f[i - 1]);
}
return f[maxn + 1];
}
};