bzoj2448 挖油

2448: 挖油

Time Limit: 10 Sec  Memory Limit: 128 MB
Submit: 140  Solved: 61
[Submit][Status][Discuss]

Description

给出一条线段,在左端点点0与右端点n+1间有n个点(n<=2000),并且在0到x之间的所有点都是有油的,在每个点钻井判断是否有油需要时间ti,求能够知道x的最坏情况下最少需要多少时间。

Input

第一行包含一个数n,如题目描述。
第二行包含n个数,表示在第i个点钻井判断是否有油需要的时间。

Output

输出包含一行,最坏情况下最少需要多少时间。

Sample Input

4
8 24 12 6

Sample Output

42

HINT

对于100%的数据,n<=2000,ti<=10^6

Source

2011福建集训

分析:个人认为比较神的一道题,如果之前没有接触过这类题根本无从下手的那种.

   做这道题之前可以先做一下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\n",f[1][n]);

    return 0;
}

 

posted @ 2018-03-01 23:47  zbtrs  阅读(602)  评论(0编辑  收藏  举报