用于求解某个元素所在的一定区间内的最优值。队列中存放元素索引,因为要根据区间来将无效的队头出队。
应用一:求滑动窗口内的最大/小值
题目链接: 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; }
当状态转移方程满足以下条件时,可以使用单调队列来优化复杂度:
其中,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; }