单调队列优化DP模型整理

135. 最大子序和(活动 - AcWing

找一个长度不超过m的连续子序列,但是并未指定这个子序列的长度,所以长度就有很多种选择,要获取任意一段长度的序列的区间和,那么显然要用到前缀和。然后我们来考虑,讨论以每个点作为结尾的序列显然可以将所有情况都不重不漏地考虑进去。那么就是考虑如何获得以某个节点作为结尾的子序列,长度为m,显然有一个思路就是暴力求解,即第一维循环尾节点,第二维循环往前延伸多少,实现是一定可以实现的,而时间复杂度就很高了,大概O(nm),所以我们要考虑考虑还有没有其他的解法。我们看一下求的过程,显然是用尾节点的前缀和减去前面的合法位置的前缀和,如果有一个点的前缀和小于它前面所有合法位置的前缀和,那么显然它前面所有位置的前缀和都不会再被用到,因为用这个点一定更优,所以我们相当于有一个容器来维护当前节点之前的合法位置的前缀和,然后保证最前面的元素一定是最小的,那么就成了滑动窗口问题。

#include<bits/stdc++.h>
using namespace std;
int a[300010],q[300010];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int res;
    for(int i=1;i<=n;i++) 
    {
        scanf("%d",&a[i]),a[i]+=a[i-1];
        if(1!=i) res=min(a[i],res);
        else res=a[i];
    }
    int hh=0,tt=0;//tt就指向末尾位置,将hh置空
    for(int i=1;i<=n;i++)//以i作为结尾,前面最多用到i-m+1位,但是求前缀和要再往前一位,故而就是i-m
    {
        while(q[hh]<i-m) hh++;//因为i-1肯定被放入了,所以不用担心队列为空,即使最开始0<1也是可以的
        res=max(res,a[i]-a[q[hh]]);//hh>tt的时候相当于弹空,弹空位置是0,刚好
        while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
        q[++tt]=i;
        //相当于已经放入了0,从1开始放前缀和
    }
    cout<<res;
}

 ps:单调队列自己写清楚就行,不是非得这么模拟。

1088. 旅行问题(活动 - AcWing

如图,我们对于每个起点都需要判断,同时还需要判断是顺时针走还是逆时针走,只要两者之间有一个满足能够回到起点,那么就是该点就是可以的。 

顺时针的如下考虑:

所以最核心的地方在于在数轴上正确找到我们要求的区间。

哦另外,这个题的数据范围可能会爆int,记得处理一下

#include<bits/stdc++.h>
#define int long long
using namespace std;
int o[1000010],d[1000010],s[2000010],ans[1000010],q[2000010],hh,tt;
signed main()
{
    int n;
    scanf("%lld",&n);
    for(int i=1;i<=n;i++) scanf("%lld%lld",&o[i],&d[i]);
    //顺时针
    for(int i=1;i<=n;i++) s[i+n]=s[i]=o[i]-d[i];
    for(int i=1;i<=2*n;i++) s[i]+=s[i-1];
    hh=tt=0;
    for(int i=2*n;i>=0;i--)
    {
        while(hh<tt&&q[hh]>i+n)hh++;
        if(i<n&&s[q[hh]]-s[i]>=0) 
        {
           // printf("1:%d %d\n",i,q[hh]);
            ans[i+1]=1;
        }
        while(hh<tt&&s[q[tt-1]]>=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=0;
    for(int i=1;i<=2*n;i++)
    {
        //4-1
        while(hh<tt&&q[hh]<i-n)hh++;
        if(i>n)
        {
            if(s[i]-s[q[hh]]>=0) 
            {
                ans[i-n]=1;
               // printf("2:%d %d %d\n",i,q[hh],i-n);
            }
        }
        while(hh<tt&&s[q[tt-1]]<=s[i]) tt--;
        q[tt++]=i;
    }
    
    for(int i=1;i<=n;i++) 
    {
        if(ans[i]==1) printf("TAK\n");
        else printf("NIE\n");
    }
}

1087. 修剪草坪(活动 - AcWing

这里首先是要得到最大效益,然后是不能有连续超过k只奶牛被选中。,然后要求最大效益。

我们可以定义dp[i]表示从前i头牛中选的合法方案。

那么第i头牛可以选也可以不选,如果不选的话,那么直接从i-1头牛出转移状态即可

如果选,那么就要注意了,我们要保证不超过连续k头被选

那么这里就需要讨论一下以i为结尾,连续多少头牛被选:

假设连续j头被选:1<=j<=k:

那么我们第i-j+1头牛是被选区间的左边界,i-j头牛一定不能被选,否则就不是连续j头牛了,那么i-j头牛在哪个值中一定不被选呢,很显然是dp[i-j-1],因为这是从前i-j-1头牛中选的合法情况,肯定不包含第i-j头牛,那么转移方程就出来了。另外我们要快速获得连续j头牛的区间和,用前缀和处理一下就好。

状态计算部分代码如下: 

dp[1]=w[1];
    for(int i=2;i<=n;i++)
    {
        dp[i]=dp[i-1];
        for(int j=1;j<=k&&i-j>=0;j++)
        {
            int l=max(i-j-1,0);
            dp[i]=max(dp[i],dp[l]+w[i]-w[i-j]);
        }
    }

显然这个嵌套循环的时间复杂度有点高,很容易超时,那么我们观察一下,往前延伸j个,要求最大值,那么不就是在前面长度为j的窗口中求最大值嘛,用单调队列优化一下即可。我们再来观察下递推的式子:dp[i]=dp[i-j-1]+w[i]-w[i-j],显然与j有关的有两个值,所以我们需要按照这两个值来维护滑动窗口。我们可以定义一个函数来获取dp[i-j-1]-w[i-j]的值,进而维护单调队列。

另外初始的时候需要把0放入队列,因为前k个更新的时候可以延伸到开头,那么计算就要用到0处的值。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll w[100010],dp[100010];
int q[100010],hh,tt;
ll get(int i)
{
    if(!i) return 0;
    return dp[i-1]-w[i];
}
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&w[i]);
        w[i] += w[i-1];
    }
    hh=0,tt=1;//初始将0放入
    for(int i=1;i<=n;i++)
    {
        dp[i]=dp[i-1];
        while(hh<tt&&q[hh]<i-k) hh++;
        dp[i]=max(dp[i],w[i]+get(q[hh]));
        while(hh<tt&&get(q[tt-1])<=get(i)) tt--;
        q[tt++]=i;
    }
    cout<<dp[n];
}

 1089. 烽火传递(活动 - AcWing

思路:这题是连续m个至少有一个发出信号,然后要求花费最小值。这里如果第i个发出信号,那么前面只要第i-m个发出信号即可,中间的发不发都无所谓,不发最好。如果第i个不发出信号,那么前面[i-m+1,i-1]中必须有一个发出信号。而且我们要确保它发信号,那么就不能笼统的定义从前i个中选,必须要精确到它发不发。可以多加一维表示该点是否发信号,当然也可以定义dp[i]表示第i个点发出信号,那么我们可以来枚举上一个发信号的位置j,显然j应该满足i-m<=j<=i-1,如果j再往前,那么显然就不满足条件了。那么我们的转移方程就出来了。不过很显然,如果要往前枚举的话时间应该会超,而且我们想要的只是前面一段区间中的最小值,所以用单调队列维护即可。

另外为了方便计算,自然要把0放入队列。还有就是答案也不一定就是最后一个位置放,我们需要从最后m个位置中找,有一个位置放即可。

#include<bits/stdc++.h>
using namespace std;
int q[200010],dp[200010],a[200010],hh,tt;
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    hh=0,tt=1;
    for(int i=1;i<=n;i++)
    {
        while(hh<tt&&q[hh]<i-m)hh++;
        dp[i]=dp[q[hh]]+a[i];
        //printf("%d %d %d\n",i,q[hh],dp[i]);
        while(hh<tt&&dp[q[tt-1]]>=dp[i]) tt--;
        q[tt++]=i;
    }
    int mi=0x3f3f3f3f;
    for(int i=n-m+1;i<=n;i++)
    {
        mi=min(mi,dp[i]);
    }
    cout<<mi;
}

1090. 绿色通道(1090. 绿色通道 - AcWing题库

思路:这道题需要求的是在总花费有限制的情况下,至少可以连续多少个不选。

这里问的是至少连续多少个,那么显然答案有很多,而且合法答案是满足单调性的,我们抽象的来看一下,很显然如果空的题多了,那么花费肯定不会超过t,如果空的题少了那么花费肯定又大于t,这里可能会疑惑是否严格满足单调性,但是我们只找最小的方案,那么就是小于这个方案的花费肯定大于t,大于这个方案的花费小于t,那么就可以再往下一点去找。所以我们只需要二分空的长度,然后在check函数里面用动态规划计算最小花费,判断是否小于t即可。

然后现在最关键的就是check函数怎么写,这里显然是要求最多空x长度时,花费最小是多少,那么和烽火台就一样了,我们定义dp[i]表示第i个位置点火,那么去找上一个点火的位置即可。

实际上还是有区别的,上题是m个中至少有一个发送信号,这里实际上可以连续m个空着,那么应该是连续m+1个至少有一个发射信号。

#include<bits/stdc++.h>
using namespace std;
int n,t;
int a[50010],dp[50010],q[50010],hh,tt;
int check(int x)
{
    memset(dp,0,sizeof dp);
    hh=0,tt=1;
    q[hh]=0;
    for(int i=1;i<=n;i++)
    {
        while(hh<tt&&q[hh]<i-x-1) hh++;
        dp[i]=dp[q[hh]]+a[i];
        while(hh<tt&&dp[q[tt-1]]>=dp[i]) tt--;
        q[tt++]=i;
    }
    int mi=0x3f3f3f3f;
    for(int i=n-x;i<=n;i++) mi=min(mi,dp[i]);
   // printf("%d\n",mi);
    if(mi<=t) return 1;
    else return 0;
}
int main()
{
    scanf("%d%d",&n,&t);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    int l=0,r=n;
    while(l<r)
    {
        int mid=(l+r)/2;
        if(check(mid)) r=mid;//空的越少越好
        else l=mid+1;
    }
    cout<<l;
}

1091. 理想的正方形(活动 - AcWing

 如图,对于一个n*n的矩形,我们将每行的最值累计到最右边,再将这一列的最值累计到右下角,那么预处理后就很容易获得这个区间的最值了。

如图的紫色区域就存了每个n*n矩形的最值。这个思路很简单,我们需要注意的就是下标的处理。

这里还有一个知识点,对于一个二维数组w[][],w[i]实际上可以用作一个一维数组。那么处理行就很简单了。对于每一行统计定长窗口中的最值。

统计列的时候,我们先一列一列的将每一列装进一个空的一维数组,然后再对这个一维数组进行上述区间找最值得操作。因为并不涉及区间与区间之间,所以我们只要对于每一个区间处理明白就行。

#include<bits/stdc++.h>
using namespace std;
int w[1010][1010],rmx[1010][1010],rmi[1010][1010],t1[1010],t2[1010],c[1010],d[1010],hh,tt,q[1010];
int n;
void get_max(int a[],int b[],int s)
{
    hh=tt=0;//可以为空
    for(int i=1;i<=s;i++)
    {
        while(hh<tt&&q[hh]<i-n+1) hh++;
        while(hh<tt&&a[q[tt-1]]<=a[i]) tt--;
        q[tt++]=i;
        b[i]=a[q[hh]];
    }
}
void get_min(int a[],int b[],int s)
{
    hh=tt=0;
    for(int i=1;i<=s;i++)
    {
        while(hh<tt&&q[hh]<i-n+1) hh++;
        while(hh<tt&&a[q[tt-1]]>=a[i]) tt--;
        q[tt++]=i;
        b[i]=a[q[hh]];
    }
}
int main()
{
    int a,b;
    scanf("%d%d%d",&a,&b,&n);
    for(int i=1;i<=a;i++)
        for(int j=1;j<=b;j++) 
            scanf("%d",&w[i][j]);
    
    for(int i=1;i<=a;i++) get_max(w[i],rmx[i],b),get_min(w[i],rmi[i],b);

   
    int res=0x3f3f3f3f;
    for(int j=n;j<=b;j++)//列
    {
        for(int i=1;i<=a;i++) t1[i]=rmx[i][j];
        get_max(t1,c,a);
        for(int i=1;i<=a;i++) t2[i]=rmi[i][j];
        get_min(t2,d,a);
        for(int i=n;i<=a;i++)  res = min(res,c[i]-d[i]);
    }
    cout<<res;
}

ps:数组的值传参可以修改数组。

虽然但是,实际上只有两种类型直接用上了dp,一种是要选尽可能多,但不能有超过连续k个被同时选,一种是选尽可能少,不能有连续超过k个不被选,dp时都是以i被选作为状态表示,去找上一个被选位置进而解决的。剩下的侧重单调队列,而非dp。

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 表示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以表示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值