最终要解决的难题:
此题左老师的讲解非常好,但我总觉得可以根据最基本的滑动窗口推出来,大家就当看个乐子吧。
1.最基本的滑动窗口模型:
1)求一个数组中区间和大于等于(或大于)某个数的最小子区间
2)求一个数组中区间和小于等于(或小于)某个数的最大子区间
1)外层循环,遍历子窗口最右边的范围,表示以j为最右边的区间,如果当前区间sum大于s(选定的那个数字),就开始向右移动左边范围直到sum小于s,在while中更新满足要求的子区间长度
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位 置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
2)求小于等于s的最长子数组,只需要把原先在while里面的对答案的更新移动到while外面即可
class Solution {
public:
// 求和 <= s 的最长连续子数组的长度
int maxSubArrayLen(int s, std::vector<int>& nums) {
int result = 0; // 初始化为0,因为我们要找最大长度。如果找不到,长度就是0。
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
for (int j = 0; j < nums.size(); j++) {
// 1. 扩张窗口:将右边的元素包含进来
sum += nums[j];
// 2. 收缩窗口:当窗口和 "超标" (sum > s) 时,
// 必须从左边收缩,直到窗口重新 "达标" (sum <= s)。
while (sum > s) {
sum -= nums[i];
i++;
}
// 3. 更新结果:在当前循环中,[i, j] 这个窗口是目前已知的、
// 以 j 结尾的、最长的、和 <= s 的子数组。
// 我们用它的长度来更新最终结果。
int currentLength = j - i + 1;
result = std::max(result, currentLength);
}
return result;
}
};
2.由此衍生出:求区间和等于某个数的最长,最短子序列
最短:
void findSubarrayWithSum1(int s, std::vector<int>& nums) {
if (nums.empty()) {
std::cout << "Min Length: 0, Max Length: 0" << std::endl;
return;
}
int minLength = INT_MAX;
//int maxLength = 0;
int sum = 0;
int i = 0; // 左指针
int k = 0;
for (int j = 0; j < nums.size(); ++j) {
sum += nums[j];
// 当 sum >= s 时,需要处理
while (sum >= s && i <= j) {
// 如果正好等于 s,我们找到了一个解
if (sum == s) {
int currentLength = j - i + 1;
minLength = min(currentLength,minLength);
}
// 收缩窗口以寻找下一个可能的解
//nums[i]==0,停在最后一个满足要求的位置上
sum -= nums[i];
i++;
}
}
std::cout << "For Non-Negative Numbers:" << std::endl;
std::cout << "Min Length: " << (minLength == INT_MAX ? 0 : minLength) << std::endl;
// std::cout << "Max Length: " << maxLength << std::endl;
}
最长:
void findSubarrayWithSum(int s, std::vector<int>& nums) {
if (nums.empty()) {
std::cout << "Min Length: 0, Max Length: 0" << std::endl;
return;
}
int maxLength = 0;
int sum = 0;
int i = 0; // 左指针
for (int j = 0; j < nums.size(); ++j) {
sum += nums[j];
// 当 sum >= s 时,需要处理
while (sum > s && i <= j) {
// 如果正好等于 s,我们找到了一个解
// 收缩窗口以寻找下一个可能的解
sum -= nums[i];
i++;
}
if (sum == s) {
int currentLength = j - i + 1;
maxLength = std::max(maxLength, currentLength);
}
}
std::cout << "For Non-Negative Numbers:" << std::endl;
//std::cout << "Min Length: " << (minLength == INT_MAX ? 0 : minLength) << std::endl;
std::cout << "Max Length: " << maxLength << std::endl;
}
这两者可以合并,写在一个for循环里。
3.继续衍生,能不能使用滑动窗口求出数组中子序列之和等于某个数的所有子集呢?
(其实更好的方式是前缀和,但是这样层层递进是为了解决最后的一个难题)
一个很自然的想法是,对于每一个右端点,都算出最长和最短的满足条件的子序列,然后两者相减再加1(假设可以重复),因为每个子序列必然有一个右端点,所以把所有右端点的满足要求的子序列都加起来即可。
int findSubarrayWithSum2(int s, std::vector<int>& nums) {
int ans = 0;
if (nums.empty()) {
std::cout << "Min Length: 0, Max Length: 0" << std::endl;
return 0;
}
int minLength = INT_MAX;
int maxLength = 0;
int sum1 = 0;
int sum2 = 0;
int i1 = 0; // 左指针
int i2 = 0;
for (int j = 0; j < nums.size(); ++j) {
//以j为结尾,区间和为n的最大与最小长度(考虑0)
minLength = INT_MAX;
maxLength = 0;
sum1 += nums[j];
sum2 += nums[j];
while (sum1 > s && i1 <= j) {
sum1 -= nums[i1];
i1++;
}
if (sum1 == s) {
int currentLength = j - i1 + 1;
maxLength = currentLength;
}
while (sum2 >= s && i2 <= j) {
// 如果正好等于 s,我们找到了一个解
if (sum2 == s) {
int currentLength = j - i2 + 1;
minLength = currentLength;
}
// 收缩窗口以寻找下一个可能的解
if(nums[i2] == 0 || sum2 > s){
//nums[i2]==0,停在最后一个满足要求的位置上
sum2 -= nums[i2];
i2++;
}else break;
}
//if(minLength == INT_MAX)continue;
if(minLength != INT_MAX){
//如果最小值不为空,当前以j为结尾,必有满足等于s的子集
ans += maxLength - minLength + 1;
printf("以%d结尾的子串数量为:",j);
cout << maxLength - minLength + 1 << endl;
}
}
return ans;
}
为什么在求最小时,非得加一个if不可呢?
举个例子:s = 5 nums = {1,2,2,0},j等于0,1时没有满足要求的子序列,j等于2是,最长最短的子序列都是{1,2,2},根据代码的逻辑,在j++之前,最长的左端点i1还是等于0,如果没有if判断,最短的左端点会i2会移动到1位置,随后j ++,继续计算3位置的最长最短子序列,你会发现,原本应该最长最短都是{1,2,2,0},但是因为i2已经到了1位置,代码会认为最短子序列不存在,minLength为INT_MAX。
加上if判断条件之后,会让i2停在最后一个满足要求的地方,避免出现上述情况。
4.种类数量问题
这种变体就相当于上面的大于等于s的最小子串,只不过使用一个hash表来看看是否满足条件罢了
如果当前子序列中的元素种类和数列大于等于t即可
class Solution {
public:
string minWindow(string s, string t) {
int a[200] = {0};//记录s
int b[200] = {0};//记录t
int have = 0,cnt = 0;
//初始化t字符串,记录其每个字符出现的次数和数量
for(int i = 0;i < t.size();i ++){
if(b[t[i]] == 0)cnt ++;
b[t[i]] ++;
}
int resStart = 0, resLens = INT_MAX;
int i = 0;
for(int j = 0;j < s.size();j ++){
if(b[s[j]] != 0){
a[s[j]] ++;
if(a[s[j]] == b[s[j]])have ++;
}
while(have == cnt){
if(j - i + 1 < resLens){
resLens = j - i + 1;
//substr(start,lens)
resStart = i;
}
if(b[s[i]] != 0){
a[s[i]] --;
if(a[s[i]] < b[s[i]]){
have --;
}
}
i ++;
}
}
return resLens == INT_MAX ? "" : s.substr(resStart,resLens);
}
};
5.最后解决开头的问题
种类问题(4)+求出字符种类数量大于等于某个字符串的全部子集(3)
就是4,3的结合,经过这么久的分析,也没啥特别的
class Solution {
public:
int subarraysWithKDistinct(vector<int>& nums, int s) {
//求出所有区间和等于s的子串数量
int ans = 0;
int a1[20006];
int a2[20006];
for(int i = 0;i < nums.size()+1;i ++){
a1[i] = 0;
a2[i] = 0;
}
int cnt1 = 0;
int cnt2 = 0;
if (nums.empty()) {
//std::cout << "Min Length: 0, Max Length: 0" << std::endl;
return 0;
}
int minLength = INT_MAX;
int maxLength = 0;
/*
int sum1 = 0;
int sum2 = 0;
*/
int i1 = 0; // 左指针
int i2 = 0;
for (int j = 0; j < nums.size(); ++j) {
//以j为结尾,区间和为n的最大与最小长度(考虑0)
minLength = INT_MAX;
maxLength = 0;
/*sum1 += nums[j];
sum2 += nums[j];*/
if(a1[nums[j]] == 0){
cnt1 ++;
}
a1[nums[j]] ++;
if(a2[nums[j]] == 0){
cnt2 ++;
}
a2[nums[j]] ++;
while (cnt1 > s && i1 <= j) {
//sum1 -= nums[i1];
if(--a1[nums[i1]] == 0){
cnt1 --;
}
i1++;
}
if (cnt1 == s) {
int currentLength = j - i1 + 1;
maxLength = currentLength;
}
while (cnt2 >= s && i2 <= j) {
// 如果正好等于 s,我们找到了一个解
if (cnt2 == s) {
int currentLength = j - i2 + 1;
minLength = currentLength;
}
// 收缩窗口以寻找下一个可能的解
if(a2[nums[i2]] > 1 || cnt2 > s){
//cout << i2;
//nums[i2]==0,停在最后一个满足要求的位置上
if(--a2[nums[i2]]==0)cnt2--;
i2++;
}else break;
}
//if(minLength == INT_MAX)continue;
//cout << "j:" << j << "i1:" << i1 << "i2:" << i2 << endl;
if(minLength != INT_MAX){
//如果最小值不为空,当前以j为结尾,必有满足等于s的子集
ans += maxLength - minLength + 1;
//printf("以%d结尾的子串数量为:",j);
//cout << maxLength << minLength << endl;
}
}
return ans;
}
};
6.写在最后
本人思考过程稍显复杂,若需要简单解法,可以观看左老师的视频
算法讲解049【必备】滑动窗口技巧与相关题目_哔哩哔哩_bilibili
其中的倒是第二题便是。