239. 滑动窗口最大值/76. 最小覆盖子串

文章介绍了如何使用滑动窗口算法解决两个问题:给定整数数组计算最大值,以及在一个字符串中查找包含另一个字符串所有字符的最小子串。通过单调队列和哈希表的数据结构优化,实现高效求解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

思路:用单调队列处理,滑动窗口的加入元素和移除元素相当于队列的pop和push,找出最大元素相当于单调队列的中最大值,也就是队列的头元素(que.front())。下一步设计一个队列,包含出队、入队、寻找最大值的操作。

入队:要维护单调队列,当元素入队时,首先和队尾元素比较,如果大于队尾元素,队尾元素出队,直到入队元素小于等于队尾元素的数值为止。

例子如下:通过这种操作可以保持队列中元素单调,滑动窗口最大值就等于队列头元素

出队:其实上面入队操作已经有元素出队了,只需要判断滑动窗口移除元素等于队列首元素时,再将队首元素出队,为什么呢

如下图:第一步到第二步时候元素1需要移除,但在元素3入队时就将1出队了,因为我们需要保持队列单调;第二步到第三步元素3需要移除,此时队首元素出队就可以,再让元素5入队(元素5入队时候会将-1,-3顶出)

最后会发现当前元素需要移除时,要不它就等于队首元素,要不它就在我们自定义入队操作时就出队了。

代码:

class Solution {
private:
    class Myque{//自定义单调队列
    public:
        deque<int> que;
        void pop(int value){//移除元素
            if(!que.empty()&&value==que.front()){
                 que.pop_front();
            }
        }
        void push(int value){//加入元素
            while(!que.empty()&&value>que.back()){//如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
                que.pop_back();
            }
            que.push_back(value);
        }
        int maxvalue() {
            return que.front();
        }
    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Myque que;
        vector<int> n;
        for(int i=0;i<k;i++){
            que.push(nums[i]);
        }
        n.push_back(que.maxvalue());//先记录最大值
        for(int i=k;i<nums.size();i++){
            que.pop(nums[i-k]);
            que.push(nums[i]);
            n.push_back(que.maxvalue());
        }
        return n;
    }
};

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

思路:通过滑动窗口,right移动直到包含t中全部元素,接下来收缩窗口,移动left,始终保持滑动窗口包含t,找到最小的窗口的长度就是答案。

        下一步就是解决怎么确定滑动窗口中包含t,我们通过哈希表记录t中元素出现的个数,再记录滑动窗口移动过程中记录t中元素出现个数,再进行比较。

        right:对于right而言,每次向右移动之后,先判断加入的元素是否出现在t的哈希表中,如果在,滑动窗口的哈希表中对应的元素个数就加一。

        left:如果t中元素出现个数在当前滑动窗口中出现个数一样,就可以收缩left

代码:

class Solution {
public:
    unordered_map<char,int> tstr,sstr;//两个哈希表
    bool check()//比较函数
    {
        for(auto tchar : tstr)
            {
                if(tchar.second > sstr[tchar.first]) return false;//注意这里的判断条件是大于
                //只要sstr中元素的second值不小于tchar中对应元素的second值就行
                //也就是判断滑动窗口中出现t中元素的个数是否和t的哈希表记录的一样
            }
        return true;
    }
    string minWindow(string s, string t) {
        //情况一,t的长的大于s
        int n1 = s.size(),n2 = t.size();
        if(n1<n2) return "";
        int len = INT_MAX;//最小窗口的长度
        int mleft=0;//最小窗口的左边界
        int mright=-1;//最小窗口的右边界
        //构造t的哈希表
        for(auto tchar : t)
            ++tstr[tchar];
        int left = 0,right = -1;//滑动窗口的左右两端指针
        while(right<n1){
            right++;//右边界移动
            if(tstr[s[right]]){
                if(sstr[s[right]]){
                    sstr[s[right]]+=1;//当前元素已经出现过
                }else{
                    sstr[s[right]]=1;//第一次出现
                }
            }
            while(check()){
                int m=right-left+1;//当前滑动窗口的长度
                if(m<len){//更新最小窗口
                    mleft=left;
                    mright=right;
                    len=m;
                }
                if(tstr[s[left]]){//左边界的元素在t中
                    sstr[s[left]]-=1;//左边界收缩时就需要减去滑动窗口哈希表中元素出现的次数
                }
                left++;//匹配成功才需要收缩左边界
            }
        }
        if (len == INT_MAX) {//如果最小窗口从来没有变化,按题目意思返回空
            return "";
        }
        return s.substr(mleft,len);//截取记录的最小窗口
    }
};

``` #include <bits/stdc++.h> // 包含所有标准库头文件(竞赛常用,但工程中不推荐) using namespace std; // 使用标准命名空间 int main() { string s; cin >> s; // 读取输入字符串 int count[128] = {0}; // ASCII字符计数器(初始化为0) int distinct_size = 0; // 记录不同字符的种类数 // 统计字符出现次数和种类数 for (int i = 0; i < s.length(); i++) { if (count[s[i]]++ == 0) { // 若该字符首次出现 distinct_size++; // 种类数+1 } } int k = s.length() / distinct_size; // 计算平衡度k(每个字符理论最大出现次数) int total = 0; // 总冗余度(所有字符超出k次的部分之和) // 计算各字符冗余度(实际出现次数 - k,负数取0) for (int c = 0; c < 128; c++) { count[c] = (int)fmax(count[c] - k, 0); // 冗余度= max(出现次数-k, 0) total += count[c]; // 累加总冗余度 } // 若无需修改(所有字符出现次数正好是k次) if (total == 0) { cout << "-1 0"; return 0; } int start = -1; // 记录目标子串起始索引 int minLen = INT_MAX; // 记录目标子串最小长度 // 滑动窗口(尺取法)寻找最小覆盖子串 // 窗口范围 [l, r],右指针r主动移动,左指针l被动追赶 for (int l = 0, r = 0; r < s.length(); r++) { // 若当前字符是冗余字符(count[s[r]]初始值>0) if (count[s[r]]-- > 0) { // 减少该字符的冗余需求 total--; // 总冗余度减少 } // 当窗口覆盖所有冗余字符时(total=0) while (total == 0) { int len = r - l + 1; // 更新最小窗口 if (len < minLen) { minLen = len; start = l; } // 左指针右移,恢复字符状态 if (count[s[l]]++ >= 0) { // 若移出的是冗余字符 total++; // 总冗余度增加 } l++; } } // 输出结果 printf("%d %d", start, minLen); return 0; }```if (count[s[l]]++ >= 0) { // 若移出的是冗余字符 total++; // 总冗余度增加 }为什么这里是>=0判断是多余字符,不应该是>0吗?
04-03
<think>嗯,用户问的是这段代码中的一个条件判断:`if (count[s[l]]++ >= 0)`,为什么这里用>=0来判断是否是冗余字符,而不是>0。我需要仔细分析这段代码的逻辑,特别是count数组在这部分的作用。 首先,我得回忆整个程序的结构。这个程序的目标是找出字符串中最小的子串,使得修改这个子串可以使得所有字符的出现次数不超过k次。这里的k是字符串长度除以不同字符的种类数。程序中用了滑动窗口的方法来寻找这个子串。 在统计冗余度的时候,程序将每个字符的出现次数减去k,如果结果为负数就取0,然后累加得到总冗余度total。如果total为0,说明无需修改,否则进入滑动窗口的处理。 滑动窗口部分,当右指针r移动时,如果当前字符是冗余的(即count[s[r]]初始值>0),那么减少该字符的冗余需求,并减少total。当total为0时,开始移动左指针l,尝试缩小窗口。此时,当左指针移动时,需要恢复字符的状态,如果移出的字符是冗余字符,则需要增加total。 现在问题出在左指针移动时的条件判断:`if (count[s[l]]++ >= 0)`。用户认为这里应该是>0而不是>=0,因为只有当count[s[l]]在恢复之前是正数的时候,才说明这个字符是冗余的,应该增加total。 我需要仔细分析count数组在滑动窗口过程中的变化。首先,在计算冗余度的时候,count数组已经被处理为每个字符的冗余度,即max(原次数 -k, 0)。所以在滑动窗口开始时,count数组中的值都是非负的,表示每个字符还需要减少多少次冗余。 当右指针r移动时,遇到字符s[r],如果count[s[r]] >0,说明这个字符是需要被包含在冗余处理中的,所以total减少。同时,count[s[r]]被减1。此时,count[s[r]]的值可能变为0或者负数吗?例如,假设原来的count是2,减1后是1,再减一次是0,这时候如果再减就会变成负数。不过这可能发生在多个相同字符连续出现的情况下。 当左指针移动时,恢复count[s[l]]的值,也就是每次左移时,count[s[l]]会加1。这里的关键是,当左指针移出一个字符时,如果这个字符在之前被右指针处理过,即它的count被减过,那么现在恢复它的时候,如果原来的count在减之前是正数,那么恢复后的count可能变为0或正数。 但具体在这个条件判断中,当左指针移动时,执行的是count[s[l]]++,并且判断是否>=0。假设原来的count[s[l]]在减到0之后,左指针移动时,会将其恢复为1。这时候,count[s[l]]在++之前是-1吗? 可能我之前的理解有误。这里需要明确count数组在滑动窗口过程中的含义。可能在滑动窗口阶段,count数组的含义发生了变化,不再是冗余度,而是用来跟踪当前窗口中需要覆盖的冗余字符的数量。 比如,初始时,count数组中的冗余度是正数,表示需要覆盖的次数。当右指针遇到一个字符,如果它的count>0,说明这个字符是冗余的,需要被包含在窗口中,所以total减少。然后count[s[r]]减1,表示这个字符还需要被覆盖的次数减少了。 当左指针移动时,如果count[s[l]]在恢复之前是>=0,那么说明这个字符在之前的处理中被当作冗余字符处理过。例如,假设原来的count是3,右指针处理后变成2,1,0,-1等。当左指针移出该字符时,count[s[l]]会加1,所以如果原来的count是0,加1后变为1,这时候需要判断是否应该增加total。 但这里的条件判断是,在恢复之前,如果count[s[l]] >=0,那么total需要增加。这似乎有点矛盾,因为如果count[s[l]]在恢复之前是0的话,加1之后变成1,这时候可能意味着这个字符的冗余需求被重新激活,所以需要total增加。 但为什么是>=0而不是>0呢?假设在左指针移动时,count[s[l]]的值是-1,那么恢复之后变成0,这时候是否会影响total? 举个例子,假设某个字符的冗余度初始为2。当右指针经过两次这个字符时,count会减到0,此时total可能变为0,窗口开始收缩。当左指针移动时,第一次遇到这个字符,count[s[l]]是0,执行count[s[l]]++之后变成1。此时,条件判断是count[s[l]]++ >=0,即原来的值是0,满足条件,所以total++。这样,total从0变为1,窗口不再满足条件,左指针停止移动。 但此时,这个字符的冗余度原本是2,现在窗口中已经处理了两次,当左指针移出一个,那么冗余度应该回到1,所以需要total增加,这样窗口就需要继续寻找下一个满足条件的右指针。 因此,当count[s[l]]在恢复之前是0的时候,实际上它已经被处理过了,但移出左指针的时候需要将其视为冗余字符的恢复,所以需要增加total。 也就是说,在左指针移动时,只要count[s[l]]在恢复之前的值是>=0,说明这个字符在初始的冗余列表中(即count初始值>0),并且在窗口中被处理过。所以当移出它的时候,需要恢复它的count,并且如果原来的count是>=0,那么说明这个字符是冗余的,需要增加total。 如果原来的count是负数,那说明这个字符原本的冗余度是0,不属于需要处理的冗余字符,所以在左指针移出时不需要调整total。 因此,这里的条件判断是>=0,而不是>0,因为当count[s[l]]在恢复前是0时,说明这个字符在初始时是冗余的(count初始值>0),并且在窗口中已经被处理到count变为0。当左指针移出该字符时,count恢复为1,此时需要将total增加,因为该字符的冗余需求又出现了。 所以,正确的条件应该是>=0,而不是>0。例如,当count[s[l]]原来是1,右指针处理后变为0,此时移出左指针时,count变成1,这需要触发total的增加。而如果原来的count是0,右指针处理后变为-1,此时移出左指针时,count恢复为0,但因为原来的冗余度是0,所以不需要增加total。因此,只有当count[s[l]]在恢复之前>=0时,才说明这个字符是冗余的,需要处理。 可能我之前误解了count数组在滑动窗口阶段的意义。实际上,在滑动窗口阶段,count数组被用来记录当前还需要覆盖的冗余次数。当右指针遇到冗余字符时,减少该次数,当次数变为0或负数时,说明该字符已经被覆盖足够次数。而当左指针移出字符时,恢复该次数,如果恢复后的次数变为正数,说明该字符的冗余次数再次需要被覆盖,因此需要增加total。 所以,在左指针移动时,执行count[s[l]]++,然后判断原来的值(即++之前的值)是否>=0。如果原来的值是0,那么++之后变为1,这时候需要将total增加,因为该字符的冗余次数又需要被覆盖了。而如果原来的值是负数,说明这个字符在初始时并不是冗余的,所以不需要处理。 因此,条件判断是>=0,而不是>0,因为当原来的值是0时,恢复后变为1,这时候该字符的冗余需求重新出现,需要total增加。</think>这是一个关于滑动窗口算法中边界条件处理的重要问题。我们通过分步解析来理解这个判断条件的必要性: **代码逻辑背景:** 1. `count[]`数组在预处理后存储每个字符的冗余度(实际出现次数 - 平衡度k,负数取0) 2. `total`表示所有字符超出k次的总冗余度 3. 滑动窗口的目标是找到包含所有冗余字符的最小区间 **条件判断的演变过程:** ```cpp if (count[s[l]]++ >= 0) { // 判断条件 total++; } ``` **逐步推演:** 1. **初始状态**(滑动窗口开始前): - 每个冗余字符的`count[c] > 0`(因为冗余度=原次数-k) - 非冗余字符的`count[c] = 0` 2. **右指针处理逻辑**: ```cpp if (count[s[r]]-- > 0) { // 进入窗口时 total--; } ``` - 当字符`s[r]`是冗余字符时,减少其冗余需求 - 处理后`count[s[r]]`可能变为:`正数 → 0 → 负数` 3. **左指针处理时的关键点**: - 我们需要检测移出窗口的字符是否是**原始冗余字符**(即预处理后count[c]>0的字符) - `count[s[l]]++`操作实际上是在**恢复该字符的冗余需求** **数值变化模拟:** | 场景描述 | 预处理后count值 | 右指针处理后 | 左指针操作前 | 条件判断 | 是否影响total | |--------------------------|-----------------|--------------|--------------|----------|---------------| | 原始冗余字符(需减少2次)| 2 → 1 → 0 | 0 | 0 | 0 >= 0 ✔ | total++ | | 非冗余字符 | 0 → -1 | -1 | -1 | -1 >= 0 ✖| 不影响 | **为什么必须是 >= 0:** - 当某个字符的冗余需求被完全满足时(`count[c]`经过右指针处理后变为0) - 移出该字符会导致冗余需求重新出现(恢复为1) - 此时必须通过`total++`反映这种变化,否则会漏判这种情况 **错误使用 > 0 的后果:** ```cpp // 错误写法示例 if (count[s[l]]++ > 0) { // 漏判 count[s[l]] == 0 的情况 total++; } ``` - 当字符的冗余需求刚好被满足(count值减到0)时 - 移出该字符不会触发total++ - 导致窗口错误地保持total=0状态,无法正确收缩 **结论:** `>= 0`的判断确保了所有曾经是冗余字符(预处理后count[c]>0)的情况都会被捕获,即使其冗余需求在窗口扩展阶段被完全满足(count值减到0)。这保证了滑动窗口收缩时能正确感知冗余需求的重新出现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值