首页 > 其他分享 >单调队列优化 DP

单调队列优化 DP

时间:2024-09-11 21:37:12浏览次数:1  
标签:le DP 队列 max int 单调 dp

单调队列优化 DP

回忆单调队列的作用,\(O(n)\) 求出每一个大小为 \(K\) 的窗口中的最大、最小值。

以最大值为例,我们可以得到如下 DP 转移方程:

\[dp[i]=\max(val[j])+base[i],i-j\leq K \]

其中 \(base[i]\) 是一个仅与 \(i\) 有关的式子,不受 \(j\) 影响,且可以预处理得到;而 \(val[j]\) 则是一个仅与 \(j\) 以及 \(dp[j]\) 有关的式子,不受 \(i\) 影响。
因此我们可以维护一个 \(val[j]\) 的单调队列,每次从队列中选出最大值(通常是队头)更新 \(dp[i]\),这个过程就是单调队列优化 DP。

显然,单调队列能优化的线段树肯定能优化,但是单调队列的时间复杂度少一个 \(\log\)。


P2627 [USACO11OPEN] Mowing the Lawn G

题目大意:

有 \(N\) 个数,第 \(i\) 个数的值为 \(a_i\),需要在 \(N\) 个数中选择若干个数,使得每个数的值 \(a_i\) 之和最大,但是不能连续选择 \(K\) 个数。(即对于任意编号为 \(i\) 的数,不能选择编号为 \([i-K+1, i]\) 中的所有数)


令 \(dp[i][0/1]\) 表示序列选到第 \(i\) 个数时,不选/选第 \(i\) 个数能获得的最大值。

显然可以得出以下转移方程:

\[dp[i][0]=\max(dp[i-1][0],dp[i-1][1]) \]

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]+\sum_{t=j+1}^{i}a_t\} \]

可以预处理出前缀和优化掉一重循环,转移方程变成:

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]+Sum[i]-Sum[j]\} \]

可以将 \(Sum[i]\) 提出,得到:

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]-Sum[j]\}+Sum[i] \]

这样就得到了全文开头所说的单调队列形式的 DP 式子,那么用一个单调队列来维护长度为 \(K\) 的区间中 \(dp[j][0]-Sum[j]\) 的最大值即可。

单调队列优化时存最大值的下标,DP 更新前保证队列长度不大于 \(K\),DP 更新后如果当前下标的 DP 值大于队列中尾元素下标的 DP 值,就把尾元素删除,直至队列为空或者小于尾元素下标的 DP 值(保证队列单调递减)。最后加入当前下标。


朴素前缀和优化 DP 代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long

ll sum[100005];
ll dp[100005][2]; // dp[i][0/1] 表示到第 i 头奶牛时不选/选第 i 头奶牛的最大效率

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, k; cin>>n>>k;
    for(int i=1; i<=n; i++){
        ll x; cin>>x;
        sum[i] = sum[i-1]+x;
    }
    for(int i=1; i<=n; i++){
        dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
        for(int j=max(0, i-k); j<i; j++){
            dp[i][1] = max(dp[i][1], dp[j][0]+sum[i]-sum[j]);
        }
    }
    cout<<max(dp[n][0], dp[n][1]);
    return 0;
}

正解加上单调队列优化 DP 后的代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 1e5+5;

ll sum[N];
ll dp[N][2]; // dp[i][0/1] 表示到第 i 头奶牛时不选/选第 i 头奶牛的最大效率

struct qwqque{
    int q[N], sz, head, tail;
    qwqque(int k){
        memset(q, 0, sizeof(q));
        head = tail = 1; // 一开始的范围内不选可以看作从0转移而来
        sz = k;
    }
    void upd(int x){
        while(head<=tail && x-q[head]>sz)
            head++;
    }
    void push(int x){
        while(head<=tail && dp[x][0]-sum[x]>=dp[q[tail]][0]-sum[q[tail]])
            tail--;
        q[++tail] = x;
    }
    ll qmax(){
        return dp[q[head]][0]-sum[q[head]];
    }
};

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, k; cin>>n>>k;
    for(int i=1; i<=n; i++){
        ll x; cin>>x;
        sum[i] = sum[i-1]+x;
    }
    qwqque Q(k);
    for(int i=1; i<=n; i++){
        dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
        Q.upd(i);
        dp[i][1] = Q.qmax()+sum[i];
        Q.push(i);
    }
    cout<<max(dp[n][0], dp[n][1]);
    return 0;
}

P2569 [SCOI2010] 股票交易

题目大意:

一共有 \(T\) 天,第 \(i\) 天最多买入 \(as_i\) 股股票,卖出 \(bs_i\) 股股票,买入每股股票需要花费 \(ap_i\) 元,卖出每股股票需要花费 \(bp_i\) 元。手持股票数目不能超过 \(maxp\) 股。股票交易的天数间隔至少为 \(w\)。即如果第 \(i\) 天进行了股票交易,那么第 \([i+1, i+w]\) 天不得进行股票交易。

\(0 \le w < T \le 2000, 1 \le maxp \le 2000, 1\le bp_i \le ap_i \le 1000, 1 \le as_i \le bs_i \le maxp\)。


观察到 \(T, maxp \le 2000\),可以有两维 \(dp\)。设 \(dp[i][j]\) 表示前 \(i\) 天拥有股票数为 \(j\) 可以获得的最大钱数。

每天可以不进行交易,买入股票,卖出股票。因为后两种操作受 \(w\) 限制,会漏掉“虚空买”的操作状态。所以一共有 4 种状态。注意初始值应该全部先赋 \(-\infty\)。

1.虚空买(前面没有进行过股票交易操作)

买 \(j\) 股股票可得转移方程:

\[dp[i][j] = -ap_i \times j \quad (0 \le j \le as_i) \]

2.不进行交易

第 \(i\) 天拥有 \(j\) 股股票的值即为第 \(i-1\) 天拥有 \(j\) 股股票的值,因为 \(i-1\) 天前的最大值都已经转移到了第 \(i-1\) 天:

\[dp[i][j] = \max(dp[i][j], dp[i-1][j]) \quad (0 \le j \le maxp) \]

3.买入股票

第 \(i\) 天进行交易的操作应该从第 \(i-w-1\) 天转移而来(可以举例子理解):

\[dp[i][j] = \max(dp[i][j], dp[i-w-1][k] - (j-k) \times ap_i) \quad (j-as_i \le k < j) \]

注意 \(i-w-1\) 可能不大于 \(0\),此时判断该种情况无法转移。所以最开始第 \(i-w-1\) 天前也必定有“虚空买”这一交易,因此可以从小到大转移。

4.卖出股票

同理可得转移方程:

\[dp[i][j] = \max(dp[i][j], dp[i-w-1][k] + (k-j) \times bp_i) \quad (j < k \le j+bs_i) \]


因为第 3 种操作和第 4 种操作时间复杂度是 \(O(n^3)\) 的,考虑如何优化掉一维。

同上一题,以第 3 种操作为例,先分离 \(k\) 和 \(j\):

\[dp[i][j] = \max\{dp[i-w-1][k]+k \times ap_i - j \times ap_i\} \]

对于每一个 \(k\) 得到的 \(dp\) 值都减去了 \(j \times ap_i\),可以将其提出得:

\[dp[i][j] = \max\{dp[i-w-1][k]+k \times ap_i \} - j \times ap_i \]

这时 \(\max\) 里的值只与 \(k\) 有关,可以单调队列优化,对于每一个 \(j\),从 \(j-q[head]\) 长度不超过 \(as_i\) 的 \(\max\) 里的值转移而来。

因为对于第 3 种操作,单调队列里存的下标应该小于 \(j\),即 \(k < j\),所以应从小到大转移而来,对于第 4 种操作,单调队列里存的下标应该大于 \(j\),即 \(k > j\),所以应从大到小转移而来。


朴素 DP 代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 2005;

int inp[N], outp[N], ins[N], outs[N];
int dp[N][N]; // dp[i][j] 表示前 i 天持有股票数为 j 已获得的最多钱
int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T, maxp, w; cin>>T>>maxp>>w;
    for(int i=1; i<=T; i++)
        cin>>inp[i]>>outp[i]>>ins[i]>>outs[i];
    memset(dp, 128, sizeof(dp)); // 按位赋 128 即为 -INF
    for(int i=1; i<=T; i++){
        // 凭空买(无 w 限制)
        for(int j=0; j<=ins[i]; j++)
            dp[i][j] = -inp[i]*j;
        // 不买也不卖(从上一天转移来)
        for(int j=0; j<=maxp; j++)
            dp[i][j] = max(dp[i][j], dp[i-1][j]);
        if(i<=w)
            continue; // 可从 i-w-1 转移而来
        // 买入
        for(int j=0; j<=maxp; j++){
            for(int k=max(0, j-ins[i]); k<j; k++)
                dp[i][j] = max(dp[i][j], dp[i-w-1][k]-(j-k)*inp[i]);
        }
        // 卖出
        for(int j=0; j<=maxp; j++){
            for(int k=min(maxp, j+outs[i]); k>j; k--)
                dp[i][j] = max(dp[i][j], dp[i-w-1][k]+(k-j)*outp[i]);
        }
    }
    int ans = 0;
    for(int i=0; i<=maxp; i++)
        ans = max(ans, dp[T][i]);
    cout<<ans;
    return 0;
}

单调队列优化后代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 2005;

int inp[N], outp[N], ins[N], outs[N];
int dp[N][N]; // dp[i][j] 表示前 i 天持有股票数为 j 已获得的最多钱

int head, tail, q[N]; // 单调队列

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T, maxp, w; cin>>T>>maxp>>w;
    for(int i=1; i<=T; i++)
        cin>>inp[i]>>outp[i]>>ins[i]>>outs[i];
    memset(dp, 128, sizeof(dp)); // 按位赋 128 即为 -INF
    for(int i=1; i<=T; i++){
        // 凭空买(无 w 限制)
        for(int j=0; j<=ins[i]; j++)
            dp[i][j] = -inp[i]*j;
        // 不买也不卖(从上一天转移来)
        for(int j=0; j<=maxp; j++)
            dp[i][j] = max(dp[i][j], dp[i-1][j]);
        if(i<=w)
            continue; // 需可从 i-w-1 转移而来
        // 买入
        head = 1, tail = 0;
        for(int j=0; j<=maxp; j++){
            while(head<=tail && j-q[head]>ins[i])
                head++;
            if(head<=tail)
                dp[i][j] = max(dp[i][j], dp[i-w-1][q[head]]+q[head]*inp[i]-j*inp[i]);
            while(head<=tail && dp[i-w-1][q[tail]]+q[tail]*inp[i]<=dp[i-w-1][j]+j*inp[i])
                tail--;
            q[++tail] = j;
        }
        // 卖出
        head = 1, tail = 0;
        for(int j=maxp; j>=0; j--){
            while(head<=tail && q[head]-j>outs[i])
                head++;
            if(head<=tail)
                dp[i][j] = max(dp[i][j], dp[i-w-1][q[head]]+q[head]*outp[i]-j*outp[i]);
            while(head<=tail && dp[i-w-1][q[tail]]+q[tail]*outp[i]<=dp[i-w-1][j]+j*outp[i])
                tail--;
            q[++tail] = j;
        }
    }
    int ans = 0;
    for(int i=0; i<=maxp; i++)
        ans = max(ans, dp[T][i]);
    cout<<ans;
    // 这里也可以是直接输出 dp[T][0],因为如果有剩余就不如当初少买点
    return 0;
}

CF939F Cutlet

待补。

标签:le,DP,队列,max,int,单调,dp
From: https://www.cnblogs.com/FlyPancake/p/18409033

相关文章

  • Linux网络——socket编程与UDP实现服务器与客户机通信
    文章目录端口号TCP/UDP网络字节序socket的常见APIUDP实现服务器与客户机通信服务器客户机运行效果如下端口号我们说即便是计算机网络,他们之间的通信也仍然是进程间通信那么要如何在这么多计算机中,找到你想要的那个进程呢在网络中标识的唯一的计算机使用的是ip地......
  • windowns 修改RDP端口
    命令行操作$portvalue=13389#修改注册表Set-ItemProperty-Path'HKLM:\SYSTEM\CurrentControlSet\Control\TerminalServer\WinStations\RDP-Tcp'-name"PortNumber"-Value$portvalue#添加防火墙规则New-NetFirewallRule-DisplayName'RDPPORTL......
  • 单调队列优化 dp
    1.概念单调队列优化的本质是借助单调性,及时排除不可能的决策,保持候选集合的秩序性。2.例题P1714切蛋糕题目大意:给定一个序列,找出长度不超过\(m\)的连续子序列,使得子序列中所有数的和最大。思路:要求区间和,首先求出前缀和,然后考虑朴素dp,不难想到用\(dp[i]\)表示包含......
  • DPVO 代码剖析
    来自:https://github.com/princeton-vl/DPVO/blob/c0c5a104c9c58663aa9be62c3f125d5b52874f3e/dpvo/altcorr/correlation.py#L33classPatchLayer(torch.autograd.Function):@staticmethoddefforward(ctx,net,coords,radius):"""forwar......
  • Windows Server 2022 rdp
    继续水一篇:2022废弃了xddm转而使用wddm,rdp的渲染有比较大的变化。高版本的unreal又需要2022支持,被迫走上魔改windows以提升2022rdp环境下抓屏帧数的道路。测试代码来自https://github.com/robmikh/Win32CaptureSample,只手动添加了输出fps逻辑。patchwindows后能在[60,90]......
  • Wordpress 知名插件漏洞致百万网站面临接管风险
        流行的 WordPressLiteSpeedCache 插件中存在一个漏洞,可能允许攻击者检索用户cookie并可能接管网站。    该问题被跟踪为CVE-2024-44000,之所以存在,是因为该插件在用户登录请求后会在调试日志文件中包含set-cookie的HTTP响应标头。    ......
  • DP
    动态规划动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。三个条件:最优子结构,无后效性和子问题重叠。最优子结构证明问题最优解的第一个组成部分是做出一个选择;对于一个给定问题,在其可能的第一步选择中,假定你已经知道哪种选择才会得到最优解......
  • 搭建 WordPress 及常见问题与解决办法
    浪浪云活动链接:https://langlangy.cn/?i8afa52文章目录环境准备安装LAMP堆栈(Linux,Apache,MySQL,PHP)配置MySQL数据库安装WordPress配置WordPress常见问题及解决办法数据库连接错误白屏问题插件或主题冲突内存限制错误本文旨在介绍如何在服务器上搭......
  • 树形DP做题回顾(上)
    题目一 ​​​​Problem-2196大致意思就是求每个点为根的最大深度;对于这个问题,很快速的我们可以想到跑两次dfs,第一次预处理出以u为根的子树的第一,二深的深度,第二次dfs进行树形dp,从u->v时推出v的最大深度,用up[v]来存储;代码如下:注意分走到第一大和第二大的路径上的决策,以......
  • 虚树+树形dp
    虚树实际上是一颗浓缩子树;使用虚树的题目大部分具有查询的点有限,同时虚树构建的信息符合规则;做虚树的题目:步骤为先想出原树的暴力求解做法,然后构建虚树同时向其中添加有用信息(比如边权);虚树的构建过程:虚树的构建大致有两种,但是两种方式都与dfs序有关;首先解释为什么与dfs序有......