【训练题28:二分+单调队列】[NOIP2017 普及组] 跳房子 | 洛谷 P3957

[NOIP2017 普及组] 跳房子 | P3957

难度

提 高 + / 省 选 − \color{cyan}提高+/省选- +/
− 8.02 k 48.38 k -\frac{8.02k}{48.38k} 48.38k8.02k

题意

  • 首先给你一个 x x x 坐标轴,开始在原点,正方向处给定 n n n 个点。
  • 给定这每个点的坐标 p o s i pos_i posi 以及每个点的收益 s c o i sco_i scoi (可正可负)
  • 给你一个弹跳力 d d d 。一开始每次跳跃只能向右移动 d d d 个单位。
  • 你花 g g g 块钱,使得弹跳力变成一个区间 S = [ max ⁡ ( 1 , d − g ) , d + g ] S=[\max(1,d-g) ,d+g] S=[max(1,dg),d+g]
    表示每次跳跃你都可以向右移动一定整数长度,这个长度要在 S S S 区间中。
  • 但是你每次必须跳在给定的点中。

问你至少花多少钱,才可以使得某种跳跃方案,你的收益超过 k k k
若无法收益超过 k k k ,输出 − 1 -1 1

数据范围

1 ≤ n ≤ 5 × 1 0 5 1\le n\le 5\times 10^5 1n5×105
1 ≤ d ≤ 2000 1\le d\le 2000 1d2000
1 ≤ p o s i , k ≤ 1 0 9 1\le pos_i,k\le 10^9 1posi,k109
∣ s c o i ∣ < 1 0 5 |sco_i|<10^5 scoi<105

思路

1.最开始的思路

首先我们抛开其他的条件,就问你:给定 g g g 块钱,你怎么算此时的收益最大值?

  • 每次都可以从一些点跑到另一些点,直接 D P DP DP 不就好了?
  • d p ( i ) dp(i) dp(i) 表示走到第 i i i 个点的最大收益,然后列出简单的状态转移方程:
  • d p ( i ) = max ⁡ { d p ( s t ) , d p ( s t + 1 ) , ⋯   , d p ( e d ) } + s c o i dp(i)=\max\{dp(st),dp(st+1),\cdots,dp(ed)\}+sco_i dp(i)=max{ dp(st),dp(st+1),,dp(ed)}+scoi
  • 因为你每次走的长度在区间 [ max ⁡ ( 1 , d − g ) , d + g ] [\max(1,d-g),d+g] [max(1,dg),d+g] 范围之内,因此转移图肯定是这样的:
    在这里插入图片描述
  • 如果是上面那种转移一推多,时间复杂度就会大幅增加。所以我们选择下面这种。
  • 对于每一个 i i i ,易得这一段 [ s t , e d ] [st,ed] [st,ed] 肯定是 像滑动窗口一样向右移动的,这就是这题的突破口。
  • 我们每次要求这一段区间的最大值,直接使用单调队列RMQ即可。时间复杂度 O ( N ) O(N) O(N)

为什么不能用 S T ST ST 表?

  • S T ST ST 表是静态的。每次你转移都会更新 d p ( i ) dp(i) dp(i),那就无法在时间内实现该效果了。

2.求花费最小?

来,跟我读:

  • 求收益最大的最小花费用二分
  • x x x 最小时 y y y 最大用二分
  • x x x 最大时 y y y 最小用二分
  • 我咋老记不住呢???

对于花费 g g g,明显符合二分性质。我们就二分它呗。

3.具体实现单调队列?

  • 这单调队列的代码实现也是让我敲得很费劲。
  • 由于我们的状态转移会修改数组的值,我们每次就 c o p y copy copy 一下原 s c o sco sco 数组变成 d p dp dp 数组
  • 首先你最开始在原点,即第 0 0 0 个点。状态转移的方程 i i i 肯定是从 1 1 1 n n n 的。
  • 因为 i i i和滑动窗口的右端点是不对应的,我们还要记录一下滑动窗口的右端点下标 s t st st

继续看图啦!在这里插入图片描述
进队列

  • 首先 e d ed ed 能进滑动窗口的条件: i − e d ≥ d 2 i-ed\ge d_2 iedd2
  • 若能进单调队列,然后再按照单调队列的代码,删重 while(Q.size() && dp[Q.back()] <= dp[ed])Q.pop_back();
  • 当然也可以每次 c h e c k check check 到一半发现收益大于 k k k 然后返回,我这里没有这么写。

出队列

  • 然后,考虑队头是否还在窗口中的条件: i − s t ≤ d 1 i-st\le d_1 istd1
  • 如果不满足,我们直接将这个点给删掉。

状态转移

  • 我们如果 Q.size()!=0 满足的,也就是说该点可以通过前面的点转移过来,直接转移
  • dp[i] = dp[Q.front()] + sc[i];
  • 如果 Q.size()==0 也就是说该点不能通过前面的转移过来,那么直接给该点设置一个负无穷大的收益即可。

答案

  • 收益最大值就是 max ⁡ { d p ( 0 ) , d p ( 1 ) , ⋯   , d p ( n ) } \max\{dp(0),dp(1),\cdots,dp(n)\} max{ dp(0),dp(1),,dp(n)}

4. 小优化

  • 数据量大,快读。
  • 如果所有的收益为正的点全拿了,都无法超过 k k k ,直接输出 − 1 -1 1

核心代码

时间复杂度: O ( N log ⁡ N ) O(N\log N) O(NlogN)
空间复杂度: O ( N ) O(N) O(N)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 5e5+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;

ll dis[MAX],sc[MAX];
ll dp[MAX];
deque<ll>Q;
int n,d,k;

ll check(int x){
    
    
    int d1 = d + x;
    int d2 = max(1,d-x);
    ll ans = 0;

    for(int i = 1;i <= n;++i){
    
    
        dp[i] = sc[i];
    }
    while(Q.size())Q.pop_back();

    int ed = -1;		/// 因为一开始原点的下标为0的点还没有进入队列

    dis[0] = 0;
    sc[0]  = 0;
    dp[0]  = 0;

    for(int i = 1;i <= n;++i){
    
    
        while(ed + 1 < i && dis[i] - dis[ed+1] >= d2){
    
    		/// 进队列
            ed++;
            while(Q.size() && dp[Q.back()] <= dp[ed])Q.pop_back();
            Q.push_back(ed);
        }
        while(Q.size() && dis[i] - dis[Q.front()] > d1)Q.pop_front();	/// 出队列

        if(Q.size()){
    
    		/// 状态转移
            dp[i] = dp[Q.front()] + sc[i];
            ans = max(ans,dp[i]);
        }else dp[i] = -LINF;
    }
    return ans;
}

int main()
{
    
    
    n = read();
    d = read();
    k = read_ll();
    ll pos = 0;
    for(int i = 1;i <= n;++i){
    
    
        dis[i] = read_ll();
        sc[i] = read_ll();
        if(sc[i] > 0)pos += sc[i];
    }
    if(pos < k){
    
    
        puts("-1");
        return 0;
    }

    int L = 0,R = dis[n];
    while(L < R){
    
    
        int M = L + R >> 1;
        if(check(M) >= k)R = M;
        else L = M + 1;
    }
    printf("%d",L);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_45775438/article/details/112757376