Skip to content

T01661: Help Jimmy

dfs, dp, http://cs101.openjudge.cn/practice/01661

"Help Jimmy" 是在下图所示的场景上完成的游戏: img 场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。

Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。

设计一个程序,计算Jimmy到底地面时可能的最早时间。

输入

第一行是测试数据的组数t(0 ≤  t ≤  20)。每组测试数据的第一行是四个整数N,X,Y,MAX,用空格分隔。N是平台的数目(不包括地面),X 和 Y 是Jimmy开始下落的位置的横竖坐标,MAX 是一次下落的最大高度。接下来的N行每行描述一个平台,包括三个整数,X1[i],X2[i] 和 H[i]。H[i]表示平台的高度,X1[i] 和X2[i] 表示平台左右端点的横坐标。1 ≤  N ≤  1000,-20000 ≤  X, X1[i], X2[i] ≤  20000,0 < H[i] < Y ≤  20000(i = 1..N)。所有坐标的单位都是米。

Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保证问题一定有解。

输出

对输入的每组测试数据,输出一个整数,Jimmy到底地面时可能的最早时间。

样例输入

1
3 8 17 20
0 10 8
0 10 13
4 14 3

样例输出

23

来源:POJ Monthly--2004.05.15, CEOI 2000, POJ 1661, 程序设计实习2007

python
import sys
from functools import lru_cache

# 优化1:增加递归深度限制,防止 N=1000 时爆栈
sys.setrecursionlimit(2000)

def solve():
    # 优化2:使用 sys.stdin.read 快速读取所有输入
    input_data = sys.stdin.read().split()
    if not input_data:
        return

    iterator = iter(input_data)
    
    try:
        num_test_cases = int(next(iterator))
    except StopIteration:
        return

    for _ in range(num_test_cases):
        try:
            N = int(next(iterator))
            ini_x = int(next(iterator))
            ini_y = int(next(iterator))
            MaxVal = int(next(iterator))
            
            p = []
            for _ in range(N):
                p.append((int(next(iterator)), int(next(iterator)), int(next(iterator))))
            
            # 按高度从大到小排序
            p.sort(key=lambda x: -x[2])
            
            @lru_cache(None)
            def dfs(x, y, parent_idx):
                # parent_idx: 刚离开的平台索引(如果是起点则为 -1)
                # 需要在 p[parent_idx+1 ... N] 中寻找接住我们的平台
                
                for i in range(parent_idx + 1, N):
                    px1, px2, ph = p[i]
                    
                    # 剪枝:因为 p 是按高度从大到小排的
                    # 如果当前平台的高度差已经超过 MaxVal,那后面更低的平台肯定也接不住,直接死掉
                    if y - ph > MaxVal:
                        return float('inf')
                    
                    # 判断横坐标是否在平台范围内
                    if px1 <= x <= px2:
                        # 找到了接住的平台 i
                        # 递归计算:(当前位置到平台左/右端的水平距离) + dfs(下一层)
                        # 注意:题目求时间,垂直时间恒为 total_Y,这里 dfs 只负责计算最小水平距离
                        
                        dist_left = x - px1 + dfs(px1, ph, i)
                        dist_right = px2 - x + dfs(px2, ph, i)
                        return min(dist_left, dist_right)
                
                # 如果循环结束都没 break,说明落到了地面 (y=0)
                if y <= MaxVal:
                    return 0
                else:
                    return float('inf')

            # 初始调用:从 (ini_x, ini_y) 开始,父节点索引传 -1,
            # 这样循环会从 0 (第一个平台) 开始搜索
            min_horizontal_dist = dfs(ini_x, ini_y, -1)
            
            if min_horizontal_dist == float('inf'):
                # 理论上题目保证有解,不会进这里
                pass 
            else:
                # 总时间 = 垂直下落距离(ini_y) + 最小水平移动距离
                print(ini_y + min_horizontal_dist)

        except StopIteration:
            break

if __name__ == '__main__':
    solve()
python
# 查达闻 2300011813
from functools import lru_cache

@lru_cache
def dfs(x, y, z):
    for i in range(z+1, N+1):
        if y - MaxVal > p[i][2]:
            return 1 << 30
        elif p[i][0] <= x <= p[i][1]:
            left = x - p[i][0] + dfs(p[i][0], p[i][2], i)
            right = p[i][1] - x + dfs(p[i][1], p[i][2], i)
            return min(left,right)
        
    if y <= MaxVal:
        return 0
    else:
        return 1 << 30


for _ in range(int(input())):
    N, ini_x, ini_y, MaxVal = map(int, input().split())
    
    p = []      #platform
    p.append( [0, 0, 1 << 30] ) # 1<<30 远大于题目最大高度 20000
    for _ in range(N):
        p.append([int(x) for x in input().split()])
    p.sort(key = lambda x:-x[2])

    print(ini_y + dfs(ini_x, ini_y, 0))

此题目的“子问题”是什么呢?

Jimmy 跳到一块板上后,可以有两种选择,向左走或向右走。走到左端和走到右端所需的时间,容易算出。

如果我们能知道,以左端为起点到达地面的最短时间,和以右端为起点到达地面的最短时间,那么向左走还是向右走,就很容选择了。

因此,整个问题就被分解成两个子问题,即Jimmy 所在位置下方第一块板左端为起点到地面的最短时间,和右端为起点到地面的最短时间。这两个子问题在形式上和原问题是完全一致的。

当Jimmy落在一个平台上后有两种选择(向左走或向右走),而Jimmy走到平台左边和右边的时间很容易计算,如果我们得到了以平台左边为起点及以平台右边为起点到地面的最短时间,那么选择往左走还是往右走就很容易了。这样,原问题就分解为两个子问题,这两个子问题和原问题的形式是一致的,也就找到了“状态”$dp[i][j], j = 0, 1 dp[i][0]idp[i][1]$表示以i号平台右边为起点,到地面的最短时间),而“状态转移方程”如下:

dp[i][0]=H[i]H[m]+Min(dp[m][0]+X1[i]X1[m],dp[m][1]+X2[m]X1[i]); m为i左边下面的平台的编号

dp[i][1]=H[i]H[m]+Min(dp[m][0]+X2[i]X1[m],dp[m][1]+X2[m]X2[i]); m为i右边下面的平台的编号

python
# Top-Down(记忆化搜索). ref: https://www.cnblogs.com/zswbky/p/6792910.html
def dfs(i:int, a:int, x:int):
    if dp[i][a]!=-1:
        return dp[i][a]
    
    left = right = 10**8    # greater than 20000*2*1000
    
    flag = True
    for j in range(i+1, N+1):
        if p[i][2] - p[j][2] > MAX:
            flag = False
            break
        
        if p[j][0] <= x <= p[j][1]:
            left = dfs(j,0,p[j][0]) + p[i][2]-p[j][2] + x-p[j][0]
            right = dfs(j,1,p[j][1]) + p[i][2]-p[j][2] + p[j][1]-x
            flag = False
            break # 这里为什么break?如果下面一直没有平台,就一直下落,直到超出MAX,
            			# 不超出的话,循环结束时间就是起初高度
    
    dp[i][a] = min(left, right)
    if flag and p[i][2]<=MAX:
        dp[i][a] = p[i][2]
    
    return dp[i][a]

for _ in range(int(input())):
    # dp[i][j]表示第i个平台下降到地面的最小时间
    # dp[i][0]表示以i号平台左边为起点,到地面的最短时间
    # dp[i][1]表示以i号平台右边为起点,到地面的最短时间
    dp = [[-1,-1] for _ in range(1000+10)]
    
    p = []      #platform

    N,X,Y,MAX = map(int, input().split())
    p.append( [X,X,Y] )  # xi1, xi2, height
    for _ in range(N):
        p.append([int(x) for x in input().split()])
    #p.append([-2000,2000,0])
    
    p.sort(key = lambda x:-x[2])
    
    print(dfs(0,1,X))

简单说,是OJ01088:滑雪 的升级版,细节更复杂,都是dfs + dp。

python
# 2021fall-cs101,秦博雅
for _ in range(int(input())):
    N,X,Y,MAX=map(int,input().split())
    plats=[[X,X,Y]]
    for __ in range(N):
        plats.append(list(map(int,input().split())))
    plats.sort(key=lambda x:x[2])
    dp=[[0]*2 for _ in range(N+1)]
    for i in range(N+1):
        countl=0
        countr=0
        for j in range(i+1):
            if plats[j][2]<plats[i][2] and plats[j][0]<=plats[i][0]<=plats[j][1] and plats[i][2]-plats[j][2]<=MAX:
                dp[i][0]=plats[i][2]-plats[j][2]+min(dp[j][0]+plats[i][0]-plats[j][0],dp[j][1]+plats[j][1]-plats[i][0])
                countl+=1
            if plats[j][2]<plats[i][2] and plats[j][0]<=plats[i][1]<=plats[j][1] and plats[i][2]-plats[j][2]<=MAX:
                dp[i][1]=plats[i][2]-plats[j][2]+min(dp[j][0]+plats[i][1]-plats[j][0],dp[j][1]+plats[j][1]-plats[i][1])
                countr+=1
        if countl==0:
            if plats[i][2]>MAX: 
                dp[i][0]=float('inf')
            else:
                dp[i][0]=plats[i][2]
        if countr==0:
            if plats[i][2]>MAX: 
                dp[i][1]=float('inf')
            else:
                dp[i][1]=plats[i][2]
    print(dp[-1][-1])

【梁天书,2021年秋】Help jimmy 这个问题很有意思,实际上是将很多个遇见挡板的问题拆分成遇见单个挡板,然后向左走或者向右走两种情况,然后进行下一步搜索。核心部分在于left 和right 的定义,写出位置转移的数学模型,判断是否可以直接降落就采用flag 的bool 类型。自己一开始犯的错误就是不知道为什么将left 和right 设置成10**8,后来明白有可能right 或者left 在计算的过程中,有可能不能向另一方向通过,因此如果left 或者right 设置太小了,就会将left 或者right的计算值给覆盖。这个题目感觉真心漂亮,和数学不大一样,数学是一条线路向前推进,一环紧密扣一环,这个题目既有是否碰上挡板的判断(flag True or False),又有转移的数学模型,同时还采用了搜索过程,应该叫多线程工作,想到对我来说是在是一件挺困难的事。