单调队列优化 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