Conversation
|
|
||
| **注意:** | ||
|
|
||
| * 对于窗口,我们必须明确左右指针的具体含义,即指针指向的元素属不属于这个窗口。在本题中,我们定义的是 `[lp...rp]` 左闭右闭的窗口,即左指针与右指针指向的元素都属于窗口,各位也可以尝试定义 `[lp...rp)` 或 `(lp...rp]` 的窗口 |
There was a problem hiding this comment.
这个细节描述得很到位。可以在后面例题讲解的时候,再提示一下读者使用不同的「定义」实现代码,或者给出 2 版示例代码,让读者体会不同的定义在编码实现上的不同细节。
让读者把握遵守「循环不变量」的定义对于编码的重要性。这一点把握好了,相信读者是可以把这个技巧迁移到其它问题的编码中的。
zerotrac
left a comment
There was a problem hiding this comment.
一些总结性的建议:
-
看一下「中文排版指北」和 weiwei 已经上线的二分查找那一章作为参考,有挺多格式问题的;
-
可以在第一部分总结出一个滑动窗口的「模板」,伪代码、文字、流程图之类的可以。举个例子,比如做一个东西,表达「我们用两个指针维护一个窗口」「如果窗口符合要求,我们移动某一个指针」「如果窗口不符合要求,我们移动某一个指针」这样的一些概念;
-
类型 3 我觉得和滑动窗口还是有点差别的,可以放到「双指针」的话题里面去(如果有的话)
|
|
||
| **面对一个问题,如果我们一时想不到好的解法,可以先从暴力解入手。本题暴力解思路如下**: | ||
|
|
||
| * 遍历由索引 i 到索引 j 的所有的连续子数组 `nums[i...j]` |
There was a problem hiding this comment.
行末都需要有分号(列表环境)或者句号(列表环境的最后一行或者普通环境),下同
| * 对于所有满足条件的解,找出长度最小的解 | ||
|
|
||
|
|
||
| 可以看出,因为子数组的长度可以由 n 到 1,所以本解法的时间复杂度为:O (n^3) |
|
|
||
| 为了使每次计算都有意义,我们可以定义一个左指针 `lp` ,一个右指针 `rp` ,而 `nums[lp...rp]` 就是一个「窗口」。应用窗口的思路如下: | ||
|
|
||
| * 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; |
| 为了使每次计算都有意义,我们可以定义一个左指针 `lp` ,一个右指针 `rp` ,而 `nums[lp...rp]` 就是一个「窗口」。应用窗口的思路如下: | ||
|
|
||
| * 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; | ||
| * 若窗口间的元素之和大于等于于 s ,即 `sum(nums[lp...rp]) >= s` ,我们记录此时窗口的长度,并让左指针滑动一位,即 `lp+1` ,使得窗口间元素之和变小,若此时的窗口仍满足条件,则再记录窗口的长度,直到窗口不满足条件; |
|
|
||
| * 若窗口间的元素之和小于 s ,即 `sum(nums[lp...rp]) < s` ,则让右指针滑动一位,即 `rp+1` ,使得窗口间元素之和变大; | ||
| * 若窗口间的元素之和大于等于于 s ,即 `sum(nums[lp...rp]) >= s` ,我们记录此时窗口的长度,并让左指针滑动一位,即 `lp+1` ,使得窗口间元素之和变小,若此时的窗口仍满足条件,则再记录窗口的长度,直到窗口不满足条件; | ||
| * 返回记录的窗口中,最小的窗口长度 |
There was a problem hiding this comment.
三个问题:
-
第一:要说明每个指针像哪个方向滑动,因为不同的题目维护的窗口方向是不一样的;
-
第二:左指针向右滑动的话并不需要一直记录窗口的长度,因为窗口的长度在一直变小,只需要记录变到最小时的长度即可;
-
第三:这边拿一个
table来记录所有的窗口长度很反直觉,因为我们并不需要记录所有的窗口,而是只要找出最小的那一个就行了,因此只需要一个变量就足够了。
There was a problem hiding this comment.
我看到了后面你进行了优化,我觉得可以直接把不优化的部分全部删掉,因为真的很反直觉
|
|
||
| **注意:** | ||
|
|
||
| * 对于窗口,我们必须明确左右指针的具体含义,即指针指向的元素属不属于这个窗口。在本题中,我们定义的是 `[lp...rp]` 左闭右闭的窗口,即左指针与右指针指向的元素都属于窗口,各位也可以尝试定义 `[lp...rp)` 或 `(lp...rp]` 的窗口 |
There was a problem hiding this comment.
我觉得如果说了这个开闭区间的问题,就应该把代码也给出来,这整个 project 应该不需要留任何给读者思考的部分?
| def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: | ||
| window = set() # 使用一个集合表示窗口 | ||
|
|
||
| for i in range(len(nums)): |
There was a problem hiding this comment.
这里用 for i, num in enumerate(nums) 更合适
|
|
||
| 根据「木桶原理」,我们知道决定一个容器能盛多少水的因素有两个,一个是容器本身有多大,二是最短的那块木板有多长。而在这道题,数组中的元素值代表木板长度,两元素间的间距代表容器本身的大小,即窗口的大小。 | ||
|
|
||
| 可以发现,我们虽然不知道木板最长是多少,但窗口可以有多大是知道的——即数组长度 - 1那么大。因此我们不妨在一开始就把窗口设为最大,每次判断当前木板的长度和窗口大小能盛多少水,再逐渐将窗口缩小。至此,代码也就呼之欲出了。 |
There was a problem hiding this comment.
这个解释不尽人意,你这根本没有证明「为什么每次要换最短的那块木板」呀,有没有考虑过最短的那块换了之后更短了,举个例子,比如 [3, 1, 5, 4],一开始容量是 min(3,4) * 3 = 9,换了短的之后是 min(1,4) * 2 = 2,而换了长的之后是 min(3, 5) * 2 = 6。
换句话说,你这个说法给人的感觉就是「我们一开始将窗口设为最大,随后每一轮迭代中将窗口减小 1,并找到两块符合窗口要求的最长的木板」,但事实上这题的思路不是这个。
| @@ -0,0 +1,3 @@ | |||
| ## 总结 | |||
|
|
|||
| 其实滑动窗口问题还是以类型 1 居多,重点在于思考左右指针的具体含义、窗口状态的具体含义,以及窗口要如何变化,变化条件又是什么。明白了这些,也就彻底理解了滑动窗口。 | |||
| @@ -0,0 +1,42 @@ | |||
| ## 类型3:使用对撞指针的滑动窗口 | |||
There was a problem hiding this comment.
说实话我觉得这个应该不太算滑动窗口。。。?
这个是 meet in the middle 吧,或者「双指针」也行,和滑动窗口还是有点区别的
| @@ -0,0 +1,8 @@ | |||
| ## 精选例题 | |||
There was a problem hiding this comment.
「滑动窗口」的问题一般难度很大,如何「滑窗」里保持的性质可能需要多做一些问题。
建议再选一些问题。最近读者有建议:标注一下「基础必做问题」、「中等必做问题」、「困难选做问题」可能指导意义更强。
第 3 题是一个非常经典的入门的问题,如果有必要,可以设计成例题,再具体写一下。
下面是我做过的一些问题,我觉得比较典型的,供您参考。我最近找时间再复习一下,看看这些问题里有没有值得可以说的。
| 题目序号 |
|---|
| 3. 无重复字符的最长子串(中等) |
| 76. 最小覆盖子串(困难) |
| 209. 长度最小的子数组(中等) |
| 239. 滑动窗口最大值(中等) |
| 424. 替换后的最长重复字符(中等) |
| 438. 找到字符串中所有字母异位词 |
| 567. 字符串的排列(中等) |
| 643. 子数组最大平均数 I(简单) |
| 978. 最长湍流子数组(中等) |
| 992. K 个不同整数的子数组(困难) |
| # 滑动窗口 | ||
|
|
||
| 滑动窗口思想常用于处理数组、字符串相关的问题,它使用两个指针,指针间表示为一个窗口,这个窗口不停地滑动,每次都记录窗口的当前状态,再找出符合条件的适合的窗口,以求得解。 | ||
|
|
There was a problem hiding this comment.
这里补充一点(待讨论,下面的语言组织可能比较混乱),「滑动窗口」问题的两个指针变量在「滑动」的过程中,通常满足这种特点:
- 右指针先主动向右移动,在移动的过程中,「窗口」内部的元素满足一定性质;
- 直到右指针移动到某个位置,「窗口」内部的元素满足的性质被破坏,为了继续维护这个性质,此时让左指针向右移动,直到「窗口」内部的性质又得到满足;
- 右指针(主动)向右、左指针(被动)向右、右指针(主动)向右、左指针(被动)向右,这样的过程是交替进行的,很像裁缝用卷尺或者手给人量体裁衣的步骤,这种「滑动窗口」的问题也称为「尺取法」,可以以线性时间复杂度解决一类问题;
- 进而指出「滑动窗口」问题的解法,也是基于「暴力解法」的优化,在「滑动」的过程中,少考虑的那一部分区间一定不存在「最优值」,这一点和「双指针」解决问题思路是一致的。
作者:watch
麻烦老师审校