LeetCode 904 水果成篮问题
初读题时觉得摸不着头脑,没有理解本题考察内容,但在阅读过示例后逐渐理解此题考察内容
本题考查数组中满足只含两种元素的子数组最大长度
结合LeetCode 76最小覆盖子串的思路,尝试使用滑动窗口解决问题。
全流程
class Solution {
public:
int totalFruit(vector<int>& fruits) {
//左右边界
int i = 0,j = 0;
//可以采摘树的最大值
int ans = 1;
//树的种类
int nums = 1;
//边界值
int length = fruits.size();
//当前树的种类 设置为0是因为没有种类为0,避免重复
int temp = 0,temp1 = fruits[0];
//指针j的临时值
int tempj = 0;
//大循环
while(i <= j && j < length-1){
//条件起始
if(nums == 1){
//右边界右移一格
j++;
//比较当前树种类与上一棵树种类
if(fruits[j] != fruits[j-1]){
nums++;
//保存右边界j的当前位置
tempj = j;
//保存该不同树的种类
temp = fruits[j];
}
//判断当前长度是否超过原长度
if(j-i+1 > ans){
ans = j-i+1;
}
continue;
}
if(nums == 2){
//右移边界
j++;
//比较当前树的种类
if(fruits[j] != fruits[j-1]){
//前树的种类与之前两种树种类都不相同
if(fruits[j] == temp || fruits[j] == temp1){
//保存当前右边界 j 的位置
tempj = j;
}
//当前树的种类与之前两种树种类的其中之一相同
if(fruits[j] != temp && fruits[j] != temp1){
nums++;
}
}
//判断当前长度是否超过原长度,更新长度
if(nums != 3){
if(j-i+1 > ans) {
ans = j-i+1;
}
}
continue;
}
if(nums == 3){
//退回种类为2的情况
nums--;
//更新左边界
i = tempj;
//更新右边界
tempj = j;
//保存此时两棵树的种类
temp = fruits[i];
temp1 = fruits[j];
continue;
}
}
return ans;
}
};
第一部分 nums=1(树种类为1)
先将右边界右移一格,比较当前树与上一棵树种类
此时有两种情况:
1. 与上一颗种类不同
则种类数+1,此时我们要保存当前右边界 j 的位置, 原因我们回头再讲。
并保存该不同树的种类。
2.与上一颗种类相同
可以继续采摘,continue即可。
3.最后判断当前长度是否超过原长度,更新长度
if(nums == 1){
//右边界右移一格
j++;
//比较当前树种类与上一棵树种类
if(fruits[j] != fruits[j-1]){
nums++;
//保存右边界j的当前位置
tempj = j;
//保存该不同树的种类
temp = fruits[j];
}
//判断当前长度是否超过原长度
if(j-i+1 > ans){
ans = j-i+1;
}
continue;
}
第二部分 nums=2(树种类为2)
依旧先右移边界,然后进行比较当前树的种类,此时依然有两种情况
1.种类不同
此时依然有两种可能:
1.1 当前树的种类与之前两种树种类都不相同
即fruits[j] != temp && fruits[j] != temp1
那很简单 只需要将种类+1即可 交给nums=3的部分处理。
1.2 当前树的种类与之前两种树种类的其中之一相同
在这种情况下,我们依然可以继续摘水果,但注意 此时我们要保存当前右边界 j 的位置
2.种类相同
此种情况较为简单,依然可以继续摘水果。无需处理
3.最后判断当前长度是否超过原长度,更新长度
注意要进行树种类数判断,因为此时有树种类为3的情况,注意排除。
if(nums == 2){
//右移边界
j++;
//比较当前树的种类
if(fruits[j] != fruits[j-1]){
//前树的种类与之前两种树种类都不相同
if(fruits[j] == temp || fruits[j] == temp1){
//保存当前右边界 j 的位置
tempj = j;
}
//当前树的种类与之前两种树种类的其中之一相同
if(fruits[j] != temp && fruits[j] != temp1){
nums++;
}
}
//判断当前长度是否超过原长度,更新长度
if(nums != 3){
if(j-i+1 > ans) {
ans = j-i+1;
}
}
continue;
}
第三部分 nums=3(树的种类为3)
种类为3 说明当前第一次摘水果的尝试结束,需要进行第二次采摘尝试。
但起始位置不能回到最初,那么我们应该从哪里开始呢?应当从最近的两种树的种类开始
保存最后两种树的种类与位置,此时变量 tempj 就派上用场了,原来右边界 j 的位置变为当前左边界 i 的位置,此时 j 的位置变为新的右边界位置。
最后再保存此时两棵树的种类,使其返回种类为2的情况即可。
if(nums == 3){
//退回种类为2的情况
nums--;
//更新左边界
i = tempj;
//更新右边界
tempj = j;
//保存此时两棵树的种类
temp = fruits[i];
temp1 = fruits[j];
continue;
}
到此为止三种情况全部分析完毕,当左边界移动到右边界或右边界抵达尽头时,循环结束。
注意大循环条件为 j<length-1,防止 j++ 超过下标长度。
文章至此遗留的问题“为何保存右边界的位置” 这个问题的答案便变得清晰了,这其实就是移动窗口的核心,左右边界的移动问题。
回顾本题思路,左边界 i 起始为数组起点,右边界 j 起始与左边界相同,逐渐向右移动,期间进行条件判断与处理。当满足条件时,持续更新结果,右边界不断右移。直至条件破坏退出第一次迭代。
此时面临左边界更新,根据本题条件,左边界应更新为原右边界位置。
此题为寻找最大窗口,接下来一道题为最小窗口应用。
首先想到统计t字符串里的各字符的出现频数。因为题解要求子字符串数量必须不少于t中字符数量,因此在寻找窗口时需将频数纳入判断条件。
此题我们借助unordered_map即无序map容器帮助实现。
class Solution {
public:
string minWindow(string s, string t) {
//定义字符串t统计容器与窗口容器
unordered_map<char,int> need,window;
//统计t字符串频数 并写入容器
for(char c : t){
need[c]++;
}
//定义窗口左右边界
int left = 0,right = 0;
//定义符合最小容量个数
int valid = 0;
//定义最小字符串起始位置,字符串长度
int start = 0,len = INT_MAX;
//生成符合条件窗口
while(right < s.size()){
//提取右边界元素
char c = s[right];
//右边界右移
right++;
//元素判断
if(need.count(c)){
//t中包含则window中对应元素容量+1
window[c]++;
//如果该元素容量满足t中要求,则valid+1
if(window[c] == need[c]){
valid++;
}
}
//右边界持续右移直到所有元素容量均符合要求
while(valid == need.size()){
//获取当前子字符串长度并比较,小于则更新
if(right - left < len ){
len = right-left;
//同时定义当前最小窗口左边界位置
start = left;
}
//提取当前左边界元素
char d=s[left];
//左边界向右收缩窗口
left++;
//判断删去当前左边界元素后是否满足条件
if(need.count(d)){
//不满足则更新valid值,以跳出该while循环
if(window[d]==need[d]){
valid--;
}
//更新该元素容量
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
};
纵观本题思路,先从左至右逐渐扩大窗口直至满足条件,而后慢慢从左边界向右收缩并判断条件,如条件破坏,则右移右边界直至重新满足条件,而后左边界重新收缩…这样不断循环直至边界破坏。
对比水果问题我们发现,最小窗口问题是左边界不断右移更新结果,最大窗口时右边界不断右移更新结果,因此我们可以总结窗口移动问题核心为 :
大体流程为:
另外我们注意到滑动窗口问题更倾向于使用 while循环解决问题,因为相对于 for循环,while循环可以很好的控制复杂度,建立起良好的边界值移动法则,便可以避免双重for循环嵌套导致的复杂度暴涨至O(n2)的问题。
关于滑动窗口的总结就暂时到这里,后续有更多的题目或者心得会持续更新。