单调队列优化DP
单调队列
- 队列是单调的,递增或递减
- 只能在队首或者队尾进行操作
- 队列中维护所有在窗口中的元素,有一些元素是没用的,以区间最大值为例:
- 所以从左到右尝试加入队列,弹出队尾比当前数更小的元素,弹出队首已经出窗口的元素,再队尾压入当前数
- 这样,队首就是窗口最大值
- 每个数只会弹入弹出一次,复杂度O(n)
点击查看单调队列模版
void getmax() { // 和上图同理int head = 0, tail = -1;for (int i = 1; i <= k; i++) {while (head <= tail) {tail--; // 移动窗口(见下文)+优化队列}q[++tail] = i; // 入队}for (int i = k; i <= n; i++) {while (head <= tail && a[q[tail]] <= a[i]) tail--; // 排除没用的值,优化队列内容。(详见上图)cout << a[q[head]] << ""; // 输出最大值}
}
点击查看代码
#include <bits/stdc++.h>
using namespace std;int a[1001010];
deque <int> id;
deque <int> q;
int k, n;
int main() {cin >> n >> k;for (int i = 1; i <= n; i++) cin >> a[i];for (int i = 1; i <= k; i++) {while (!q.empty() and q.back() > a[i]) {q.pop_back();id.pop_back();}q.push_back (a[i]);id.push_back (i);}for (int i = k + 1; i <= n + 1; i++) {cout << q.front() << " ";
// cout << q.back() << endl;if (id.front() < i - k + 1) {id.pop_front();q.pop_front();}while (!q.empty() and q.back() > a[i]) {q.pop_back();id.pop_back();}q.push_back (a[i]);id.push_back (i);}for (int i = 1; i <= k; i++) {while (!q.empty() and q.back() < a[i]) {q.pop_back();id.pop_back();}q.push_back (a[i]);id.push_back (i);}cout<<endl;for (int i = k + 1; i <= n + 1; i++) {cout << q.front() << " ";
// cout << q.back() << endl;if (id.front() < i - k + 1) {id.pop_front();q.pop_front();}while (!q.empty() and q.back() < a[i]) {q.pop_back();id.pop_back();}q.push_back (a[i]);id.push_back (i);}
}
最大连续和
- 给你一个长度为n的整数序列。要求从中找一段连续长度不超过m的子序列,并且和最大
- 求前缀和,转化成:
- 即:
- 滑动窗口求第二项即可
点击查看代码
int MaxSubSequence(const int A[], int N){ int ThisSum,MaxSum,i,j,k; MaxSum = 0; for(i=0;i<N;i++) { for(j=i;j<N;j++) { ThisSum = 0; for(k=i;k<=j;k++) { ThisSum += A[k]; } if(ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum;
}
修建草坪
-
\(FJ\) 有 \(N\) (\(1 <= N <= 100,000\))只排成一排的奶牛。每只奶牛的效率是不同的,奶牛 \(i\) 的效率为 \(E_i\)。
计算 \(FJ\) 选奶牛可以得到的最大效率,并且该方案中没有连续的超过 \(K\) 只奶牛。 -
设 \(dp[i][0]\) 表示以i为结尾不选i的最大值, \(dp[i][1]\) 表示以 \(i\) 为结尾选 \(i\) 的最大值:
转化成
可以单调队列优化.
旅行问题
- \(John\) 打算驾驶一辆汽车周游一个环形公路。公路上总共有 \(n\) 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。 \(John\) 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。在一开始的时候,汽车内油量为零,\(John\) 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
- 判断以每个车站为起点能否按条件成功周游一周。
解法
- 拆环成链,设每个加油站有 \(d[i]\) 油,到下一个加油站要 \(s[i]\) 千米
- 那么从一个点出发,\(d[i]-s[i]\) 前缀和必须是非负数
- 在 $2n的链上维护 \(n\) 的滑动窗口,求区间最小值,判断是不是负数
BANK NOTEs
-
一共有 \(n\) 种面值的硬币,面值分别为 \(b_1, b_2,..., b_n\). 但是每种硬币有 \(c_i\) 个,现在我们想要凑出面值 \(k\) 求最少要用多少个硬币.
-
\[n≤200,b_i,c_i,k≤20000 \]
单调队列优化多重背包
不了解背包 \(DP\) 的请先阅读背包 \(DP\)。设 \(f_{i,j}\) 表示前 \(i\) 个物品装入承重为 \(j\) 的背包的最大价值,朴素的转移方程为
时间复杂度 \(O(W\sum k_i)_0\)
考虑优化\(f_i\)的转移。为方便表述,设\(g_{x,y}=f_{i,x\times w_i+y}, g'_{x,y}=f_{i-1,x\times w_i+y}\),其中\(0\leq y<w_i\),则转移方程可以表示为:
设\(G_{x,y}=g'_{x,y}-v_i\times x\)。则方程可以表示为:
这样就转化为一个经典的单调队列优化形式了。\(G_{x,y}\) 可以 \(O(1)\) 计算,因此对于固定的 \(y\),我们可以在 \(O\left(\left[\frac{W}{w_i}\right]\right)\) 的时间内计算出 \(g_{x,y}\)。因此求出所有 \(g_{x,y}\) 的复杂度为
这样转移的总复杂度就降为 \(O(nW)\)。
核心代码
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=205,M=20005;
int n,m,B[N],C[N],dp[M];
struct Data {int Shuz,Weiz;
} Ddq[M];
int main() {int i,j,k;cin>>n;for(i=1; i<=n; i++) cin>>B[i];for(i=1; i<=n; i++) cin>>C[i];cin>>m;memset(dp,63,sizeof dp);dp[0]=0;for(i=1; i<=n; i++) {for(j=0; j<B[i]; j++) {int Head=1,Tail=0;for(k=0;; k++) {int x=k*B[i]+j;if(x>m) break;while(Head<Tail&&Ddq[Head].Weiz<k-C[i]) Head++;while(Head<=Tail&&dp[x]-k<Ddq[Head].Shuz-Ddq[Head].Weiz) Tail--;Ddq[++Tail]= {dp[x]-k,k};dp[x]=min(dp[x],Ddq[Head].Shuz+k);}}}cout<<dp[m]<<endl;return 0;
}
烽火传递
- 在某两座城市之间有 \(n\) 个烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确的传递,在连续 \(m\) 个烽火台中至少要有一个发出信号。现输入 \(n\) 、\(m\) 和每个烽火台发出的信号的代价,请计算总共最少需要话费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确的传递.
-
\[1<=m<=n<=1,000,000 \]
- 状态表示:\(f[i]\) 标识前 \(i\) 个烽火台并点燃第 \(i\) 个烽火台的最小合法代价.
- 状态转移:\(f[i]=min(f[j]+w[i],i-m\le j\le i-1)\),最后扫描 \(m\) 个 \(f[i]\) 的值。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int dp[100010];
int a[100010];
int q[100010];
int main(){int n,m;scanf("%d%d",&n,&m);int head=1;//表示队首的下标。int tail=1;//表示队尾的下标。for(int i=1;i<=n;i++){scanf("%d",&a[i]);}for(int i=1;i<=n;i++){dp[i]=dp[q[head]]+a[i];while(tail>=head&&dp[i]<=dp[q[tail]])//每次更新都表示弹出队尾元素。tail--;q[++tail]=i;//弹完后,我们保证这个队列是单调的且新加入的元素一定是队尾元素。while(q[head]<i+1-m)//判断队首元素是否合法,如果不合法,将其弹掉。head++;}printf("%d",dp[q[head]]);//这一步比较经典。我们维护的单调队列中,队首元素一定是合法的(在最后的m个烽火台之内),//所以我们选择这其中的最小者更新即可return 0;
}