UVa #12170 Easy Climb (例题9-25)

86 篇文章 0 订阅

每次的调整都要依赖于上一次的决策,因此是一个多阶段决策问题。多节段决策如果不用打印解,最好用滚动数组优化


设 dp(i,j) 表示将第i个山坡高度改为j,还需要修改前i-1个山坡,所需要的代价总合。


然而 j 的可能性太多(d<10^9),因此这个状态的设计是不可行的。不过n的值却很小(n<100),因此可以从n上下手


深入分析问题,发现如果前后两个山坡的高度已经固定了,那么当前山坡调整后的高度应该属于[ max(h[i-1], h[i+1])-d , min(h[i-1], h[i+1])+d ]。如果这个区间为空则无解。如果山坡已经属于这个高度则不用调整,否则应调整到就近的边界值。


将这种情况扩展一下,可以发现改变后的山坡高度总能表示为h[p] + q*d,p属于[0,n-1],q属于(-n,n)。因此有用的高度值其实只有n*2n个。将所有高度值的可能性算出来存在数组x中,并去重和排序。设dp(i,j)表示将第i个山坡的高度改为x[j],还需要修改前i-1个山坡,所需要的代价总合。


转移方程:dp(i,j) = abs(h[i]-x[j]) + min{dp(i-1,y)},x[y]属于[x[j]-d, x[j]+d]。这里的各种定义有点绕。有些是根据数组x的下标定义,有些是直接根据山坡高度定义。需要谨慎区分。


当j固定时,y的定义域则是0-(n-1)上的一段连续子区间。当j从0移动到n-1时,y就形成了一个滑动窗口。只不过这个滑动窗口的左右边界的更新与之前的有些不同,并不是j每次移动都会固定的出一个、入一个,而是应进行判断。对于每个j,我们要取出y的定义域内dp[i-1][y]的最小值来更新dp[i][j]。


Rujia的滚动数组很好用。t和t^1的设计使得滚动数组的滚动方向不再受到限制。至于滑动窗口的实现,我直接用了普通的单调队列,而Rujia的代码中巧妙的使用了一个k,对数据特征进行了进一步剖析。


Run Time: 0.768s

#define UVa  "LT9-25.12170.cpp"		//Easy Climb
char fileIn[30] = UVa, fileOut[30] = UVa;

#include<cstring>
#include<cstdio>
#include<algorithm>
#include<iostream>

using namespace std;

//Global Variables. Reset upon Each Case!
typedef long long LL;
const int maxn = 100 + 5, maxx = 2*maxn*maxn;
const LL INF = (1LL)<<60;
int T, n, nx;
LL d, h[maxn], x[maxx], dp[2][maxx];
/

struct Monoqueue {
    LL q[maxx];
    int front, rear;
    Monoqueue():front(-1),rear(-1){}
    void maintain_left(LL v) {
        if(front != -1 && q[rear] == v) rear++;
    }
    void insert(LL v) {
        if(front == -1) {
            rear = front = 0;
            q[front] = v;
        }
        else {
            while(q[front] > v && front >= rear) front --;
            q[++front] = v;
        }
    }
    LL get_min() {
        return q[rear];
    }
};




int main() {
    scanf("%d", &T);
    while(T--) {
        scanf("%d%d", &n, &d);
        for(int i = 0; i < n; i ++) scanf("%lld", &h[i]);

        if(abs(h[0] - h[n-1]) > (n-1)*d) {
            cout << "impossible\n";
            continue;
        }

        nx = 0;
        for(int i = 0; i < n; i ++)
            for(LL j = -n+1; j < n; j ++)
                x[nx++] = h[i]+j*d;

        sort(x, x + nx);

        nx = unique(x, x+nx) - x;

        int t = 0;
        for(int i = 0; i < nx; i ++) {
            if(x[i] == h[0]) dp[0][i] = 0;
            else dp[0][i] = INF;
        }
        for(int i = 1; i < n; i ++) {
            Monoqueue mq = Monoqueue();
            int p1 = 0, p2 = 0;                 //p1 points to first element >= x-d. p2 points to first element > x+d
            for(int j = 0; j < nx; j ++) {
                while(p1 < nx && x[p1] < x[j] - d) {
                    mq.maintain_left(dp[t][p1]);            //pop out from monoqueue.
                    p1 ++;
                }
                if(p2 < p1) p2 = p1;
                while(p2 < nx && x[p2] <= x[j] + d) {
                    mq.insert(dp[t][p2]);                   //insert into monoque.
                    p2 ++;
                }
                if(mq.get_min() == INF) dp[t^1][j] = INF;
                else dp[t^1][j] = abs(x[j]-h[i]) + mq.get_min();
            }
            t^=1;
        }
        for(int i = 0; i < nx; i ++)
            if(x[i] == h[n-1]) cout<<dp[t][i]<<endl;
    }

    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值