复习:单调队列那些事er

最近GDKOI就要来了我决定好好复习算法,单调队列也是其中之一。这个算法我认为主要是一种优化的思想。并不是单单用来解决DP的题目。比如我复习凸包,旋转卡壳等都有用到这种思想,仙人掌也是直接运用了单调队列来优化其中的DP。

我自己大概总结了一下这种算法缩不了空间,只能缩时间,往往能缩掉一个n的复杂度,在DP之类的题可能用的上。

那么它是怎么优化时间的呢,首先如其名这是一个队列,并且成单调性递增或递减,那么当前队头的值就一定是最优值,直接继承即可。

大概过程就是:我们每当有数进队列时,就筛一遍队头,再筛一遍队尾,再把它放进队列里。这样讲果然难懂-_-!那么我们来看一道例题。

T1caioj1174

【题意】
一个数列有N(1 <= N <= 100,000)个数(0<=ai<=10^9)。
开始选数,要求选出来的数和最大。
限制:不能选中超过连续M(M<=N)个。
【输入格式】
第一行两个整数N和M;
下来给出n个数。
【输出格式】
一行一个整数,表示可以得到的最大的和。
【样例输入】
5 2
1
2
3
4
5
【样例输出】
12

【思路】
这一道题首先我跟瀚哥想到了一个大暴力。
vio1错误方向:二维
f[i][j]表示访问到i已经连续了j个数
易得DP方程分两种:
f[i][j] = f[i - 1][j - 1] + a[i]
直接继承上一位
f[i][1] = min(f[i - 2][j] + a[i])
隔一位进行继承。
为什么不隔两位? 人家叫你不连续就好了,隔一个就行了,还隔两个-_-!

这一个暴力很明显是二维的,肯定不能有单调队列缩了。。

for i 1 ~ n
    for j 2 ~ m
        f[i][j] = f[i - 1][j - 1] + a[i]
    for j 1 ~ m
        f[i][1] = min(f[i - 2][j] + a[i])

vio2正确方向:一维
们不妨反过来想,题意变成:
从队列中选一些数,这些数之间间距不超过m,使它们的和最小。
这时候就很简单了,f[i]为到i时的最优解
易得DP方程:f[i] = min(f[j] + a[i])

for i 1 ~ n
    for j i - m - 1 ~ i - 1
        f[i] = min(f[j] + a[i])

/*
正解!!
根据vio2的做法我们可以得出一个较普遍规律:前面的数小于后面的。
也就是说每次继承都会尽可能选择前面的,那么我们就可以构造单调队列了!

for i 1 ~ n
    while1 //筛队头,使队头与队尾的差距在m+1以内
    用队头求f[i]
    while2 //用f[i]筛队尾,因为如果排位较后的数的f值比前面的数的f值还小,那么前面这个数就没有用处了,举个例子:
    //m = 1  f[list[tail]] = 10 f[i] = 9
    //据list[tail] < i得f[list[tail]影响的数较少,也就是list[tail]能影响的,i也能影响
    //又f[list[tail]] > f[i],所以都会选择f[i]继承,f[list[tail]]就没用了
    i入队列

【代码】

#include <cstdio>
#include <cstring>

using namespace std;
const int maxn = 110000;

struct node {
    int x, p;
} list[maxn]; int head, tail;
int f[maxn], a[maxn], n, m;

int main() {
    scanf("%d%d", &n, &m); m++;
    long long sum = 0;
    for(int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        sum += a[i];
    }
    tail = 1; head = 1;
    for(int i = 1; i <= n; i++) {
        while(head <= tail && i - list[head].p > m) head++;
        f[i] = list[head].x + a[i];
        while(head <= tail && f[i] <= list[tail].x) tail--;
        tail++; list[tail].x = f[i]; list[tail].p = i;
    }
    int ans = 999999999;
    for(int i = n - m + 1; i <= n; i++) if(f[i] < ans) ans = f[i];
    printf("%lld\n", sum - ans);
    return 0;
}

那么再来一道题

T2caioj1175

【题意】
一个猴子要吃香蕉,一共N棵香蕉树排列在一条直线上,它一开始在第一棵树上。
每棵树上有不同数量的香蕉,猴子每次最多的跳跃距离为D,而且最多只能跳M次,问猴子最多能吃到多少香蕉?
【输入格式】
第一行 三个整数 N,D,M (M<=N<=5000,D<=10000)
下面N行 每行两个整数 ai,bi (ai,bi<=1000000) 分别表示每棵树上的香蕉数目,以及每棵树的位置(树的位置是递增的)
数据保证没有两棵香蕉树在同一位置,以及b1=0
【输出格式】
一个整数,表示猴子最多吃到的香蕉数。
【样例输入】
5 5 2
6 0
8 3
4 5
6 7
9 10
【样例输出】
20

【思路】
/*
vio
f[i][j]表示到达第i位,跳了n次
易得:DP方程f[i][j]=f[k][j-1]+a[i]
*/

for i 1~n
    for j 1~m
        for k 1~i-1
            if(b[i]-b[k]<=d)
                f[i][j]=f[k][j-1]+a[i]

/*
我们发现当j固定时f[i][j]满足普遍递增性,可以套用单调队列,对于当前j我们可以用以求出的j-1求出。所以有一个细节就是只有当f[i][j-1]是有意义时才可以入队列,因为队列里存的值是跳j-1次的情况。
*/

#include <cstdio>
#include <cstring>

using namespace std;
const int maxn = 5100;
int _max(int x, int y){return x > y ? x : y;}

struct node {
    int x, p;
}a[maxn];
int f[maxn][maxn], list[maxn], tail, head;

int main() {
    int n, d, m; scanf("%d%d%d", &n, &d, &m);
    for(int i = 1; i <= n; i++) scanf("%d%d", &a[i].x, &a[i].p);
    int ans = f[1][0] = a[1].x;
    for(int j = 1; j <= m; j++) {
        head=1;tail=1;
        list[1] = j;
        for(int i = j + 1; i <= n; i++) {
            while(head <= tail && a[i].p - a[list[head]].p > d) head++;
            if(head > tail) break;
            f[i][j] = f[list[head]][j - 1] + a[i].x;
            if(f[i][j -1 ] != 0) {
                while(head <= tail && f[list[tail]][j - 1] < f[i][j - 1]) tail--;
                list[++tail] = i;
            }
            ans = _max(f[i][j], ans);
        }
    }
    printf("%d\n", ans);
    return 0;
}

接下来一道难一点的题

T3caioj1176

【题意】
给一个N*M的数矩阵
现在求一个子矩阵 要求子矩阵中最大值与最小值的差<=C。而且子矩阵的宽度(横)不超过100(长(竖)没有限制)。 求子矩阵的最大面积。
【输入格式】
第一行两个整数 M(左右方向),N(上下方向)和 C (N,M<=500 0<=C<= 10 )
接下来 N行 每行M个数 每个数(-30000~30000)
【输出格式】
子矩阵的最大面积
【样例输入】
10 15 4
41 40 41 38 39 39 40 42 40 40
39 40 43 40 36 37 35 39 42 42
44 41 39 40 38 40 41 38 35 37
38 38 33 39 36 37 32 36 38 40
39 40 39 39 39 40 40 41 43 41
39 40 41 38 39 38 39 39 39 42
36 39 39 39 39 40 39 41 40 41
31 37 36 41 41 40 39 41 40 40
40 40 40 42 41 40 39 39 39 39
42 40 44 40 38 40 39 39 37 41
41 41 40 39 39 40 41 40 39 40
47 45 49 43 43 41 41 40 39 42
42 41 41 39 40 39 42 40 42 42
41 44 49 43 46 41 42 41 42 42
45 40 42 42 46 42 44 40 42 41
【样例输出】
35
【思路】
//vio
min[i][j][k]表示第i行j~k的最小值
max[i][j][k]表示第i行j~k的最大值
时间复杂度O(n*m*m)

for j 1 ~ m
    for j2 1 ~ j
        for i 1 ~ n
            int maxx = 0, minn = 999999999
            for i2 1 ~ i
                maxx = _max(maxx, max[i2][j2][j])
                minn = _min(minn, min[i2][j2][j])
                if(maxx - minn > C) break
                ans = _max((i - i2 + 1) * (j - j2 + 1))

//std
据上可得当j、j2、i固定时i2越小,min越小,max越大,满足单调性开单调队列维护。
定义两个队列,第一个是维护max呈单调递减性,第二个是维护min呈单调递增性,使得队头差值大,队尾差值小,每次取队头作差。
但这里要注意当前的数要先进入队列,因为它也在这个矩阵中,所以也要算min、max。
筛队尾:维护队列单调性,加入第i行的情况(max和min)
队头:1、保证满足最大与最小值相差在c之内

因为我们OJ的问题,所以得把min和max数组缩成一维的。。。

#include <cstdio>
#include <cstring>

using namespace std;
int _max(int x,int y){return x>y?x:y;}
int _min(int x,int y){return x<y?x:y;}

int a[510][510];
int maxx[510], minn[510];
int list1[510], list2[510], head1, tail1, head2, tail2;

int main() {
    int m, n, c; scanf("%d%d%d", &m, &n, &c);
    int ans = 0;
    for(int i = 1; i <= n; i++) for(int j = 1; j <= m;j++)scanf("%d",&a[i][j]);
    for(int l = 1; l <= m; l++) {
        for(int i = 1; i <= n; i++) minn[i] = maxx[i] = a[i][l];
        for(int r = l; r <= _min(l + 99, m); r++) {
//因为最大宽度不超过100
            for(int i = 1; i <= n; i++) {
                minn[i] = _min(minn[i], a[i][r]);
                maxx[i] = _max(maxx[i], a[i][r]);
            }
            head1 = head2 = 1;
            tail1 = tail2 = 0;
            int j = 1;
            for(int i = 1; i <= n; i++) {
                while(head1 <= tail1 && minn[list1[tail1]] > minn[i]) tail1--;
                list1[++tail1] = i;
                while(head2 <= tail2 && maxx[list2[tail2]] < maxx[i]) tail2--;
                list2[++tail2] = i;
                for( ;j <= i && maxx[list2[head2]] - minn[list1[head1]] > c; j++) {
                    while(head1 <= tail1 && list1[head1] <= j) head1++;
                    while(head2 <= tail2 && list2[head2] <= j) head2++;
                }
                ans = _max(ans, (i - j + 1) * (r - l + 1));
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

那么我们来总结一下,一般单调队列会用在DP类题目中,当我们在题目中找到单调性的规律,就要考虑要单调队列来优化,有时候能不要只执着与单调队列硬做,往往要结合其他算法,经常是要在你想到一些解法后,才考虑用单调队列。等你能将单调队列变成一种优化的思维时就上了一个阶段了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值