给定一个序列,求出一个不含重复元素的最长子序列[AL,AR]。
这个题目看似非常简单,但是却很经典,值得反复捉摸。
首先,最简单的办法就是用一个大小为k(k从大到小,发现符合条件的就可以停了)的窗口开始扫描,O(n^2)可以解决。左出右进。但是这个效率还是太低了。
所以我们可以考虑这样的方法,先从0开始扫描,不停扩展右边,当右边不能扩展(有重复元素),就将左边的窗口进行滑动,直到右边可以添加进来,这样一来就复杂度立刻变成O(N).
def slide_window(array):
Left,Right = 0,0
n = len(array)
slide_set = set()
max_offset = 0
while Right<n:
while Right < n and array[Right] not in slide_set:
slide_set.add(array[Right])
if Right-Left > max_offset:
max_offset = Right-Left
maxl,maxr = Left,Right
Right += 1
slide_set.remove(array[Left])
Left += 1
print(maxl,maxr)
其实我们可以对比一下leecode上的最大长方形问题,有没有发现有一点相似呢,我们也是不断拓展右边界(只要可以递增),如果不能拓展了,就“滑动窗口”使得最右边的元素可以添加进来。(同时在这个过程里需要计算面积)。
为了总结一下这类问题的解决逻辑,再看一个问题:
输入正整数k和一个长度为n的序列,定义f(i)为从i开始连续k个元素的最小值,需要求f(1),f(2),f(3)…f(n-k+1).
最简单的思路还是直接用滑动窗口进行扫描,但是用堆来存储,这样复杂度是O(n-k)logk.具体细节就不说了。但是有O(n)的做法!:
[1]我们这样来分析,如果一个窗口里存在两个元素2,1(1在2右边),这意味着2在离开窗口之前永远不可能成为最小值!所以可以删除元素2,保持队列单调递增。当我们滑动窗口的时候,只需要使得最右的元素最大,所以从右往左边删除元素,直到满足队列单调递增即可~。这样每一个元素都只被最多删除一次,所以算法的复杂度是O(n).
总结:
首先我们要能够利用序列的信息,理论上来讲很多时候直接扫描都是没有利用到前面的信息的。所以通过滑动窗口这样的一个做法就可以最大限度的维护信息的利用
其次我们应该考虑如何拓展窗口(注意不是滑动),这取决于两点,第一:什么时候停止拓展?第二:哪些元素应该添加,哪些元素不应该添加。
对于第一点,这就取决于我们的目标,比如寻找最长不重复序列,那么有重复值的时候就应该停止拓展了,对于寻找k个元素最小值,拓展到k个就应该停止了,对于求最大长方形(这个比较复杂),当我们可以计算出某一个长方形的长度时就应该停止拓展了。
对于第二点,这个做法叫作删除无用的元素,等价于维护窗口里队列的某一种性质。对于第一个问题,我们维护的是不重复原则,对于k元素问题和长方形,我们维护的是单调性。那么怎么确定哪些元素没有意义呢?(或者说应该使得队列里存在哪些性质呢),这取决于我们的目标:
第一个问题的目标太明显不说,后面k序列我们的目标就是找到窗口里的最小值,而且还是在常数时间里找到,这就给我们一个思路,要使得最小的元素始终在第一个(在最后一个应该也可以,但是好像不太直观),这样保持单调递增就是有必要的了,因为不符合这个规律的元素就不可能是最小的元素,没有必要维护。
对于长方形的问题,可以这样看,当我要寻找一个元素所在长方形的长度时,怎么才能在常数的时间里获取答案?我们只要找到比这个高度小的两个边界即可。这还是要求我们维护队列的单调递增性质。因为这样它前面的元素就是左边界,而右边界就是某个元素进入的时候不符合单调的规律,为了进入就要从右边开始删除,这样每一个被删除的元素都是能同时找到左右边界(右边界市要进入的元素)。
最后我们要看怎么滑动窗口,滑动窗口有几种情况:第一是为了进算新的答案,第二是为了删除某些元素让新的元素进来。
最后说一下这些都只是自己的一个总结思考,题目的类型千变万化,很可能方式完全不一样,但是如果我们能按照自己的思考逻辑来分析,肯定是可以帮助我们找到问题的线索的