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

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

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

题目链接: 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;
}

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

你可能感兴趣的:(数据结构)