Codeforces-1601B: Frog Traveler

Codeforces-1601B: Frog Traveler

题目

题目传送门:codeforces1601B

题目截图

Codeforces-1601B: Frog Traveler

样例描述

Codeforces-1601B: Frog Traveler

样例解释:第一个例子是从井底深3米处起跳,因此跳 a 3 = 2 a_3=2 a3​=2米,到达深1米的地方,掉落 b 1 = 1 b_1=1 b1​=1米,于是起跳点变成了2米深的地方,之后再跳 a 2 = 2 a_2=2 a2​=2米,就可以跳到地面上。

题目大意

    Gorf先生正在旅行,不幸的是在一次跳跃中它掉到了深 n n n 米的井里。现在它在井的底部,有很高的距离要跳。但井的表面的平滑程度是不均匀的,有的地方很光滑,有的地方又很容易起跳。即,在地面下 x x x 处,它一次只能跳 [ 0 , a x ] [0, a_x] [0,ax​] 米,且它不想往下跳。
    不幸的是,Gorf先生年老体弱,在每次跳以后得歇一下,也就是跳到 x x x后,它会下滑 b x b_x bx​的距离,求Gorf先生跳到地面的最少跳跃次数,并输出每次跳跃到达的深度(在没有下滑之前)。

题目解析

    深度从 n n n 到 0 0 0 开始考虑,设 d p [ i ] dp[i] dp[i] 代表从 0 0 0 到 i i i 的最小走的步数(下滑后)。
    那么其实对于每个起始位置 x x x ,它能跳到的位置是 { s } = { x + b [ x ] . . . x − a [ x ] + b [ x − a [ x ] ] } \{s\}=\{x+b[x]...x-a[x]+b[x-a[x]]\} {s}={x+b[x]...x−a[x]+b[x−a[x]]},那么转移方程就变成了 d p [ i ] = min ⁡ ( d p [ x ] + 1 , d p [ i ] ) , i ∈ { s } dp[i]=\min (dp[x]+1, dp[i]), i \in \{s\} dp[i]=min(dp[x]+1,dp[i]),i∈{s}。其实这个最小值是对一段连续的序列更新的,所以可以用线段树的lazy标记顺序向浅层更新一段序列的最小值,这个复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)的。
    但其实这道题还有 O ( n ) O(n) O(n)的做法。考虑更新到 x x x 的时候,此时前面 x + 1 , . . . , n x+1,...,n x+1,...,n的 d p dp dp 值已经被更新了,而如果我们更新完 x x x(作为起跳点) ,从 x x x 到 x x x 能到达的深度最浅的地方 y = x − a [ x ] y=x-a[x] y=x−a[x] i.e. [x, …, x-a[x]] ,其实 d p dp dp 值已经确定了。更一般地,我们不需要更新历史能到达深度以内的位置 d p dp dp 值,因为如果假设 y y y是 x x x及其之前能到达的位置,那么 d p [ y ] dp[y] dp[y] 在 [ x + 1 , . . . y − 1 ] [x+1,... y-1] [x+1,...y−1] 这一段更新时,最好情况也不会比 x x x或 x x x之前到达 y y y所用的最短步数少。关于这一点,可以用一个大致图来表示(没有画一步一步,而是几步几步,以便说明):
Codeforces-1601B: Frog Traveler

这张图表明,首先更新步长为 1 1 1 的结点时,肯定是先更新从开头连续的一小段。之后,每个结点下滑一段距离,如果下滑后的结点不能跳到橙色区域,那么往后肯定不会是最优步数,因为本可以一步跳到对应位置。如果能跳到橙色区域(需要两步跳到),由于每次可以选择一段连续的区间作为落地点,所以扩展出的橙色区域是二次起跳后落地点的最大并集,也是连续的。依此类推,其实最终我们得到的最少次数分布(只考虑下滑前的位置),是以一些连续的段呈现的(不考虑无法到达的位置),且这些段从开始到结束次数不断顺序 + 1 +1 +1。当然,这并不意味着 d p dp dp 值是连续的,因为也会有如 a i a_i ai​ 和 b i b_i bi​ 都很大的情况,也就是跳到 i i i 会直接跌落井底,从 i i i 跳能直接跳到地面的情况。但这种未下滑前的落地点的连续性,可以帮我们在计算时,把大量不需要更新的点去除掉,只更新有可能的点。
    其实也可以用图论来考虑这道题,除了能直接到达地面的位置(这些位置直接连终止点),其余,给每个落脚点(下滑后)建一个长度为 1 1 1 的边,那么直接建一个图(过程需要上面结论和一些其它减少边数的方式),求解最短路径即可。而上一种的做法其实本质思想也类似于Dijkstra求最短路,因为先更新下滑前落地所需步数最短的结点,只不过这个更新结点,在入队的时候需要的步数随入队顺序会不断递增,因此不需要堆或优先队列。

Code

#include <bits/stdc++.h>
using namespace std;

const int inf = 0x7fffffff;
const int maxn = 3e5 + 7;
int a[maxn], b[maxn], dp[maxn], q[maxn], stk[maxn], stand[maxn], jump[maxn];


int main(){
    int n, x, v, up;
    int l = 0, r = 0, top=0, upside=inf;
    memset(dp, 0x3f, sizeof(dp));
    cin >> n;
    for(int i=1; i<=n; ++i) cin >> a[i];
    for(int i=1; i<=n; ++i) cin >> b[i];
    dp[n] = 0; q[r++] = n; stand[n] = n; jump[n] = n;

    while (r > l) {
        x = q[l++];
        for(int i=a[x]; i>=0; --i) {
            up = x - i; v = up + b[up];
            if (up <= 0) {dp[0]=min(dp[0], dp[x]+1);stand[0] = x;r=l-1;break;}
            if (up >= upside) break;
            if (v <= n && dp[v] > dp[x] + 1) {
                dp[v] = dp[x] + 1; q[r++] = v;
                stand[v] = x; jump[v] = i;
            }
        }
        upside = min(upside, x - a[x]);
    }

    if (dp[0] > n) cout << -1 << endl; else {
        cout << dp[0] << endl;
        x = 0;
        while(x != n) {
            v = stand[x] - jump[x];
            stk[top++] = v;
            x = stand[x];
        }
        for(int i=top-1; i>=1; --i) cout << stk[i] << " ";
        cout << 0 << endl;
    }
    return 0;
}

上一篇:51单片机头文件reg51.h详解


下一篇:Codeforces Round #752 (Div. 2)