滑动窗口、尺取法学习笔记

滑动窗口解决什么问题?

1. 需要输出或比较的结果在原数据结构中是连续排列的;

2. 每次窗口滑动时,只需观察窗口两端元素的变化,无论窗口多长,每次只操作两个头尾元素,当用到的窗口比较长时,可以显著减少操作次数;

3. 窗口内元素的整体性比较强,窗口滑动可以只通过操作头尾两个位置的变化实现,但对比结果时往往要用到窗口中所有元素。

 

滑动窗口、尺取法关系?

尺取法就是双指针(two points)嘛。而滑动窗口也要用到二个指针,所以滑动窗口是一种特殊的尺取法。

 

经典例题:

leetcode 239. 滑动窗口最大值

题意:

在一个数组中找长度为k的子序列 中的最大值,如下图所示:

  滑动窗口的位置                最大值
  ---------------                        -----
[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

 

题解:

长度为k的子序列,说明窗口的大小是固定的,且为k。

找每个窗口中的最大值,我们可以遍历窗口中的所有值,从而找到最大值,此时,时间复杂度为O(n * m)

用单调双端队列找最大值可以使时间复杂度变为O(n)。

下面用的单调双端队列 是单调递减的。那么队列的第一个元素即为最大值,而且,队列前面的数 一定 比队列后面的数放入时间早。

class Solution {
public:
	vector maxSlidingWindow(vector& nums, int k) {
		vector res;
		if (nums.size() <= 0 || k < 1 || k > nums.size()){
			return res;
		}

		deque list;//创建双端队列
		for (int i = 0; i < nums.size(); i++){ //i表示窗口的结尾下标
			while (list.size() != 0 && nums[*(list.end() - 1)] <= nums[i]){
				list.pop_back();
			}//while
			list.push_back(i);

			if (*list.begin() == i - k){
				list.pop_front();
			}//if

			if (i >= k - 1){ //i 要到 k - 1位置处才是一个完整的窗口
				res.push_back(nums[*list.begin()]);
			}//if
		}//for
		return res;
	}
};

 

例题二:

最大值减去最小值小于等于num   的子数组数量
要求: 如果数组长度为N,请实现时间复杂度为O(N)的解法。

暴力解法:

class Solution {
public:
	int getNum(vector arr, int num){
		int res = 0;
		for (int start = 0; start < arr.size(); start++){
			for (int end = start; end < arr.size(); end++){
				if (isValid(arr, start, end, num)){
					//cout << start << "  " << end << endl;
					res++;
				}//if
			}//inner for
		}//extren for
		return res;
	}

	bool isValid(vector arr, int start, int end, int num){
		int Max = INT_MIN;
		int Min = INT_MAX;
		for (int i = start; i <= end; i++){
			Max = max(Max, arr[i]);
			Min = min(Min, arr[i]);
		}

		return Max - Min <= num;
	}
};


int main()
{
	vector arr;
	arr.push_back(1);
	arr.push_back(2);
	arr.push_back(3);

	Solution s;
	cout << s.getNum(arr, 1) << endl;

	return 0;
}

 

用窗口,先移动窗口的R,当窗口内的最大值减最小值 大于num , 说明要窗口要变小,此时我们移动L,直到窗口内的最大值减最小值 小于等于num。  这个过程循环进行,直到L 和 R 都到达数组尾部。

每移动一次L , 说明以L - 1 开头的子数组,已经找到所有满足题意的可能性了。


class Solution {
public:
	int getNum(vector arr, int num){
		if (arr.size() == 0) return 0;

		int res = 0;
		//创建两个记录最大值和最小值的双端队列
		deque dpMin;
		deque dpMax;
		//设置左右指针
		int L = 0;
		int R = 0;

		while (L < arr.size()){ // move left point
			while (R < arr.size()){ //move right point
				//更新两个双端队列
				while (dpMax.size() != 0 && arr[*(dpMax.end() - 1)] <= arr[R]){
					dpMax.pop_back();
				}
				dpMax.push_back(R);

				while (dpMin.size() != 0 && arr[*(dpMin.end() - 1)] >= arr[R]){
					dpMin.pop_back();
				}//while
				dpMin.push_back(R);

				if (arr[*dpMax.begin()] - arr[*dpMin.begin()] > num){
					break;
				}
				R++;
			}//medium while

			//窗口的右边界要移动了
			if (*dpMax.begin() == L){
				dpMax.pop_front();
			}
			if (*dpMin.begin() == L){
				dpMin.pop_front();
			}
			res += R - L;
			L++;
		}//extren while
		return res;
	}
};


int main()
{
	vector arr;
	arr.push_back(1);
	arr.push_back(2);
	arr.push_back(3);

	Solution s;
	cout << s.getNum(arr, 1) << endl;

	return 0;
}

用了三层的while,可是时间复杂度仅仅为O(n)。因为窗口的L 和 R 都不回退的。

可以看到窗口 和 单调双端队列 配合使用,可以在O(1) 的时间内找到窗口内的最值。

 

Leetcode 209. 长度最小的子数组

题目:

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

题解:

窗口,移动右指针,当满足要求,就移动左指针来达到减小窗口内 sum 的效果。当左指针移动到sum 小于 s 了,就又移右指针,相当于扩大窗口,从而达到增加sum 的效果。

class Solution {
public:
	int minSubArrayLen(int s, vector& nums) {
		int len = nums.size();
		if (len == 0){
			return 0;
		}

		int ans = INT_MAX;
		int sum = 0, L = 0, R = 0;
		while (R < len){
			sum += nums[R];
			R++;
			while (sum >= s){
				ans = min(ans, R - L);
				sum -= nums[L];
				L++;
			}//inner while
		}//extren while
		return ans == INT_MAX ? 0 : ans;

	}
};

 

洛谷1147 连续自然数和

题意:

对一个给定的自然数M,求出所有的连续的自然数段,这些连续的自然数段中的全部数之和为M。

例子:1998+1999+2000+2001+2002 = 100001998+1999+2000+2001+2002=10000,所以从1998到2002的一个自然数段为M=10000的一个解。

题解:

用i,j代表区间的左右端点

当sum小于目标值M时,将右端点右移(j++),sum会变大

当sum大于目标值M时,将左端点右移(i++),sum会变小

在双指针移动的过程中,如果有sum==M的情况就输出。

因为两个指针都是单调向右移动,也只扫一遍,可以证明时间复杂度为O(n) 左端点大于m/2时即可停止,因为只要长度为2的连续序列和就一定大于m。

#pragma GCC optimize(3,"Ofast","inline")
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define F(i,s,t) for(int i=(s);i<=(t);i++)
#define D(i,s,t) for(int i=(s);i>=(t);i--)
#define dBug(i) printf("Value=%d\n",i)
#define ddBug(i,j) printf("Value=%d %d\n",i,j)
#define ed putchar('\n')
#define FO freopen("D:\\in.txt","r",stdin)
#define IOS cin.tie(0) ,cout.tie(0), cout.sync_with_stdio(0)
typedef long long ll;
//const int INF = 1 << 30;
//const double EPS = 1e-6;
//#define MX 102
//#define Mod 10000
using namespace std;


int main()
{
	int M;
	scanf("%d", &M);

	int L = 1, R = 2, sum = 3;
	while (R <= M / 2 + 1){
		if (sum == M){
			printf("%d %d\n", L, R);
			sum -= L;
			L++;
		}
		else if (sum < M){
			R++;
			sum += R;
		}
		else if (sum > M){
			sum -= L;
			L++;
		}
	}//while
	return 0;
}

解二:先枚举开头元素i,其结尾元素用二分查找。用高斯公式求和!

 

题五:

题目:

输入n ( n<= 100,000)个整数,找出其中的两个数,它们之和等于整数m(假定

肯定有解)。题中所有整数都能用 int 表示

题解:

1) 将数组排序,复杂度是O(n×log(n))

2) 查找的时候,设置两个变量i和j,i初值是0,j初值是n-1.看a[i]+a[j],如果大于m,就让j 减1,如果小于m,就让i加1,直至a[i]+a[j]=m。

这种解法总的复杂度是O(n×log(n))的。(尺取法)

#include
#include
using namespace std;
int main()
{
	int n, m, i, j;
	static int a[100002];
	scanf("%d", &n);
	scanf("%d", &m);
	for (i = 0; i < n; i++) scanf("%d", &a[i]);
	sort(a, a + n);
	i = 0, j = n - 1;
	while (a[i] + a[j] != m){
		if (a[i] + a[j]>m) j--;
		else if (a[i] + a[j] < m) i++;
	}
	printf("%d %d\n",a[i],a[j]);
	return 0;
}

 

你可能感兴趣的:(左神笔记)