当前位置: 首页 > ds >正文

「越短越合法」型滑动窗口

也被称为 ​​「最短满足条件子串」问题。其核心特点是:​所需满足的条件(合法性)往往更容易在较短的子串中得到满足

核心思想:​

  1. 1.目标:​​ 在一个序列(通常是字符串或数组)中,寻找长度最小的连续子区间(窗口)​,使得该子区间满足特定的条件。
  2. 2.​关键观察:​​ ​如果一个较长的子串满足条件,那么它内部(很可能)存在一个更短的子串也满足条件。​​ 更通俗地说,​窗口变短通常不会让满足条件变得更困难,反而可能更容易满足条件。​
  3. 3.​策略:​​ 基于上述观察,采用收缩窗口左边界的策略来尝试找到更小的合法窗口,在保持合法性的前提下追求最小长度。

为什么叫“越短越合法”?​

这个名称源于这类问题的一个关键特性:对于满足条件的窗口,​缩短窗口长度通常有助于维持或更容易达到条件。​​ 这通常与条件的性质有关:

  • 条件对长度敏感(通常要求资源更少):​
    • 最小覆盖子串:​​ 要求子串包含 t中所有字符(出现频次≥ t中的频次)。更长的子串更容易包含额外的无关字符,而较短的子串只要能覆盖必需的字符就是合法的。缩短长度可以去除冗余字符,只要不破坏覆盖性。
    • 最小乘积/K 数的最小子数组:​​ 寻找乘积≥K(或元素和≥K)的最小窗口。由于乘积或和是累加的,较长的窗口更容易达到较大的值。一旦一个窗口达到了 K,缩短它(减小乘积或和)有时(特别是在元素为正数时)​可能会让值低于 K(打破条件),但也可能仍然≥K(保持合法)​。收缩左边界(即尝试缩短)就是在探索是否能在保持值≥K的前提下让窗口更短
  • 直观理解:​​ 想象你要凑够某种“资源”(字符覆盖、乘积、和值)才能满足条件。一个大的窗口(资源仓库)更容易凑够资源,满足条件。但满足条件后,你怀疑可能只用仓库的一部分(更小的窗口)就凑够了所需资源。收缩左边界就是一点点清理你的仓库,看看最少的资源在哪里。

算法框架(模板):​

def minWindow(s: str, t: str) -> str:# 1. 初始化:left = 0min_len = float('inf')  # 记录最小长度result = ""  # 记录结果# 创建计数器 (counter) 和条件检查器 (need)# (具体形式取决于问题:字符频次、元素乘积和、总和等等)# 例如,对于覆盖子串:from collections import Counterneed = Counter(t)  # t中每个字符需要的数量window = Counter()  # 窗口中当前字符的数量valid = 0  # 计数器:窗口中有多少种字符的数量已经满足need的要求 (window[char] >= need[char])# 2. 移动右指针 (right), 扩大窗口for right in range(len(s)):char_right = s[right]# 更新窗口数据 (window, 累加计数器等)# 例如,覆盖子串:if char_right in need:window[char_right] += 1if window[char_right] == need[char_right]:valid += 1  # 该字符数量已达要求# 3. 判断当前窗口是否满足条件 (内层循环收缩左边界)while valid == len(need):  # 当条件满足时 (对于覆盖子串,即所有所需字符频次都达标)# 4. 更新结果:尝试用当前[Left, right]窗口更新最小窗口记录if right - left + 1 < min_len:min_len = right - left + 1result = s[left:right+1]  # 或记录left, right位置# 5. 收缩窗口:移动左指针 (left), 缩小窗口 (尝试找更小的窗口)char_left = s[left]# 更新窗口数据 (window, 减少计数器等)# 例如,覆盖子串:if char_left in need:if window[char_left] == need[char_left]:valid -= 1  # 即将移除的字符是"关键"字符,数量即将低于要求window[char_left] -= 1left += 1  # 左指针右移,收缩窗口# 6. 返回结果return result

关键步骤讲解:​

  1. 1.初始化:​​ 设置两个指针 leftright,通常都从起始位置(0)开始。初始化记录最小长度和最终结果的变量。根据问题初始化必要的计数器(如 need, window, valid)或累加器(product, curr_sum)。
  2. 2.​移动右指针 (right),扩大窗口:​
    • 每次循环将 right向右移动一位。
    • 将新元素 s[right]纳入窗口。
    • 更新窗口状态信息:​​ 更新频率计数器、和、积等,判断新元素是否影响了条件满足的关键计数器(如 valid)。
  3. 3.​判断条件满足 & 收缩左边界(核心体现“越短越合法”):​
    • 当 ​valid达到要求​(覆盖子串中所有字符都满足频次)或 ​curr_sumK​ 或 ​productK​(当元素为正时),表示当前窗口 [left, right]满足条件
    • 当条件满足时,进入内层 while循环(这是关键)。这表明我们找到了一个合法窗口,现在尝试通过收缩左边界 (left右移)​​ 来找到可能存在的更小的合法窗口,这正是利用了“越短越可能合法”的特性。
      • 更新结果:​​ 在循环开始处(收缩前)检查当前窗口长度是否是最小的,更新最小长度和结果。
      • 移动左指针 (left),收缩窗口:​​ 将 left指向的元素移出窗口。
      • 更新窗口状态信息:​​ 更新频率计数器、和、积等,并更新条件状态检查器(如 valid)。如果移出的关键元素导致条件不再满足(例如,某个字符频次低于要求了,或者和/积跌破阈值了),则退出内层 while循环(因为继续收缩会打破条件)。否则,继续收缩尝试寻找更短窗口。
  4. 4.返回结果:​​ 外层循环结束后,返回找到的最短合法子串(或长度/位置)。

​​“越短越合法”在本算法中的体现:​

  • 步骤 3 的内层 while循环是核心体现。一旦窗口满足条件,我们立即进入循环,尝试收缩左边界(使窗口变短)。这是因为:
    • 在当前窗口 [left, right]满足条件的前提下,​更小的窗口 [left+1, right]可能也满足条件
    • 不断收缩左边界,直到窗口刚好不再满足条件为止。那么收缩前一刻的窗口[left-1, right](内层循环更新结果后收缩然后打破条件的那个状态)就是以当前 right结束的最短合法子串
    • 这个过程不断寻找以每个可能的 right为结束位置的最短合法子串,最终从这些候选中选出全局最短的。

典型例题:​

  1. 1.​76. 最小覆盖子串 (Minimum Window Substring):​
    • •​给定字符串 st,在 s中找出涵盖 t所有字符的最小子串。​​ 经典中的经典,完美符合“越短越合法”——覆盖 t后,去除多余的字符,只要不破坏覆盖性,更短的窗口依然合法。
  2. 2.209. 长度最小的子数组 (Minimum Size Subarray Sum):​
    • •​给定正整数数组 nums和目标值 target,找出和 ≥ target的长度最小的连续子数组。​​ 满足“和 ≥ target”条件后,收缩左边界尝试缩短窗口(和会减少),但可能​(如果左边小数被移出,剩余和仍≥target)依然合法。需要尝试到刚好不合法为止。当 nums全为正数时,是典型的“越短越可能不再合法但需尝试”,符合广义思路。
  3. 3.​713. 乘积小于 K 的子数组 (Subarray Product Less Than K):​​ (对比理解)
    • •这个题目要求的是 ​乘积 < K的子数组个数,虽然也用了滑动窗口,但其特性是“越长越容易非法”(因为正数数组乘积随长度增大),算法核心是计算以 right结尾的合法窗口数量。这与“越短越合法”的目标 ​≥ K找最小窗口​ 形成了鲜明对比。注意区分问题要求的是 小于 K还是 大于等于 K以及是最小窗口还是计数。

注意事项:​

  • 初始化:​​ 结果初始值要设为一个明显无效的值(如空字符串或极大数)。
  • 指针边界:​​ 确保 left不超过 right。通常内层 while循环条件是 while (valid == condition and left <= right)或其他确保左边界不越界的条件。
  • 频率计数:​​ 对于字符覆盖问题,用字典(Counter)维护 need(所需频次)和 window(当前频次)非常方便。
  • 数值判断:​​ 对于求和、求积问题,注意数值溢出的可能性(特别是乘积),并且要理解当元素包含负数或0时,收缩窗口不一定导致和/积减小,问题会复杂很多(经典模板主要适用全正数情况)。
  • 复杂度:​​ 由于每个元素最多被 leftright各访问一次,时间复杂度通常是 O(n)O(n + m)(覆盖子串初始化 need可能需要 O(m))。空间复杂度通常是 O(字符集大小)O(1)

总结:​

「越短越合法」型滑动窗口问题的核心在于,当找到一个满足条件的(可能较大的)窗口后,立即尝试通过收缩左边界来寻找是否存在更短但依然满足条件的窗口。其算法模板(含内层 while循环收缩)清晰地体现了这一策略。掌握好模板,理解 need/window/valid的维护,并灵活应用到类似问题上,是解决这类问题的关键。

 

http://www.xdnf.cn/news/18651.html

相关文章:

  • 解释一下,Linux,shell,Vmware,Ubuntu,以及Linux命令和shell命令的区别
  • 111、【OS】【Nuttx】【周边】效果呈现方案解析:-print0 选项
  • Linux操作系统编程——网络
  • 第二阶段WinFrom-6:文件对话框,对象的本地保存,序列化与反序列化,CSV文件操作,INI文件读写
  • 08.21总结
  • Claude Code 三类.md文件
  • Day2--HOT100--283. 移动零,11. 盛最多水的容器,15. 三数之和
  • PCB电路设计学习2 元件原理图封装的添加 手工设计元件封装
  • 大型前端项目如何实现css 隔离:利用浏览器原生的 Shadow DOM 完全隔离 DOM 结构与样式...
  • Linux 下的网络编程
  • 学习嵌入式的第二十四天——数据结构——队列和树
  • Git 提交除某个文件外的其他所有文件
  • 微信开发者工具:更改 AppID 失败
  • 嵌入式-EXTI的工作原理和按钮实验-Day19
  • 我从零开始学习C语言(13)- 循环语句 PART2
  • QT-窗口类部件
  • K8S高可用集群
  • K8s的相关知识总结
  • 如何理解面向过程和面向对象,举例说明一下?
  • Qt5 的跨平台开发详细讲解
  • 计算机毕设选题推荐 基于Spark的家庭能源消耗智能分析与可视化系统 基于机器学习的家庭能源消耗预测与可视化系统源码
  • 告别第三方流氓工具,如何实现纯净系统维护
  • DIC技术极端环境高温案例分享——从1600℃的锆合金力学性能测试到3000℃变形测试的DIC测量
  • 手机、电脑屏幕的显示坏点检测和成像原理
  • k8s----学习站点搭建
  • C++显示类型转换运算符static_cast使用指南
  • 贪吃蛇--C++实战项目(零基础)
  • 大模型微调:从理论到实践的全面指南
  • 【链表 - LeetCode】19. 删除链表的倒数第 N 个结点
  • Laravel 使用阿里云OSS S3 协议文件上传