当青训营遇上码上掘金之”寻友之旅“

2,055 阅读4分钟

Tips为了更好地整合文章,我已经将本文章收录至专栏“青训营题解”,专栏内有其他方向的内容,感兴趣的小伙伴可以关注一下专栏。

当青训营遇上码上掘金

题目:寻友之旅

小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
请帮助小青通知小码,小青最快到达时间是多久?
输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)

解题思路

解法一:广度优先搜索

这道题目可以使用广度优先搜索算法来解决,即从小青所在的地点开始,依次搜索小青可以到达的所有节点,直到找到小码所在的节点。

我们可以使用一个字典来存储每个节点的步数,然后使用双端队列来存储每个节点。对于每个节点,我们都将它的左右节点和公交节点加入队列,并更新字典中对应节点的步数。

当我们找到小码所在的节点时,就可以返回它的步数,表示小青到小码家的最短时间。如果在搜索过程中队列为空,说明没有找到小码所在的节点,此时应该返回 -1。

具体可以看看代码实现

代码实现(Python):

from collections import deque

def shortest_time(n, k):
    # 用于存储每个节点的步数
    steps = {}
    # 用于存储每个节点的父节点
    parents = {}
    # 将小青的位置加入队列
    queue = deque([n])
    # 将小青的位置设为 0 步
    steps[n] = 0
    # 将小青的父节点设为 None
    parents[n] = None

    # 循环直到队列为空
    while queue:
        # 取出队首元素
        curr = queue.popleft()
        # 如果当前节点就是小码所在的节点,直接返回步数
        if curr == k:
            return steps[k]
        # 将当前节点的左右节点加入队列
        if curr - 1 >= 0:
            if curr - 1 not in steps:
                queue.append(curr - 1)
                steps[curr - 1] = steps[curr] + 1
                parents[curr - 1] = curr
        if curr + 1 <= 100000:
            if curr + 1 not in steps:
                queue.append(curr + 1)
                steps[curr + 1] = steps[curr] + 1
                parents[curr + 1] = curr
        # 将当前节点的公交节点加入队列
        if curr * 2 <= 100000:
            if curr * 2 not in steps:
                queue.append(curr * 2)
                steps[curr * 2] = steps[curr] + 1
                parents[curr * 2] = curr
    # 如果没有找到小码所在的节点,返回 -1
    return -1

解法二:堆优化的迪杰斯特拉算法

为了优化时间复杂度,这道题目我们可以使用堆优化的迪杰斯特拉算法来解决。

堆优化的迪杰斯特拉算法

堆优化的迪杰斯特拉算法是一种用于解决单源最短路径问题的最短路径算法。它的基本思想是从起点开始,逐步扩展可以到达的节点,并通过使用堆数据结构来维护已扩展节点的信息,从而减少枚举节点的次数。

假设有一个带权有向图,需要求出单源最短路径。那么堆优化的迪杰斯特拉算法的流程如下:

  1. 将起点加入到堆中,并设置起点的距离为 0。
  2. 取出堆顶元素,如果已经求出了终点的最短路径,则停止算法。
  3. 对于当前节点的每一个相邻节点,如果通过当前节点到达该相邻节点的路径比之前已经求出的路径短,则更新该相邻节点的距离,并将该相邻节点加入到堆中。
  4. 重复步骤 2 和 3 直到堆为空或者已经求出了终点的最短路径。

对于本题而言,思路如下:

  • 首先,我们需要定义一个字典来存储每个节点到起点的距离。
  • 然后,我们可以使用一个堆来维护所有未被拓展的节点的距离,并在每一次拓展节点时,将该节点的所有相邻节点加入堆中。
  • 最后,我们可以在遍历完整个图后,返回终点的距离即可。

具体可以看看代码实现:

代码实现(Python):

import heapq

def dijkstra(n, k):
    # 存储每个节点的距离
    distances = [float('inf') for _ in range(100001)]
    # 将起点设为 0 步
    distances[n] = 0
    # 存储已经确定的节点
    processed = set()
    # 初始化堆
    heap = [(0, n)]

    while heap:
        # 取出堆中最小的节点
        distance, curr = heapq.heappop(heap)
        # 如果节点已经被确定,跳过
        if curr in processed:
            continue
            # 将节点标记为已经确定
        processed.add(curr)
        # 遍历当前节点的邻接点
        for neighbor in (curr - 1, curr + 1, curr * 2):
            # 如果邻接点不在范围内或者已经被确定,跳过
            if neighbor < 0 or neighbor > 100000 or neighbor in processed:
                continue
            # 如果可以更新邻接点的距离,则将新的距离加入堆中
            if distance + 1 < distances[neighbor]:
                distances[neighbor] = distance + 1
                heapq.heappush(heap, (distances[neighbor], neighbor))
        # 返回小码所在的节点的距离
    return distances[k]

结果展示

测试用代码:

# 读入输入
n, k = map(int, input("输入小青和小码的位置,用空格间隔:").split())
# 计算最短路径
result1 = shortest_time(n,k)
result2 = dijkstra(n, k)
# 输出结果
print("BDF解法,最短时间:",result1,"分钟")
print("堆优化的迪杰斯特拉,最短时间:",result2,"分钟")

测试用例

2 4096

输出结果 image.png

解法比较

先说结论:我认为,在这道题目中,堆优化的迪杰斯特拉算法更优。

原因是广度优先搜索是一种暴力枚举的方法,时间复杂度为 O(N),空间复杂度也为 O(N)。在本题中,N可以达到 100000,因此广度优先搜索的时间和空间复杂度都较大,不能很好地解决这道题目。

相反,堆优化的迪杰斯特拉算法是一种使用堆数据结构的最短路径算法,设节点为V、边数为E,算法的时间复杂度为 O((V+E)logV),空间复杂度为 O(V)。在本题中,由于图中边数 E 比节点数 V 少得多,因此堆优化的迪杰斯特拉算法的时间复杂度为 O(VlogV),相对于广度优先搜索的 O(N) 来说,时间复杂度要小得多,能更快地解决这道题目。

此外,堆优化的迪杰斯特拉算法的空间复杂度也比广度优先搜索的空间复杂度小得多,更适合解决这道题目。

因此,在这道题目中,堆优化的迪杰斯特拉算法更优。

文章完整代码&结尾

见代码片段: 主题 3:寻友之旅 - 码上掘金 (juejin.cn)
如果你有其他的思路或者更好的解法,亦或者你发现了文章出现了错误或有不足,欢迎在评论区和我交流,我看到了一定会回复。

写文章不易,如果你觉得文章对你有帮助,麻烦点一下点赞、收藏,你的支持是我写文章的最大动力!