2448: 挖油
Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 140 Solved: 61
[Submit][Status][Discuss]
Description
Input
Output
Sample Input
8 24 12 6
Sample Output
HINT
对于100%的数据,n<=2000,ti<=10^6
Source
分析:个人认为比较神的一道题,如果之前没有接触过这类题根本无从下手的那种.
做这道题之前可以先做一下poj3783,都是要使得最大值最小,猜答案的那种.
想法还是dp,能不能直接拿poj3783的状态来用呢?显然不行,这道题的答案和编号还是有关的,而poj3783是无关的.
类似的分析方法:先考虑在一个点k钻井,因为要使得结果最坏,如果k不是两个区间的端点,都不能知道答案,需要继续.这个时候就有两种选择了:1.往左. 2.往右. 每次取max,直到到达边界. 这个过程很像区间dp,有左右端点的限制嘛.那么令f[i][j]表示区间
[i,j]最坏情况下需要的最少时间.转移就是枚举一个点k,f[i][j] = min{max{f[i][k - 1],f[k + 1][j]} + a[k]}. 初始化f[i][i] = a[i].
这个dp的想法就是每次考虑当前钻哪个井,结果最坏就必须要钻其它位置的井,由此来扩展,取max是为了得到最坏情况.
区间dp一般都是O(n^3)的,这道题也不例外. 怎么优化呢?括号内的max限制了f[i][j]从哪个状态转移过来.如果没有了max,方程就变成了一个递推式.
考虑如何去掉max.可以发现当k大到一定程度的时候,f[i][k - 1]一定大于f[k + 1][j]的,那么就只会从f[i][k - 1]转移过来,因为f[i][k - 1]随着k的增大是单调不减的,所以可以用很多效率高的方法维护,一个比较好的方法就是单调队列. 每次将f[i][k-1]和a[k]的整体存到单调队列中,弹出最小值即可. 对于j也一样.
现在的问题是如何找到这个分界点.一个结论:区间[i,j + 1]的分界点一定在区间[i,j]的分界点右边,或者相同.这个挺好想的,因为左右两边的转移实际上是一种竞争关系嘛,如果分界点不增加,那么右边的值就会增大. 分界点的位置是单调的了,那么固定i,枚举j的时候,分界点也是在右移的,只需要维护一个指针表示分界点的位置就好了. 如果左边决策比不上右边决策,则指针往右.
上面考虑的是固定i,每次从左区间转移的答案. 固定j也是一样的,只是变成了指针向左扫. 这样的话有一个问题:每枚举到1个i,就要从i+1到n枚举j,难道每次都要清空优先队列吗? 不需要!
区间dp为了先处理得到小区间的值,倒序枚举i,顺序枚举j,对于j的单调队列不需要清空(i只会从上一个i的位置向左移动),为了避免重复占用同一个单调队列,开n个单调队列,而对于i的则必须清空了. 用一个单调队列维护固定i的答案,n个单调队列维护固定j的答案,总共就是n+1个单调队列了.
Question:单调队列维护的都是队首元素最小的队列. 最后的答案也是在两个单调队列中取min.那为什么没有max呢? 因为max只是决定了从哪个状态转移而来,最后对答案有影响的主要还是取min的部分,取max的部分在维护两个指针的过程中已经考虑了.
这类题型要熟记,对于决策会有分界点的dp优化也要熟练掌握.
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int maxn = 2010; int n,a[maxn],f[maxn][maxn],l[maxn],r[maxn],q[maxn][maxn]; int cal1(int x,int y,int z) { return f[x][z - 1] + a[z]; } int cal2(int x,int y,int z) { return f[z + 1][y] + a[z]; } int main() { memset(f,127/3,sizeof(f)); scanf("%d",&n); for (int i = 1; i <= n; i++) scanf("%d",&a[i]); for (int i = n; i >= 1; i--) { f[i][i] = a[i]; l[0] = 0; r[0] = 1; q[0][++r[0]] = i; for (int j = i + 1; j <= n; j++) { while (l[0] <= r[0] && cal1(i,j,q[0][l[0]]) < cal2(i,j,q[0][l[0]])) //这实际上就是维护一个指针 l[0]++; while (l[0] <= r[0] && cal1(i,j,j) < cal1(i,j,q[0][r[0]])) r[0]--; q[0][++r[0]] = j; while (l[j] <= r[j] && cal2(i,j,q[j][l[j]]) < cal1(i,j,q[j][l[j]])) l[j]++; while (l[j] <= r[j] && cal2(i,j,i) < cal2(i,j,q[j][r[j]])) r[j]--; q[j][++r[j]] = i; f[i][j] = min(cal1(i,j,q[0][l[0]]),cal2(i,j,q[j][l[j]])); } } printf("%d ",f[1][n]); return 0; }