【leetcode刷题】Part 6 状态机DP
今天来复盘一下第六部分:状态机DP
首先要弄清楚定义:状态机DP是什么?
我的理解是:就是把当前元素的状态(通常不多)纳入DP数组考虑
我们以买卖股票引入
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 10^50 <= prices[i] <= 10^4
Solution
我们用\(dp[i][j]\)表示第\(i\)支股票在状态\(j\)下的最大利润,\(dp[i][0]\)表示手上没有股票,而\(dp[i][1]\)表示手上有股票
可以看到,这道题我们为了只交易一次,直接考虑转移方程
此时,\(dp[i][1]\)的转移方程是保证只会转移一次的,如果需要转移无限次,只需要把方程改成这样
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n=prices.size();
const int INF=2e9;
vector<vector<int>>dp(n+5,vector<int>(2,-INF));
dp[0][0]=0;
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
dp[i][1]=max(dp[i-1][1],-prices[i-1]);
}
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,dp[i][0]);
return ans;
}
};
188. 买卖股票的最佳时机 IV
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
1 <= k <= 1001 <= prices.length <= 10000 <= prices[i] <= 1000
Solution
很显而易见的一点是,我们需要新增一个维度表示购买的次数
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n=prices.size();
const int INF=2e9+7;
vector<vector<vector<int>>>dp(n+5,vector<vector<int>>(k+5,vector<int>(2,-INF)));
for(int i=0;i<=n;i++) dp[i][0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]);
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
}
}
int ans=0;
for(int i=1;i<=k;i++){
ans=max(ans,dp[n][i][0]);
}
return ans;
}
};
3573. 买卖股票的最佳时机 V
给你一个整数数组 prices,其中 prices[i] 是第 i 天股票的价格(美元),以及一个整数 k。
你最多可以进行 k 笔交易,每笔交易可以是以下任一类型:
-
普通交易:在第
i天买入,然后在之后的第j天卖出,其中i < j。你的利润是prices[j] - prices[i]。 -
做空交易:在第
i天卖出,然后在之后的第j天买回,其中i < j。你的利润是prices[i] - prices[j]。
注意:你必须在开始下一笔交易之前完成当前交易。此外,你不能在已经进行买入或卖出操作的同一天再次进行买入或卖出操作。
通过进行 最多 k 笔交易,返回你可以获得的最大总利润。
示例 1:
输入: prices = [1,7,9,8,2], k = 2
输出: 14
解释:
我们可以通过 2 笔交易获得 14 美元的利润:
- 一笔普通交易:第 0 天以 1 美元买入,第 2 天以 9 美元卖出。
- 一笔做空交易:第 3 天以 8 美元卖出,第 4 天以 2 美元买回。
示例 2:
输入: prices = [12,16,19,19,8,1,19,13,9], k = 3
输出: 36
解释:
我们可以通过 3 笔交易获得 36 美元的利润:
- 一笔普通交易:第 0 天以 12 美元买入,第 2 天以 19 美元卖出。
- 一笔做空交易:第 3 天以 19 美元卖出,第 4 天以 8 美元买回。
- 一笔普通交易:第 5 天以 1 美元买入,第 6 天以 19 美元卖出。
提示:
2 <= prices.length <= 10^31 <= prices[i] <= 10^91 <= k <= prices.length / 2
Solution
我们需要增加一个状态\(dp[i][j][2]\),用于表示做空交易卖出后的状态。于是有以下转移方程:
class Solution {
public:
long long maximumProfit(vector<int>& prices, int k) {
int n=prices.size();
const long long INF=1e18;
vector<vector<vector<long long>>>dp(n+5,vector<vector<long long>>(k+5,vector<long long>(3,-INF)));
for(int i=0;i<=n;i++) dp[i][0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i][j][1]=max(dp[i-1][j-1][0]-(long long)prices[i-1],dp[i-1][j][1]);
dp[i][j][2]=max(dp[i-1][j][2],dp[i-1][j-1][0]+(long long)prices[i-1]);
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+(long long)prices[i-1]);
dp[i][j][0]=max(dp[i][j][0],dp[i-1][j][2]-(long long)prices[i-1]);
}
}
long long ans=0;
for(int i=1;i<=k;i++) ans=max(ans,dp[n][i][0]);
return ans;
}
};
309. 买卖股票的最佳时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
提示:
1 <= prices.length <= 50000 <= prices[i] <= 1000
Solution
这道题其实在前面训练过那些之后也比较清晰了,我们直接写出转移方程:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n=prices.size();
const int INF=2e9+7;
vector<vector<int>>dp(n+5,vector<int>(2,-INF));
for(int i=0;i<=n;i++) dp[i][0]=0;
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
if(i>=2) dp[i][1]=max(dp[i-1][1],dp[i-2][0]-prices[i-1]);
else dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
}
return dp[n][0];
}
};
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
提示:
1 <= prices.length <= 5 * 10^41 <= prices[i] < 5 * 10^40 <= fee < 5 * 10^4
Solution
也是前面的变种,我们直接写出转移方程:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n=prices.size();
const int INF=2e9+7;
vector<vector<int>>dp(n+5,vector<int>(2,-INF));
dp[0][0]=0;
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]-fee);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
}
return dp[n][0];
}
};
1911. 最大交替子序列和
一个下标从 0 开始的数组的 交替和 定义为 偶数 下标处元素之 和 减去 奇数 下标处元素之 和 。
- 比方说,数组
[4,2,5,3]的交替和为(4 + 5) - (2 + 3) = 4。
给你一个数组 nums ,请你返回 nums 中任意子序列的 最大交替和 (子序列的下标 重新 从 0 开始编号)。
一个数组的 子序列 是从原数组中删除一些元素后(也可能一个也不删除)剩余元素不改变顺序组成的数组。比方说,[2,7,4] 是 [4,2,3,7,2,1,4] 的一个子序列(加粗元素),但是 [2,4,2] 不是。
示例 1:
输入:nums = [4,2,5,3]
输出:7
解释:最优子序列为 [4,2,5] ,交替和为 (4 + 5) - 2 = 7
示例 2:
输入:nums = [5,6,7,8]
输出:8
解释:最优子序列为 [8] ,交替和为 8 。
示例 3:
输入:nums = [6,2,1,2,4,5]
输出:10
解释:最优子序列为 [6,1,5] ,交替和为 (6 + 5) - 1 = 10 。
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^5
Solution
这道题我们也是可以简单写出转移方程的,唯一要注意的一点就是,一个数可能可以作为新的一个子序列的开端
class Solution {
public:
long long maxAlternatingSum(vector<int>& nums) {
int n=nums.size();
vector<vector<long long>>dp(n+1,vector<long long>(2,LLONG_MIN+2e5));
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+(long long)nums[i-1]);
dp[i][0]=max(dp[i][0],(long long)nums[i-1]);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-(long long)nums[i-1]);
}
return max(dp[n][0],dp[n][1]);
}
};
1186. 删除一次得到子数组最大和
给你一个整数数组,返回它的某个 非空 子数组(连续元素)在执行一次可选的删除操作后,所能得到的最大元素总和。换句话说,你可以从原数组中选出一个子数组,并可以决定要不要从中删除一个元素(只能删一次哦),(删除后)子数组中至少应当有一个元素,然后该子数组(剩下)的元素总和是所有子数组之中最大的。
注意,删除一个元素后,子数组 不能为空。
示例 1:
输入:arr = [1,-2,0,3]
输出:4
解释:我们可以选出 [1, -2, 0, 3],然后删掉 -2,这样得到 [1, 0, 3],和最大。
示例 2:
输入:arr = [1,-2,-2,3]
输出:3
解释:我们直接选出 [3],这就是最大和。
示例 3:
输入:arr = [-1,-1,-1,-1]
输出:-1
解释:最后得到的子数组不能为空,所以我们不能选择 [-1] 并从中删去 -1 来得到 0。
我们应该直接选择 [-1],或者选择 [-1, -1] 再从中删去一个 -1。
提示:
1 <= arr.length <= 10^5-104 <= arr[i] <= 10^4
Solution
我们可以用\(dp[i][j][k]\)表示第\(i\)个元素在状态\((j,k)\)下的最大值,\((0,0)\)表示从未使用过删除这一手段的最大值,\((1,0)\)表示使用过删除这一手段,但是并不是使用于当前元素的最大值,\((1,1)\)表示使用过删除元素,且使用于当前元素的最大值
我们有转移方程:
class Solution {
public:
int maximumSum(vector<int>& arr) {
int n=arr.size();
vector<vector<vector<int>>>dp(n+1,vector<vector<int>>(2,vector<int>(2,INT_MIN+2e4)));
for(int i=1;i<=n;i++){
dp[i][0][0]=max(dp[i-1][0][0]+arr[i-1],arr[i-1]);
dp[i][1][0]=max(dp[i-1][1][0]+arr[i-1],dp[i-1][1][1]+arr[i-1]);
dp[i][1][1]=max(dp[i-1][0][0],0);
}
int ans=INT_MIN;
for(int i=1;i<=n;i++){
ans=max(ans,dp[i][0][0]);
ans=max(ans,dp[i][1][0]);
if(dp[i][1][1]!=0) ans=max(ans,dp[i][1][1]);
}
return ans;
}
};
1537. 最大得分
你有两个 有序 且数组内元素互不相同的数组 nums1 和 nums2 。
一条 合法路径 定义如下:
- 选择数组
nums1或者nums2开始遍历(从下标0处开始)。 - 从左到右遍历当前数组。
- 如果你遇到了
nums1和nums2中都存在的值,那么你可以切换路径到另一个数组对应数字处继续遍历(但在合法路径中重复数字只会被统计一次)。
得分 定义为合法路径中不同数字的和。
请你返回 所有可能 合法路径 中的最大得分。由于答案可能很大,请你将它对 10^9 + 7 取余后返回。
示例 1:

输入:nums1 = [2,4,5,8,10], nums2 = [4,6,8,9]
输出:30
解释:合法路径包括:
[2,4,5,8,10], [2,4,5,8,9], [2,4,6,8,9], [2,4,6,8,10],(从 nums1 开始遍历)
[4,6,8,9], [4,5,8,10], [4,5,8,9], [4,6,8,10] (从 nums2 开始遍历)
最大得分为上图中的绿色路径 [2,4,6,8,10] 。
示例 2:
输入:nums1 = [1,3,5,7,9], nums2 = [3,5,100]
输出:109
解释:最大得分由路径 [1,3,5,100] 得到。
示例 3:
输入:nums1 = [1,2,3,4,5], nums2 = [6,7,8,9,10]
输出:40
解释:nums1 和 nums2 之间无相同数字。
最大得分由路径[6,7,8,9,10]得到。
提示:
1 <= nums1.length, nums2.length <= 10^51 <= nums1[i], nums2[i] <= 10^7nums1和nums2都是严格递增的数组。
Solution
我们可以看到,状态的切换是在相同的点之间,那么,我们只需要将两个切换点之间的前缀和处理出来,这样就化归为之前的一个选A选B的问题
class Solution {
public:
int maxSum(vector<int>& nums1, vector<int>& nums2) {
int n=nums1.size(),m=nums2.size();
const int INF=1e9+7;
vector<long long>pre1(n+1,0);
vector<long long>pre2(m+1,0);
for(int i=1;i<=n;i++) pre1[i]=nums1[i-1]+pre1[i-1];
for(int i=1;i<=m;i++) pre2[i]=nums2[i-1]+pre2[i-1];
vector<long long>ma1;
vector<long long>ma2;
int las1=0,las2=0;
for(int i=0;i<=n-1;i++){
auto x=lower_bound(nums2.begin(),nums2.end(),nums1[i]);
if(x==nums2.end()) continue;
if(*x!=nums1[i]) continue;
int tem=x-nums2.begin();
ma1.push_back(pre1[i+1]-pre1[las1]);
ma2.push_back(pre2[tem+1]-pre2[las2]);
las1=i+1;las2=tem+1;
}
if(las1!=n) ma1.push_back(pre1[n]-pre1[las1]);
if(las2!=m) ma2.push_back(pre2[m]-pre2[las2]);
int t=max(ma1.size(),ma2.size());
vector<vector<long long>>dp(t+1,vector<long long>(2,LLONG_MIN/2));
dp[0][0]=0,dp[0][1]=0;
for(int i=1;i<=t;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1])+ma1[i-1];
if(i<=ma2.size()) dp[i][1]=max(dp[i-1][1],dp[i-1][0])+ma2[i-1];
else dp[t][1]=dp[t][0];
}
return max(dp[t][0],dp[t][1])%INF;
}
};
2919. 使数组变美的最小增量运算数
给你一个下标从 0 开始、长度为 n 的整数数组 nums ,和一个整数 k 。
你可以执行下述 递增 运算 任意 次(可以是 0 次):
- 从范围
[0, n - 1]中选择一个下标i,并将nums[i]的值加1。
如果数组中任何长度 大于或等于 3 的子数组,其 最大 元素都大于或等于 k ,则认为数组是一个 美丽数组 。
以整数形式返回使数组变为 美丽数组 需要执行的 最小 递增运算数。
子数组是数组中的一个连续 非空 元素序列。
示例 1:
输入:nums = [2,3,0,0,2], k = 4
输出:3
解释:可以执行下述递增运算,使 nums 变为美丽数组:
选择下标 i = 1 ,并且将 nums[1] 的值加 1 -> [2,4,0,0,2] 。
选择下标 i = 4 ,并且将 nums[4] 的值加 1 -> [2,4,0,0,3] 。
选择下标 i = 4 ,并且将 nums[4] 的值加 1 -> [2,4,0,0,4] 。
长度大于或等于 3 的子数组为 [2,4,0], [4,0,0], [0,0,4], [2,4,0,0], [4,0,0,4], [2,4,0,0,4] 。
在所有子数组中,最大元素都等于 k = 4 ,所以 nums 现在是美丽数组。
可以证明无法用少于 3 次递增运算使 nums 变为美丽数组。
因此,答案为 3 。
示例 2:
输入:nums = [0,1,3,3], k = 5
输出:2
解释:可以执行下述递增运算,使 nums 变为美丽数组:
选择下标 i = 2 ,并且将 nums[2] 的值加 1 -> [0,1,4,3] 。
选择下标 i = 2 ,并且将 nums[2] 的值加 1 -> [0,1,5,3] 。
长度大于或等于 3 的子数组为 [0,1,5]、[1,5,3]、[0,1,5,3] 。
在所有子数组中,最大元素都等于 k = 5 ,所以 nums 现在是美丽数组。
可以证明无法用少于 2 次递增运算使 nums 变为美丽数组。
因此,答案为 2 。
示例 3:
输入:nums = [1,1,2], k = 1
输出:0
解释:在这个示例中,只有一个长度大于或等于 3 的子数组 [1,1,2] 。
其最大元素 2 已经大于 k = 1 ,所以无需执行任何增量运算。
因此,答案为 0 。
提示:
3 <= n == nums.length <= 10^50 <= nums[i] <= 10^90 <= k <= 10^9
Solution
这道题,我一开始的想法还是用\(dp[i][j]\)表示能保证\(1 \sim i\)均保持条件的时候,所用的最小操作数;后来发现不行,因为它如果是由\(i-2\)转移过来的和\(i-1\)转移过来的结果不一样,于是有了以下代码:
class Solution {
public:
long long minIncrementOperations(vector<int>& nums, int k) {
int n=nums.size();
vector<vector<long long>>dp(n+1,vector<long long>(3,LLONG_MIN));//0表示自己动
//1表示自己不动,因为i-2动了,2表示自己不动,是因为i-1动了
dp[0][0]=0,dp[0][1]=0,dp[0][2]=0;
dp[1][0]=k-nums[0]>0?k-nums[0]:0;
dp[1][1]=0,dp[1][2]=0;
dp[2][0]=k-nums[1]>0?k-nums[1]:0;
dp[2][1]=0;dp[2][2]=dp[1][0];
for(int i=3;i<=n;i++){
dp[i][0]=min(min(dp[i-1][0],dp[i-1][1]),dp[i-1][2])+(k-nums[i-1]>0?k-nums[i-1]:0);
dp[i][1]=dp[i-1][2];
dp[i][2]=dp[i-1][0];
}
return min(min(dp[n][0],dp[n][1]),dp[n][2]);
}
};
然后,通过学习题解,我发现了更巧妙的解法,就是设\(dp[i]\)为改变\(i\)且满足条件的当前最小值,最后只需要返回\(min(dp[n-2],dp[n-1],dp[n])\)就可以了
转移方程是这样的:
就是说,有时候不要被题目类型给困住了,你看这题就是
3434. 子数组操作后的最大频率
给你一个长度为 n 的数组 nums ,同时给你一个整数 k 。
你可以对 nums 执行以下操作 一次 :
- 选择一个子数组
nums[i..j],其中0 <= i <= j <= n - 1。 - 选择一个整数
x并将nums[i..j]中 所有 元素都增加x。
请你返回执行以上操作以后数组中 k 出现的 最大 频率。
子数组 是一个数组中一段连续 非空 的元素序列。
示例 1:
输入:nums = [1,2,3,4,5,6], k = 1
输出:2
解释:
将 nums[2..5] 增加 -5 后,1 在数组 [1, 2, -2, -1, 0, 1] 中的频率为最大值 2 。
示例 2:
输入:nums = [10,2,3,4,5,5,4,3,2,2], k = 10
输出:4
解释:
将 nums[1..9] 增加 8 以后,10 在数组 [10, 10, 11, 12, 13, 13, 12, 11, 10, 10] 中的频率为最大值 4 。
提示:
1 <= n == nums.length <= 10^51 <= nums[i] <= 501 <= k <= 50
Solution
我们先观察这道题的数据范围,\(nums[i]\)的范围是\(1 \sim 50\),那说明解法多半与枚举有关了
再细想,我们对于每一个数,要考虑的只是它本身跟\(k\)的数量而已,我们考虑一个子数组num[i...j],设整个数组中\(k\)的数量为\(S\),这个子数组中\(k\)的数量是\(X\),\(nums[i]\)的数量是Y,那么此时得出的最大频率就是\(S-X+Y\),所以,我们只需考虑找出\(Y-X\)的最大值即可
然后,我们可以把这个模型抽象成\(k=-1,nums[i]=1,nums[j]=0\)的模型,只需求出它的最大子数组和就可以,时间复杂度为\(O(kn)\)
class Solution {
public:
int maxFrequency(vector<int>& nums, int k) {
int n=nums.size();
const int INF=1e9+7;
int ans=0;
int res=0;
map<int,int>ma;
for(int num:nums){
if(num==k) ans++;
}
for(int num:nums){
if(ma[num]) continue;
ma[num]++;
if(num==k){
res=max(res,ans);continue;
}
vector<int>dp(n+1,-INF);
dp[0]=0;
for(int j=1;j<=n;j++){
if(nums[j-1]!=num&&nums[j-1]!=k) dp[j]=max(dp[j-1],0);
if(nums[j-1]==k) dp[j]=max(dp[j-1]-1,-1);
if(nums[j-1]==num) dp[j]=max(dp[j-1]+1,1);
}
int cnt=-INF;
for(int j=1;j<=n;j++) cnt=max(cnt,dp[j]);
res=max(res,ans+cnt);
}
return res;
}
};
2272. 最大波动的子字符串
字符串的 波动 定义为子字符串中出现次数 最多 的字符次数与出现次数 最少 的字符次数之差。
给你一个字符串 s ,它只包含小写英文字母。请你返回 s 里所有 子字符串的 最大波动 值。
子字符串 是一个字符串的一段连续字符序列。
示例 1:
输入:s = "aababbb"
输出:3
解释:
所有可能的波动值和它们对应的子字符串如以下所示:
- 波动值为 0 的子字符串:"a" ,"aa" ,"ab" ,"abab" ,"aababb" ,"ba" ,"b" ,"bb" 和 "bbb" 。
- 波动值为 1 的子字符串:"aab" ,"aba" ,"abb" ,"aabab" ,"ababb" ,"aababbb" 和 "bab" 。
- 波动值为 2 的子字符串:"aaba" ,"ababbb" ,"abbb" 和 "babb" 。
- 波动值为 3 的子字符串 "babbb" 。
所以,最大可能波动值为 3 。
示例 2:
输入:s = "abcde"
输出:0
解释:
s 中没有字母出现超过 1 次,所以 s 中每个子字符串的波动值都是 0 。
提示:
1 <= s.length <= 10^4s只包含小写英文字母
Solution
这道题我们一看到最大值与最小值之差,就想到上一道题我们用的抽象模型方法。
然后,还有一个要注意的:一旦看到全是小写字母就想到可以枚举,这个很重要,毕竟就26个。
然后,我们就枚举,但是这题的难点在于,我们选出的子数组中必定要含有\(-1\),这怎么处理呢?
我们在原有的\(dp[i]\)基础上,再设\(fdp[i]\),表示子数组中以\(i\)结尾且含有\(-1\)时的最大值,\(dp[0]=0,fdp[0]=-INF\)(因为不合法),推出以下转移方程:
class Solution {
public:
int largestVariance(string s) {
int l=s.length();
const int INF=1e9+7;
int ans=0;
for(int i=1;i<=26;i++){//最高
for(int j=1;j<=26;j++){//最低
if(i==j) continue;
vector<int>dp(l+1,0);
vector<int>fdp(l+1,0);
dp[0]=0,fdp[0]=-INF;
for(int k=1;k<=l;k++){
if(s[k-1]=='a'+i-1){
dp[k]=max(dp[k-1]+1,1);
fdp[k]=fdp[k-1]+1;
}
else if(s[k-1]=='a'+j-1){
dp[k]=max(dp[k-1]-1,-1);
fdp[k]=dp[k];
}
else{
dp[k]=max(dp[k-1],0);
fdp[k]=fdp[k-1];
}
}
for(int k=1;k<=l;k++) ans=max(ans,fdp[k]);
}
}
return ans;
}
};
1955. 统计特殊子序列的数目
特殊序列 是由 正整数 个 0 ,紧接着 正整数 个 1 ,最后 正整数 个 2 组成的序列。
- 比方说,
[0,1,2]和[0,0,1,1,1,2]是特殊序列。 - 相反,
[2,1,0],[1]和[0,1,2,0]就不是特殊序列。
给你一个数组 nums (仅 包含整数 0,1 和 2),请你返回 不同特殊子序列的数目 。由于答案可能很大,请你将它对 10^9 + 7 取余 后返回。
一个数组的 子序列 是从原数组中删除零个或者若干个元素后,剩下元素不改变顺序得到的序列。如果两个子序列的 下标集合 不同,那么这两个子序列是 不同的 。
示例 1:
输入:nums = [0,1,2,2]
输出:3
解释:特殊子序列为 [0,1,2,2],[0,1,2,2] 和 [0,1,2,2] 。
示例 2:
输入:nums = [2,2,0,0]
输出:0
解释:数组 [2,2,0,0] 中没有特殊子序列。
示例 3:
输入:nums = [0,1,2,0,1,2]
输出:7
解释:特殊子序列包括:
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
- [0,1,2,0,1,2]
提示:
1 <= nums.length <= 10^50 <= nums[i] <= 2
Solution
这道题其实是典题,考虑\(0\),\(01\)和\(012\)的贡献法即可.
class Solution {
public:
int countSpecialSubsequences(vector<int>& nums) {
int n=nums.size();
const int INF=1e9+7;
long long mx1=0,mx2=0,mx3=0;
vector<int>ma(n+1,0);
ma[0]=1;
for(int i=1;i<=n;i++){
ma[i]=ma[i-1]*2;
ma[i]%=INF;
}
for(int i=0;i<=n-1;i++){
if(nums[i]==0) mx1++;
if(nums[i]==1){
mx2=(mx2*2)%INF+(ma[mx1]-1)%INF;
mx2%=INF;
}
if(nums[i]==2){
mx3=(mx3*2)%INF+mx2%INF;
mx3%=INF;
}
}
return mx3;
}
};
LCP 19.秋叶收藏集调整问题
小扣出去秋游,途中收集了一些红叶和黄叶,他利用这些叶子初步整理了一份秋叶收藏集 leaves,字符串 leaves 仅包含小写字符 r 和 y,其中字符 r 表示一片红叶,字符 y 表示一片黄叶。出于美观整齐的考虑,小扣想要将收藏集中树叶的排列调整成「红、黄、红」三部分。每部分树叶数量可以不相等,但均需大于等于 1。每次调整操作,小扣可以将一片红叶替换成黄叶或者将一片黄叶替换成红叶。请问小扣最少需要多少次调整操作才能将秋叶收藏集调整完毕。
示例 1:
输入: leaves = "rrryyyrryyyrr"
输出: 2
解释:调整两次,将中间的两片红叶替换成黄叶,得到 "rrryyyyyyyrr"
示例 2:
输入: leaves = "ryr"
输出: 0
解释:已符合要求,不需要额外操作
提示:
3 <= leaves.length <= 10^5leaves中只包含字符'r'和字符'y'
Solution
我的想法是设\(dp[i][j]\),\(dp[i][0]\)表示这个字母作为左边r的结尾的时候,需要在左边调整多少个,\(dp[i][2]\)表示这个字母作为右边r开头的时候,需要在右边调整多少个,然后得到以下转移方程:
然后,最终答案的形式是这样:
注意到这里可以用前缀和优化,于是就过了
class Solution {
public:
int minimumOperations(string leaves) {
int l=leaves.length();
const int INF=1e9;
vector<int>pre(l+1,0);
for(int i=1;i<=l;i++){
if(leaves[i-1]=='r') pre[i]=pre[i-1];
else pre[i]=pre[i-1]+1;
}
if(pre[l]==0) return 1;
vector<int>fpre(l+1,0);
for(int i=1;i<=l;i++){
if(leaves[i-1]=='y') fpre[i]=fpre[i-1];
else fpre[i]=fpre[i-1]+1;
}
vector<vector<int>>dp(l+1,vector(3,0));
for(int i=1;i<=l;i++){
if(leaves[i-1]=='r'){
dp[i][0]=pre[i];
if(pre[i]==0) dp[i][2]=INF;
else dp[i][2]=pre[l]-pre[i];
}
else{
dp[i][0]=pre[i];
if(pre[i]>1) dp[i][2]=pre[l]-pre[i-1];
else dp[i][2]=INF;
}
}
int ans=INF;
int mx=dp[1][0]-fpre[1];
for(int i=3;i<=l;i++){
mx=min(mx,dp[i-2][0]-fpre[i-2]);
ans=min(ans,mx+dp[i][2]+fpre[i-1]);
}
return ans;
}
};
但是,答案的做法更为简洁。\(dp[i][0]\)表示从头到这里是全为r的最小修改次数,\(dp[i][1]\)表示从头到这里一段r一段y的最小修改次数,\(dp[i][2]\)表示为三段的最小修改次数,这个解法也很巧妙。
下学期,也请各位继续关注:
《System beats!》
《大二病也要学离散!》
《数算の旅》
《某信息学的概率统计》
还有——
《我的算法竞赛不可能这么可爱》
本期到此结束!


浙公网安备 33010602011771号