交通工程Python最短路算法求换乘次数

·
m1ngFU
m1ngFU
落花慵扫,留衬苍苔

Dijkstra算法
#

简介
#

迪杰斯特拉Dijkstra算法是典型的最短路径算法,由荷兰计算机科学家迪杰斯特拉于1959 年提出,用于求解带权有向图或无向图中的最短路径。

该算法的核心思想是贪心算法,即对问题求解时,总是做出在当前看来是最好的选择。具体些说,每次选择距离起始点最近且未访问过的相邻节点,逐步扩展到终点为止。

步骤
#

1.初始化

  • 定义距离数组dist,记录从起点到每个节点的最短距离
    • 起点的距离为 0 ,表示起点到自身的距离为0,即 dist[start] = 0
    • 其他所有节点的距离设为 ∞ ,表示其他节点暂不可达,即 dist[v] = ∞
  • 定义优先队列,存储待处理的节点,按距离升序
    • 将初始将起点放入队列:(0, start)
  • 定义访问标记集合,记录已确定最短距离的节点 2.主循环
  • 当优先队列不为空: 1.从有先队列中取出当前距离起点最近的节点u 2.该节点表位已访问 3.遍历该节点所有为访问的相邻节点v:
    • 计算u到v的距离d
    • 若d < dist[v]
      • 更新dist[v]=d,将(d, v)加入优先队列
    • 反之跳过 3.终止
  • 优先队列为空
  • 目标节点已被标记

举例
#

最短路径图
在图中可以看到边的权重如下:

  • A -> B: 5
  • A -> C: 8
  • B -> C: 2
  • B -> D: 11
  • B -> E: 100
  • C -> E: 7
  • D -> E: 4
  • D -> F: 9
  • E -> F: 8

Dijkstra算法的计算步骤
#

计算从A到F的最短路径

初始化
#
  • 距离表:
    • A: 0
    • 其他节点: ∞
  • 优先队列:[(0, A)]
  • 已访问:{}
步骤1:处理A
#
  • 当前节点:A(距离0)
  • 更新邻居:
    • B: min(∞, 0+5) = 5
    • C: min(∞, 0+8) = 8
  • 新距离表:
    • A:0, B:5, C:8
  • 优先队列:[(5,B), (8,C)]
  • 已访问:{A}
步骤2:处理B(距离5)
#
  • 当前节点:B
  • 更新邻居:
    • C: min(8, 5+2) = 7
    • D: min(∞, 5+11) = 16
    • E: min(∞, 5+100) = 105
  • 新距离表:
    • A:0, B:5, C:7, D:16, E:105
  • 优先队列:[(7,C), (16,D), (105,E)]
  • 已访问:{A,B}
步骤3:处理C(距离7)
#
  • 当前节点:C
  • 更新邻居:
    • E: min(105, 7+7) = 14
  • 新距离表:
    • A:0, B:5, C:7, D:16, E:14
  • 优先队列:[(14,E), (16,D)]
  • 已访问:{A,B,C}
步骤4:处理E(距离14)
#
  • 当前节点:E
  • 更新邻居:
    • F: min(∞, 14+8) = 22
  • 新距离表:
    • A:0, B:5, C:7, D:16, E:14, F:22
  • 优先队列:[(16,D), (22,F)]
  • 已访问:{A,B,C,E}

步骤5:处理D(距离16)

  • 当前节点:D
  • 更新邻居:
    • E: min(14, 16+4) = 14(不变)
    • F: min(22, 16+9) = 22(不变)
  • 优先队列:[(22,F)]
  • 已访问:{A,B,C,E,D}

步骤6:处理F(距离22)

  • 算法终止

最终结果

节点 最短距离 最短路
F 22 A→B→C→E→F

验证:

  • A→B→C→E→F: 5+2+7+8 = 22
  • 其他路径:
    • A→B→D→F: 5+11+9=25
    • A→C→E→F: 8+7+8=23
    • A→B→E→F: 5+100+8=113

图中有个很突兀的数据,B-E距离为100。这是原因是因为有些人在第一次接触到这个算法时会误认为它和穷举法差不多,将所有路径都尝试了一遍。实际上并非如此,比如这条长度为100的路径,在本次计算中只是被访问,并没有计算(B→E→其他)。如果把它换换成一个比较小的数,他就会被计算出来。

在线编译网站 菜鸟工具
示例代码(C++)

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <limits>
#include <iomanip>
#include <algorithm>

using namespace std;

// 无向图
unordered_map<char, vector<pair<char, int>>> graph = {
    {'A', {{'B', 5}, {'C', 8}}},
    {'B', {{'A', 5}, {'C', 2}, {'D', 11}, {'E', 100}}},
    {'C', {{'A', 8}, {'B', 2}, {'E', 7}}},
    {'D', {{'B', 11}, {'F', 9}, {'E', 4}}},
    {'E', {{'B', 100}, {'C', 7}, {'D', 4}, {'F', 8}}},
    {'F', {{'D', 9}, {'E', 8}}}
};

// 打印距离表的辅助函数
void printDistances(const unordered_map<char, int>& distances) {
    cout << "当前距离表: ";
    for (const auto& entry : distances) {
        char node = entry.first;
        int dist = entry.second;
        if (dist == numeric_limits<int>::max()) {
            cout << node << ":∞ ";
        } else {
            cout << node << ":" << dist << " ";
        }
    }
    cout << endl;
}

// Dijkstra算法
void dijkstra(char start, char end) {
    // 初始化距离表
    unordered_map<char, int> distances;
    for (const auto& entry : graph) {
        char node = entry.first;
        distances[node] = numeric_limits<int>::max();
    }
    distances[start] = 0;

    // 优先队列(最小堆),存储(距离, 节点)
    priority_queue<pair<int, char>, vector<pair<int, char>>, greater<pair<int, char>>> pq;
    pq.push({0, start});

    // 记录前驱节点用于重建路径
    unordered_map<char, char> previous;

    cout << "开始计算...." << endl;
    int step = 1;

    while (!pq.empty()) {
        // 取出当前距离最小的节点
        pair<int, char> top = pq.top();
        int current_dist = top.first;
        char current_node = top.second;
        pq.pop();

        cout << "\n步骤 " << step++ << ": 处理节点 " << current_node 
             << " (距离=" << current_dist << ")" << endl;

        // 如果已经处理过该节点(有更短路径),则跳过
        if (current_dist > distances[current_node]) {
            cout << "  跳过,已有更短路径 " << distances[current_node] << endl;
            continue;
        }

        // 遍历所有邻居
        for (const auto& edge : graph[current_node]) {
            char neighbor = edge.first;
            int weight = edge.second;
            int new_dist = current_dist + weight;
            cout << "  检查 " << current_node << "→" << neighbor 
                 << " (权重=" << weight << "), 新距离=" << new_dist;

            // 如果找到更短路径
            if (new_dist < distances[neighbor]) {
                distances[neighbor] = new_dist;
                previous[neighbor] = current_node;
                pq.push({new_dist, neighbor});
                cout << " 更新" << endl;
            } else {
                cout << " 保持原距离" << endl;
            }
        }

        printDistances(distances);

        // 如果到达终点,可以提前终止
        if (current_node == end) {
            cout << ">> 到达终点 " << end << ",算法终止 <<" << endl;
            break;
        }
    }

    // 输出最终结果
    cout << "\n最终结果:" << endl;
    cout << "从 " << start << " 到各节点的最短距离:" << endl;
    for (const auto& entry : distances) {
        char node = entry.first;
        int dist = entry.second;
        cout << "  " << node << ": " << (dist == numeric_limits<int>::max() ? "∞" : to_string(dist)) << endl;
    }

    // 重建路径
    if (distances[end] != numeric_limits<int>::max()) {
        cout << "\n最短路径 " << start << "→" << end << " (距离=" << distances[end] << "): ";
        vector<char> path;
        for (char at = end; at != start; at = previous[at]) {
            path.push_back(at);
        }
        path.push_back(start);
        reverse(path.begin(), path.end());
        for (size_t i = 0; i < path.size(); ++i) {
            if (i != 0) cout << "→";
            cout << path[i];
        }
        cout << endl;
    } else {
        cout << "\n无法到达节点 " << end << endl;
    }
}

int main() {
    // 计算从A到F的最短路径
    dijkstra('A', 'F');
    return 0;
}

当然,本文是基于Python的,上述这么复杂的代码在Python中只需要这么一点:

import networkx as nx

# 构建无向图(两种方法共用同一个图)
G = nx.Graph()
edges = [
    ('A', 'B', 5), ('A', 'C', 8), ('B', 'C', 2),
    ('B', 'D', 11), ('B', 'E', 100), ('C', 'E', 7),
    ('D', 'E', 4), ('D', 'F', 9), ('E', 'F', 8)
]
G.add_weighted_edges_from(edges)

# 法1:Dijkstra算法
print("\n法1:nx.dijkstra_path")
def calc_short(G, start, end):
    if start == end:
        return 0, [start], True
    try:
        path = nx.dijkstra_path(G, start, end, weight='weight')
        dist = nx.dijkstra_path_length(G, start, end, weight='weight')
        return dist, path, True
    except nx.NetworkXNoPath:
        return float('inf'), [], False

distance, path, success = calc_short(G, 'A', 'F')
if success:
    print(f"最短路径: {' → '.join(path)}")
    print(f"最短距离: {distance}")
else:
    print("无法到达目标节点")

# 法2:使用更通用的networkx函数
print("法2:nx.shortest_path")
try:
    dist = nx.shortest_path_length(G, 'A', 'F', weight='weight')
    path = nx.shortest_path(G, 'A', 'F', weight='weight')
    print(f"最短路径: {' → '.join(path)}")
    print(f"最短距离: {dist}")
except nx.NetworkXNoPath:
    print("节点不可达")

算法部分结束了,下面介绍如何计算换乘次数。 计算换乘次数必须知道线路。

最短路径图

import networkx as nx

# 初始化图
G = nx.Graph()

# 修正后的线路数据
line_data = {
    "L1": [("A", 5), ("B", 6), ("X", 2), ("C", 7), ("D", None)],
    "L2": [("E", 3), ("B", 6), ("F", 6), ("G", None)],
    "L3": [("H", 7), ("F", 8), ("I", None)],
    "L4": [("K", 4), ("F", 2), ("C", 5), ("N", 2), ("J", None)],
    "L5": [("M", 1), ("N", 3), ("O", 4), ("P", None)],
    "L6": [("Q", 3), ("O", 6), ("R", None)],
}

# 添加边
for line, stations in line_data.items():
    for i in range(len(stations) - 1):
        station, dist = stations[i]
        next_station, _ = stations[i + 1]
        if dist is not None:
            G.add_edge(station, next_station, weight=dist, line=line)

# 计算换乘次数(核心)
def calculate_transfers(G, path):
    if len(path) <= 1:
        return 0
    transfers = 0
    prev_line = None
    for i in range(len(path) - 1):
        u, v = path[i], path[i + 1]
        edge_data = G.get_edge_data(u, v)
        current_line = edge_data['line']
        if prev_line is not None and current_line != prev_line:
            transfers += 1
        prev_line = current_line
    return transfers

# 计算 X 到所有站点的最短路径和换乘次数
start = "X"
results = {}
for station in G.nodes():
    if station == start:
        continue
    try:
        path = nx.dijkstra_path(G, start, station, weight='weight')
        distance = nx.dijkstra_path_length(G, start, station, weight='weight')
        transfers = calculate_transfers(G, path)
        results[station] = {"distance": distance, "transfers": transfers}
    except nx.NetworkXNoPath:
        results[station] = {"distance": float('inf'), "transfers": -1}

# 输出结果
print(f"从 {start} 出发到各站的最短距离和换乘次数:")
print("{:<5} {:<10} {:<10}".format("站点", "距离", "换乘次数"))
for station, data in sorted(results.items()):
    print("{:<5} {:<10} {:<10}".format(
        station, 
        data["distance"] if data["distance"] != float('inf') else "不可达",
        data["transfers"] if data["transfers"] != -1 else "不可达"
    ))