单调队列及单调队列优化DP
首先是单调队列:
其实单调队列就是一种队列内的元素有单调性(单调递增或者单调递减)的队列,答案(也就是最优解)就存在队首,而队尾则是最后进队的元素。因为其单调性所以经常会被用来维护区间最值或者降低$DP$的维数已达到降维来减少空间及时间的目的。
类似于滑动窗口等,单调队列具有时序性的储存区间最大值或区间最小值,以下为例题:
P1886 滑动窗口 /【模板】单调队列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1440 求m区间内的最小值 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
上题均可用滑动窗口模板解决:
#include<bits/stdc++.h> using namespace std; const int N=2e6+10; int n,m,hh,tt,a[N],q[N],k; int main() { cin>>n>>k; for(int i=1;i<=n;i++) cin>>a[i]; hh=1,tt=0; for(int i=1;i<=n;i++){ while(hh<=tt&&q[hh]<i-k+1) hh++; while(hh<=tt&&a[q[tt]]>=a[i]) tt--; q[++tt]=i; if(i>=k) cout<<a[q[hh]]<<' '; } cout<<endl; hh=1,tt=0; for(int i=1;i<=n;i++){ while(hh<=tt&&q[hh]<i-k+1) hh++; while(hh<=tt&&a[q[tt]]<a[i]) tt--; q[++tt]=i; if(i>=k) cout<<a[q[hh]]<<' '; } return 0; }
对于单调队列优化$DP$,在状态转移的时候会发生我们需要转移到一个区间的某一个点上,而在最佳答案的情景下这个点一般为最大值或者最小值,考虑遍历区间复杂度较高,可采用求区间最值方法来确定一个点的位置即可:
P1725 琪露诺 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
对于该题,我们从位置 $0$ 开始,然后在位置 $i$ 时,无非是是从区间 $[i-r,i-l]$ 转移而来,故我们使用单调队列储存 $dp$ 状态在这个区间的最大值即可,区间最大值加上现在位置的贡献即为这个位置状态的贡献,如果从某个位置开始可以直接跳走,即 $i+r>n$ 则从此时开始更新迭代答案即可,这里再解释以下为什么是 $i-l$ 入队,因为我们向前走一步,即 $i变成i+1$, 我们的区间窗口也要滑动,此时新进来的数就是 $i-l$
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; int n,l,r,q[N],hh=1,tt,dp[N],a[N],res=-1e18; signed main(){ cin>>n>>l>>r; for(int i=1;i<=2*n;i++) dp[i]=-1e18; for(int i=0;i<=n;i++) cin>>a[i]; for(int i=l;i<=n;i++){ while(hh<=tt&&q[hh]<i-r) hh++; while(hh<=tt&&dp[q[tt]]<dp[i-l]) tt--; q[++tt]=i-l,dp[i]=dp[q[hh]]+a[i]; if(i+r>n) res=max(res,dp[i]); } cout<<res; }
P3957 [NOIP2017 普及组] 跳房子 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目要求最小金钱数目,则可以考虑用二分,因为我们可以具有区间灵活度,花费金钱越大灵活度越大,所以最终的答案一定是递增的,故可以使用二分答案
接下来是 $check$ 函数,考虑使用单调队列DP,由于每次进行的步数都不一样,所以此时进行状态转移即可得到最优解,首先确定每次跳跃的区间左端点和右端点,然后进行遍历,设置一个变量 $j$,表示最后一个加入队伍的编号,对于 $i$ 之前的距离,如果 $i和j$ 之间相差的距离满足跳跃区间,那么可以考虑是否加入队列,此时单调队列更新完成,更新 $dp[i]$ 即可,由于游戏随时可以停止,所以要时刻判断
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; pair<int,int>robot[N]; int n,d,k,dp[N],q[N],sum; bool check(int g){ int l,r,hh=1,tt=0,j=0; if(g>=d) l=1,r=d+g; else l=d-g,r=d+g; for(int i=1;i<=n;i++) dp[i]=-2e9,q[i]=0; dp[0]=0,q[0]=0; for(int i=1;i<=n;i++){ while(robot[i].first-robot[j].first>=l&&j<i){ if(dp[j]>-2e9){ while(hh<=tt&&dp[q[tt]]<=dp[j]) tt--; q[++tt]=j; } j++; } while(hh<=tt&&robot[i].first-robot[q[hh]].first>r) hh++; if(hh<=tt) dp[i]=dp[q[hh]]+robot[i].second; if(dp[i]>=k) return true; } return false; } signed main(){ cin>>n>>d>>k; for(int i=1;i<=n;i++){ int a,b; cin>>a>>b; robot[i]={a,b}; } int l=1,r=1e9; while(l<r){ int mid=l+r>>1; if(check(mid)) r=mid; else l=mid+1; } cout<<(r==1e9?-1:r); }
单调队列优化多重背包:
若用F[i][j]表示对容量为j的背包,处理完前i种物品后,背包内物品可达到的最大总价值,并记$m[i] = min(n[i], j / v[i])$。放入背包的第i种物品的数目可以是:$0、1、2……$,可得:
$F[i][j] = max { F[i - 1] [j – k * v[i] ] + k * w[i] } (0 <= k <= m[i]) ㈠$
如何在O(1)时间内求出F[i][j]呢?
先看一个例子:取$m[i] = 2, v[i] = v, w[i] = w, V > 9 * v$,
并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:
$j = 6*v: f(6*v)、f(5*v)+w、f(4*v)+2*w$ 这三个中的最大值
$j = 5*v: f(5*v)、f(4*v)+w、f(3*v)+2*w$ 这三个中的最大值
$j = 4*v: f(4*v)、f(3*v)+w、f(2*v)+2*w$ 这三个中的最大值
显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:
$j = 6*v: f(6*v)-6*w、f(5*v)-5*w、f(4*v)-4*w$ 这三个中的最大值
$j = 5*v: f(5*v)-5*w、f(4*v)-4*w、f(3*v)-3*w$ 这三个中的最大值
$j = 4*v: f(4*v)-4*w、f(3*v)-3*w、f(2*v)-2*w$ 这三个中的最大值
很明显,要求最大值的那些项,有很多重复。
根据这个思路,可以对原来的公式进行如下调整:
假设$d = v[i]$,$a = j / d$,$b = j % d$,即 $j = a * d + b$,代入公式㈠,并用$k$替换$a - k$得:
$F[i][j] = max { F[i - 1] [b + k * d] - k * w[i] } + a * w[i] (a – m[i] <= k <= a) ㈡$
对$F[i - 1][y] (y= b b+d b+2d b+3d b+4d b+5d b+6d … j)$
$F[i][j]$就是求j的前面$m[i] + 1$个数对应的$F[i - 1] [b + k * d] - k * w[i]$的最大值,加上$a * w[i]$,如果将F[i][j]前面所有的$F[i - 1][b + k * d] – k * w$放入到一个队列,那么,F[i][j]就是求这个队列最大长度为$m[i] + 1$时,队列中元素的最大值,加上$a * w[i]$。因而原问题可以转化为:$O(1)$时间内求一个队列的最大值。
该问题可以这样解决:
① 用另一个队列B记录指定队列的最大值(或者记录最大值的地址),并通过下面两个操作保证队列B的第一个元素(或其所指向的元素)一定是指定队列的当前最大值。
② 当指定队列有元素M进入时,删除队列B中的比M小的(或队列B中所指向的元素小等于M的)所有元素,并将元素M(或其地址)存入队列B。
③ 当指定队列有元素M离开时,队列B中的第一个元素若与M相等(或队列B第一个元素的地址与M相等),则队列B的第一个元素也离队。
经过上述处理,可以保证队列B中的第一个元素(或其指向的元素)一定是所指定队列所有元素的最大值。显然队列B的元素(或其所指向的元素)是单调递减的,这应该就是《背包九讲》中的提到的“单调队列”吧,初看的时候被这个概念弄得稀里糊涂,网上的资料提到“维护队列的最大值”,刚开始还以为是维护这个单调队列的最大值,对其采用的算法,越看越糊涂。其实,只要明白用一个“辅助队列”,求另一个队列的最值,那么具体的算法,和该“辅助队列”的性质(单调变化),都很容易推导出来。
在多重背包问题中,所有要进入队列的元素个数的上限值是已知的,可以直接用一个大数组模拟队列。
//“多重背包”通用模板 const int MAX_V = 100004; //v、n、w:当前所处理的这类物品的体积、个数、价值 //V:背包体积, MAX_V:背包的体积上限值 //f[i]:体积为i的背包装前几种物品,能达到的价值上限。 inline void pack(int f[], int V, int v, int n, int w) { if (n == 0 || v == 0) return; if (n == 1) { //01背包 for (int i = V; i >= v; --i) if (f[i] < f[i - v] + w) f[i] = f[i - v] + w; return; } if (n * v >= V - v + 1) { //完全背包(n >= V / v) for (int i = v; i <= V; ++i) if (f[i] < f[i - v] + w) f[i] = f[i - v] + w; return; } int va[MAX_V], vb[MAX_V]; //va/vb: 主/辅助队列 for (int j = 0; j < v; ++j) { //多重背包 int *pb = va, *pe = va - 1; //pb/pe分别指向队列首/末元素 int *qb = vb, *qe = vb - 1; //qb/qe分别指向辅助队列首/末元素 for (int k = j, i = 0; k <= V; k += v, ++i) { if (pe == pb + n) { //若队列大小达到指定值,第一个元素X出队。 if (*pb == *qb) ++qb; //若辅助队列第一个元素等于X,该元素也出队。 ++pb; } int tt = f[k] - i * w; *++pe = tt; //元素X进队 //删除辅助队列所有小于X的元素,qb到qe单调递减,也可以用二分法 while (qe >= qb && *qe < tt) --qe; *++qe = tt; //元素X也存放入辅助队列 f[k] = *qb + i * w; //辅助队列首元素恒为指定队列所有元素的最大值 } } }

浙公网安备 33010602011771号