滑动窗口-最大值最小值更新结构

一、滑动窗口是什么

        滑动窗口是一种想象出来的数据结构: 滑动窗口有左边界L和有边界R 在数组或者字符串或者一个序列上,记为S,窗口就是S[L..R]这一部分 L往右滑意味着一个样本出了窗口,R往右滑意味着一个样本进了窗口 L和R都只能往右滑。

这里有一个小小的问题,假如说一个数组[5,7,6,5,4,8,3],我想要求每个窗口的最大值,我怎么可以实现?

one hour letter...

我们可以使用一个双端队列来实现,假如说窗口都从0开始,R++,这是5从右边入队列,R++,7比5大,那么5从右边弹出,7入队列,R++ ,6比7 小,直接从右边入队列,R++,5比6小,直接入队列,假如说这个时候我想缩小了,L++,双端队列左边的就是当前最大值,双端队列中存储的是数组的下标,如果L移动到7右边,那么L 7就从双端队列的左边出队列,队列要严格维持从左到右依次变小,如果相同,队列中的从右边出队列,新的入队列。下面我们来实战一下。

题目一

假设一个固定大小为W的窗口,依次划过arr, 返回每一次滑出状况的最大值 例如,arr = [4,3,5,4,3,3,6,7], W = 3 返回:[5,5,5,4,6,7]。

这个题目就是用上面的技巧来解的。直接上代码:

public static int[] getMaxWindow(int[] arr,int w){
        if (arr == null || w < 1 || arr.length < w){
            return null;
        }
        int N = arr.length;
        int[] result = new int[N - w + 1];
        int index  = 0;

        //双端队列
        LinkedList<Integer> qMax = new LinkedList<>();

        //窗口右端依次往右
        for (int R = 0; R < N; R++) {

            //如果队列不为空,并且窗口最右端的值比队列尾的值大,就一直弹出,直到小于
            while (!qMax.isEmpty()  && arr[qMax.peekLast()] <= arr[R]){
                qMax.pollLast();
            }
            //窗口右端的值入队列
            qMax.add(R);

            //查看队列头部的值是否过期,如果过期就移除,不过期就保留
            if (qMax.peekFirst() == R - w){
                qMax.pollFirst();
            }

            //判断是否能正常形成一个窗口如果可以形成一个窗口,才开始计答案
            if (R > w - 1){
                //双端队列的头部,就是最大值
                result[index++] = arr[qMax.peekFirst()];
            }

        }
        return result;
    }

怎么样是不是既简单,又清晰,接下来我们再看一个稍难一些的

题目二

给定一个整型数组arr,和一个整数num 某个arr中的子数组sub,如果想达标,必须满足: sub中最大值 – sub中最小值 <= num, 返回arr中达标子数组的数量

这一题先给大家说一个结论。就是在R往右走的过程中,如果达标,那么所有的子序列都达标。如果往右走开始不达标了,那么在往右也不会达标。这一点我们很好想,假如说一个0 - 100 的数组达标,那么 0 - 50 必定达标, 为啥,因为在50 - 100 这些数中,最小值只可能比0 - 50 中的小 ,最大值也只可可能比 0 - 50 大,他们的差值,就会比0 - 50 大, 如果 0 - 50 都不达标,那么,差值更大的就更不会达标,当然,如果差值大的都达标了,那么差值小的不可能不达标。

这里我们准备一个装最大值的双端列表和一个装最小值的双端列表,R每移动一次,让他们相减,如果达标,就继续,不达标就停止,这是L++。如此重复

public static int AllLessNumSubArray(int[] arr,int sum){
        if (arr == null || sum < 0 || arr.length < 0){
            return 0;
        }

        int N = arr.length;

        int count = 0;

        int R  = 0;

        //存储最小值的双端队列
        LinkedList<Integer> qMin = new LinkedList<>();
        //存储最大值的双端对垒
        LinkedList<Integer> qMax = new LinkedList<>();
        for (int L = 0; L < N; L++) {
            // 移动窗口右端不能大于数组的长度
            while (R < N){

                while (!qMax.isEmpty() && qMax.peekLast() <= arr[R] ){
                    qMax.pollLast();
                }
                while (!qMin.isEmpty() && qMin.peekLast() >= arr[R]){
                    qMin.pollLast();
                }

                //两个队列添加值
                qMax.add(R);
                qMin.add(R);

                //最大值减最小值 大于 sum  初次不达标  break  否则 R++ 继续向后括
                if (arr[qMax.peekFirst()] - arr[qMin.peekFirst()] > sum){
                    break;
                }else {
                    R ++;

                }

                count += R - L;

                if (qMax.peekFirst() == L){
                    qMax.pollFirst();
                }

                if (qMin.peekFirst() == L){
                    qMin.pollFirst();
                }

            }
        }
        return count;
    }

看是不是很好用,也不是很难。

题目三

加油站的良好出发点问题

这道题我们先处理一下,啥gas数组,cost数组,太烦,我们自己加工出一个数组,arr,我们用gas- cost 作为arr每个位置的值,arr[-1,-1,2,0],从各个加油站出发,中途只要掉到0以下,就熄火了,a一上来就熄火了,b也是一上来就熄火了,c可以一直不熄火,d跑了一个加油站后熄火。

我们可以用暴力方法来解这题,时间复杂度是O(n^2),当然,我们也可以用滑动窗口来解,这里,我们可以转换一下,思路,通过求累加和的方式求解。例如arr = [-2,-3,4,-2,6,-1]首先,我们先对其求累加和[-2,-5,-1,-3,3,2,0,-3,1,-1,5,4] 这里,我们对arr这个数组求了两遍累加和,我们为什么要做两遍累加和呢,我们把 6位置,也就是值为0 的这个地方想象为0 位置,-3想象为1位置,1想象为2位置,-1想象为3位置。5想象4位置,4想象为5位置 ,这样我们在从头看,从-2到2相当于我们从0到5跑了一圈,-5到0 表示 从1位置出发回到0位置。-1到 -3 表示从2位置出发回到1位置,依次类推。所有原始数组中出发的位置,在这个长数组中 都能把原始的累加和数组给加工出来,想达成这样的目的,怎么加工出来,我们从哪个位置出发就用这个长数组减去前一个位置的数,就能出来,例如,从二位置出发,我们需要的累加和为[4,2,8,7,5,2],我们再看这个长数组,2位置为1,它前面位置为-5,那么我们把每个值都减-5,这样是不是就加工出我们需要的数组了。

// 这个方法的时间复杂度O(N),额外空间复杂度O(N)
	public static int canCompleteCircuit(int[] gas, int[] cost) {
		boolean[] good = goodArray(gas, cost);
		for (int i = 0; i < gas.length; i++) {
			if (good[i]) {
				return i;
			}
		}
		return -1;
	}

	public static boolean[] goodArray(int[] g, int[] c) {
		int N = g.length;
		int M = N << 1;
		int[] arr = new int[M];
		for (int i = 0; i < N; i++) {
			arr[i] = g[i] - c[i];
			arr[i + N] = g[i] - c[i];
		}
		for (int i = 1; i < M; i++) {
			arr[i] += arr[i - 1];
		}
		// 举个例子说明一下
		// 比如纯能数组(也就是燃料 - 距离之后)的数组 :
		// 纯能数组 = 3, 2,-6, 2, 3,-4, 6
		// 数组下标 = 0  1  2  3  4  5  6
		// 客观上说:
		// 0位置不是良好出发点
		// 1位置不是良好出发点
		// 2位置不是良好出发点
		// 3位置是良好出发点
		// 4位置不是良好出发点
		// 5位置不是良好出发点
		// 6位置是良好出发点
		// 把数组增倍之后 : 
		// arr   = 3, 2,-6, 2, 3,-4, 6, 3, 2,-6, 2, 3,-4, 6
		// 然后计算前缀和 :
		// arr   = 3, 5,-1, 1, 4, 0, 6, 9,11, 5, 7,10, 6,12
		// index = 0  1  2  3  4  5  6  7  8  9 10 11 12 13
		// 这些就是上面发生的过程
		// 接下来生成长度为N的窗口
		LinkedList<Integer> w = new LinkedList<>();
		for (int i = 0; i < N; i++) {
			while (!w.isEmpty() && arr[w.peekLast()] >= arr[i]) {
				w.pollLast();
			}
			w.addLast(i);
		}
		// 上面的过程,就是先遍历N个数,然后建立窗口
		// arr   =[3, 5,-1, 1, 4, 0, 6],9,11, 5, 7,10, 6,12
		// index = 0  1  2  3  4  5  6  7  8  9 10 11 12 13
		// w中的内容如下:
		// index:  2 5 6
		// value: -1 0 6
		// 左边是头,右边是尾,从左到右严格变大
		// 此时代表最原始的arr的这部分的数字: 
		// 原始的值 = [3, 2,-6, 2, 3,-4, 6],3, 2,-6, 2, 3,-4, 6
		// 原始下标 =  0  1  2  3  4  5  6  0  1  2  3  4  5  6
		// 上面这个窗口中,累加和最薄弱的点,就是w中最左信息
		// 也就是会累加出,-1这个值,所以会走不下去。
		// 宣告了此时0位置不是良好出发点。
		// 接下来的代码,就是依次考察每个点是不是良好出发点。
		// 目前的信息是:
		// 计算的前缀和 :
		// arr   =[3, 5,-1, 1, 4, 0, 6],9,11, 5, 7,10, 6,12
		// index = 0  1  2  3  4  5  6  7  8  9 10 11 12 13
		// w中的内容如下:
		// index:  2 5 6
		// value: -1 0 6
		// 此时代表最原始的arr的这部分的数字: 
		// 原始的值 = [3, 2,-6, 2, 3,-4, 6],3, 2,-6, 2, 3,-4, 6
		// 原始下标 =  0  1  2  3  4  5  6  0  1  2  3  4  5  6
		// 现在让窗口往下移动
		// 计算的前缀和 :
		// arr   = 3,[5,-1, 1, 4, 0, 6, 9],11, 5, 7,10, 6,12
		// index = 0  1  2  3  4  5  6  7   8  9 10 11 12 13
		// w中的内容如下:
		// index:  2 5 6 7
		// value: -1 0 6 9
		// 此时代表最原始的arr的这部分的数字: 
		// 原始的值 =  3,[2,-6, 2, 3,-4, 6, 3],2,-6, 2, 3,-4, 6
		// 原始下标 =  0  1  2  3  4  5  6  0  1  2  3  4  5  6
		// 上面这个窗口中,累加和最薄弱的点,就是w中最左信息
		// 但是w最左的值是-1啊!而这个窗口中最薄弱的累加和是-4啊。
		// 对!所以最薄弱信息 = 窗口中的最左信息 - 窗口左侧刚出去的数(代码中的offset!)
		// 所以,最薄弱信息 = -1 - 0位置的3(窗口左侧刚出去的数) = -4
		// 看到了吗?最薄弱信息,依靠这种方式,加工出来了!
		// 宣告了此时1位置不是良好出发点。
		// 我们继续,让窗口往下移动
		// 计算的前缀和 :
		// arr   = 3, 5,[-1, 1, 4, 0, 6, 9,11], 5, 7,10, 6,12
		// index = 0  1   2  3  4  5  6  7  8   9 10 11 12 13
		// w中的内容如下:
		// index:  2  5  6  7  8
		// value: -1  0  6  9 11
		// 此时代表最原始的arr的这部分的数字: 
		// 原始的值 =  3, 2,[-6, 2, 3,-4, 6, 3, 2],-6, 2, 3,-4, 6
		// 原始下标 =  0  1   2  3  4  5  6  0  1   2  3  4  5  6
		// 上面这个窗口中,累加和最薄弱的点,就是w中最左信息
		// 但是w最左的值是-1啊!而这个窗口中最薄弱的累加和是-6啊。
		// 对!所以最薄弱信息 = 窗口中的最左信息 - 窗口左侧刚出去的数(代码中的offset!)
		// 所以,最薄弱信息 = -1 - 1位置的5(窗口左侧刚出去的数) = -6
		// 看到了吗?最薄弱信息,依靠这种方式,加工出来了!
		// 宣告了此时2位置不是良好出发点。
		// 我们继续,让窗口往下移动
		// 计算的前缀和 :
		// arr   = 3, 5, -1,[1, 4, 0, 6, 9,11, 5], 7,10, 6,12
		// index = 0  1   2  3  4  5  6  7  8  9  10 11 12 13
		// w中的内容如下:
		// index:  5  9
		// value:  0  5
		// 没错,9位置的5进来,让6、7、8位置从w的尾部弹出了,
		// 同时原来在w中的2位置已经过期了,所以也弹出了,因为窗口左边界已经划过2位置了
		// 此时代表最原始的arr的这部分的数字: 
		// 原始的值 =  3, 2, -6,[2, 3,-4, 6, 3, 2, -6],2, 3,-4, 6
		// 原始下标 =  0  1   2  3  4  5  6  0  1   2  3  4  5  6
		// 上面这个窗口中,累加和最薄弱的点,就是w中最左信息
		// 但是w最左的值是0啊!而这个窗口中最薄弱的累加和是1啊
		// 对!所以最薄弱信息 = 窗口中的最左信息 - 窗口左侧刚出去的数(代码中的offset!)
		// 所以,最薄弱信息 = 0 - 2位置的-1(窗口左侧刚出去的数) = 1
		// 看到了吗?最薄弱信息,依靠这种方式,加工出来了!
		// 宣告了此时3位置是良好出发点。
		// 往下同理
		boolean[] ans = new boolean[N];
		for (int offset = 0, i = 0, j = N; j < M; offset = arr[i++], j++) {
			if (arr[w.peekFirst()] - offset >= 0) {
				ans[i] = true;
			}
			if (w.peekFirst() == i) {
				w.pollFirst();
			}
			while (!w.isEmpty() && arr[w.peekLast()] >= arr[j]) {
				w.pollLast();
			}
			w.addLast(j);
		}
		return ans;
	}

看是不是很清晰,下面,一道是很熟悉的题:

题目四:

arr是货币数组,其中的值都是正数。再给定一个正数aim。 每个值都认为是一张货币, 返回组成aim的最少货币数 注意: 因为是求最少货币数,所以每一张货币认为是相同或者不同就不重要了

这一题我们在动态规划中写过,用从左到右的尝试模型,从左到右,依次要还是不要,然后往下做决策。知道滑动窗口后,我们换一种其他的尝试方式。我们就又有了其他的解法啦,我们可以把面值去重后取出来,作为一个数组,然后再把对应的张数作为一个数组。

在这里我们如果想用临近格子替代,因为这里不是求累加和,而是求最小值,d出去之后,我的最小值不知道该怎么变 了。这也是这个尝试区别于之前尝试的地方,之前的尝试是求累加和的,我们用临近的格子,减去d的位置,加上a就行了,但这是直接求的最小张数,d出去之后就不知道怎么变了。

我们可以这么求。

如果i 是 3元,我们求完0之后求3,这时,b窗口形成,求完3之后求6这是c窗口形成,接下来求9元。一直到不超过面值,然后在求1元,4元,7元10元,不超过面值,一直往后求。

这里我们准备一个双端队列放最小值,那进双端队列要怎么pk呢,因为是最少张数,假如说x元a张,y元b张,那么就是a+(y-x)/面值   和  b 进行pk 。

在这里,把各种方式写的代码一块给大家。在这些方法里面,使用滑动窗口进行优化的性能最好

public static int minCoins(int[] arr, int aim) {
		return process(arr, 0, aim);
	}

	public static int process(int[] arr, int index, int rest) {
		if (rest < 0) {
			return Integer.MAX_VALUE;
		}
		if (index == arr.length) {
			return rest == 0 ? 0 : Integer.MAX_VALUE;
		} else {
			int p1 = process(arr, index + 1, rest);
			int p2 = process(arr, index + 1, rest - arr[index]);
			if (p2 != Integer.MAX_VALUE) {
				p2++;
			}
			return Math.min(p1, p2);
		}
	}

	// dp1时间复杂度为:O(arr长度 * aim)
	public static int dp1(int[] arr, int aim) {
		if (aim == 0) {
			return 0;
		}
		int N = arr.length;
		int[][] dp = new int[N + 1][aim + 1];
		dp[N][0] = 0;
		for (int j = 1; j <= aim; j++) {
			dp[N][j] = Integer.MAX_VALUE;
		}
		for (int index = N - 1; index >= 0; index--) {
			for (int rest = 0; rest <= aim; rest++) {
				int p1 = dp[index + 1][rest];
				int p2 = rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : Integer.MAX_VALUE;
				if (p2 != Integer.MAX_VALUE) {
					p2++;
				}
				dp[index][rest] = Math.min(p1, p2);
			}
		}
		return dp[0][aim];
	}

	public static class Info {
		public int[] coins;
		public int[] zhangs;

		public Info(int[] c, int[] z) {
			coins = c;
			zhangs = z;
		}
	}

	public static Info getInfo(int[] arr) {
		HashMap<Integer, Integer> counts = new HashMap<>();
		for (int value : arr) {
			if (!counts.containsKey(value)) {
				counts.put(value, 1);
			} else {
				counts.put(value, counts.get(value) + 1);
			}
		}
		int N = counts.size();
		int[] coins = new int[N];
		int[] zhangs = new int[N];
		int index = 0;
		for (Entry<Integer, Integer> entry : counts.entrySet()) {
			coins[index] = entry.getKey();
			zhangs[index++] = entry.getValue();
		}
		return new Info(coins, zhangs);
	}

	// dp2时间复杂度为:O(arr长度) + O(货币种数 * aim * 每种货币的平均张数)
	public static int dp2(int[] arr, int aim) {
		if (aim == 0) {
			return 0;
		}
		// 得到info时间复杂度O(arr长度)
		Info info = getInfo(arr);
		int[] coins = info.coins;
		int[] zhangs = info.zhangs;
		int N = coins.length;
		int[][] dp = new int[N + 1][aim + 1];
		dp[N][0] = 0;
		for (int j = 1; j <= aim; j++) {
			dp[N][j] = Integer.MAX_VALUE;
		}
		// 这三层for循环,时间复杂度为O(货币种数 * aim * 每种货币的平均张数)
		for (int index = N - 1; index >= 0; index--) {
			for (int rest = 0; rest <= aim; rest++) {
				dp[index][rest] = dp[index + 1][rest];
				for (int zhang = 1; zhang * coins[index] <= aim && zhang <= zhangs[index]; zhang++) {
					if (rest - zhang * coins[index] >= 0
							&& dp[index + 1][rest - zhang * coins[index]] != Integer.MAX_VALUE) {
						dp[index][rest] = Math.min(dp[index][rest], zhang + dp[index + 1][rest - zhang * coins[index]]);
					}
				}
			}
		}
		return dp[0][aim];
	}

	// dp3时间复杂度为:O(arr长度) + O(货币种数 * aim)
	// 优化需要用到窗口内最小值的更新结构
	public static int dp3(int[] arr, int aim) {
		if (aim == 0) {
			return 0;
		}
		// 得到info时间复杂度O(arr长度)
		Info info = getInfo(arr);
		int[] c = info.coins;
		int[] z = info.zhangs;
		int N = c.length;
		int[][] dp = new int[N + 1][aim + 1];
		dp[N][0] = 0;
		for (int j = 1; j <= aim; j++) {
			dp[N][j] = Integer.MAX_VALUE;
		}
		// 虽然是嵌套了很多循环,但是时间复杂度为O(货币种数 * aim)
		// 因为用了窗口内最小值的更新结构
		for (int i = N - 1; i >= 0; i--) {
			for (int mod = 0; mod < Math.min(aim + 1, c[i]); mod++) {
				// 当前面值 X
				// mod mod + x mod + 2*x mod + 3 * x
				LinkedList<Integer> w = new LinkedList<>();
				w.add(mod);
				dp[i][mod] = dp[i + 1][mod];
				for (int r = mod + c[i]; r <= aim; r += c[i]) {
					while (!w.isEmpty() && (dp[i + 1][w.peekLast()] == Integer.MAX_VALUE
							|| dp[i + 1][w.peekLast()] + compensate(w.peekLast(), r, c[i]) >= dp[i + 1][r])) {
						w.pollLast();
					}
					w.addLast(r);
					int overdue = r - c[i] * (z[i] + 1);
					if (w.peekFirst() == overdue) {
						w.pollFirst();
					}
					if (dp[i + 1][w.peekFirst()] == Integer.MAX_VALUE) {
						dp[i][r] = Integer.MAX_VALUE;
					} else {
						dp[i][r] = dp[i + 1][w.peekFirst()] + compensate(w.peekFirst(), r, c[i]);
					}
				}
			}
		}
		return dp[0][aim];
	}

	public static int compensate(int pre, int cur, int coin) {
		return (cur - pre) / coin;
	}

  • 30
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值