删除路径后的最短路问题题解 | 豆包MarsCode AI 刷题

73 阅读10分钟

一. 问题描述

小F正在探索一个有 n 个点的地图,每个点都有一个二维坐标 (xi,yi)。起点是第 s 个点,终点是第 t 个点。原本所有点之间都有一条线段,表示可以通行,并且长度为欧几里得距离。但是由于某些意外,起点 s 和终点 t 之间的直接通行路径被删除了。小F希望你帮忙计算从 s 到 t 的最短路径,允许经过其他点,但不能直接通过删除的那条线。

点 i 和点 j 之间的欧几里得距离计算公式为:

d(i,j)=sqrt((xi​−xj​)^2+(yi​−yj​)^2)

你需要输出从起点 s 到终点 t 的最短距离,结果需要四舍五入到小数点后两位。

输入格式

  • n: 整数,表示点的数量。
  • s: 整数,表示起点的索引(1-based)。
  • t: 整数,表示终点的索引(1-based)。
  • x: 列表,包含 n 个整数,表示每个点的 x 坐标。
  • y: 列表,包含 n 个整数,表示每个点的 y 坐标。

输出格式

  • 字符串,表示从起点 s 到终点 t 的最短距离,结果四舍五入到小数点后两位。

测试样例

样例 1

输入:

n = 5, s = 1, t = 5, x = [17253, 25501, 28676, 30711, 18651], y = [15901, 15698, 32041, 11015, 9733]

输出:

'17333.65'
样例 2

输入:

n = 4, s = 2, t = 4, x = [5000, 12000, 8000, 14000], y = [3000, 9000, 1000, 4000]

输出:

'15652.48'
样例 3

输入:

n = 6, s = 3, t = 6, x = [20000, 22000, 24000, 26000, 28000, 30000], y = [15000, 13000, 11000, 17000, 19000, 21000]

输出:

'11772.70'

二. 解题思路

这个问题可以被建模为一个加权无向图,其中每个点代表地图上的一个位置,边的权重是两个点之间的欧几里得距离。由于所有点之间最初都有一条边,表示可以直接通行,但起点 s 和终点 t 之间的边被删除了。因此,我们需要找到从 st 的最短路径,可能需要经过其他点。

为了找到最短路径,可以使用 Dijkstra算法,这是一个经典的用于计算单源最短路径的算法,特别适用于边权为非负数的图。

2.1 步骤:

  1. 构建图的邻接表

    • 对于每个点,计算它与其他所有点之间的欧几里得距离,除去 (s, t) 这条被删除的边。
  2. 实现Dijkstra算法

    • 使用优先队列(最小堆)来高效地选择当前距离最短的未处理点。
    • 初始化距离数组,将起点 s 的距离设为 0,其他点设为正无穷大。
    • 逐步更新每个点的最短距离,直到终点 t 被处理。
  3. 处理浮点数精度

    • 计算的距离需要保留两位小数,并进行四舍五入。

2.2 注意事项:

  • 索引问题:确保节点编号从 1n 与列表索引正确对应。在实现时,可以将节点编号 1n 转换为 0n-1 的索引,以便于列表操作。
  • 性能考虑:由于是一个完全图(除了一条边),对于较大的 n,邻接表会非常大。但根据样例数据,假设 n 不会过大,可以接受这种实现方式。

2.3 一个特殊的测试用例引发的思考:

image.png

问题分析

当起点 s 和终点 t 相同时,期望的最短路径长度为 2.00,而现有的代码返回了 0.00。这是因为当前的实现将 s == t 的情况视为起点和终点重合,直接返回了距离 0.00

根据问题描述,当 st 是同一个点时,直接的路径被删除了。因此,我们需要找到一条从 st 的路径,必须经过至少一个其他点。这意味着即使 st 是同一个点,我们也需要计算一个环路(即从 s 出发,经过至少一个其他点,再回到 s)。

这表明我们需要找到一个经过至少一个其他点的最短环路。例如,从 s=2 出发,到达某个最近的点 a,然后再返回到 s=2,总距离为 2 * distance(s, a)。在此测试用例中,点 5(索引 4,0-based 为 4)与点 2 的距离为 1.00,因此环路的总距离为 2.00

2.4 解决方案

为了正确处理 s == t 的情况,我们可以采用以下步骤:

  1. 检查是否 s == t

    • 如果是,则寻找距离 s 最近的其他点 a,计算 2 * distance(s, a) 作为最短路径。
  2. 如果 s != t

    • 使用原有的 Dijkstra 算法,去除 st 之间的直接边,计算从 st 的最短路径。

2.5 代码实现

import heapq
import math

def solution(n: int, s: int, t: int, x: list, y: list) -> str:
    # 将节点编号从1-based转换为0-based
    s -= 1
    t -= 1

    # 构建邻接表
    adjacency = [[] for _ in range(n)]
    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            if (i == s and j == t) or (i == t and j == s):
                continue  # 删除边(s,t)
            # 计算欧几里得距离
            distance = math.sqrt((x[i] - x[j]) ** 2 + (y[i] - y[j]) ** 2)
            adjacency[i].append((j, distance))
    
    # 如果s == t,需要找到最小的环路
    if s == t:
        min_distance = math.inf
        for a in range(n):
            if a == s:
                continue
            # 计算s -> a 和 a -> s的距离(相同)
            distance_sa = math.sqrt((x[s] - x[a]) ** 2 + (y[s] - y[a]) ** 2)
            total_distance = 2 * distance_sa
            if total_distance < min_distance:
                min_distance = total_distance
        if min_distance == math.inf:
            return "-1"  # 如果没有其他点
        return "{0:.2f}".format(min_distance)
    
    # Dijkstra算法初始化
    distances = [math.inf] * n
    distances[s] = 0.0
    visited = [False] * n
    heap = [(0.0, s)]
    
    while heap:
        current_distance, u = heapq.heappop(heap)
        
        if visited[u]:
            continue
        visited[u] = True
        
        if u == t:
            break  # 到达终点
        
        for neighbor, weight in adjacency[u]:
            if not visited[neighbor]:
                new_distance = current_distance + weight
                if new_distance < distances[neighbor]:
                    distances[neighbor] = new_distance
                    heapq.heappush(heap, (new_distance, neighbor))
    
    # 如果终点不可达,返回一个特殊值(根据题意可能不需要处理)
    if distances[t] == math.inf:
        return "-1"
    
    # 四舍五入到小数点后两位,并格式化为字符串
    result = "{0:.2f}".format(distances[t])
    return result

# 测试样例
if __name__ == "__main__":
    # 样例1
    print(solution(
        n = 5,
        s = 1,
        t = 5,
        x = [17253, 25501, 28676, 30711, 18651],
        y = [15901, 15698, 32041, 11015, 9733]
    ))  # 输出: '17333.65'
    
    # 样例2
    print(solution(
        n = 4,
        s = 2,
        t = 4,
        x = [5000, 12000, 8000, 14000],
        y = [3000, 9000, 1000, 4000]
    ))  # 输出: '15652.48'
    
    # 样例3
    print(solution(
        n = 6,
        s = 3,
        t = 6,
        x = [20000, 22000, 24000, 26000, 28000, 30000],
        y = [15000, 13000, 11000, 17000, 19000, 21000]
    ))  # 输出: '11772.70'
    
    # 测试用例4(s == t)
    print(solution(
        n = 10,
        s = 2,
        t = 2,
        x = [11,3,5,6,2,4,15,14,16,8],
        y = [5,8,7,14,8,10,5,4,2,9]
    ))  # 输出: '2.00'

2.6 代码说明

  • 转换索引

    • 将节点编号从 1-based 转换为 0-based,以便于列表索引操作。
  • 构建邻接表

    • 对于每对不同的节点 ij,计算它们之间的欧几里得距离,并将其添加到邻接表中。
    • 特别地,删除了 (s, t)(t, s) 这条边,避免直接通过已删除的路径。
  • 处理 s == t 的情况

    • 如果起点和终点是同一个点,我们需要找到一个经过至少一个其他点的最短环路。
    • 通过遍历所有其他点 a,计算 s -> a -> s 的总距离,并记录最小值。
    • 如果没有其他点(即 n == 1),返回 -1 表示不可达。
  • Dijkstra 算法

    • 如果 s != t,则使用 Dijkstra 算法计算从 st 的最短路径。
    • 初始化距离数组,设置起点 s 的距离为 0.0,其他点为 inf
    • 使用最小堆(优先队列)来选择当前距离最短的未处理点。
    • 逐步更新每个点的最短距离,直到终点 t 被处理。
  • 结果处理

    • 如果终点不可达(即距离仍为 inf),返回 -1
    • 否则,将距离四舍五入到小数点后两位,并格式化为字符串。

2.7 进一步优化

对于更大的 n,当前的邻接表构建方式可能会导致较高的时间和空间复杂度,因为这是一个几乎完全连接的图(除了一条边)。在实际应用中,可以考虑以下优化:

  1. 空间优化

    • 使用稀疏图的表示方法,或者在动态生成边时避免存储所有边。
  2. 算法优化

    • 使用 A* 算法,结合启发式函数(如欧几里得距离的直线距离),以减少需要访问的节点数量。
  3. 并行计算

    • 对于边的计算和图的构建,可以采用并行计算方法以提高效率。

然而,根据当前的问题描述和样例数据,标准的 Dijkstra 算法已经足够有效。

三、知识点总结

heapq 是 Python 标准库中的一个模块,提供了基于堆的优先队列实现。堆是一种特殊的完全二叉树,其中每个父节点的值都小于或等于其子节点的值(最小堆),或者每个父节点的值都大于或等于其子节点的值(最大堆)。heapq 模块实现了最小堆。

3.1 基本操作

1 创建堆

heapq 模块没有直接创建堆的函数,通常使用一个列表来表示堆。初始时,列表可以是空的,也可以包含一些元素。

import heapq

# 创建一个空堆
heap = []
2 插入元素

使用 heapq.heappush(heap, item) 将元素 item 插入堆中,并保持堆的性质。

heapq.heappush(heap, 5)
heapq.heappush(heap, 3)
heapq.heappush(heap, 7)
3 弹出最小元素

使用 heapq.heappop(heap) 弹出并返回堆中的最小元素。

min_element = heapq.heappop(heap)  # 返回 3
4 查看最小元素

使用 heap[0] 查看堆中的最小元素,而不弹出它。

min_element = heap[0]  # 返回 5

3.2 堆化(Heapify)

使用 heapq.heapify(list) 将一个列表转换为堆。

list = [5, 3, 7, 1, 4]
heapq.heapify(list)

3.3 合并多个有序序列

使用 heapq.merge(*iterables) 合并多个有序序列,返回一个迭代器。

list1 = [1, 3, 5]
list2 = [2, 4, 6]
merged = heapq.merge(list1, list2)
print(list(merged))  # 输出: [1, 2, 3, 4, 5, 6]

3.4 最大堆

heapq 模块默认实现最小堆,但可以通过插入元素的负值来实现最大堆。

max_heap = []
heapq.heappush(max_heap, -5)
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -7)

max_element = -heapq.heappop(max_heap)  # 返回 7

3.5 优先队列

heapq 模块常用于实现优先队列。优先队列中的每个元素都有一个优先级,优先级最高的元素最先被弹出。

import heapq

# 优先队列中的元素是一个元组 (priority, item)
priority_queue = []
heapq.heappush(priority_queue, (3, 'task1'))
heapq.heappush(priority_queue, (1, 'task2'))
heapq.heappush(priority_queue, (2, 'task3'))

# 弹出优先级最高的元素
priority, task = heapq.heappop(priority_queue)  # 返回 (1, 'task2')

3.6 复杂度

  • 插入元素 (heappush) 的时间复杂度为 O(log⁡n)。
  • 弹出最小元素 (heappop) 的时间复杂度为 O(log⁡n)。
  • 堆化 (heapify) 的时间复杂度为 O(n)。