单调队列优化DP
单调队列
例题 P1886 滑动窗口 /【模板】单调队列
单调队列里面的值都是单调的,而且相当于在一个双端队列上操作。
对于求最小值:当遍历到一个 \(a_i\) 之后,如果队首比它还大,那么这个队首在后面的区间内永远就不会成为那个最小值,所以直接出队列就行了。然后把该弹出的弹出,入队列就行了。然后整个队列的长度不用大于 \(k\),一旦大于 \(k\) 了直接弹出队尾就行了。然后在 \(i\ge k\) 的时候输出队尾的值就行了。
对于求最大值同理。
人生又不是独木桥,每个人有自己的步调。
#include <bits/stdc++.h>
#define N 1000006
using namespace std;
int n,k,a[N],q[N];
int main(){
scanf("%d%d",&n,&k);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]);
int head = 1,tail = 0;
for(int i = 1;i <= n;i ++){
while(tail >= head and a[q[tail]] >= a[i]) tail --;
q[++tail] = i;
while(q[head] <= i - k) head ++;
if(i >= k) printf("%d ",a[q[head]]);
}puts("");
head = 1,tail = 0;
for(int i = 1;i <= n;i ++){
while(tail >= head and a[q[tail]] <= a[i]) tail --;
q[++tail] = i;
while(q[head] <= i - k) head ++;
if(i >= k) printf("%d ",a[q[head]]);
}
return 0;
}
习题
1. P2698 [USACO12MAR] Flowerpot S
以 \(x\) 为关键字进行排序。开两个单调队列,分别记录区间最小值和区间最大值。花盆的左右端点肯定在水滴的边缘上,正确性显然。每次固定 \(L\),向右遍历 \(R\),直到最大值减去最小值 \(\ge D\) 或者遍历到了最右侧。然后我们始终记录我们每次遍历到的这个右端点,因为下一次答案一定在这个右端点右侧(包括此右端点);因为当我们左端点右移的时候,我们的区间最大值不会增,最小值不会减。这样就保证了每一个水滴最多进一次队列,出一次队列。
不是偷懒,这叫做……灵活变通地努力。
#include <bits/stdc++.h>
#define N 100005
using namespace std;
int n,D,q1[N],q2[N],num = 2e9;
struct A{int x,y;}a[N];
bool cmp(A a,A b){return a.x < b.x;}
int main(){
scanf("%d%d",&n,&D);
for(int i = 1;i <= n;i ++) scanf("%d%d",&a[i].x,&a[i].y);
sort(a + 1,a + n + 1,cmp);
int head1 = 1,head2 = 1,tail1 = 0,tail2 = 0,j = 1;
for(int i = 1;i <= n;i ++){
if(i != 1){
while(a[q1[head1]].x < a[i].x) head1 ++;
while(a[q2[head2]].x < a[i].x) head2 ++;
}
for(;j <= n;j ++){
if(a[q1[head1]].y - a[q2[head2]].y >= D){num = min(num,abs(a[q1[head1]].x - a[q2[head2]].x));break;}
while(tail1 >= head1 and a[j].y >= a[q1[tail1]].y) tail1 --;q1[++tail1] = j;
while(tail2 >= head2 and a[j].y <= a[q2[tail2]].y) tail2 --;q2[++tail2] = j;
}
if(a[q1[head1]].y - a[q2[head2]].y >= D) num = min(num,abs(a[q1[head1]].x - a[q2[head2]].x));
if(j == n + 1) break;
}
if(num == 2e9) puts("-1");
else printf("%d",num);
return 0;
}
单调队列优化DP
例题 CF372C Watching Fireworks is Fun
\(f_{i,j}\) 表示在放第 \(i\) 个烟花时,在第 \(j\) 个位置可以获得的最大的快乐值。
得到状态转移方程 \(f_{i,j}=\max\{f_{i-1,k}+b_i-|a_i-j|\}=\max\{f_{i-1,k}\}+b_i-|a_i-j|,j-(t_i-t_{i-1})\times d\le k\le j+(t_i-t_{i-1})\times d\)。
使用单调队列维护并使用滚动数组优化空间。
做自己内心的君主,胜过众望所归的英雄。
#include <bits/stdc++.h>
#define N 150004
#define int long long
using namespace std;
int n,m,d,a[N],b[N],t[N],ans = -1e18,f[2][N],q[N],fl = 1;
signed main(){
scanf("%lld%lld%lld",&n,&m,&d);
for(int i = 1;i <= m;i ++) scanf("%lld%lld%lld",&a[i],&b[i],&t[i]);
for(int i = 1;i <= n;i ++) f[1][i] = -1e18;
for(int i = 1;i <= m;i ++){
int head = 1,tail = 0,k = 1;
for(int j = 1;j <= n;j ++){
for(;k <= min(n,j + d * (t[i] - t[i - 1]));k ++){
while(tail >= head and f[fl ^ 1][q[tail]] <= f[fl ^ 1][k]) tail --;
q[++tail] = k;
}
while(tail >= head and q[head] < max(1ll,j - d * (t[i] - t[i - 1]))) head ++;
f[fl][j] = f[fl ^ 1][q[head]] - abs(a[i] - j) + b[i];
}
fl ^= 1;
}
for(int i = 1;i <= n;i ++) ans = max(ans,f[fl ^ 1][i]);
printf("%lld",ans);
return 0;
}
习题
1. Loj10176 最大连续和
朴素做法:\(num_i\) 表示前缀和,\(f_i\) 表示以 \(a_i\) 为结尾可以取到的最大的和,易得状态转移方程为 \(f_i=max\{num_i-num_{i-j}\},1\le j\le min(i,m)\) 。
然后我们可以转化为 \(f_i=num_i-min\{num_x\},max(0,i-m)\le x\le i-1\),后者用单调队列维护即可。
别羡慕,我可是凭本事摸鱼的。
#include <bits/stdc++.h>
#define N 200005
using namespace std;
int n,m,a[N],num[N],ans = -1e9,q[N];
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++) scanf("%d",&a[i]),num[i] = num[i - 1] + a[i];
int head = 1,tail = 1;q[1] = 0;
for(int i = 1;i <= n;i ++){
ans = max(ans,num[i] - num[q[head]]);
while(tail >= head and num[q[tail]] >= num[i]) tail --;
q[++tail] = i;while(q[head] <= i - m) head ++;
}
printf("%d",ans);
return 0;
}
2. P2254 [NOI2005] 瑰丽华尔兹
\(f_{t,i,j}\) 表示 \(t\) 时刻在第 \(i\) 行第 \(j\) 列的位置走过的最长路径。易得状态转移方程为 \(f_{t,i,j}=max\{f_{t-1,i,j},f_{t-1,i',j'}\}\),\(i'\)、\(j'\) 表示上一个合法的转移位置。复杂度 \(O(TNM)\)
考虑优化。\(f_{t,i,j}\) 表示在第 \(t\) 个时间段在位置 \(i'\)、\(j'\) 位置的最长路径。易得状态转移方程为 \(f_{t,i,j}=max\{f_{t-1,i',j'}+dis\}\),\(dis\) 表示上一个合法位置 \(i',j'\) 与当前位置 \(i,j\) 的距离。不过我们还是需要对每个时间段内的时刻划分出来去维护。复杂度 \(O(KN^2M)\)。
再次考虑优化。我们知道,当前位置一定与上一个合法位置同一行或者同一列,这取决于这次移动的方向。
我们以当前时间段向南为例,设当前时间段的长度为 \(len\),那么我们有 \(f_{t,i,j}=\max\limits_{i-len\le pos\le i}\{f_{t-1,pos,j}+i-pos\}=\max\limits_{i-len\le pos\le i}\{f_{t-1,pos,j}-pos\}+i\)。于是滑动窗口即可。同时需要去处理一些细节问题。
对自己严苛的人,怎么可能对他人宽容。
#include <bits/stdc++.h>
#define N 205
using namespace std;
int f[2][N][N],q[N],n,m,X,Y,fl,ans;char a[N][N];
void work1(int len){
for(int j = 1;j <= m;j ++){
int head = 1,tail = 0,k = 1;
for(int i = 1;i <= n;i ++){
if(a[i][j] == 'x') continue;
k = max(k,i);
for(;k <= min(i + len,n);k ++){
if(a[k][j] == 'x') break;
while(tail >= head and f[fl ^ 1][k][j] + k >= f[fl ^ 1][q[tail]][j] + q[tail]) tail --;
q[++tail] = k;
}
while(tail >= head and q[head] < i) head ++;
f[fl][i][j] = f[fl ^ 1][q[head]][j] + q[head] - i;
}
}
fl ^= 1;
}
void work2(int len){
for(int j = 1;j <= m;j ++){
int head = 1,tail = 0,k = n;
for(int i = n;i >= 1;i --){
if(a[i][j] == 'x') continue;
k = min(k,i);
for(;k >= max(i - len,1);k --){
if(a[k][j] == 'x') break;
while(tail >= head and f[fl ^ 1][k][j] - k >= f[fl ^ 1][q[tail]][j] - q[tail]) tail --;
q[++tail] = k;
}
while(tail >= head and q[head] > i) head ++;
f[fl][i][j] = f[fl ^ 1][q[head]][j] - q[head] + i;
}
}
fl ^= 1;
}
void work3(int len){
for(int i = 1;i <= n;i ++){
int head = 1,tail = 0,k = 1;
for(int j = 1;j <= m;j ++){
if(a[i][j] == 'x') continue;
k = max(k,j);
for(;k <= min(j + len,m);k ++){
if(a[i][k] == 'x') break;
while(tail >= head and f[fl ^ 1][i][k] + k >= f[fl ^ 1][i][q[tail]] + q[tail]) tail --;
q[++tail] = k;
}
while(tail >= head and q[head] < j) head ++;
f[fl][i][j] = f[fl ^ 1][i][q[head]] + q[head] - j;
}
}
fl ^= 1;
}
void work4(int len){
for(int i = 1;i <= n;i ++){
int head = 1,tail = 0,k = m;
for(int j = m;j >= 1;j --){
if(a[i][j] == 'x') continue;
k = min(k,j);
for(;k >= max(j - len,1);k --){
if(a[i][k] == 'x') break;
while(tail >= head and f[fl ^ 1][i][k] - k >= f[fl ^ 1][i][q[tail]] - q[tail]) tail --;
q[++tail] = k;
}
while(tail >= head and q[head] > j) head ++;
f[fl][i][j] = f[fl ^ 1][i][q[head]] - q[head] + j;
}
}
fl ^= 1;
}
int main(){
int k;scanf("%d%d%d%d%d",&n,&m,&X,&Y,&k);
for(int i = 1;i <= n;i ++) for(int j = 1;j <= m;j ++) cin >> a[i][j],f[0][i][j] = -1e7;
f[0][X][Y] = 0,fl = 1;
for(int i = 1,s,t,d;i <= k;i ++){
scanf("%d%d%d",&s,&t,&d);
if(d == 1) work1(t - s + 1);
if(d == 2) work2(t - s + 1);
if(d == 3) work3(t - s + 1);
if(d == 4) work4(t - s + 1);
}
for(int i = 1;i <= n;i ++) for(int j = 1;j <= m;j ++) ans = max(f[fl ^ 1][i][j],ans);
printf("%d",ans);
return 0;
}