You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.2 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 472. 跳房子

一、题目描述

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

跳房子的游戏规则如下: 

在地面上确定一个起点,然后在起点右侧画 n 个格子,这些格子都在同一条直线上。

每个格子内有一个数字(整数),表示到达这个格子能得到的分数

玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。

第二次再从当前位置继续向右跳,依此类推。

规则规定:玩家每次都必须跳到当前位置右侧的一个格子内。

玩家可以在 任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。 

现在小 R 研发了一款弹跳机器人来参加这个游戏。

但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 d

R 希望改进他的机器人,如果他花 g 个金币改进他的机器人,那么他的机器人灵活性就能增加 g,但是需要注意的是,每次弹跳的距离至少为 1

具体而言,当 g<d 时,他的机器人每次可以选择向右弹跳的距离为 dg,dg+1,dg+2d+g2d+g1d+g;否则(当 g≥d 时),他的机器人每次可以选择向右弹跳的距离为 123d+g2d+g1d+g。 

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

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

接下来 n 行,每行两个正整数 x_i,s_i,分别表示起点到第 i 个格子的距离以及第 i 个格子的分数。

两个数之间用一个空格隔开,保证 x_i 按递增顺序输入。

输出格式 共一行,一个整数,表示至少要花多少金币来改造他的机器人。

若无论如何他都无法获得至少 k 分,输出 1

数据范围 1≤n≤500000,1≤d≤2000,1≤x_i,k≤10^9,|s_i|≤10^5

输入样例

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

输出样例

2

样例解读

A-G分别表示1号到7号格子,红色文字表示该格子对应的分值。

初始状态,当d = 4时,无法由起点跳到其它点,此时需要花费2金币改造,改造后机器人的移动范围变为[2,6],此时:

  1. 机器人跳到A点,得6分,总分6
  2. 机器人跳到B点,得-3分,总分3
  3. 机器人跳到C点,得3分,总分6
  4. 机器人跳到E点,得1分,总分7
  5. 机器人跳到F点,得6分,总分13

所以,当花费2金币进行改造时,得分不低于10分。

二、题目解析

1、想到二分

假设答案为ans,那么必然存在一种跳法,使得可以得到>=k的分数,此时的跳跃步数为[d-ans,d+ans]。 此时,如果增大ans,那么上面的跳跃区间就会因为ans的变大而区间严格变大。

所以,本题可以通过二分来以log_2n的复杂度来去掉一维,就是评估一个g值,问它是不是可以满足要求。

2、动态规划 + 单调队列优化

假设f[i]表示到达i号格子时可以获得的最大分值,来思考它是从哪个格子转移过来的:(动态规划)

在给定 g个金币的情况下,机器人可以跳d这么远,如果现在在x个位置上,那么机器人可以跳到距离范围就是

\large [d-g , d+g]

也就是说,距离i点距离在(d-g),(d+g)之间的所有格子都是可以跳到i这个格子上来了,这是一个固定长度的区间,我们需要在这个区间上找出所有的格子,看看哪个格子的f[x]最大,就选择从它这个位置上跳到i上去,这就是一个前序固定长度区间找最大值问题,想到用滑动窗口求最值问题,使用 单调队列 进行优化。

我们假设可以跳到i号格子的前序区间 距离范围[j,k],这个范围内的所有点都可能是f[i]的前序转移点:

\large f[i]=f[y]+w[i]

那如何确定这个y是哪个点呢?就是这个区间里f[]最大值的那个嘛~

思路与步骤DP+动态规划 ② 从左到右去填充f[i]统计结果数据表,动态维护一个范围为[d-g,d+g]的范围,用x[i]x[y]之间的距离差(x[i]>x[y])判断是不是在上面的范围内

三、实现代码

#include <bits/stdc++.h>
using namespace std;
const int N = 500010;

typedef long long LL;
int n, d, k;    // n:格子数d:机器人可以跳的距离,k:想要取得的分值
int x[N], w[N]; // 距离数组x[i]表示i号格子距离出发点的距离w[i]从i号格子中可以获得的分值
LL f[N];        // f[i]:到达i点时可以获取到的最大分值
int q[N];       // 单调队列,记录的是格子号

bool check(int g) { // g 描述的是给定的金币数量
    LL res = 0;

    // 多次dp,每次需要清空统计数组
    memset(f, -0x3f, sizeof f); // 预求最大,先设最小
    f[0] = 0;                   // 递推起点数据分值为0,这个要先看状态转移方程,再思考整体初始化、起点初始化
    int hh = 0, tt = -1;

    // L,R : 左右边界
    int L = max(1, d - g), R = d + g;

    /*
      ④ 为什么要先处理入队列,再处理出队列?反过来为什么是错的呢?
      答:
    */
    for (int i = 1, j = 0, k = 0; i <= n; i++) {
        // ① 新元素入队列
        while (x[i] - x[k] >= L) {                     // i走的挺快k有资格参评了
            while (hh <= tt && f[q[tt]] <= f[k]) tt--; // 年龄比k大值比k小的都去死
            q[++tt] = k++;                             // k入队列
        }

        // ② 越界元素出队列
        while (x[i] - x[j] > R) j++;        // j出界,j无法跳到i
        while (hh <= tt && q[hh] < j) hh++; // j都出界了单调队列也需要维护把比j小的都干掉

        // ③ 队列非空,队列头中保存的就是前面[L,R]范围内的分数最大值所对应的y号格子
        if (hh <= tt) f[i] = f[q[hh]] + w[i]; // 再加上i号格子中的w[i]分值,就是总分值

        // 时刻更新最大值
        res = max(res, f[i]);
    }

    // 因为分值中存在负数一旦是在半途中出现可以获取到的分值大于等于k时就可以停止掉表示已经找到了一个金币数量满足增加了自由度后可以取得k这样的分值
    return res >= k;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    cin >> n >> d >> k;                               // 格子数量,机器人每次可以跳的距离,想要拿到的分数
    for (int i = 1; i <= n; i++) cin >> x[i] >> w[i]; // 起点到第i个格子的距离,第i个格子的分数

    int L = 0, R = 1e9, ans = -1; // 二分的左右无脑边界值
    /*
    二分的最后结果有两种方式:
    1、使用ans变量记录最终结果ans初始化为-1,每次找到check()通过的mid,ans=mid,这样如果最终有答案ans就记录的是答案否则就记录的是-1。
    2、不使用ans变量记录而是最后再用!check(L)然后输出-1。
    个人认为方法1更易理解。
    */
    while (L < R) {
        int mid = L + R >> 1;
        if (check(mid)) {
            R = mid;
            ans = mid;
        } else
            L = mid + 1;
    }
    printf("%d\n", ans);
    return 0;
}