动态规划优化

在多重背包问题3中,我们用到了单调队列优化DP,实际上,不止这个问题,很多问题我们仔细观察,就能发现它有类似的性质,我们就能用单调队列优化,我们通过几道题的练习来观察观察其中的规律。
最大子序和
题目链接:最大子序和
分析:看到这题,我们很容易想到比较暴力的方法:我们预处理出来前缀和数组,然后枚举右端点,那么f[i]=max(s[i]-s[j])(1<=i-j<=m),那我们很容易想出来用两重循环来解决这个问题,但是看这个数据范围毫无疑问是会超时的,因此我们可以考虑优化。我们发现,对于每一个i,有f[i]=s[i]-min(s[j])(1<=i-j<=m),因此我们可以用一个单调队列(不懂的可以看看滑动窗口)来动态维护s[j]的最小值。也就是对每一个i,我们的队头就是满足i-m<=j<=i-1的最小的s[j]对应的下标j。
代码实现:

#include<iostream>
#define x first
#define y second
using namespace std;
const int N=3e5+10;
int n,m,s[N],hh,tt=-1,q[N],res=-1e9;
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];//处理前缀和数组
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-m)hh++;//j超过滑动窗口范围的就出去
        while(hh<=tt&&s[q[tt]]>=s[i-1]) tt--;//队尾大于等于新加入值的出队
        q[++tt]=i-1;//新元素入队
        res=max(res,s[i]-s[q[hh]]);//res取max
    } 
    cout<<res;
    return 0;
}


修剪草坪
题目链接:修剪草坪
分析:我们用f[i]表示考虑1~i的所有奶牛的所有选择方案中贡献的最大值,那么对于状态转移,我们可以枚举最后一次没有选择的元素,对集合进行划分,设这个没有选择的数是j,贡献值的前缀和数组为s,则f[i]=max(f[j-1]+s[i]-s[j])=max(f[j-1]-s[j])+s[i](i-m<=j<=i),这里我们就能很明显的看出这题要用滑动窗口了,因为这题比较特殊,j的初始值可以为0,表示都不选0,即都选,因此我们需要最初把j=0的情况加入队列中。(另一种思路:题目要求我们最多连续选k个使效率最大,我们可以转换为每k+1个里面必须不选一个,使不选的总效率最小,然后用总效率减去不选的效率就是答案,剩下的就可以参考下面烽火传递那道题的思路了。)
代码实现:

#include<iostream>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int q[N],tt,hh,n,k;
LL s[N],f[N];
LL get(int x){//由于我们多次需要用到求f[i-1]-s[i],我们就单独写个函数
    return f[max(0,x-1)]-s[x];//这里当x=0时返回f[0]-s[0]即可,也就是0,这种情况返回什么应该结合具体问题具体分析
}
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];
    hh=0,tt=-1;
    q[++tt]=0;
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-k) hh++;
        while(hh<=tt&&get(q[tt])<=get(i)) tt--;
        q[++tt]=i;
        f[i]=s[i]+get(q[hh]);
    }
    cout<<f[n]<<endl;
    return 0;
}


旅行问题
题目链接:旅行问题
分析:对于这道题,每个节点有两种属性:油量和到下一节点的距离,我们可以简化为一个属性,油量减去到下一节点的距离,如果这一属性大于等于0,就说明能够到达下一节点,否则就不能,每经过一个点我们就累加此属性,如果我们从1节点出发,按顺时针走到n节点的途中经过每个点时的值都大于等于0,就说明我们能够转一圈,我们破环为链,把原数组再复制一遍,然后先考虑顺时针的情况(逆时针的就再算一遍,差不多的)。我们用s[i]1表示从1到i点所有的油量减去到下一节点的距离之差之和,对顺时针的情况,从某个点i出发,对任意i<=j<=i+n-1,都有s[j]-s[i-1]大于等于0,即固定i之后,我们要找出s[j](i<=j<=i+n-1)的最小值。遍历的时候从大到小遍历,用单调队列维护s[j]的最小值,逆时针的时候相同的思路就能写出来了。
烽火传递
题目链接:烽火传递
分析:这是一道很典型的DP问题,我们先按照暴力的分析方法来求解这个DP问题,f[i]表示考虑1~i的烽火台,点燃第i个烽火台的所有方案中的最小花费,那么最终答案就是f[n-m+1 ~n]中的最小值,对于每一个f[i],它可以由f[j] (i-m<=j<i)转移而来,看到这里我们就知道如何用单调队列优化了。
代码实现:

#include<iostream>
using namespace std;
typedef long long LL;
const int N=2e6+10;
LL s[N];
bool flag[N];
int n,q[N],hh,tt,oil[N],dist[N];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>oil[i]>>dist[i];
        s[i]=s[i+n]=oil[i]-dist[i];//破环为链
    } 
    for(int i=1;i<=2*n;i++) s[i]+=s[i-1];//处理前缀和
    hh=0,tt=-1;
    for(int i=2*n;i;i--){
        while(hh<=tt&&q[hh]>i+n-1) hh++;
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;
        if(i<=n&&s[q[hh]]-s[i-1]>=0) flag[i]=true;
    }
    dist[0]=dist[n];//逆时针时每个点的s为oid[i]-dist[i-1],而第一个点应该减去dist[n],因此把dist[0]赋值为dist[n]
    hh=0,tt=-1;//清空队列
    for(int i=n;i;i--) s[i]=s[i+n]=oil[i]-dist[i-1];//处理s的值
    for(int i=2*n;i;i--) s[i]+=s[i+1];//处理后缀和
    for(int i=1;i<=2*n;i++){//对每个s[i],需要s[j]-s[i+1]>=0(i-n+1<=j<=i)
        while(hh<=tt&&q[hh]<i-n+1) hh++;
        while(hh<=tt&&s[q[tt]]>=s[i]) tt--;
        q[++tt]=i;
        if(i>n&&s[q[hh]]-s[i+1]>=0) flag[i-n]=true;//这里不要忘记了i-n让其回到在1~n中的原位置
    }
    for(int i=1;i<=n;i++)
        if(flag[i]) puts("TAK");
        else puts("NIE");
    return 0;
}


代码很easy
绿色通道
题目链接:绿色通道
分析:看到这个题,我们很容易发现答案具有单调性,因此我们可以二分这个最长空题段,然后求出这个最长空题段对应的最短时间(这不就是上一题吗?),因此本题还是比较简单的。不过要注意,上一题是连续m个位置必须有一个,这一题是可以有连续m个空着的,因此连续m+1个必须有一个。
代码实现:

#include<iostream>
using namespace std;
const int N=5e4+10;
int n,t,w[N],q[N],hh,tt,dp[N];
bool check(int m){
    hh=0,tt=-1;//和上题一毛一样
    for(int i=1;i<=n;i++){
        while(hh<=tt&&q[hh]<i-m) hh++;
        while(hh<=tt&&dp[q[tt]]>=dp[i-1]) tt--;
        q[++tt]=i-1;
        dp[i]=dp[q[hh]]+w[i];
    }
    for(int i=n-m+1;i<=n;i++)
        if(dp[i]<=t)//有一种方案满足时间小于等于t就返回true
            return true;
    return false;//返回false
}
int main(){
    cin>>n>>t;
    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+1)) r=mid;
        else l=mid+1;
    }
    cout<<l<<endl;
    return 0;
}


理想的正方形
题目链接:理想的正方形
分析:二维RMQ问题?没有这么麻烦,因为正方形的边长是固定的。这一题如果列数为1的话不就是滑动窗口吗?受此启发,假设用w数组存储所有值,k表示边长,我们先处理出来一个子矩阵中每一行的最值,然后把这些值排为一列,处理出来它的最值,就求出了子矩阵的最值。
代码实现:

#include <cstring>
#include <iostream>
#include <algorithm>
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];//row_min[i][j]存储以i,j为右下角的子矩阵的行最小值,row_max同理
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) hh ++ ;//边长为k
        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) hh ++ ;
        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;
        b[i] = a[q[hh]];
    }
}
int main(){
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &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];//a,b,c是三个临时数组
    for (int i = k; i <= m; i ++ ){//枚举列,从k开始就行了
        for (int j = 1; j <= n; j ++ ) a[j] = row_min[j][i];//a数组存储下来第j列的行最小值
        get_min(a, b, n);//再取一遍滑动窗口,找出最小值放在b数组中
        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(res, c[j] - b[j]);//更新答案
    }
    printf("%d\n", res);
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

litian355

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值