算法思路(17.16)
状态表示:
在处理线性动态规划问题时,我们可以通过“经验 + 题目要求”来定义状态表示。通常有两种选择:- 以某个位置为结尾的情况;
- 以某个位置为起点的情况。
本题中,我们选择更常用的方式,以某个位置为结尾,并结合题目要求,定义状态表示如下:
dp[i] 表示选择到位置 i 时的最长预约时长。但是在处理位置 ii 时,我们会面临“选择”或“不选择”的两种决策,因此需要将状态细分为:
- f[i] 表示选择到 i 位置时,nums[i] 必选,此时的最长预约时长。f[i] 表示选择到 i 位置时,nums[i] 必选,此时的最长预约时长。
- g[i] 表示选择到 i 位置时,nums[i] 不选,此时的最长预约时长。g[i] 表示选择到 i 位置时,nums[i] 不选,此时的最长预约时长。
状态转移方程:
由于状态表示涉及两个部分,我们分别分析这两个状态的转移关系:对于 f[i]:
- 如果选择 nums[i] 必选,我们只需考虑位置 i−1 在不选择时的最长预约时长,因此:
对于 g[i]:
- 如果 nums[i] 不选,则 i−1 位置可以选择或不选择,因此:
初始化:
f[0]=nums[0],g[0]=0
本题的初始化相对简单,无需添加辅助节点。只需进行以下初始化即可:填表顺序:
根据状态转移方程,填表顺序为“从左往右”,同时填充两个状态表。返回值:
max(f[n−1],g[n−1])
根据状态表示,最终返回的结果为:这代表在处理到最后一个位置时的最长预约时长。
C++:
class Solution {
public:
int massage(vector<int>& nums) {
// 1. 创建⼀个 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = nums.size();
if(n == 0) return 0; // 处理边界条件
vector<int> f(n);
auto g = f;
f[0] = nums[0];
for(int i = 1; i < n; i++)
{
f[i] = g[i - 1] + nums[i];
g[i] = max(f[i - 1], g[i - 1]);
}
return max(f[n - 1], g[n - 1]);
}
};
Java:
class Solution
{
public int massage(int[] nums)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = nums.length;
if(n == 0) return 0; // 处理边界条件
int[] f = new int[n];
int[] g = new int[n];
f[0] = nums[0];
for(int i = 1; i < n; i++)
{
f[i] = g[i - 1] + nums[i];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
return Math.max(g[n - 1], f[n - 1]);
}
}
算法思路(213)
本问题是“按摩师”问题的变种,具体而言,它涉及一个“环形”模式,而不是单排模式。由于首尾房屋相连,这使得问题的处理方式有所不同。但我们可以将“环形”问题转化为两个单排问题的求解:
偷第一个房屋:在这种情况下,最高金额为 x。此时由于偷了第一个房屋,不能再偷最后一个房屋,因此只需在区间 [0,n−2] 内进行偷窃。
不偷第一个房屋:在这种情况下,最高金额为 y。此时可以偷最后一个房屋,因此只需在区间 [1,n−1] 内进行偷窃。
最终结果:通过计算上述两种情况的最大值,我们可以得到环形问题的最终结果。也就是说,问题已有效转化为求解两个单排问题的最大值:
结果=max(x,y)
C++:
class Solution
{
public:
int rob(vector<int>& nums){
int n = nums.size();
// 两种情况下的最⼤值
return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
}
int rob1(vector<int>& nums, int left, int right){
if(left > right) return 0;
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = nums.size();
vector<int> f(n);
auto g = f;
f[left] = nums[left]; // 初始化
for(int i = left + 1; i <= right; i++)
{
f[i] = g[i - 1] + nums[i];
g[i] = max(f[i - 1], g[i - 1]);
}
return max(f[right], g[right]);
}
};
Java:
class Solution
{
public int rob(int[] nums)
{
int n = nums.length;
return Math.max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
}
public int rob1(int[] nums, int left, int right)
{
if(left > right) return 0;
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回
int n = nums.length;
int[] f= new int[n];
int[] g= new int[n];
f[left] = nums[left];
for(int i = left + 1; i <= right; i++)
{
f[i] = g[i - 1] + nums[i];
g[i] = Math.max(g[i - 1], f[i - 1]);
}
return Math.max(f[right], g[right]);
}
}
算法思路(740)
本题实质上是“按摩师”问题的变种。注意到题目描述,当选择某个数字 x 时,x−1 和 x+1 的数值不能被选择。这与“按摩师”问题中的逻辑相似:选择位置 i 的金额后,不能选择位置 i−1 和 i+1 的金额。
因此,我们可以通过以下步骤解决这个问题:
创建哈希数组:
- 根据题目中的数据范围,创建一个大小为 10001 的哈希数组 hash。
- 遍历数组 nums,将数组中每个元素 x 的值累加到哈希数组的对应位置 hash[x] 上。有了这个哈希数组,我们能将相同金额的数值进行聚合。
应用按摩师策略
C++:
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
const int N = 10001;
// 1. 预处理
int arr[N] = { 0 };
for(auto x : nums) arr[x] += x;
// 2. 在 arr 数组上,做⼀次 “打家劫舍” 问题
// 创建 dp 表
vector<int> f(N);
auto g = f;
// 填表
for(int i = 1; i < N; i++)
{
f[i] = g[i - 1] + arr[i];
g[i] = max(f[i - 1], g[i - 1]);
}
// 返回结果
return max(f[N - 1], g[N - 1]);
}
};
Java:
class Solution
{
public int deleteAndEarn(int[] nums)
{
int n = 10001;
// 1. 预处理
int[] arr = new int[n];
for(int x : nums) arr[x] += x;
// 2. dp
// 创建 dp 表
int[] f = new int[n];
int[] g = new int[n];
// 初始化
f[0] = arr[0];
// 填表
for(int i = 1; i < n; i++)
{
f[i] = g[i - 1] + arr[i];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
// 返回值
return Math.max(f[n - 1], g[n - 1]);
}
}
算法思路(091)
状态表示:
在处理线性动态规划问题时,我们可以通过“经验 + 题目要求”来定义状态表。通常有两种选择:- 以某个位置为结尾;
- 以某个位置为起点。
本题中,我们选择以某个位置为结尾的方法,并结合题目要求,定义状态表示为:
- dp[i][0]:表示到达位置 ii 时,如果最后一个位置刷上“红色”,所需的最小花费;
- dp[i][1]:表示到达位置 ii 时,如果最后一个位置刷上“蓝色”,所需的最小花费;
- dp[i][2]:表示到达位置 ii 时,如果最后一个位置刷上“绿色”,所需的最小花费。
状态转移方程:
由于状态表示定义了三个状态,我们需要针对每个状态推导其转移方程:对于 dp[i][0](最后一个位置刷红色):
dp[i][0]=min(dp[i−1][1],dp[i−1][2])+costs[i−1][0]这里,我们需要获取位置 i−1i−1 刷蓝色或绿色情况下的最小花费,然后加上当前位置的红色刷费。
对于 dp[i][1](最后一个位置刷蓝色):
dp[i][1]=min(dp[i−1][0],dp[i−1][2])+costs[i−1][1]对于 dp[i][2](最后一个位置刷绿色):
dp[i][2]=min(dp[i−1][0],dp[i−1][1])+costs[i−1][2]
初始化:
设定一个辅助节点来帮助我们初始化工作。当我们添加一个辅助节点时,需确保两个关键点:- 辅助节点内的值:应保证能正确初始化后续状态表;
- 下标映射关系:在本题中,我们添加的辅助节点默认设置为 0。
填表顺序:
根据状态转移方程,应按顺序“从左往右”填充这三个状态表。返回值:
min(dp[n][0],dp[n][1],dp[n][2])
最终,我们需要返回在所有颜色情况下的最小花费:这里 nn 表示房屋的数量,确保能够在最后一个位置中选择刷上任一颜色的最小花费。
C++:
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
// dp[i][j] 第i个房⼦刷成第j种颜⾊最⼩花费
int n = costs.size();
vector<vector<int>> dp(n + 1, vector<int>(3));
for (int i = 1; i <= n; i++) {
dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
dp[i][2] = min(dp[i - 1][1], dp[i - 1][0]) + costs[i - 1][2];
}
return min(dp[n][0], min(dp[n][1], dp[n][2]));
}
};
Java:
class Solution
{
public int minCost(int[][] costs)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = costs.length;
int[][] dp = new int[n + 1][3];
for(int i = 1; i <= n; i++)
{
dp[i][0] = Math.min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
dp[i][2] = Math.min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2];
}
return Math.min(dp[n][0], Math.min(dp[n][1], dp[n][2]));
}
}
算法思路(309)
状态表示:
在处理线性动态规划问题时,我们可以通过“经验 + 题目要求”来定义状态表。针对本问题,有三种状态:- 买入状态;
- 可交易状态;
- 冷冻期状态。
因此,我们可以定义一个三维状态数组 dpdp:
- dp[i][0]:表示在第 ii 天结束后处于“买入”状态时的最大利润;
- dp[i][1]:表示在第 ii 天结束后处于“可交易”状态时的最大利润;
- dp[i][2]:表示在第 ii 天结束后处于“冷冻期”状态时的最大利润。
状态转移方程:
我们需要根据规则推导状态转移方程:对于 dp[i][0](买入状态):
- 可以由之前持有股票(保持不变)和今天购入股票(从可交易状态转变)得到:
其中,dp[i−1][1]−prices[i] 表示在可交易状态下买入股票的利润计算。
对于 dp[i][1] (可交易状态):
- 由之前在冷冻期不执行任何交易(停滞)和之前处于可交易状态不执行任何交易得到:
对于 dp[i][2] (冷冻期状态):
- 由之前的持有状态卖出股票进入冷冻期得到:
初始化:
第一行的状态初始化较为关键,因为后续状态均依赖于第一行值:- dp[0][0]:为了处于买入状态,必须在第一天买入股票,故:
- dp[0][1]:什么也不做的情况下,利润为0:
- dp[0][2]:进入冷冻期时手中没有股票,利润为0:
填表顺序:
根据状态转移方程,依次从第一天填充到最后一天,三个状态表需要一起更新,方向为“从左到右”。返回值:
max(dp[n−1][1],dp[n−1][2])
最后,我们需要返回在可交易状态或冷冻期状态下的最大利润值:
C++:
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(3));
dp[0][0] = -prices[0];
for(int i = 1; i < n; 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][2]);
dp[i][2] = dp[i - 1][0] + prices[i];
}
return max(dp[n - 1][1], dp[n - 1][2]);
}
};
Java:
class Solution
{
public int maxProfit(int[] prices)
{
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回值
int n = prices.length;
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
for(int i = 1; i < n; i++)
{
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2]);
dp[i][2] = dp[i - 1][0] + prices[i];
}
return Math.max(dp[n - 1][1], dp[n - 1][2]);
}
}