zoukankan      html  css  js  c++  java
  • 浅谈单调队列优化

    引子

    单调队列其实是一种具有单调性的双端队列,通常最佳答案位于队首。可以用(O(n))的时间复杂度可以求出整个队列的区间最值,调用时为(O(1))

    可以用于求区间最值,和降低DP的时间复杂度。

    具体实现

    通过一个数组和两个指针来维护,一个维护队首head,一个维护队尾tail

    初值

            head = 1; //从1开始
    	tial = 0; //为前一位的位置
    

    维护答案

    		while(i - q[head].side + 1 > m && head <= tial)head ++; //因为dp[i]已经更新,head维护的是dp[i + 1]的值所以要加1
    

    维护队尾

    将答案放进对应的位置,维护队列的单调性

    		while(a[i] < q[tial].num && tial >= head)tial --;
    		q[++ tial].num = a[i];
    		q[tial].side = i;
    

    关键代码

            head = 1;
    	tial = 0;
    	for(int i = 1; i <= n; i ++){
    		scanf("%d", &a[i]);
    		printf("%d
    ", q[head].num);
    		while(i - q[head].side + 1 > m && head <= tial)head ++;
    		while(a[i] < q[tial].num && tial >= head)tial --;
    		q[++ tial].num = a[i];
    		q[tial].side = i;
    	}
          
    

    例题

    求m区间内的最小值

    题目描述
    一个含有n项的数列((n <= 2000000)),求出每一项前m个数的最小值。若前面的数不足m项,则从第1个数开始,若前面没有数则输出0。

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int maxn = 2000005;
    
    struct node{
    	int num, side;
    };
    int n, m;
    int a[maxn];
    node q[maxn];
    int head, tial;
    int main() {
    	scanf("%d %d", &n, &m);
    	head = 1;
    	tial = 0;
    	for(int i = 1; i <= n; i ++){
    		scanf("%d", &a[i]);
    		printf("%d
    ", q[head].num);
    		while(i - q[head].side + 1 > m && head <= tial)head ++;
    		while(a[i] < q[tial].num && tial >= head)tial --;
    		q[++ tial].num = a[i];
    		q[tial].side = i;
    	}
    	return 0;
    }
    

    修建草坪

    题目描述

    原题来自:USACO 2011 Open Gold

    在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。

    然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。FJ 有N只排成一排的奶牛,编号为1N。每只奶牛的效率是不同的,奶牛i的效率为E[i]

    靠近的奶牛们很熟悉,如果 FJ 安排超过K只连续的奶牛,那么这些奶牛就会罢工去开派对。因此,现在 FJ 需要你的帮助,计算 FJ 可以得到的最大效率,并且该方案中没有连续的超过K只奶牛。

    思路

    很明显,这是一道DP。dp[i][1]表示选第i头奶牛的最高效率,dp[i][0]为不选第i头的最高效率。
    (dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]))
    $dp[i][1] = min(dp[i][1], dp[j][0] + sum[i] - sum[j - 1])) $j在i - 1i - k + 1
    对于dp[j][0]进行一个单调队列维护就好了

    代码

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <cmath>
    const int maxn = 1e5 + 5;
    using namespace std;
    
    struct node{
    	long long num;
    	int side;
    };
    
    int n, m;
    long long a[maxn], dp[maxn][2], sum[maxn];
    node q[maxn];
    int main() {
    	scanf("%d %d", &n, &m);
    	for(int i = 1; i <= n; i ++){
    		scanf("%d", &a[i]);
    		sum[i] = sum[i - 1] + a[i];
    	}
    	int head = 1, tial = 1; //tail为当前位置
    	for(int i = 1; i <= n; i ++){
    		dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
    		while(i - q[head].side > m && head <= tial)head ++;//维护当前位置
    		dp[i][1] = q[head].num + sum[i];
    		while(q[tial].num < (dp[i][0] - sum[i]) && head <= tial)tial --;
    		q[++ tial].num = dp[i][0] - sum[i];
    		q[tial].side = i;
    //		printf("%d %d
    ", dp[i][1], dp[i][0]); 
    	}
    	printf("%lld", max(dp[n][1], dp[n][0]));
    	return 0;
    }
    

    旅行问题

    题目描述

    原题来自:POI 2004

    John 打算驾驶一辆汽车周游一个环形公路。公路上总共有 车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

    任务:判断以每个车站为起点能否按条件成功周游一周。

    思路

    这道题代码的细节令人ex
    对于每一个点,sum为行走到这个点还剩多少油,因为我们只需要知道能否走完,所以我们只需要知道能否到达sum最小的点就可以了
    思路没什么难道,细节看代码注释吧

    代码

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #define int long long
    using namespace std;
    const int maxn = 1e6 + 5;
    
    struct node{
    	int p, d, s;
    	int sum;
    };
    
    struct data{
    	int num;
    	int side;
    };
    int n;
    node a[maxn * 2];
    bool flag[maxn * 2];
    int min_l;
    data q[maxn];
    data q2[maxn];
    int Min[2 * maxn];
    int Min2[2 * maxn];
    bool ans[maxn];
    signed main() {
    	memset(ans, false, sizeof(ans));
    	scanf("%d", &n);
    	for(int i = 1; i <= n; i ++){
    		scanf("%d %d", &a[i].p, &a[i].d);
    		a[i].s = a[i].p - a[i].d; //因为 a[i].d为到下一个点的路径,所以处理逆时针的时候减去的是i
    		a[i + n].s = a[i].s; //一个环不好维护,就通过倍长这个路径来处理
    	}
    	a[0].sum = 0;
    	for(int i = 1; i <= 2 * n; i ++){
    		a[i].sum = a[i - 1].sum + a[i].s; //逆时针走到这一站还剩多少油 
    	}
    	int head2 = 0, tail2 = -1; //和head = 1, tail = 0是一样的
    	for(int i = 2 * n; i >= 1; i --){
    		while(head2 <= tail2 && q2[head2].side >= i + n)head2 ++;
    		while(head2 <= tail2 && q2[tail2].num >= a[i].sum)tail2 --;
    		q2[++ tail2].side = i;
    		q2[tail2].num = a[i].sum;
    		if(i <= n && q2[head2].num >= a[i - 1].sum){
    			ans[i] = 1;
    		}
    	}
    	a[0].sum = 0; 
    	a[0].d = a[n].d;
    	for(int i = 1; i <= n; i ++){
    		a[i].s = a[i].p - a[i - 1].d; //a[i].s为到这个点时还剩多少油,这里为顺时针,减去的自然是上一个点到这个点的路径
    		a[i + n].s = a[i].s;
    	}
    	for(int i = 1; i <= 2 * n; i ++){ //顺时针
    		a[i].sum = a[i - 1].sum + a[i].s; 
    	}
    	int head = 0, tail = -1;
    	for(int i = 1; i <= 2 * n; i ++){
    		while(head <= tail && i - q[head].side >= n)head ++;
    		if(i > n && a[i].sum - q[head].num >= 0)
    			ans[i - n] = 1;
    		while(head <= tail && q[tail].num <= a[i].sum)tail --;
    		q[++ tail].side = i;
    		q[tail].num = a[i].sum;
    	}
    	for(int i = 1; i <= n; i ++){
    		if(ans[i]){
    			printf("TAK
    ");
    		}
    		else{
    			printf("NIE
    ");
    		}
    	}
    	return 0;
    }
    

    辨析 head 和 tail 的初值及获取答案的位置

    tail = head - 1

    tail 和 head 在 维护之后保存的都是对下一个点的影响,所以赋值应在维护前

    tail = head

    tail 和 head 保存的是对当前点的影响,所以赋值在维护后

    夜空中最亮的星,请照亮我前行
  • 相关阅读:
    替代谷歌、雅虎、必应的十大搜索引擎(1)
    CUDA Toolkit 4.1 RC1 新增LLVM编译器
    抢先报!GTC中国抢先报名,团购5折还送限量大礼
    Jacket for Matlab常见问题
    Jacket for Matlab 1.8新增功能介绍
    如何安装Jacket for Matlab
    Jacket for Matlab 2.1版本发布
    Matlab Jacket 2.0发布
    Attribute在.net编程中的应用(三)
    Attribute在.net编程中的应用(六)
  • 原文地址:https://www.cnblogs.com/Nefelibata/p/14398862.html
Copyright © 2011-2022 走看看