學習筆記——單調佇列優化dp
演算法
使用單調佇列優化dp 廢話
對與一些dp的轉移方程,我們可以通過拆使它與某個區間的最值相關。
這時可以用單調佇列算出區間最值,進行優化。
例題
最大子段和
題意
給出一個長度為 \(n\) 的整數序列,從中找出一段長度不超過 \(m\) 的連續子序列,使得整個序列的和最大。
思路
設 \(sum_i\) 為 \(i\) 的字首和,易得答案為:
\[\max_\limits{1\le i\le n}\{sum_i-\min_\limits{i-m\le k\le i-1}\{sum_k\}\} \]其中 \(\min_\limits{i-m\le k\le i-1}\{sum_k\}\)
那麼算起來就變得簡單多了。
程式碼
點選檢視程式碼
#include<bits/stdc++.h> #define _for(i,a,b) for(int i=a;i<=b;++i) #define for_(i,a,b) for(int i=a;i>=b;--i) #define ll long long using namespace std; const int N=3e5+10; int n,m,a[N],sum[N],f[N],ans; int q[N],h=1,t=0; inline int rnt(){ int x=0,w=1;char c=getchar(); while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*w; } void tmp(int k){ while(h<=t&&q[h]<k-m)++h; ans=max(ans,sum[k]-sum[q[h]]); while(h<=t&&sum[q[t]]>sum[k])--t; q[++t]=k; } int main(){ n=rnt(),m=rnt(); _for(i,1,n){ a[i]=rnt(); sum[i]=sum[i-1]+a[i]; tmp(i); } printf("%d\n",ans); return 0; }
修剪草坪
題意
給定一個 \(n\ (1\le n\le 10^5)\) 個元素的序列,可任選多個數,要求任意一段連續選的數長度不能超過 \(k\)。
\(1\le N\le 10^5\)
思路
設 \(f_{i,0/1}\) 表示第 \(i\) 個數選(1)或是不選(0),\(sum_i\) 表示 \(i\) 的字首和。
轉移方程為:
\[f_{i,0}=\max\{f_{i-1,0},f_{i-1,1}\}\\\ f_{i,1}=\max_\limits{i-k\le j<i}\{f_{j,0}+(sum_i-sum_j)\} \]\(f_{i,1}\) 轉移方程可以拆成:
\[f_{i,1}=sum_i+\max_\limits{i-k\le j<i}\{f_{j,0}-sum_j\} \]用單調佇列維護 \(f_{j,0}-sum_j\)
程式碼
點選檢視程式碼
#include<bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const ll N=3e5+10;
ll n,k,a[N],sum[N],f[N][2],ans;
ll q[N],h=1,t=0;
inline ll rnt(){
ll x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*w;
}
void tmp(ll i){
while(h<=t&&q[h]<i-k)++h;
f[i][1]=f[q[h]][0]+sum[i]-sum[q[h]];
while(h<=t&&f[q[t]][0]-sum[q[t]]<f[i][0]-sum[i])--t;
q[++t]=i;
}
int main(){
n=rnt(),k=rnt();
tmp(0);
_for(i,1,n){
a[i]=rnt();
sum[i]=sum[i-1]+a[i];
f[i][0]=max(f[i-1][0],f[i-1][1]);
tmp(i);
}
printf("%lld\n",max(f[n][0],f[n][1]));
return 0;
}
瑰麗華爾茲
題意
給出一個 \(N*M\) 的船上地圖,有空地也有傢俱。
船上有 \(K\) 段時間,在每段時間都會往不同的方向傾斜,鋼琴也會朝著那個方向傾斜,但不允許碰上傢俱。
求鋼琴最長的滑行距離。
\(1\le N,M\le200,K\le200\)
思路
設 \(f_{i,j,k}\) 表示在第 \(i\) 段時間滑行到 \(j,k\) 的位置。
那麼轉移方程就是:
\[f_{i,j,k}=\max_\limits{(jj,kk)是i時刻可以滑到(j,k)的點}{f_{i,jj,kk}+|jj-j|+|kk-k|} \]瞎寫的
按著傾斜的方向遍歷,再用單調佇列優化優化就好了。
程式碼
點選檢視程式碼
#include<bits/stdc++.h>
#define _for(i,a,b) for(int i=a;i<=b;++i)
#define for_(i,a,b) for(int i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=210,inf=0x3f3f3f3f;
int n,m,x,y,k,mp[N][N];
int f[N][N][N],la[N][N][N],ans;
inline ll rnt(){
ll x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*w;
}
inline char rch(){
char c=getchar();
while(c!='.'&&c!='x')c=getchar();
return c;
}
struct dq{//deque
int q[N],h,t;
void nw(){
memset(q,0,sizeof(q));
h=1,t=0;
}
bool empty(){return h>t;}
int front(){return q[h];}
int back(){return q[t];}
void pop_f(){++h;}
void pop_b(){--t;}
void push(int x){q[++t]=x;}
};
void dp(int d,int s,int t,int fx){
int len=(t-s+1);
if(fx==1){
_for(j,1,m){
dq q;q.nw();
for_(i,n,1){
if(mp[i][j]){
while(!q.empty())q.pop_f();
continue;
}
while(!q.empty()&&q.front()>i+len)q.pop_f();
while(!q.empty()&&f[d-1][q.back()][j]+q.back()-i<f[d-1][i][j])q.pop_b();
q.push(i);
if(f[d-1][q.front()][j]>-1)
f[d][i][j]=f[d-1][q.front()][j]+q.front()-i;
ans=max(ans,f[d][i][j]);
}
}
}
else if(fx==2){
_for(j,1,m){
dq q;q.nw();
_for(i,1,n){
if(mp[i][j]){
while(!q.empty())q.pop_f();
continue;
}
while(!q.empty()&&q.front()<i-len)q.pop_f();
while(!q.empty()&&f[d-1][q.back()][j]+i-q.back()<f[d-1][i][j])q.pop_b();
q.push(i);
if(f[d-1][q.front()][j]>-1)
f[d][i][j]=f[d-1][q.front()][j]+i-q.front();
ans=max(ans,f[d][i][j]);
}
}
}
else if(fx==3){
_for(i,1,n){
dq q;q.nw();
for_(j,m,1){
if(mp[i][j]){
while(!q.empty())q.pop_f();
continue;
}
while(!q.empty()&&q.front()>j+len)q.pop_f();
while(!q.empty()&&f[d-1][i][q.back()]+q.back()-j<f[d-1][i][j])q.pop_b();
q.push(j);
if(f[d-1][i][q.front()]>-1)
f[d][i][j]=f[d-1][i][q.front()]+q.front()-j;
ans=max(ans,f[d][i][j]);
}
}
}
else{
_for(i,1,n){
dq q;q.nw();
_for(j,1,m){
if(mp[i][j]){
while(!q.empty())q.pop_f();
continue;
}
while(!q.empty()&&q.front()<j-len)q.pop_f();
while(!q.empty()&&f[d-1][i][q.back()]+j-q.back()<f[d-1][i][j])q.pop_b();
q.push(j);
if(f[d-1][i][q.front()]>-1)
f[d][i][j]=f[d-1][i][q.front()]+j-q.front();
ans=max(ans,f[d][i][j]);
}
}
}
}
int main(){
n=rnt(),m=rnt(),x=rnt(),y=rnt(),k=rnt();
_for(i,1,n)
_for(j,1,m)
mp[i][j]=(bool)(rch()=='x');
memset(f,-inf,sizeof(f));
f[0][x][y]=0;
_for(i,1,k){
int s=rnt(),t=rnt(),fx=rnt();
dp(i,s,t,fx);
}
printf("%d\n",ans);
return 0;
}
股票交易
題意
一共有 \(T\) 天,知道了每天股票的買入金額 \(ap_i\),賣出金額 \(bp_i\),買入限制 \(as_j\),賣出限制 \(bs_j\)。要求持有股票數不能超過 \(MaxP\) ,兩次交易相隔 \(w\) 天。
\(1\le w<T\le 2*10^3,1\le MaxP\le 2*10^3\)
思路
設 \(f_{i,j}\) 表示第 \(i\) 天持有 \(j\) 個股票的最大錢數。
有四種情況:
- 憑空買
轉移方程:
- 不買不賣
轉移方程:
- 只買
轉移方程:
- 只賣
轉移方程:
此時時間複雜度是 \(O(T*MaxP^2)\)。
觀察只買和只賣的情況,可以發現它們的轉移方程可以用單調佇列優化掉一維。
那麼時間複雜度就被優化成了 \(O(T*MaxP)\),完全可過。
程式碼
點選檢視程式碼
#include<bits/stdc++.h>
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const ll N=4010,inf=0x3f3f3f3f;
ll t,mxp,w,ap[N],bp[N],as[N],bs[N];
inline ll rnt(){
ll x=0,w=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*w;
}
struct dq{//deque
ll q[N],h,t;
void nw(){
memset(q,0,sizeof(q));
h=1,t=0;
}
bool empty(){return h>t;}
ll front(){return q[h];}
ll back(){return q[t];}
void pop_f(){++h;}
void pop_b(){--t;}
void push(int x){q[++t]=x;}
};
namespace SOLVE{
ll f[N][N];
void DpPkm(ll i){//憑空買
_for(j,0,min(mxp,as[i]))
f[i][j]=-ap[i]*j;
return;
}
void DpBmbm(ll i){//不買也不賣
_for(j,0,mxp)
f[i][j]=max(f[i][j],f[i-1][j]);
return;
}
void DpBuy(ll i){//原基礎上買來
dq q;q.nw();
_for(j,0,mxp){
while(!q.empty()&&j-q.front()>as[i])q.pop_f();
while(!q.empty()&&f[i-w-1][j]>f[i-w-1][q.back()]-(j-q.back())*ap[i])q.pop_b();
q.push(j);
f[i][j]=max(f[i][j],f[i-w-1][q.front()]-(j-q.front())*ap[i]);
}
return;
}
void DpSell(ll i){//原基礎上賣出
dq q;q.nw();
for_(j,mxp,0){
while(!q.empty()&&q.front()-j>bs[i])q.pop_f();
while(!q.empty()&&f[i-w-1][j]>f[i-w-1][q.back()]+(q.back()-j)*bp[i])q.pop_b();
q.push(j);
f[i][j]=max(f[i][j],f[i-w-1][q.front()]+(q.front()-j)*bp[i]);
}
return;
}
ll Solve(){
memset(f,-inf,sizeof(f));
_for(i,1,t){
DpPkm(i);
DpBmbm(i);
if(i-w>1){
DpBuy(i);
DpSell(i);
}
}
return f[t][0];
}
}
int main(){
t=rnt(),mxp=rnt(),w=rnt();
_for(i,1,t)
ap[i]=rnt(),bp[i]=rnt(),as[i]=rnt(),bs[i]=rnt();
printf("%lld\n",SOLVE::Solve());
return 0;
}/*
*/