ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

单调队列优化dp

2022-07-04 17:00:47  阅读:81  来源: 互联网

标签:ch 队列 位置 int dp 单调


如果 dp 方程需要你从一个区间的 dp 数组的值中选择某些最大值,那么太好了,dp 方程设定的这个区间相当于滑动窗口,需要最大最小值都可以用单调队列优化。

P2627 [USACO11OPEN]Mowing the Lawn G

不能选超过 \(k\) 个奶牛,那么状态中必定要有关于是否被选的一维。一开始我想的是 \(dp_{i,j}\) 表示目前选到第 \(i\) 个,上一个被选的奶牛是第 \(j\) 个,但很显然,这样的做法空间无法被接受。

在我思考了一段时间后,我想到了可以考虑当前这个位置是否被选,这样也可以转移:\(dp_{i,0}\) 表示目前选到第 \(i\) 个,且第 \(i\) 个没有被选。\(dp_{i,1}\) 表示目前选到第 \(i\) 个,且第 \(i\) 个被选了。

容易发现,如果你不选第 \(i\) 个,那么你这一位对答案没有任何贡献,使用上一位的答案转移就行了,也就是:

\[dp_{i,0} = \max(dp_{i - 1,0},dp_{i - 1,1}) \]

如果你选了第 \(j\) 个,你就可以从之前的选或者不选来转移了。

如果你从之前的不选的位置转移。

\[dp_{i,1} = \max(dp_{j,0}) + \text{sum}(j + 1,i) (i - k \leq j \leq i - 1) \]

按照常理来说,应该还需要做一个 \(dp_{i,1} = \max(dp_{j,1})\) ,因为如果你枚举被选过的位置,这一位被选的个数不同,所以不能取区间和了。

但是仔细思考后会发现,这个方程就相当于上面那个方程所做的事情。只是你从中间选了一个断点,甚至还浪费了后面位置的贡献。所以该方程没有作用,舍去。

整理出目前的两条方程,发现按照常理只能做到 \(O(n^2)\) 。但是,由于这是一道单调队列优化 dp 的题,我们需要用单调队列优化它。

按照目前的方程形式,我们无法将 \(dp_{j,0}\) 或者是 \(dp_{j,0} + \text{sum}(j + 1,i)\) 推入单调队列,因为无法消除 \(\text{sum}(j + 1,i)\) 对我们答案的影响,甚至还有可能影响单调队列的单调性。

在这个节点,我卡了很久,最终使用了一种大佬介绍的方式解决。

考虑将 \(\text{sum}(j + 1,i)\) 拆成前缀和形式,那么便是 \(pre_i - pre_j\) 。这个时候,只有 \(pre_i\) 随着位置的变化而变化,那么我们维护 \(dp_{j,0} - pre_j\) ,在计算到每一位的时候,取队列中最大的值,加上 \(pre_i\) 就好了!

/*
   深山踏红叶,耳畔闻鹿鸣
   飘摇风雨中,睹物思故乡
      可叹,落叶飘零
*/

#include<bits/stdc++.h>

#define int long long
#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator

using namespace std;

inline int read(){
   int s = 0,w = 1;
   char ch = getchar();
   while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
   while(ch >= '0' && ch <= '9')s = s * 10 + ch - '0',ch = getchar();
   return s * w;
}

struct node{
    int w;
    int id;
};

int n,m;
int a[1000010];
int dp[1000010][2];
int pre[1000010];

deque <node> q;

signed main(){
    cin>>n>>m;
    for(int i = 1;i <= n;i ++)a[i] = read();
    for(int i = 1;i <= n;i ++)pre[i] = pre[i - 1] + a[i];
    //dp[1][1] = a[1];
    q.push_back({0,0});
    for(int i = 1;i <= n;i ++){
        dp[i][0] = max(dp[i - 1][0],dp[i - 1][1]);
        int v;
        v = dp[i][0] - pre[i];
        while(!q.empty() && q.back().w < v)q.pop_back();
        q.push_back({v,i});
        while(!q.empty() && i - q.front().id > m)q.pop_front();
        dp[i][1] = q.front().w + pre[i];
    }
    cout<<max(dp[n][0],dp[n][1]);
}

P3957 [NOIP2017 普及组] 跳房子

考虑二分答案。

对于选出来的 \(mid\) ,用一次 \(dp\) 判断它是否可行。

具体一点:

设 \(dp_i\) 为到达第 \(i\) 个格子能够获得的最大金币。

可得:

\[dp_i = \max(dp_j) (i - r \leq j \leq \min(i - l,i - 1)) + a_i \]

由于不是每个位置都有格子,而且位置的值域非常大,无法将复杂度依赖于此(\(n\log^2n\) 的复杂度甚至无法通过),所以只能使用两个指针,指向可以转移的最左位置和最右位置,相当于手动滑动窗口。对于滑动出来的窗口使用单调队列处理,复杂度为 \(O(n)\) 级别的。

每一次到下一个格子,都手动把两个指针右移。两个指针分别最多右移 \(n\) 次,复杂度为 \(O(n)\) ,二分复杂度为 \(O(\log \max a_i)\),所以最终复杂度为 \(O(n\log\max a_i)\) ,足以通过此题。

P1725 琪露诺

这道题太菜了,没资格让我写题解。

/*
   深山踏红叶,耳畔闻鹿鸣
   飘摇风雨中,睹物思故乡
      可叹,落叶飘零
*/

#include<bits/stdc++.h>

#define int long long
#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator

using namespace std;

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

struct node{
    int w;
    int id;
};

int n,l,r;
int a[1000010];
int dp[1000010];
int ans = -1e18;
int pre[1000010];
int pos;

deque <node> q;
vector <int> p;

signed main(){
    mem(dp,-0x3f);
    cin>>n>>l>>r;
    for(int i = 1;i <= n + 1;i ++)a[i - 1] = read();
    dp[0] = a[0];
    for(int i = l;i <= n + r + 1;i ++){
        int v = dp[i - l];
        while(!q.empty() && q.back().w <= v)q.pop_back();
        q.push_back({v,i - l});
        while(!q.empty() && i - q.front().id > r)q.pop_front();
        dp[i] = q.front().w + a[i];
        pre[i] = q.front().id;
    }
    for(int i = n + 1;i <= n + r + 1;i ++){
        if(dp[i] > ans){
            ans = dp[i];
            pos = i;
        }
    }
    cout<<ans<<endl;
    while(1){
        pos = pre[pos];
        p.push_back(pos);
        if(pos == 0)break;
    }
    for(int i = p.size() - 1;i >= 0;i --)printf("%lld ",p[i]);
    puts("-1");
}

P3572 [POI2014]PTA-Little Bird

这个题第一眼看到时,由于发现有高低限制,所以不能直接用单调队列维护 dp 。

但是思考了一下,转移的位置只有两种:

  1. 从该位置走到这里,不需要增加疲劳值,且这个位置是在可以走过来的区间内 dp 值最小,或者比最小值仅大 1 的。

  2. 从该位置走到这里,需要增加疲劳值,且这个位置是在可以走过来的区间内 dp 值最小的。

那么,我们从单调队列左边开始往右遍历,由于单调队列满足单调性,所以到达第一个比最小值大 2 的位置就可以停下了。记录遍历到的地方的最小值,这个最小值要算上是否要增加 1 疲劳值,也就是说我们优先选择高度较高的位置。

但是,这个做法过不了。

可以看到,经过我的优化,速度确实加快了,但没什么实际作用。

然后,我思考了一下,不一定要比我即将入队的这个值大才可以出队,如果和我这个值一样大,而且这个位置的高度还比我低,也可以出队。

把这个东西加上去,就过了。

然后觉得好牛逼!我乱搞AC了!!!

思考了一下,发现好像如果这样出队的话,我根本不需要遍历了。

把遍历去掉,AC了!

一点开提交记录,我草,怎么是最优解,很震撼。

点开题解,我草,怎么我的做法和题解一样。

/*
   深山踏红叶,耳畔闻鹿鸣
   飘摇风雨中,睹物思故乡
      可叹,落叶飘零
*/

#include<bits/stdc++.h>

#define mem(x,y) memset(x,y,sizeof(x))
#define si set <node> :: iterator

using namespace std;

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

struct node{
    int w;
    int id;
}q[10000010];

int n,Q,m;
int a[10000010];
int dp[10000010];
int l,r;
int ans[10000010];

signed main(){
    cin>>n;
    for(int i = 1;i <= n;i ++)a[i] = read();
    cin>>Q;
    while(Q --){
        m = read();
        if(ans[m]){
            printf("%d\n",ans[m]);
            continue;
        }
        l = 1,r = 0;
        dp[1] = 0;
        for(int i = 2;i <= n;i ++){
            int v = dp[i - 1];
            while(l <= r && q[r].w > v)r --;
            while(l <= r && q[r].w == v && a[q[r].id] <= a[i - 1])r --;
            q[++ r] = {v,i - 1};
            while(l <= r && i - q[l].id > m)l ++;
            int minn = q[l].w;
            int pos = a[q[l].id];
            if(l <= r){
                for(int j = l;j <= r;j ++){
                    if(q[j].w != minn)break;
                    if(a[q[j].id] > pos){
                        pos = a[q[j].id];
                        l = j;
                    }
                }
                dp[i] = minn;
                if(pos <= a[i])dp[i] ++;
            }
        }
        ans[m] = dp[n];
        printf("%d\n",dp[n]);
    }
}

标签:ch,队列,位置,int,dp,单调
来源: https://www.cnblogs.com/enderdeer/p/16443544.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有