单调队列及优化DP poj2823/poj1821/poj2373

来源:互联网 发布:咫尺网络小程序 编辑:程序博客网 时间:2024/06/08 12:31

    用于求解某个元素所在的一定区间内的最优值。队列中存放元素索引,因为要根据区间来将无效的队头出队。

应用一:求滑动窗口内的最大/小值

题目链接: 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 1int 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 105struct 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 0x3f3f3f3fint 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;}

    注意注释中对等号的强调,使用单调队列时,队尾与当前元素的比较,使用<或>,不要加等号。否则很容易出错。

0 0
原创粉丝点击