Welcome to the Nigh|

XiaoLe_MC

园龄:1年2个月粉丝:3关注:9

2024-05-31 18:08阅读: 40评论: 0推荐: 0

[学习笔记] 单调队列优化DP - DP

单调队列优化DP

简单好想的DP优化

真正的教育是把学过的知识忘掉后剩下的东西 —— ***

对于一个转移方程类似于 dp[i]=max(min){dp[j]+b[j]+a[i]}  xi<=j<=yi 的DP,如果暴力实现的话复杂度是 O(n2),实现方法是双层for循环嵌套。但如果区间 [xi,yi] 与区间 [xi+1,yi+1] 存在交集,或者说当 i 变化时,不同的 i 所对应的 j 区间存在重叠,那么我们在使用 j 进行遍历时就会产生重复计算,而单调队列优化DP就是解决这一重复计算的法宝。

如何用单调队列进行优化呢?可以将 j 所在的区间看作一个滑动窗口,每次循环 i 的时候将元素进队,并且更新 head 的值(找到合法区间),这样就可以将每次寻找最大值的时间复杂度均摊为 O(1),再加上dp的 n 次转移,时间复杂度为 O(1)O(n)=O(n)。完美~

使用这一优化方法的前提是: max(min)里的东西必须只与 j 相关,不然没办法优化。

单调队列优化多重背包

我们知道多重背包的朴素DP表达式为:dp[j]=max{dp[jkci]+kwi},其中 0kmin{mi,j/ci}。但是这个式子和单调队列优化DP的普通形式 dp[i]=max{dp[i]+b[j]}+a[i]   L(i)jR(i) 差太多了,无法直接用单调队列优化。

考虑到单调队列优化的前提是存在重复的计算,显然有 jj+ci 在计算时存在重复计算。那么也就是说,当j1j2 mod ci 时是存在重复计算的,那么问题就很清楚了。

b=j%ciy=j/ci,那么 j=b+yci。于是有:

dp[b+yci]=max{dp[b+(yk)ci]+kwi}

x=yk,则有:

dp[b+yci]=max{dp[b+xci]+(yx)wi}=max{dp[b+xci]xwi}+ywiymin{mi,y}xy

这个DP式即可进行单调队列优化。

就一般的题目而言,只要是蓝题及以上的,满足单调队列优化的狮子都不会太明显,需要一步一步去转化。优化多重背包就是个很巧妙的转化的例子。掌握这类转化的技巧对这类DP很有帮助。

例题

「一本通 5.5 练习 1」烽火传递

纯纯的单调队列优化DP。建议打通这道题,对后面的理解很有帮助。

根据题目可知转移方程为:dp[i]=min{dp[j]+a[i]},其中 j[im,i1]。为了看得清楚,把min里面和 i 有关的东西全都踢出去:dp[i]=min{dp[j]}+a[i]。那么就可以建立一个关于dp的单调队列,在每次计算dp[i]前要先把dp[i-1]入队。最后答案为 i[nm+1,n] 中的dp最大值。

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, a[N], dp[N], p[N], tail, head=1, ans = INT_MAX;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n>>m;
for(int i=1; i<=n; ++i) cin>>a[i];
for(int i=1; i<=n; ++i){
while(tail >= head && dp[p[tail]] >= dp[i-1]) --tail; // 关于dp数组的单调队列
p[++tail] = i-1;
if(p[head] < i-m) ++head;
dp[i] = dp[p[head]] + a[i];
if(i > n-m) ans = min(ans, dp[i]);
} return cout<<ans, 0;
}

「一本通 5.5 例 2」最大连续和

纯纯的单调队列题。根据题目可知转移方程(假了)为:dp[i]=max{sum[i]sum[j]},其中 j[im,i1]。转化为能看懂的 dp[i]=sum[i]min{sum[j]}。用单调队列求解即可。复杂度 O(n)

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, sum[N], p[N], tail, head = 1, ans = INT_MIN;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n>>m;
for(int i=1, a; i<=n; ++i) cin>>a, sum[i] = sum[i-1] + a;
for(int i=1; i<=n; ++i){
while(tail >= head && sum[p[tail]] >= sum[i-1]) --tail;
p[++tail] = i-1;
while(p[head] < i-m) ++head;
ans = max(ans, sum[i]-sum[p[head]]);
} return cout<<ans, 0;
}

[USACO11OPEN] Mowing the Lawn G

很好的单调队列DP入门题。设 dp[i] 表示选择第 i 项元素的合法序列的最大和。那么可得转移方程 dp[i]=max{dp[j1]sum[j]}+sum[i]sum[] 表示前缀和。其中 j[im,i1],这里的 j 可以理解为两段连续区间的断开处,并且 j 是可以等于 0。但是如果DP包含了 0,那必然会涉及 dp[1] 的计算。不妨在整个序列前加入一个 0,在进行DP,那么就可以解决这一问题。

image

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
int n, k, dp[N], sum[N], p[N], tail=1, head=0, ans = INT_MIN;
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n>>k;
for(int i=2, a; i<=n+1; ++i) cin>>a, sum[i] = sum[i-1] + a;
for(int i=1; i<=n+1; ++i){
while(tail > head && dp[p[tail]-1]-sum[p[tail]] <= dp[i-1]-sum[i]) --tail;
p[++tail] = i;
while(p[head] < i-k) ++head;
dp[i] = dp[p[head]-1] - sum[p[head]] + sum[i];
ans = max(ans, dp[i]);
} return cout<<ans, 0;
}

[POI2005] BAN-Bank Notes

下面只给出计算最小硬币数的代码,方案数略去。

#include<bits/stdc++.h>
using namespace std;
const int N = 2e4 + 1;
int n, k, dp[N], c[N], m[N], p[N], num[N], tail, head;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n;
for(int i=1; i<=n; ++i) cin>>c[i];
for(int i=1; i<=n; ++i) cin>>m[i];
cin>>k;
for(int i=1; i<=k; ++i) dp[i] = INT_MAX;
for(int i=1; i<=n; ++i){
if(m[i] > k / c[i]) m[i] = k/c[i];
for(int b=0; b<c[i]; ++b){
tail = 0, head = 1;
for(int y=0; y<=(k-b)/c[i]; ++y){
int tmp = dp[b + y*c[i]] - y;
while(tail >= head && p[tail] >= tmp) --tail;
p[++tail] = tmp, num[tail] = y;
while(head <= tail && num[head] < y-m[i]) ++head;
dp[b+y*c[i]] = min(dp[b+y*c[i]], p[head] + y);
}
}
} return cout<<dp[k], 0;
}

[SCOI2010] 股票交易

这道题的题目非常的繁琐啊,看的人眼花缭乱的。不过如果你注意力十分充沛的话就可以发现,这道题其实和背包DP很像。我们可以列出总的DP转移方程式:令 dp[i][j] 表示第 i 天拥有 j 支股票的最大收益。则有:

dp[i][j]=max{dp[iw1][ja+b]aAPi+bBPi}

但似乎这狮子又臭又长,没法处理,那么考虑分开来转移:先处理卖出股票的转移,再处理买入股票的转移。先看卖出股票:

dp[i][j]=max{dp[iw1][ja]aAPi}0amin{j,AS}

k=ja 则有:

dp[i][j]=max{dp[iw1][k]+kAPi}jAPijmin{j,AS}kj

处理成功!同理,卖出股票的转移方程也是一样:

dp[i][j]=max{dp[iw1][k]+kBPi}jBPijkmin{MaxPj,BS}+j

考虑到最开始时手里没有股票,所以需要先全部买入,再进行后面的转移。当然我们也可以选择什么也不做直接由昨天转移到今天。

#include<bits/stdc++.h>
using namespace std;
const int N = 2e3 + 1;
int T, MaxP, w, AP, BP, AS, BS, dp[N][N], p[N], num[N], head, tail, ans = INT_MIN;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>T>>MaxP>>w;
memset(dp, -0x7f, sizeof dp);
for(int i=1; i<=T; ++i){
cin>>AP>>BP>>AS>>BS;
for(int j=0; j<=min(AS, MaxP); ++j) dp[i][j] = -j * AP; //全部买入
for(int j=0; j<=MaxP; ++j) dp[i][j] = max(dp[i][j], dp[i-1][j]); //什么也不做
if(i <= w) continue;
tail = 0, head = 1;
for(int j=0; j<=MaxP; ++j){ //买入
int tmp = dp[i-w-1][j] + j*AP;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = j;
while(head <= tail && num[head] < j-min(j, AS)) ++head;
dp[i][j] = max(dp[i][j], p[head]-j*AP);
}
tail = 0, head = 1;
for(int j=MaxP; j>=0; --j){ // 卖出
int tmp = dp[i-w-1][j] + j*BP;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = j;
while(head <= tail && num[head] > min(MaxP-j, BS)+j) ++head;
dp[i][j] = max(dp[i][j], p[head]-j*BP);
}
} return cout<<dp[T][0], 0;
}

[NOI2005] 瑰丽华尔兹

披着暴力外衣的DP。跟魔法没半毛钱关系。

首先,定义状态 dp[i][j] 表示当前第 i 行第 j 列所走的最大距离。然后全部初始化为 -inf,这样就可以保证转移出来数字一定是能走到的地方。因为要从最开始的地方进行转移,那么把 dp[x][y] 设为 0

接着考虑转移,我们可以根据方向把整张图都转移一遍,比如方向为 1 那就从下往上刷。如果刷到障碍物,就单调队列归零,continue跳过障碍物重新开始刷。这样就可以保证DP值为正的地方就是能走的地方。然后ans取max即可。

四个方向的DP转移方程式如下:

{dir=1dp[i][j]=max{dp[k][j]+k}iikmin{n,dmax+i}dir=2dp[i][j]=max{dp[k][j]k}+iimin{i,dmax}kidir=3dp[i][j]=max{dp[i][k]+k}jjkmin{n,dmax+j}dir=4dp[i][j]=max{dp[k][j]k}+jjmin{j,dmax}kj

用单调队列优化后即可得到 O(nm) 的转移复杂度。总复杂度为 O(knm)

#include<bits/stdc++.h>
using namespace std;
int n, m, x, y, K, dmax, dir, num[201], tail, head;
char ch;
bitset<201> G[201];
long long ans, dp[201][201], p[201], tmp;
int main(){
ios::sync_with_stdio(0), cin.tie(0) ,cout.tie(0);
cin>>n>>m>>x>>y>>K;
for(int i=1; i<=n; ++i) for(int j=1; j<=m; ++j){
cin>>ch;
if(ch == 'x') G[i][j] = 1;
}
memset(dp, -0x7f, sizeof dp);
dp[x][y] = 0;
for(int i=1, l, r; i<=K; ++i){
cin>>l>>r>>dir; dmax = r-l+1;
if(dir == 3) for(int j=1; j<=n; ++j){
tail = 0, head = 1;
for(int k=m; k>=1; --k){
if(G[j][k]){tail = 0, head = 1; continue; }
tmp = dp[j][k] + k;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = k;
while(head <= tail && num[head] > min(m, k+dmax)) ++head;
dp[j][k] = max(dp[j][k], p[head]-k);
ans = max(ans, dp[j][k]);
}
}else if(dir == 4) for(int j=1; j<=n; ++j){
tail = 0, head = 1;
for(int k=1; k<=m; ++k){
if(G[j][k]){tail = 0, head = 1; continue; }
tmp = dp[j][k] - k;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = k;
while(head <= tail && num[head] < k-min(k, dmax)) ++head;
dp[j][k] = max(dp[j][k], p[head]+k);
ans = max(ans, dp[j][k]);
}
}else if(dir == 1) for(int j=1; j<=m; ++j){
tail = 0, head = 1;
for(int k=n; k>=1; --k){
if(G[k][j]){tail = 0, head = 1; continue; }
tmp = dp[k][j] + k;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = k;
while(head <= tail && num[head] > min(n, dmax+k)) ++head;
dp[k][j] =max(dp[k][j], p[head]-k);
ans = max(ans, dp[k][j]);
}
}else if(dir == 2) for(int j=1; j<=m; ++j){
tail = 0, head = 1;
for(int k=1; k<=n; ++k){
if(G[k][j]){tail = 0, head = 1; continue; }
tmp = dp[k][j] - k;
while(tail >= head && p[tail] <= tmp) --tail;
p[++tail] = tmp, num[tail] = k;
while(head <= tail && num[head] < k-min(k, dmax)) ++head;
dp[k][j] = max(dp[k][j], p[head]+k);
ans = max(ans, dp[k][j]);
}
}
} return cout<<ans, 0;
}

[USACO13NOV] Pogo-Cow S

神题好吧。一般单调队列是固定左右端点移动中间的 k 值,这个是固定中间的 k 值不断扩展左右端点。

定义状态 dp[j][i] 表示从第 j 个点跳到第 i 个点的最大分数。

#include<bits/stdc++.h>
using namespace std;
int n, dp[1001][1001], ans;
struct target{ int x, p; }tg[1001];
bool cmp(target a, target b){ if(a.x < b.x) return 1; return 0; }
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n;
for(int i=1; i<=n; ++i) cin>>tg[i].x>>tg[i].p;
sort(tg+1, tg+n+1, cmp);
for(int j=1; j<=n; ++j){
dp[0][j] = dp[j][j] = tg[j].p;
for(int i=j+1, h=j+1; i<=n; ++i){
dp[j][i] = dp[j][i-1] - tg[i-1].p;
while(h >= 0 && tg[i].x - tg[j].x >= tg[j].x - tg[h-1].x)
dp[j][i] = max(dp[j][i], dp[--h][j]);
dp[j][i] += tg[i].p;
ans = max(ans, dp[j][i]);
}
}
for(int j=n; j>=1; --j){
dp[j][0] = tg[j].p;
for(int i=j-1, h=j-1; i>0; --i){
dp[j][i] = dp[j][i+1] - tg[i+1].p;
while(h <= n && tg[j].x - tg[i].x >= tg[h+1].x - tg[j].x)
dp[j][i] = max(dp[j][i], dp[++h][j]);
dp[j][i] += tg[i].p;
ans = max(ans, dp[j][i]);
}
} return cout<<ans, 0;
}

本文作者:XiaoLe_MC

本文链接:https://www.cnblogs.com/xiaolemc/p/18225077

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   XiaoLe_MC  阅读(40)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起