BZOJ2448 挖油

题目链接:戳我

看完题目我们有一个很明显的DP方程可以想出——(dp[i][j]=min{max(dp[i][k-1],dp[k+1][j])+a[k]}),这个时间复杂度是(O(n^3))的。

解释一下为什么是取max,因为我们是要从最坏的情况转移而来。

但是这个时间复杂度对于此题的数据范围显然不太合适,之后我们就应该想到。。。DP优化!

但是DP优化有那么多种,这道题到底应该怎么解决呢?

我们考虑如何把最后一层枚举k的那个循环拿掉。首先是需要解决取max的问题,就是我们需要知道什么时候到底从哪一个决策点转移过来是最坏的情况呢?

我们观察上面列出的DP转移式子,可以发现所有转移来自的情况也不外乎是两种,一种是包含i的,我们可以称之为“左半部分"。一种包含j的,我们可以称之为”右半部分“。首先需要知道,对于一个区间[i,j],我们将j右移。如果选择“左区间”,这个右端点是一定不会向左移动;选择“右区间”,左端点也不会向右移动。那么这样就具有了单调性。普通的遍历转移显然很慢,需要另外一个(O(n)),需要把这一层去掉,所以我们现在就是把这些情况附加到枚举区间右端点(即j)的时候。

怎么附加呢?我们可以开n+1个单调队列。其中q[i][]表示固定i的时候,选择”左区间“,该”左区间“的右端点。每次更换i的时候需要重置,左右指针分别为l[0],r[0]。q[j][]表示固定j,选择”右区间“,该”右区间“的左端点,左右指针分别为l[j],r[j]。

我们维护一个单调上升的优先队列。转移的时候,如果不满足从最坏情况转移过来的约束,就把队首弹出。最后计算dp[i][j]的时候,直接在表示“选择左右区间”的两个队列里面弹出队首(最小值)即可。

代码如下:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cmath>
#define MAXN 2010
using namespace std;
int n;
int dp[MAXN][MAXN],a[MAXN],q[MAXN][MAXN],l[MAXN],r[MAXN];
inline int calc1(int x,int y,int mid){return dp[x][mid-1]+a[mid];}
inline int calc2(int x,int y,int mid){return dp[mid+1][y]+a[mid];}
int main()
{
	#ifndef ONLINE_JUDGE
	freopen("ce.in","r",stdin);
	#endif
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	memset(dp,0x3f,sizeof(dp));
	for(int i=n;i>=1;i--)
	{
		dp[i][i]=a[i];
		l[0]=r[0]=0;
		q[0][++r[0]]=i;
		for(int j=1+i;j<=n;j++)
		{
			while(l[0]<=r[0]&&calc1(i,j,q[0][l[0]])<calc2(i,j,q[0][l[0]])) l[0]++;
			while(l[0]<=r[0]&&calc1(i,j,j)<calc1(i,j,q[0][r[0]])) r[0]--;
			q[0][++r[0]]=j;
			while(l[j]<=r[j]&&calc2(i,j,q[j][l[j]])<calc1(i,j,q[j][l[j]])) l[j]++;
			while(l[j]<=r[j]&&calc2(i,j,i)<calc2(i,j,q[j][r[j]])) r[j]--;
			q[j][++r[j]]=i;
			dp[i][j]=min(calc1(i,j,q[0][l[0]]),calc2(i,j,q[j][l[j]]));
		}
	}
	printf("%d
",dp[1][n]);
	return 0;
}
原文地址:https://www.cnblogs.com/fengxunling/p/10354856.html