动态规划+单调队列

最近在做一个动态规划相关的题目,发现了有一些动态规划题目中可以使用单调队列来简化计算的复杂度,本来以为动态规划以及很厉害了,看到了单调队列才不禁发现,原来是算法或者结构还可以这么玩。

定义

单调队列是指一个队列内部的元素具有严格单调性的一种数据结构,分为单调递增队列和单调递减队列。

单调队列满足两个性质:

1.单调队列必须满足从队头到队尾的严格单调性。

2.排在队列前面的比排在队列后面的要先进队。

元素进队列的过程对于单调递增队列,对于一个元素a 如果 a > 队尾元素, 那么直接将a扔进队列, 如果 a <= 队尾元素 则将队尾元素出队列,直到满足 a 大于队尾元素即可;同理对于单调递减队列,也是类似的,这里不再详细说了。

举个栗子:

[1,4,3,5,2,7,8]构造单调队列 注意:这里不考虑单调队列的长度

1进队列 【1】

4>1 所以4进列 【1,4】

3<4 所以4出列 【1,3】

5>3 所以5进列 【1,3,5】

2<5 所以5,3出列 然后2进【1,2】

7>2 所以进列 【1,2,7】

8>7 所以进列 【1,2,7,8】

对于要考虑队列长度的,可以看看如下栗子(https://www.cnblogs.com/tham/p/8038828.html)

数列为:6 4 10 10 8 6 4 2 12 14

N=10,K=3;

那么我们构造一个长度为3的单调递减队列:

首先,那6和它的位置0放入队列中,我们用(6,0)表示,每一步插入元素时队列中的元素如下

插入6:(6,0);

插入4:(6,0),(4,1);

插入10:(10,2);

插入第二个10,保留后面那个:(10,3);

插入8:(10,3),(8,4);

插入6:(10,3),(8,4),(6,5);

插入4,之前的10已经超出范围所以排掉:(8,4),(6,5),(4,6);

插入2,同理:(6,5),(4,6),(2,7);

插入12:(12,8);

插入14:(14,9);

在动态规划中用到单调队列的主要就是滑动窗口最大最小值问题了,什么是滑动窗口问题呢?如下:对于数列a=[1,3,2,7,4,5],选取3的滑动窗口,则每次滑动后的结果为:

第一次: (1 3 2) 7 4 5

第二次: 1 (3 2 7) 4 5

第三次: 1 3 (2 7 4) 5

那么这个动态规划问题就是要求每次滑动的最大或者最小值。好吧,无不无聊,每次滑动后直接比较不就ok了吗?我曹,好像可以,那就直接每次滑动后直接比较呗,但是会发现超时。因为第一次和第二次是不是多比较了一次,人呐,总想要更加简化,那怎么简化这个问题,就是上面说的单调队列。也就是搞一个长度为3的单调队列来跑,看看代码吧,代码中我只显示了下降的单调队列。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int n, K, i, a[10];
int q1[10], q2[10], ans1[10], ans2[10];
int l1 = 1, l2 = 1, r1, r2;
int main()
{
    scanf("%d%d", &n, &K);
    for (i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (i = 1; i <= n; i++){
        while (l1 <= r1&&q1[l1] <= i - K) //因为我们这个窗口为3,每次都要判断一下l1(队列的头是不是太远了,太远了就没用了。这里l1<=r1好像要不要都没关系)
            l1++;
        //while (l2 <= r2&&q2[l2] <= i - K)l2++;
        while (l1 <= r1&&a[i]<a[q1[r1]])//重点,如果来了一个新的值,这个新的值必队尾的值还小,果断不要队尾,注意这里q1保存的是下标,不是具体的那个位置的值
            r1--;
        q1[++r1] = i;       //更新下标的值
        //while (l2 <= r2&&a[i]>a[q2[r2]])r2--;
        //q2[++r2] = i;
        ans1[i] = a[q1[l1]]; //每次保存队首元素
        //ans2[i] = a[q2[l2]];

    }
    for (i = K; i <= n - 1; i++)printf("%d ", ans1[i]); printf("%d\n", ans1[n]);
    return 0;
    //for (i = K; i <= n - 1; i++)printf("%d ", ans2[i]); printf("%d", ans2[n]);
}

大家可以调试一下,主要看一下q1是怎么变化的。大致就可以明白了。
这道题对应的leetcode题目是https://leetcode.com/problems/sliding-window-maximum/
在youtube上的视频是 https://www.youtube.com/watch?v=ShbRCjvB_yQ

烽火传递

一道动态规划的题目,不用我说,肯定也是用单调队列来实现的,先介绍题目吧。

Description
  烽火台又称烽燧,是重要的军事防御设施,一般建在险要或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息;夜晚燃烧干柴,以火光传递军情,在某两座城市之间有n个烽火台,每个烽火台发出信号都有一定代价。为了使情报准确地传递,在连续m个烽火台中至少要有一个发出信号。请计算总共最少花费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确传递。
Input
  第一行:两个整数N,M。其中N表示烽火台的个数,M表示在连续m个烽火台中至少要有一个发出信号。接下来N行,每行一个数Wi,表示第i个烽火台发出信号所需代价。
Output
  一行,表示答案。
Sample Input
5 3
1
2
5
6
2
Sample Output
4
Data Constraint
对于50%的数据,M≤N≤1,000 。 对于100%的数据,M≤N≤ 100,000,Wi≤100。

博客 https://blog.csdn.net/A1847225889/article/details/77777009
先用简单的动态规划来试试吧

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int w[100001];
int f[100001];
int main()
{
    scanf("%d%d",&n,&m);
    int i,j;
    for (i=1;i<=n;++i)
        scanf("%d",&w[i]);
    memset(f,127,sizeof f);
    f[0]=0;
        for (int i = 1; i <= n; i++)
    {
        for (j = max(0, i - m); j<i; ++j)//这里把博客中的合并了
            f[i] = min(f[i], f[j]);
        f[i] += w[i];
    }


    int ans = 0x7f7f7f7f;
    for (i = n - m + 1; i <= n; ++i)
        ans = min(ans, f[i]);
    printf("%d\n", ans);
}

来看看我们的单调队列是怎么做的吧?

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n, m;
int w[10];
int que[10], head = 0, tail = 0;
int f[10];
int main()
{
    scanf("%d%d", &n, &m);
    int i, j;
    for (i = 1; i <= n; ++i)
        scanf("%d", &w[i]);
    memset(f, 127, sizeof f);
    f[0] = 0;
    que[0] = 0;
    for (i = 1; i <= n; ++i)
    {
        if (que[head]<i - m)
            ++head;//将超出范围的队头删掉
        f[i] = f[que[head]] + w[i];//转移(用队头)
        while (head <= tail && w[que[tail]]>w[i])
            --tail;//将不比它优的全部删掉
        que[++tail] = i;//将它加进队尾
    }
    int ans = 0x7f7f7f7f;
    for (i = n - m + 1; i <= n; ++i)
        ans = min(ans, f[i]);
    printf("%d\n", ans);
}

对比一下就可以发现这个代码是怎么一步一步写到这里的。

个人比较喜欢Python的代码,简洁表达如下:

# -*- coding: utf-8 -*-
##code2
##用来求滑动窗口最小值问题
a=[0,4,2 ,5, 6, 1, 7]
n=len(a)-1
m=3 #窗口大小
head=1
tail=0 #这里初始化为0是有必要的
dp=[0]*10
queue=[0]*10
ansl=[0]*10
for i in range(1,n+1):
    while (head<=tail and queue[head]<= i-m):
        head=head+1 #head的递增是有条件的,即不能让下标的距离太大,m=3,超过肯定不行,前初始化tail=0也是有道理的
    dp[i]=dp[queue[head]]+a[i]
    while (head<=tail and a[i]<a[queue[tail]]):#发现比队尾还小的情况,那就果断出队
        tail=tail-1
    tail=tail+1
    queue[tail]=i
    ansl[i]=a[queue[head]]

for ii in range(m,n+1):
    print ansl[ii]
print min(dp[n-m+1:n]) #输出总体最小值


## code2
## 用来求烽火传递问题
a=[0,4,2 ,5, 6, 1, 7]
n=len(a)-1
m=3 #窗口大小
head=0
tail=0 #这里初始化为0是有必要的
dp=[0]*10
queue=[0]*10
ansl=[0]*10
for i in range(1,n+1):
    while (head<=tail and queue[head]< i-m):
        head=head+1 #head的递增是有条件的,即不能让下标的距离太大,m=3,超过肯定不行,前初始化tail=0也是有道理的
    dp[i]=dp[queue[head]]+a[i]
    while (head<=tail and a[i]<a[queue[tail]]):#发现比队尾还小的情况,那就果断出队
        tail=tail-1
    tail=tail+1
    queue[tail]=i
    ansl[i]=a[queue[head]]

for ii in range(m,n+1):
    print ansl[ii]
print min(dp[n-m+1:n]) #输出总体最小值

猜你喜欢

转载自blog.csdn.net/hguo11/article/details/80350124