用于求解某个元素所在的一定区间内的最优值。队列中存放元素索引,因为要根据区间来将无效的队头出队。
应用一:求滑动窗口内的最大/小值
题目链接: poj2823 Sliding Window
当区间长度固定时,对第i个元素,有效区间为[i - k + 1, i]。以最大值为例,维护一个单调下降的队列,存放当前的最大值、次大值……
从左至右扫描数组,每次将a[i]与队尾j比较,若a[i] > a[j],则i比j更优,因为对位于i之后的某个元素t而言,若j在t的窗口内,则i一定也在t的窗口内(i的位置在j之后),而a[i] > a[j],所以i比j更优,将队尾出队,直到队尾不小于a[i],然后将a[i]入队。
对于队头元素,因为随着窗口的滑动,前面元素的区间最大值可能会移出窗口,当队头位于窗口之外,即:小于区间下限时,要将其出队,因为它对当前元素及后面的元素都没有意义。
代码如下:
#include <cstdio>
using namespace std;
#define N 1000005
#define MIN 0
#define MAX 1
int n, k, a[N], qu[2][N], tail[2], head[2], res[2][N];
void process(int pos, int type){
if(type == MIN){
while(tail[type] > head[type] && a[qu[type][tail[type] - 1]] > a[pos])
--tail[type];
}
else{
while(tail[type] > head[type] && a[qu[type][tail[type] - 1]] < a[pos])
--tail[type];
}
qu[type][tail[type] ++] = pos;
while(qu[type][head[type]] < pos - k + 1)
++head[type];
if(pos >= k - 1)
res[type][pos] = a[qu[type][head[type]]];
}
int main(){
scanf("%d %d", &n, &k);
for(int i = 0; i < n; ++i)
scanf("%d", &a[i]);
for(int i = 0; i < n; ++i){
process(i, MIN);
process(i, MAX);
}
for(int i = 0; i < 2; ++i){
for(int j = k - 1; j < n; ++j)
printf("%d ", res[i][j]);
printf("\n");
}
return 0;
}
应用二:优化DP
当状态转移方程满足以下条件时,可以使用单调队列来优化复杂度:
其中,k在i的有效区间内,f[k]是可以根据k在常数时间内确定的唯一的常数。并且和随i单调不降。即:随着i的推进,有效区间是向右滑动的(也可能一端不动),但至少不会左移。
例题①:上限固定,下限递增
题目链接: poj1821 Fence
dp[i][j]表示前i个工人负责前j块木板,则dp[i][j - 1]表示第j块木板不涂,dp[i - 1][j]表示第i个工人不涂
i. 时,,因为第i个工人必须涂s[i],否则不涂
ii. 时,
iii. 时,
其中第二个转移方程,变换得:
因为下标k随着j递增,满足适用条件,因此可以使用单调队列优化。
因为k关于j单调,关于i则不一定,所以应该将i放在外层循环,j在内层,则区间相对于j来说,下限递增,上限固定为s[i]。注意入队时,一定要保证入队的索引在有效区间内!
代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 16005
#define K 105
struct worker{
int l, s, p;
friend bool operator< (const worker& a, const worker& b){
return a.s < b.s;
}
}w[K];
int dp[K][N], left[K], right[K], qu[N];
int main(){
int n, m;
scanf("%d %d", &n, &m);
for(int i = 1; i <= m; ++i)
scanf("%d %d %d", &w[i].l, &w[i].p, &w[i].s);
sort(w + 1, w + m + 1);
for(int i = 1; i <= m; ++i){
left[i] = max(w[i].s - w[i].l, 0);
right[i] = min(w[i].s + w[i].l, n + 1);
}
w[0].l = w[0].s = w[0].p = 0;
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= m; ++i){
int head = 0, tail = 0;
for(int j = 0; j < w[i].s; ++j)
dp[i][j] = dp[i - 1][j];
for(int j = left[i]; j < w[i].s; ++j){ //入队的k的范围
int tmp = dp[i - 1][j] - j * w[i].p;
while(head < tail && (dp[i - 1][qu[tail - 1]] - qu[tail - 1] * w[i].p) < tmp)
--tail;
qu[tail ++] = j;
}
for(int j = w[i].s; j < right[i]; ++j){
while(qu[head] < j - w[i].l)
++head;
int tmp = dp[i - 1][qu[head]] - qu[head] * w[i].p;
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
dp[i][j] = max(dp[i][j], tmp + j * w[i].p);
}
for(int j = right[i]; j <= n; ++j)
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
printf("%d\n", dp[m][n]);
return 0;
}
例题②: 上下限都递增
题目链接: poj2373 Dividing the Path
dp[i]表示前i块区域恰好被覆盖完所需的喷头数,显然只有偶数下标的点才有解。
dp[i] = min{dp[k]} + 1, i - 2 * b <= k <= i - 2 * a;
由于每个range内的点只能被一个喷头覆盖,当两个range有重叠时,需要用一个喷头去覆盖这两个range。则这中间的点都不是合法解,否则,区间会以该点为边界,被两个喷头覆盖。所以先对点进行标记,位于区间内的点不进行求解。初始化时,dp[0]为合法解,设为0,并将下标0入队。
队列中可能存在INF的元素,但不影响结果。因为本身就存在无解的点。
队头出队时需要判断索引是否满足下限,将队头作为最优解时需要判断是否满足上限(不能出队,因为该点可能满足后续元素的上限)
代码如下:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#define L 1000005
#define N 1005
#define INF 0x3f3f3f3f
int dp[L], qu[L];
bool covered[L];
int main(){
int n, l, A, B, cnt = 0;
scanf("%d %d %d %d", &n, &l, &A, &B);
for(int i = 0; i < n; ++i){
int st, en;
scanf("%d %d", &st, &en);
memset(covered + st + 1, true, (en - st - 1) * sizeof(bool));
}
memset(dp, INF, sizeof(dp));
dp[0] = 0, qu[0] = 0;
int head = 0, tail = 1;
for(int i = 2; i <= l; i += 2){
if(covered[i])
continue;
while(head < tail && qu[head] < i - 2 * B)
++head;
if(head < tail && qu[head] <= i - 2 * A && dp[qu[head]] < INF)
dp[i] = dp[qu[head]] + 1;
while(head < tail && dp[qu[tail - 1]] > dp[i]) //注意没有等号,贡献N个WA
--tail;
qu[tail ++] = i;
}
printf("%d\n", dp[l] >= INF ? -1 : dp[l]);
return 0;
}
注意注释中对等号的强调,使用单调队列时,队尾与当前元素的比较,使用<或>,不要加等号。否则很容易出错。