[NOIP2017 普及组] 跳房子(dp+二分+单调队列优化)

108 篇文章 0 订阅

[NOIP2017 普及组] 跳房子

题目背景

NOIP2017 普及组 T4

题目描述

跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。

跳房子的游戏规则如下:

在地面上确定一个起点,然后在起点右侧画 n n n 个格子,这些格子都在同一条直线上。每个格子内有一个数字(整数),表示到达这个 格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:

玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。

现在小 R 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 d d d。小 R 希望改进他的机器人,如果他花 g g g 个金币改进他的机器人,那么他的机器人灵活性就能增加 g g g,但是需要注意的是,每 次弹跳的距离至少为 1 1 1。具体而言,当 g < d g<d g<d 时,他的机器人每次可以选择向右弹跳的距离为 d − g , d − g + 1 , d − g + 2 , … , d + g − 1 , d + g d-g,d-g+1,d-g+2,\ldots,d+g-1,d+g dg,dg+1,dg+2,,d+g1,d+g;否则当 g ≥ d g \geq d gd 时,他的机器人每次可以选择向右弹跳的距离为 1 , 2 , 3 , … , d + g − 1 , d + g 1,2,3,\ldots,d+g-1,d+g 1,2,3,,d+g1,d+g

现在小 R 希望获得至少 k k k 分,请问他至少要花多少金币来改造他的机器人。

输入格式

第一行三个正整数 n , d , k n,d,k n,d,k,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数。相邻两个数 之间用一个空格隔开。

接下来 n n n 行,每行两个整数 x i , s i x_i,s_i xi,si,分别表示起点到第 i i i 个格子的距离以及第 i i i 个格子的分数。两个数之间用一个空格隔开。保证 x i x_i xi 按递增顺序输入。

输出格式

共一行,一个整数,表示至少要花多少金币来改造他的机器人。若无论如何他都无法获得至少 k k k 分,输出 − 1 -1 1

样例 #1

样例输入 #1

7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2

样例输出 #1

2

样例 #2

样例输入 #2

7 4 20
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2

样例输出 #2

-1

提示

样例 1 说明

花费 2 2 2 个金币改进后,小 R 的机器人依次选择的向右弹跳的距离分别为 $ 2, 3, 5, 3, 4,3$,先后到达的位置分别为 2 , 5 , 10 , 13 , 17 , 20 2, 5, 10, 13, 17, 20 2,5,10,13,17,20,对应 $ 1, 2, 3, 5, 6, 7$ 这 6 6 6 个格子。这些格子中的数字之和 $ 15$ 即为小 R 获得的分数。

样例 2 说明

由于样例中 7 7 7 个格子组合的最大可能数字之和只有 18 18 18,所以无论如何都无法获得 20 20 20 分。

数据规模与约定

本题共 10 组测试数据,每组数据等分。

对于全部的数据满足 1 ≤ n ≤ 5 × 1 0 5 1 \le n \le 5\times10^5 1n5×105 1 ≤ d ≤ 2 × 1 0 3 1 \le d \le2\times10^3 1d2×103 1 ≤ x i , k ≤ 1 0 9 1 \le x_i, k \le 10^9 1xi,k109 ∣ s i ∣ < 1 0 5 |s_i| < 10^5 si<105

对于第 1 , 2 1, 2 1,2 组测试数据,保证 n ≤ 10 n\le 10 n10

对于第 3 , 4 , 5 3, 4, 5 3,4,5 组测试数据,保证 n ≤ 500 n \le 500 n500

对于第 6 , 7 , 8 6, 7, 8 6,7,8 组测试数据,保证 d = 1 d = 1 d=1

思路

  • 对于这道题,我们首先肯定会想到用二分,就是给定一个金币数,看看能不能使得小 R 获得至少 k k k 分。这样就产生了关系。
  • 那么我们写着写着,发现:由于小 R 在比赛过程中是可以任意时刻退出比赛的,因此如果我们贪心去做的话,肯能会出错,因为他也可以任意时刻进场。因此我们使用dp。
  • 那么此时的dp的状态表示为: f i f_i fi 表示所有以 i i i 结尾的最高分数的集合。
  • 状态转移: f i = m a x ( f i , f k + w i ) , 0 ≤ k < i f_i=max(f_i,f_k+w_i),0\le k<i fi=max(fi,fk+wi),0k<i。注意,因为本道题可能有负数,所以我们的 f f f 值要 memset-0x3f
  • 那么我们此时能写出:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>

#define int long long

using namespace std;

const int N = 5e5+10;

int x[N],w[N];
int n,d,k;
int ans;
int maxv;
int f[N];

bool check(int mid){
    int l;
    memset(f,-0x3f,sizeof f);
    if(mid<d){
        l=max(1ll,d-mid);
    }else{
        l=1;
    }
    //超时
    
    f[0]=0ll;
    
    for(int i=1;i<=n;i++){
        for(int j=i-1;j>=0;j--){
            if(x[i]-x[j]>mid+d)break;
            if(x[i]-x[j]<l)continue;
            f[i]=max(f[i],f[j]+w[i]);
        }
        if(f[i]>=k)return true;
    }
    
    return false;
}

signed main(){
    cin>>n>>d>>k;
    
    for(int i=1;i<=n;i++){
        cin>>x[i]>>w[i];
        if(w[i]>0){
            ans+=w[i];
        }
    }
    
    if(ans<k){
        puts("-1");
    }else{
        int l=-1,r=x[n]-x[1]+1;
        
        
        while(l+1!=r){
            int mid=(l+r)>>1;
            if(check(mid))r=mid;
            else l=mid;
        }
        
        cout<<r;
    }
    
    return 0;
    
}

但不管你在输入输出优化,发现只能得 80   p t s 80\ pts 80 pts,因为时间复杂度接近 O ( n 2 ) O(n^2) O(n2)

  • 因此我们得优化。

  • 观察上面 dp 转移式中 k k k 需满足的条件,会发现,显然对于每一个 i i i k k k 的取值范围都是一个区间。有着丰富经验的选手应该可以一眼看出,随着 i i i 不断变大,这个区间的 ( l , r ) (l,r) (l,r) 也是不断递增的,于是我们就可以用单调队列来优化这个 dp 了。

  • 优化后每次 dp 的时间复杂度为 O ( n ) O(n) O(n),加上二分的复杂度,总时间复杂度为 O ( n log ⁡ x n ) O(n\log x_n) O(nlogxn),足以通过此题。

  • 对于不满足题意的就是把正数加起来小于 p p p 就输出 -1

  • 细节1:最好开 long long

  • 细节2:我的代码是用双端队列来优化的,思路跟单调队列优化类似,都是区间优化类的。

AC 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<deque>

#define int long long

using namespace std;

const int N = 5e5+10;

int x[N],w[N];
int n,d,k;
int ans;
int maxv;
int f[N];
int q[N];

bool check(int mid){
    int l;
    memset(f,-0x3f,sizeof f);
    if(mid<d){
        l=max(1ll,d-mid);
    }else{
        l=1;
    }
    //超时
    
    f[0]=0ll;
    
    // for(int i=1;i<=n;i++){
    //     for(int j=i-1;j>=0;j--){
    //         if(x[i]-x[j]>mid+d)break;
    //         if(x[i]-x[j]<l)continue;
    //         f[i]=max(f[i],f[j]+w[i]);
    //     }
    //     if(f[i]>=k)return true;
    // }
    
    deque<int>q;//单调队列
    int now=0;
    
    for(int i=1;i<=n;i++){
        while(x[now]+l<=x[i]){//在i之前找到满足能跳到i的,把它们入列
            while(q.size()&&f[q.back()]<f[now]){//列内的尾巴的价值不行,就把它弹走,把那个now加入
            //这里就是单调队列的求最大值的模板
                q.pop_back();
            }
            q.push_back(now);
            now++;
        }
        
        while(q.size()&&x[q.front()]+mid+d<x[i]){
        //如果列的前端元素的坐标+最大能跳的距离小于当前坐标,不能要了
            q.pop_front();
        }
        
        if(q.size()){//此时更新
            f[i]=f[q.front()]+w[i];
        }
        
        if(f[i]>=k)return true;//随时return
        
    }
    
    
    return false;
}

signed main(){
    scanf("%lld%lld%lld",&n,&d,&k);
    
    for(int i=1;i<=n;i++){
        scanf("%lld%lld",&x[i],&w[i]);
        if(w[i]>0){
            ans+=w[i];
        }
    }
    if(ans<k){
        puts("-1");
    }else{
        int l=-1,r=x[n]-x[1]+1;
        while(l+1!=r){
            int mid=(l+r)>>1;
            if(check(mid))r=mid;
            else l=mid;
        }
        
        printf("%lld",r);
    }
    return 0;
}
  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

green qwq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值