震惊!单调队列优化DP竟如此简单——Monkey(原题:POI2014 Little Bird)

目录

前言

题目

初步分析 

正解

结尾 


前言

此题是用单调队列来优化DP的实例,十分适合初学单调队列的人做,并且类似于单调队列优化多重背包,对今后进一步学习DP有很大的作用。

(如果你不知道什么是单调队列,看看这篇博客(矩形面积)

LGOJ P3572 Little Bird


 

题目

问题 A(3768): 猴子

时间限制: 2 Sec  内存限制: 128 MB

 

题目描述

有Q只猴子要从第一棵树到第n棵树去,第i只猴子一次跳跃的最远距离为Ki。如果它在第x棵树,那它最远可以跳到第x+Ki棵树。如果第j棵树的高度比第i棵树高或相等,那么它从第i棵树直接跳到第j棵树,它的劳累值会增加1。所有猴子一开始在第一棵树,请问每只猴子要跳到第n棵树花费的劳累值最小。

 

输入

第一行一个整数n,表示有n棵树。(2<=n<=1000000)

接下来第二行给出n个正整数D1,D2,……,Dn(1<=Di<=10^9),其中Di表示第i棵树的高度。

第三行给出了一个整数Q(1<=Q<=25),接下来Q行,给出了每只猴子一次跳跃的最远距离Ki(1<=Ki<=N-1)。

 

输出

输出Q行,每行一个整数,表示一只猴子的最小的劳累值。

 

样例输入

4 6 3 6 3 7 2 6 5
2
2
5

样例输出

2
1

 

初步分析 

此题,懂点DP的人第一眼看到便知道是一个DP,很简单的,用f[i]表示猴子爬到第i棵树的最小花费,h[i]表示第i棵树的高度,k[i]

表示第i个猴子最多爬多远的距离,那么我们就可以得到第一个状态转移方程:

f[i]=min(f[j]+h[i]>=h[j])i-k[i]<=j<=i-1

枚举一个j表示可以从第j棵树爬到第i棵树,h[i]>=h[j]表示如果第i棵树的高度如果大于等于第j棵树的高度,那么就会返回1,否则返回0,最后取最小值,便是最优状态,代码如下

//此代码是我考场上打的代码,尽管不是最优解,但也骗了许多分,虽然与上面的转移方程有出入,但主体思想大体是一样的
//这个DP代码理解起来应该十分容易,所以我就不打注释了
#include 
#include 
#include 
#include 

using namespace std;

int read() {
    int f=1,s=0;char a=getchar();
    while(!(a>='0'&&a<='9')) { if(a=='-') f=-1; a=getchar(); }
    while(a>='0'&&a<='9') {s=s*10+a-'0';a=getchar();}
    return f*s;
}

#define N 1000010
#define M 30
#define LL long long

struct node {
    LL h,num;
};

LL h[N],n,k,s[M],dp[N],inf;
deque < LL > q;
int main()
{
    //freopen("monkey.in","r",stdin);
    //freopen("monkey.out","w",stdout);
    memset(dp,0x3f,sizeof(dp));
    inf=dp[1];
    dp[1]=0;
    n=read();
    for(int i=1;i<=n;++i)
        h[i]=read();
    k=read();
    for(int i=1;i<=k;++i)
        s[i]=read();
    for(int i=1;i<=k;++i) {
        for(int j=1;j<=n;++j) {
            for(int p=1;p<=s[i];++p) {
                if(h[j]>h[j+p])
                    dp[j+p]=min(dp[j+p],dp[j]);
                else
                    dp[j+p]=min(dp[j+p],dp[j]+1);
            }
        }
        cout<

那么这道题就解决了

 


 

正解

如果你觉得这道题就这么简单的解决,那就太天真了,此题的数据N是10^6,这个代码的复杂度是O(N^2Q),明显不够。

那么,我们能否在DP的基础上有一定的优化呢?答案是肯定的,我们观察,发现枚举p这个阶段是有重复性的,也就是进行了多余的运算,这也是我们优化的切入口(如果你无法理解这句话,请看下面这张图)。

震惊!单调队列优化DP竟如此简单——Monkey(原题:POI2014 Little Bird)_第1张图片

红色部分表示在这两次我们重复计算的部分,我们要求的是f[i]到f[i+ki]这个部分的f[j]的最小值,那么他就肯定在这个红色的重叠部分中,那我们为什么还要再计算一次呢?只需要一个单调队列就可以用O(1)的时间把这个min给求出来,并把这个单调队列维护到f[i-1]到f[i-ki]这个区间内,最后把这个最小值存储到f这个数组内,就完美的把时间复杂度缩短到O(NQ)

此外,还有一些细枝末节的事需要我们考虑:如果在维护时,两个f值是相等的,那么应该选择哪一个?我们可以贪心式地想,选择高度更大地那个,因为这样可以保证结果最优(如果不懂可以在这里好好想想) 

有了矩形面积这道题的铺垫,我们就可以熟练地打出以下的代码:

#pragma GCC optimize(2)
#include 
#include 
#include 
#include 
 
using namespace std;
 
#define inf 0x7f7f7f7f
#define N 1000010
 
int read() {
    int f=1,s=0;char a=getchar();
    while(!(a>='0'&&a<='9')) {if(a=='-') f=-1 ; a=getchar(); }
    while(a>='0'&&a<='9') {s=s*10+a-'0'; a=getchar();}
    return f*s;
}

int n,m,a[N],f[N],dis[N];
deque q;//q的队尾存储的是最优解f数组的下标
 
int main()
{
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    m=read();
    for(int i=1;i<=m;i++)//dis用来存储这m个询问
        dis[i]=read();
    for(int k=1;k<=m;k++) {
        q.clear();
        memset(f,0,sizeof(f));
        f[1]=0;
        q.push_back(1);//初始化,不用赘述
        for(int i=2;i<=n;i++) {
            while(!q.empty() && q.front()+dis[k] f[i] || ( f[q.back()] == f[i] && a[i] >= a[q.back()] ) ) )//维护这个单调队列,原因我已经解释过了,这里不再赘述
            	q.pop_back();
            q.push_back(i);
        }
        cout<

 

结尾 

这是我一次接触到单调队列对DP的优化,感觉受益颇多呢。

我的QQ:2716776569欢迎跟我一起交流算法

震惊!单调队列优化DP竟如此简单——Monkey(原题:POI2014 Little Bird)_第2张图片

 

 

你可能感兴趣的:(C++,单调队列,单调队列专栏——从入门到放弃)