「青训营 X 码上掘金」之寻友之旅

71 阅读3分钟

当青训营遇上码上掘金之寻友之旅,本文记录解决此问题的一些思路与代码。

问题重述

原问题如下:

小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。

  • 步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1

  • 公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)

由于原问题中提到“小码在自己家原地不动等待小青”,故我们可以将这个问题抽象成从起点N出发,到达终点K的最短路径问题(Shortest Path Problem),只是与传统的最短路径问题不同,此问题中移动的方式被限定以下3种:

  • 步行L(向左单步移动):向左移动一步
  • 步行R(向右单步移动):向右移动一步
  • 公交(跳跃移动):直接移动到当前坐标乘2的坐标位置,且当前坐标小于0时无效

需要注意的是,“公交”的定义说明这种移动方式不可能向左移动,故向左移动的唯一方法是“步行L”。

问题解决

首先,由于题目没有说明N和K的大小关系,那么N有可能是大于或者等于K的,此时N移动到K的唯一可能方式是一步一步向左移动到K,故此时二者的距离就是需要时间。

接下来讨论N小于K的情况:我们可以使用搜索的方法来找到最佳路径,这里以广度优先搜索为例:在每一次移动后我们就将可能的三种移动的结果(以及当前耗时)放入一个队列中,每处理完一个状态就又从队列中取出一个状态来搜索。

需要注意到的是:向左/向右移动方式需要的前置条件不太一样:

  • 向左移动:仅当当前位置超过0时才有意义,否则会向负方向无限搜索下去
  • 向右移动:仅当当前位置没到达N时才有意义,否则任何一种向右移动方式都会导致超过并远离目的地
from collections import deque, namedtuple

State = namedtuple("State", ["pos", "elapsed_time"])

def min_steps(n: int, k: int) -> int:
    if n >= k:
        return n - k  # 只能一步一步向左移动

    # 初始可用的状态
    next_states = deque([State(pos=n, elapsed_time=0)])

    # 记录目前搜索到的最短耗时
    min_time = float('inf')

    while next_states:  # 只要还有可用的状态没搜索完就继续搜索
        cur_pos, cur_time = next_states.popleft()

        if cur_pos == k:
            min_time = min(min_time, cur_time)
            continue
        
        next_pos_set = []
        if cur_pos > 0: 
            next_pos_set.append(cur_pos-1)
        if cur_pos < k:
            next_pos_set.append(cur_pos+1)
            next_pos_set.append(cur_pos*2)

        for next_pos in next_pos_set:
            next_states.append(State(pos=next_pos, elapsed_time=cur_time+1))
        
    return min_time


if __name__ == '__main__':
    print(min_steps(5, 17))

经过实测,这样暴力搜索的耗时太大了。这种方案实际上经历了许多不必要的重复搜索(同一个pos被放到队列里好几次),因此我们可以通过记录已搜索过的位置来避开这种重复的检查。

from collections import deque, namedtuple

State = namedtuple("State", ["pos", "elapsed_time"])

def min_steps(n: int, k: int) -> int:
    if n >= k:
        return n - k  # 只能一步一步向左移动

    # 初始可用的状态
    next_states = deque([State(pos=n, elapsed_time=0)])

    # 记录已搜素过的位置
    visited = set([n])

    # 记录目前搜索到的最短耗时
    min_time = float('inf')

    while next_states:  # 只要还有可用的状态没搜索完就继续搜索
        cur_pos, cur_time = next_states.popleft()

        if cur_pos == k:
            min_time = min(min_time, cur_time)
            continue
        
        next_pos_set = []
        if cur_pos > 0: 
            next_pos_set.append(cur_pos-1)
        if cur_pos < k:
            next_pos_set.append(cur_pos+1)
            next_pos_set.append(cur_pos*2)

        for next_pos in next_pos_set:
            if next_pos not in visited:
                next_states.append(State(pos=next_pos, elapsed_time=cur_time+1))
                visited.add(next_pos)
        
    return min_time


if __name__ == '__main__':
    print(min_steps(5, 17))
    print(min_steps(0, 100000))
    print(min_steps(51, 17))

经过基本的测试,这一算法可以在较短的时间内找到解。

掘金完整代码在线体验