动态规划(8)单调队列优化

 

目录

一、概述

二、例题

(1)最大子序和

(2)修剪草坪

(3)旅行问题

(4)烽火传递

 (5)绿色通道

(6)理想的正方形


 

一、概述

引入单调栈、单调队列两种思想,本质上都是借助单调性,及时排除不可能的决策,保持候选集合的高度有效性和秩序性。最多是使用滑动窗口的思想来优化

二、例题

(1)最大子序和

0fa04a7a712543949c413439c23e73db.png

 求子序和,想到前缀和数组。因此题目转化为,在i-m+1~i之间找一个最小值s(k),这样便使得s(i)-s(k)差值最大,即以i结尾的长度不超过m的连续子序列和最大值。

求出这些最大值,在取max,得答案。分析至此,要解决的问题只有如何快速找出前面m个数的最小值。这实际上是一个滑动窗口求最小值问题,之前已经讲解过。

细节处理: 提前入队一个0,使得在枚举长度小于m时,不需要特判,答案直接是s(i)-0。

其次,由于前缀和的性质:[l,r]=s[r]-s[l-1],我们q[hh]的范围并不是i-m+1~i,而是i-m~i-1。

#include<iostream>
#include<algorithm>
using namespace std;

const int N =300010;

int s[N];
int q[N],hh=0,tt=-1;
int n,m;

int main()
{
    cin>>n>>m;
    //一个长度为i-m+1~i的长度为m窗口内 最大值为s[i]-s(min)
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        s[i]+=s[i-1];
    }

    int res=-1e9;
    q[++tt]=0; //入队一个0

    //q[hh]属于i-m~i-1


    for(int i=1;i<=n;i++)
    {
        if(q[hh]<i-m) hh++;
        res=max(res,s[i]-s[q[hh]]);
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;
    }

    cout<<res;

}



作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4184726/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(2)修剪草坪

4c1f2d8b107341e285f683dc9a860c09.png

 易想到用动态规划处理,f(i)表示从前i头牛中选择,且合法的所有方案中效率最高的。

状态划分也易知为选择i与不选择i。不选择i则为f(i-1),若选择i则再需细分考虑选择i的所有方案,即选择以i结尾的连续j头牛,j的取值为1~k。

状态转移方程为:

gif.latex?f%5Bi%5D%3Dmax%28f%5Bi-j-1%5D&plus;s%5Bi%5D-s%5Bi-j%5D%29

注意这里根据定义,第i-j头牛是不选择的。b1cbd3bed70546a891f75d12ec9a0448.png

gif.latex?f%5Bi-j-1%5D&plus;s%5Bi-j%5D%3Dg%5Bi-j%5D,这样我们只需要求出max(g[i-k]~g[i-1])即可,即一个滑动窗口最大值,因此使用单调队列优化。

细节处理:遍历到i时,队列内下标范围为i-k~i-1。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =1e5+10;
typedef long long LL;

LL s[N];
LL f[N];//前i个选 合法的最大值
int n,m;
int q[N];

LL g(int i)
{
    return f[i-1]-s[i];
}


int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];

    int hh=0,tt=-1;
    q[++tt]=0;

    for(int i=1;i<=n;i++)
    {
        if(hh<=tt&&q[hh]<i-m) hh++;
        f[i]=max(f[i-1],g(q[hh])+s[i]);
        while(hh<=tt&&g(q[tt])<=g(i)) tt--;
        q[++tt]=i;
    }

    cout<<f[n];

}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4188950/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(3)旅行问题

797e5cb4f4eb4ff9a16109e4af3763b3.png

 拆环成链,预处理从1号点到达每个点时旅行的代价(得到的油-耗油),判断某个点i~i+n这一段能不能走通,即在i~i+n-1里找一个最小的前缀和s[k],判断s[k]-s[i-1]是否大于0。

本题思路简单,困难点在细节处理。

  1. 顺逆时针走结果不同,因此本题要做两边,一次沿顺时针一次沿逆时针
  2. 要求回到起点,所以区间长度为n+1.
  3. 单个数据1e9级别,注意开longlong
  4. 顺时针走时,令s[i]=s[i+n]=o[i]-d[i],表示从i走到i+1点,不拾取第i+1点的油量的油量变化。再处理前缀和,得到s数组意义为:从1走到i+1点,不拾取第i+1点油量的总油量(不拾取第i+1点的油量才能判断能不能走通)
  5. 判断从i为起点的这一段能不能走通,即在[i,i+n-1]里找一个最小的前缀和s[k],判断s[k]-s[i-1]是否大于0。
  6. 顺时针从后往前处理,因为k>=i

逆时针时:

  1. s(i)=o(i)-d(i-1)表示从第i个点走到第i-1个点,不拾取第i-个点的油量变化。第一个点的前一个点时第n个点,因此初始化d(0)=d(n),在能使整个s数组符合定义。
  2. 求得前缀和,s(i)-s(k)(k<i)表示以i为起点走到k-1,不拾取k-1点油量的总油量。
  3. 判断从i为起点能不能走通,即从i+n为起点,判断能不能走到i。即在[i+1,i+n]内找到一个最大的前缀和s[k],判断s[i]-s[k]是否大于0.
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =1e6+10;

int o[N],d[N];
int n;
long long s[2*N];
int q[N],hh=0,tt=-1;
bool ans[N];


int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>o[i]>>d[i],s[i]=s[i+n]=o[i]-d[i];;

    //顺时针

    //判断某个点i能不能走通 即在i~i+n这一段里面最小的s[k] 看s[k]-s[i-1]是否大于0

    for(int i=1;i<=n*2;i++) s[i]+=s[i-1];

    q[++tt]=2*n+1;

    for(int i=n*2;i>=0;i--)
    {
        if(hh<=tt&&q[hh]>i+n) hh++;

        if(i<n)
        {
            if(s[q[hh]]-s[i]>=0) ans[i+1]=true;
        }
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;

    }

    //逆时针
    d[0]=d[n];
    for(int i=1;i<=n;i++) s[i+n]=s[i]=o[i]-d[i-1];
    for(int i=1;i<=2*n;i++) s[i]+=s[i-1];

    hh=0,tt=-1;
    q[++tt]=0;

    //q应该存最大值
    for(int i=1;i<=2*n;i++)
    {
        if(hh<=tt&&q[hh]<i-n) hh++;
        if(i>n)
        {
            if(s[i]-s[q[hh]]>=0) ans[i-n]=true;
        }
        while(hh<=tt&&s[q[tt]]<=s[i]) tt--;
        q[++tt]=i;

    }



    for(int i=1;i<=n;i++) 
    if(ans[i])
        puts("TAK");
    else puts("NIE");
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4187632/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(4)烽火传递

f9ca7b174ba749a0aa28364be1d1032e.png

 猜测状态表示:在前i个烽火台中选择,符合要求的所有方案中代价最小值。状态划分为不选i,以及选择i,发现状态转移方程貌似写不出来。因此状态表示更改为以i结尾的,在前i个烽火台中选择符合方案中代价最小的方案。

则状态划分为上一个烽火台放置在哪,状态转移方程为:

gif.latex?f%5Bi%5D%3Dmin%28f%5Bi-1%5D%2Cf%5Bi-2%5D%2C%2C%2Cf%5Bi-m%5D%29&plus;w%5Bi%5D

因此也是个滑动窗口求最小值问题,使用单调队列优化。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =2e5+10;

int n,m;
int w[N];
int f[N];//以第i个结尾的 满足连续m个必有一个  代价的最小值
int q[N],hh=0,tt=-1;


int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];



    q[++tt]=0;

    for(int i=1;i<=n;i++)
    {
        if(hh<=tt&&q[hh]<i-m) hh++;//这里第i个烽火台会发射信号,所以j可以取到i - m
        f[i]=f[q[hh]]+w[i];

        while(hh<=tt&&f[q[tt]]>=f[i]) tt--;
        q[++tt]=i;
    }

    int res=1e9;
    for(int i=n-m+1;i<=n;i++) res=min(res,f[i]);
    cout<<res;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4187874/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 (5)绿色通道

a5eb5c5d50c14276995bc7866773d631.png

 和上一题类似,上一题是给定限制空段,求代价最小值。本题是在某个代价要求下,求空段的最大值。

由上一题知道,如果我们已知空段的限制,我们求可以求出这个限制下的代价最小方案。因此容易想到本题应用二分可解。判断是否合法即检验在该空段限制下求得的最小代价是否小于题目要求。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N =5e4+10;

int f[N],w[N],n,m;
int q[N];

bool check(int limit)
{
    memset(f,0,sizeof f);
    int hh=0,tt=-1;
    q[++tt]=0;
    for(int i=1;i<=n;i++)
    {
        if(hh<=tt&&q[hh]<i-limit-1) hh++; //允许最长空缺是limit 即上一题的m+1 所以j可以取到i-limit-1
        f[i]=f[q[hh]]+w[i];
        while(hh<=tt&&f[q[tt]]>=f[i]) tt--;
        q[++tt]=i;
    }
    int res=1e9;
    for(int i=n-limit;i<=n;i++) res=min(res,f[i]);

    return res<=m;
}


int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];

    int l=0,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4188673/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(6)理想的正方形

7f29962d79ff453cbe585d33c8bedb2c.png

 求出每一行滑动窗口最大最小值。

根据每一行滑动窗口最大最小值,即可求出每一列滑动区域最大最小值。

由常见公式

gif.latex?max%28a%2Cb%2Cc%2Cd%29%3Dmax%5Bmax%28a%2Cb%29%2Cmax%28c%2Cd%29%5D 

可知

我们可以预处理出每 列 的行中最值,再对该列求一个最值,就是该子矩阵的最值

该 降维手段,使得一个 二维滑动窗口问题 变成了 n行+n列 的 一维滑动窗口问题

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =1010,INF=1e9;

int n,m,k;
int w[N][N];
int row_min[N][N],row_max[N][N];
int q[N];

void get_min(int a[],int b[],int tot)
{
    int hh=0,tt=-1;
    for(int i=1;i<=tot;i++)
    {
        if(hh<=tt&&q[hh]<i-k+1) hh++;
        while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
        q[++tt]=i;
        b[i]=a[q[hh]];
    }

}

void get_max(int a[],int b[],int tot)
{
    int hh=0,tt=-1;
    for(int i=1;i<=tot;i++)
    {
        if(hh<=tt&&q[hh]<i-k+1) hh++;
        while(hh<=tt&&a[q[tt]]<=a[i]) tt--;
        q[++tt]=i;
        b[i]=a[q[hh]];
    }

}

int main()
{
    cin>>n>>m>>k;

    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>w[i][j];

    for(int i=1;i<=n;i++)//枚举行求 滑动窗口最大最小值
    {
        get_min(w[i],row_min[i],m);
        get_max(w[i],row_max[i],m);
    }

    int res=INF;
    int a[N],b[N],c[N];

    for(int i=k;i<=m;i++)//枚举列
    {
        for(int j=1;j<=n;j++) a[j]=row_min[j][i];
        get_min(a,b,n);

        for(int j=1;j<=n;j++) a[j]=row_max[j][i];
        get_max(a,c,n);

        for(int j=k;j<=n;j++) res=min(c[j]-b[j],res);

    }

    cout<<res;
    return 0;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4189360/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值