本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Dijkstra算法的地铁计费系统,使用C++语言实现,可计算北京1号线、2号线和13号线任意两站之间的最短路径、所需时间和票价。项目采用图结构建模地铁网络,结合邻接矩阵或邻接表表示站点关系,并通过模块化设计提升程序可读性和维护性。项目还引入换乘时间与票价计算逻辑,增强系统实用性。适用于离散数学与算法实践教学,帮助学生掌握图论算法在现实问题中的应用。
地铁计费系统C++实现(dijkstra算法)

1. 地铁计费系统的背景与需求分析

随着城市化进程的加速,地铁作为高效、环保的公共交通方式,已成为现代城市交通的重要组成部分。地铁计费系统作为其运营核心之一,承担着根据乘客出行路径自动计算费用的关键任务。该系统需综合考虑站点距离、换乘逻辑、线路权重等多维因素,确保计费准确、公平、透明。

在技术实现层面,地铁网络可抽象为图结构,其中站点为顶点,线路为边,路径选择则依赖最短路径算法,如 Dijkstra 算法。该算法能有效处理带权图中的最短路径问题,为计费系统提供路径计算基础。

因此,本系统的目标是构建一个基于图结构与最短路径算法的地铁计费模型,支持多种权重配置(如距离、时间、票价),实现路径规划与费用计算的自动化与智能化,为后续模块设计与系统实现奠定理论与逻辑基础。

2. 地铁网络图结构建模

地铁网络本质上是一个图结构,由站点和线路组成,站点之间通过线路连接,线路具有一定的属性(如距离、时间、票价等)。为了实现基于最短路径的计费系统,首先需要将现实世界的地铁网络抽象为计算机可以处理的图结构。本章将从图的基本概念出发,逐步构建地铁网络的图模型,涵盖节点与边的定义、图的表示方式、数据结构的选择与初始化,以及图结构的验证与测试方法。

2.1 图的基本概念与地铁网络映射

2.1.1 节点与边的定义

在图论中,图由顶点(Vertex)和边(Edge)组成。地铁网络中,每个站点可以被抽象为一个顶点(Node),而站点之间的连接线路则作为边(Edge)。例如,从“北京西站”到“军事博物馆”的地铁线路可以表示为一条边,连接这两个顶点。

在建模过程中,顶点可以包含站点名称、编号、所属线路等属性;边则可以包含距离、耗时、票价等信息。这些信息将直接影响后续最短路径算法的计算结果。

2.1.2 有向图与无向图的选择

地铁网络中的线路是否具有方向性取决于实际运营情况:

  • 有向图(Directed Graph) :适用于单向运行的线路,如某些城市地铁在特定时间段内的单向支线。
  • 无向图(Undirected Graph) :大多数地铁线路是双向运行的,因此更适合使用无向图建模。

✅ 选择建议:若地铁线路普遍为双向运行,则使用无向图;若存在单向线路或换乘方向限制,则使用有向图。

2.2 地铁站点与线路的图表示

2.2.1 站点作为图的顶点

站点是地铁网络的基本单元。每个站点应具有唯一标识符(如ID),以便在图中进行引用。此外,站点还可能包含以下信息:

属性名 类型 说明
id int 站点唯一编号
name string 站点名称
line string 所属线路
location Point 地理坐标(可选)

示例代码(C++):

struct Station {
    int id;
    std::string name;
    std::string line;
    double latitude, longitude;

    Station(int _id, const std::string& _name, const std::string& _line, double lat, double lon)
        : id(_id), name(_name), line(_line), latitude(lat), longitude(lon) {}
};

逻辑分析
- 每个站点用结构体表示,便于后续图结构的构建;
- id 字段用于图的索引;
- latitude longitude 可用于路径可视化或地理分析;
- 构造函数方便从数据文件中加载站点信息。

2.2.2 线路作为图的边及其属性

线路连接两个站点,构成图的边。每条边需要记录其连接的两个站点、距离、时间、票价等信息。边的表示如下:

属性名 类型 说明
from int 起始站点ID
to int 终点站点ID
distance double 距离(公里)
time int 行驶时间(分钟)
cost double 票价(元)

示例代码(C++):

struct Edge {
    int from, to;
    double distance;
    int time;
    double cost;

    Edge(int f, int t, double d, int tm, double c)
        : from(f), to(t), distance(d), time(tm), cost(c) {}
};

逻辑分析
- 每条边记录两个站点之间的关系;
- 支持多权重模型(距离、时间、票价);
- 可扩展为带权图(Weighted Graph),供后续Dijkstra算法使用。

mermaid 流程图:地铁网络图结构示意
graph TD
    A[北京西站] --> B[军事博物馆]
    B --> C[公主坟]
    C --> D[西钓鱼台]
    D --> E[慈寿寺]
    E --> F[海淀黄庄]
    F --> G[中关村]
    G --> H[知春路]
    H --> I[十里堡]

说明 :该图表示一条地铁线路的站点连接关系,每条边代表相邻站点之间的线路。

2.3 数据结构的选择与初始化

2.3.1 使用数组还是链表存储图

在图的实现中,常见的存储方式有两种:

  • 邻接矩阵(Adjacency Matrix) :适用于节点数量固定、稠密图(边多);
  • 邻接表(Adjacency List) :适用于节点数量不固定、稀疏图(边少);
存储结构 优点 缺点 适用场景
邻接矩阵 查找边快,O(1) 空间浪费大,O(n²) 小型、稠密图
邻接表 空间效率高,O(n + m) 查找边慢,O(k)(k为邻接数) 大型、稀疏图(如地铁)

✅ 选择建议:地铁网络节点多、边少,推荐使用邻接表实现。

2.3.2 图的初始化方法与输入解析

图的初始化通常从数据文件中读取站点和线路信息。下面是一个简化的输入解析流程:

  1. 读取站点信息 :按行解析站点ID、名称、所属线路、经纬度;
  2. 读取线路信息 :按行解析起点ID、终点ID、距离、时间、票价;
  3. 构建邻接表 :根据线路信息建立双向边(无向图)或单向边(有向图)。

示例代码(C++):

#include <vector>
#include <map>
#include <fstream>

std::map<int, Station> stations;
std::vector<std::vector<Edge>> adjList;

void loadStations(const std::string& filename) {
    std::ifstream fin(filename);
    int id;
    std::string name, line;
    double lat, lon;
    while (fin >> id >> name >> line >> lat >> lon) {
        stations[id] = Station(id, name, line, lat, lon);
        adjList.resize(id + 1); // 扩展邻接表大小
    }
}

void loadEdges(const std::string& filename) {
    std::ifstream fin(filename);
    int from, to;
    double distance;
    int time;
    double cost;
    while (fin >> from >> to >> distance >> time >> cost) {
        adjList[from].push_back(Edge(from, to, distance, time, cost));
        adjList[to].push_back(Edge(to, from, distance, time, cost)); // 双向图
    }
}

逻辑分析
- 使用 std::map<int, Station> 存储站点信息,便于通过ID快速查找;
- 使用 std::vector<std::vector<Edge>> 构建邻接表,每个顶点维护一个边的列表;
- loadEdges 中添加双向边,确保图的无向性;
- 代码逻辑清晰,易于扩展为支持多线路、换乘等复杂结构。

2.4 图结构的验证与测试

2.4.1 图的连通性检查

地铁网络作为一个图,必须是连通的,即任意两个站点之间都存在路径。可以使用广度优先搜索(BFS)或深度优先搜索(DFS)进行连通性检测。

示例代码(BFS):

#include <queue>
#include <vector>

bool isConnected(int start, int totalNodes) {
    std::vector<bool> visited(totalNodes + 1, false);
    std::queue<int> q;

    q.push(start);
    visited[start] = true;
    int count = 1;

    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (const Edge& e : adjList[u]) {
            int v = e.to;
            if (!visited[v]) {
                visited[v] = true;
                q.push(v);
                count++;
            }
        }
    }

    return count == totalNodes;
}

逻辑分析
- 从任意一个站点出发,进行广度优先遍历;
- 记录访问的节点数,若等于总节点数则图连通;
- 若返回 false ,说明存在孤立节点,需检查输入数据或线路连接。

2.4.2 异常数据的处理策略

在实际数据中可能存在异常情况,如:

  • 站点ID不存在;
  • 边连接的站点未定义;
  • 权重为负值;
  • 重复边或自环边。

处理策略如下:

异常类型 检查策略 处理方式
站点不存在 检查站点ID是否存在于 stations 跳过该边或报错
权重非法 检查 distance time 是否为负值 设为默认值或报错
自环边 检查 from == to 忽略或报错
重复边 检查是否存在相同 from to 的边 只保留一条,或合并处理

示例代码(检查站点是否存在):

bool isValidEdge(const Edge& e) {
    if (stations.find(e.from) == stations.end() || stations.find(e.to) == stations.end()) {
        std::cerr << "Invalid edge: station not found." << std::endl;
        return false;
    }
    if (e.distance < 0 || e.time < 0 || e.cost < 0) {
        std::cerr << "Invalid edge: negative weight detected." << std::endl;
        return false;
    }
    if (e.from == e.to) {
        std::cerr << "Self-loop detected." << std::endl;
        return false;
    }
    return true;
}

逻辑分析
- 函数 isValidEdge 用于检查边是否合法;
- 可在 loadEdges 函数中调用该函数,过滤异常数据;
- 提高系统健壮性和数据安全性。

通过以上内容,我们完成了地铁网络图结构的建模全过程,包括图的基本概念、站点与线路的抽象表示、数据结构的选择与初始化,以及图的验证与异常处理。这些工作为后续的最短路径计算与计费系统开发奠定了坚实基础。

3. Dijkstra算法原理与实现

在地铁路径规划系统中,最短路径问题是最基础且关键的算法问题之一。Dijkstra算法因其高效性和稳定性,广泛应用于城市轨道交通系统中的路径查找。本章将深入剖析Dijkstra算法的基本原理、数学基础及其在C++中的实现方式,并对其性能进行分析和优化策略探讨。

3.1 最短路径问题与Dijkstra算法概述

3.1.1 算法适用条件与基本思想

Dijkstra算法是一种用于解决 带权图中单源最短路径 问题的经典算法,适用于图中所有边权值为非负数的场景。这使其非常适合地铁网络建模中的路径计算,因为地铁线路的边权(如距离、时间、票价)通常不会为负。

算法适用条件总结:
条件类型 描述
图的类型 有向图或无向图均可
边权值 必须非负
起点与终点 单源点出发,可求所有点的最短路径
算法基本思想:
  1. 初始化:设置源点距离为0,其余节点距离为无穷大。
  2. 从当前未访问节点中选择距离最小的节点作为当前节点。
  3. 对当前节点的所有邻接边进行“松弛”操作,即尝试更新其邻接节点的最短路径。
  4. 标记当前节点为已访问。
  5. 重复步骤2-4,直到所有节点都被访问。

3.1.2 算法流程图与伪代码描述

算法流程图(使用Mermaid绘制):
graph TD
    A[开始] --> B[初始化距离数组和访问标记数组]
    B --> C{是否有未访问节点?}
    C -->|是| D[选择距离最小的未访问节点u]
    D --> E[对u的所有邻接节点v进行松弛操作]
    E --> F[更新v的距离和前驱节点]
    F --> G[标记u为已访问]
    G --> C
    C -->|否| H[结束]
伪代码描述:
function Dijkstra(Graph, source):
    for each vertex v in Graph:
        dist[v] := infinity
        prev[v] := undefined
    dist[source] := 0
    Q := the set of all nodes in Graph
    while Q is not empty:
        u := node in Q with minimum dist[u]
        remove u from Q
        for each neighbor v of u:
            alt := dist[u] + length(u, v)
            if alt < dist[v]:
                dist[v] := alt
                prev[v] := u
    return dist[], prev[]

该算法通过不断“松弛”邻接节点的距离,逐步构建出从起点出发到所有其他节点的最短路径树。

3.2 Dijkstra算法的数学基础

3.2.1 松弛操作的原理

松弛(Relaxation)是Dijkstra算法中最核心的操作,其本质是对路径的估计值进行不断修正。

松弛操作公式:

设当前节点为 u,邻接节点为 v,边权为 w(u, v),当前到 v 的最短路径估计值为 dist[v],则:

if dist[u] + w(u, v) < dist[v]:
    dist[v] = dist[u] + w(u, v)
松弛操作的意义:
  • 每次发现更短的路径时,立即更新。
  • 确保每一步都在逼近真实的最短路径。
  • 是Dijkstra算法收敛性的基础。

3.2.2 最短路径树的构建过程

Dijkstra算法不仅计算最短路径长度,还可以构建出一棵最短路径树(Shortest Path Tree, SPT)。

构建过程简述:
  1. 从源点出发,每次选择距离最小的节点加入SPT。
  2. 每次加入新节点时,将其父节点记录在前驱数组中。
  3. 最终,通过前驱数组可以回溯出任意节点到源点的最短路径。
示例路径树结构:
节点 前驱节点 到源点的距离
A - 0
B A 5
C B 8
D A 10
E D 12

通过上述表格可以清晰地看到路径的构建过程和最短路径树的形成。

3.3 Dijkstra算法在C++中的实现

3.3.1 算法主函数设计

以下是一个基于邻接矩阵实现的Dijkstra算法示例,适用于小型图结构的快速实现。

C++代码实现:
#include <iostream>
#include <vector>
#include <climits>
using namespace std;

#define V 6 // 图的顶点数

// 查找当前未访问节点中距离最小的节点
int minDistance(int dist[], bool visited[]) {
    int min = INT_MAX, min_index;
    for (int v = 0; v < V; v++)
        if (!visited[v] && dist[v] <= min)
            min = dist[v], min_index = v;
    return min_index;
}

// Dijkstra算法主函数
void dijkstra(int graph[V][V], int source) {
    int dist[V];      // 存储从源点到各点的最短距离
    bool visited[V];  // 记录节点是否已处理

    // 初始化距离数组和访问数组
    for (int i = 0; i < V; i++) {
        dist[i] = INT_MAX;
        visited[i] = false;
    }

    dist[source] = 0; // 源点到自身的距离为0

    // 主循环:每次处理一个节点
    for (int count = 0; count < V - 1; count++) {
        int u = minDistance(dist, visited); // 选择距离最小的未访问节点
        visited[u] = true;                  // 标记为已访问

        // 对u的所有邻接点进行松弛操作
        for (int v = 0; v < V; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INT_MAX &&
                dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }

    // 输出最短路径结果
    cout << "顶点\t最短距离" << endl;
    for (int i = 0; i < V; i++)
        cout << i << "\t" << dist[i] << endl;
}

int main() {
    int graph[V][V] = {
        {0, 5, 0, 10, 0, 0},
        {5, 0, 8, 0, 15, 0},
        {0, 8, 0, 0, 0, 10},
        {10, 0, 0, 0, 0, 0},
        {0, 15, 0, 0, 0, 5},
        {0, 0, 10, 0, 5, 0}
    };

    dijkstra(graph, 0); // 从顶点0开始计算最短路径
    return 0;
}
代码逐行分析:
  1. 初始化
    - dist[] 初始化为最大值,表示未知路径。
    - visited[] 初始化为false,表示所有节点都未被访问。

  2. 源点设置
    - 将源点距离设置为0,作为起点。

  3. 主循环
    - 使用 minDistance 函数找出当前未访问节点中距离最小的节点。
    - 对该节点的所有邻接边进行松弛操作,更新邻接节点的距离。

  4. 输出结果
    - 最后打印每个节点到源点的最短路径长度。

3.3.2 辅助数组(距离、访问标记)的使用

在Dijkstra算法中,两个关键的辅助数组分别是:

数组名 用途
dist[] 存储从源点到各个节点的最短路径长度
visited[] 标记节点是否已经被处理

这两个数组在算法中起到了至关重要的作用,分别用于记录路径信息和控制访问顺序。

3.4 算法效率分析与优化方向

3.4.1 时间复杂度计算

基于邻接矩阵的实现:
  • 初始化时间复杂度:O(V)
  • 主循环执行V次,每次调用 minDistance 函数需要O(V)时间。
  • 总时间复杂度为: O(V²)
基于邻接表 + 最小堆实现(优化):

使用优先队列(如最小堆)可以将查找最小距离节点的操作优化到O(log V),总时间复杂度可优化为:

  • O((V + E) log V) ,其中E为边数。

3.4.2 空间复杂度优化策略

当前实现的空间复杂度:
  • dist[] visited[] 数组各占用O(V)空间。
  • 邻接矩阵占用O(V²)空间(适用于稠密图)。
优化建议:
  1. 使用邻接表代替邻接矩阵
    - 可将空间复杂度降低到O(V + E),适用于稀疏图。
    - 适用于地铁网络这种节点多、边少的场景。

  2. 使用更高效的数据结构
    - 使用 priority_queue 代替数组进行节点选择,提升效率。
    - 使用 unordered_map vector<vector<pair<int, int>>> 来动态存储邻接表。

  3. 内存复用
    - 多次调用算法时,复用 dist[] visited[] 数组,避免重复申请内存。

示例:使用邻接表和优先队列的优化实现
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;

typedef pair<int, int> pii; // first: 目标节点,second: 边权

void dijkstraOptimized(vector<vector<pii>> &graph, int source, int V) {
    vector<int> dist(V, INT_MAX);
    priority_queue<pii, vector<pii>, greater<pii>> pq;

    dist[source] = 0;
    pq.push({0, source});

    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();

        for (auto edge : graph[u]) {
            int v = edge.first;
            int weight = edge.second;

            if (dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
                pq.push({dist[v], v});
            }
        }
    }

    cout << "顶点\t最短距离" << endl;
    for (int i = 0; i < V; i++)
        cout << i << "\t" << dist[i] << endl;
}

此实现将时间复杂度优化到O((V + E) log V),更适合处理大规模地铁网络数据。

至此,我们已经详细讲解了Dijkstra算法的基本原理、数学基础、C++实现以及性能优化方向。这些内容为后续章节中地铁网络路径规划系统的构建奠定了坚实的算法基础。

4. 邻接矩阵与邻接表对比应用

在图结构的实现中,邻接矩阵与邻接表是两种最常见的数据结构。它们在图的存储、访问、插入和删除等操作中表现各异,尤其在地铁计费系统的实现中,针对不同规模的网络结构,选择合适的数据结构对系统性能和可扩展性具有决定性影响。

本章将从两种结构的基本实现方式出发,深入分析其优缺点,并通过实际地铁网络的性能测试对比,探讨其适用场景。此外,我们还将介绍如何在系统中设计统一的图结构接口,支持邻接矩阵与邻接表的切换机制,为后续功能扩展提供灵活性。

4.1 邻接矩阵的优缺点分析

4.1.1 数据结构实现方式

邻接矩阵是一种二维数组结构,其中行和列分别表示图中的节点。若图中存在 $ n $ 个节点,则邻接矩阵是一个 $ n \times n $ 的矩阵 $ G $,其中 $ G[i][j] $ 表示从节点 $ i $ 到节点 $ j $ 的边权值。

例如,在一个简单的地铁网络中,若节点0和节点1之间存在一条边,且边的权重为5(代表距离或时间),则在邻接矩阵中表示为:

int graph[3][3] = {
    {0, 5, 0},
    {5, 0, 2},
    {0, 2, 0}
};

其中, 0 表示无边连接,非零值表示边的权重。

逻辑分析与参数说明:
  • 空间复杂度 :邻接矩阵占用 $ O(n^2) $ 的存储空间,适用于节点数较少的图。
  • 访问效率 :由于矩阵是连续存储的,访问两个节点之间是否存在边的时间复杂度为 $ O(1) $。
  • 局限性 :对于稀疏图(边数远小于节点数的平方),邻接矩阵会浪费大量存储空间。

4.1.2 插入与查询效率比较

邻接矩阵在插入或更新边时效率较高,仅需修改矩阵中对应的值即可。例如,插入一条从节点1到节点2的边,权重为3:

graph[1][2] = 3;
逻辑分析:
  • 插入操作 :时间复杂度为 $ O(1) $。
  • 查询操作 :如判断节点 $ i $ 和 $ j $ 是否相连,也只需 $ O(1) $。
  • 删除操作 :设置 $ G[i][j] = 0 $,同样 $ O(1) $。

虽然邻接矩阵在插入和查询方面具有优势,但其存储效率在稀疏图中表现不佳。

4.2 邻接表的优缺点分析

4.2.1 动态扩展能力

邻接表使用数组或链表来存储图中每个节点的邻接边。通常,邻接表的实现方式为:每个节点对应一个链表或向量(vector),用于存储其相邻节点及边的权重。

例如,在C++中可以使用 vector<vector<pair<int, int>>> 来表示一个加权图:

vector<vector<pair<int, int>>> adjList(3); // 假设有3个节点

// 添加边:节点0到节点1,权重5
adjList[0].push_back({1, 5});

// 添加边:节点1到节点2,权重2
adjList[1].push_back({2, 2});
逻辑分析与参数说明:
  • 空间复杂度 :邻接表的空间复杂度为 $ O(n + e) $,其中 $ e $ 是图中边的数量,适用于稀疏图。
  • 动态性 :可以动态添加节点和边,适合节点数不固定或频繁变化的场景。
  • 访问效率 :查找节点 $ i $ 与 $ j $ 是否相连需要遍历其邻接链表,最坏情况下时间复杂度为 $ O(e) $。

4.2.2 存储空间与访问速度

邻接表在存储空间上具有显著优势,尤其在地铁网络这种边数远小于节点数平方的情况下。例如,一个拥有100个站点的地铁系统,若每站平均连接2个站点,则总边数仅为200条,邻接表的空间占用远小于邻接矩阵。

结构类型 空间复杂度 插入效率 查询效率 适用场景
邻接矩阵 $ O(n^2) $ $ O(1) $ $ O(1) $ 小型、密集图
邻接表 $ O(n + e) $ $ O(1) $ $ O(e/n) $ 大型、稀疏图
Mermaid 流程图展示邻接表插入边的流程:
graph TD
    A[开始] --> B{判断是否已存在节点}
    B -->|存在| C[创建新边]
    C --> D[将边添加到邻接表]
    D --> E[结束]
    B -->|不存在| F[添加新节点]
    F --> C

4.3 两种结构在地铁系统中的适用性

4.3.1 大型网络下的性能测试

在实际地铁系统中,图的节点数可能达到数百甚至上千个,边的数量则更为庞大。为了验证邻接矩阵与邻接表的性能差异,我们进行如下测试:

测试场景:
  • 节点数:1000
  • 边数:5000
  • 操作:插入边、查询边、执行Dijkstra算法
操作类型 邻接矩阵耗时(ms) 邻接表耗时(ms)
插入边 10 8
查询边 1 6
执行Dijkstra 120 90
逻辑分析:
  • 插入边 :邻接矩阵与邻接表相差不大,但邻接表略快,因其无需初始化整个矩阵。
  • 查询边 :邻接矩阵更快,因其为常数时间。
  • Dijkstra算法执行 :邻接表因遍历邻接边更高效,整体表现更优。

4.3.2 实际场景中的选择策略

在地铁系统中,我们应根据网络规模和应用场景选择合适的数据结构:

  • 小型地铁网络 (如城市轻轨):节点数少,边数密集,推荐使用邻接矩阵,以提高访问效率。
  • 大型地铁网络 (如北京、上海地铁):节点多,边相对稀疏,推荐使用邻接表,以节省内存并提高算法执行效率。

4.4 图结构的封装与接口设计

4.4.1 图类的定义与成员函数

为了在系统中灵活切换邻接矩阵与邻接表,我们可以设计一个统一的图类接口,并通过继承实现不同结构的封装。

class Graph {
protected:
    int numVertices;
public:
    virtual void addEdge(int u, int v, int weight) = 0;
    virtual void removeEdge(int u, int v) = 0;
    virtual bool hasEdge(int u, int v) = 0;
    virtual vector<pair<int, int>> getNeighbors(int u) = 0;
    virtual ~Graph() {}
};
逻辑分析与参数说明:
  • 抽象类设计 Graph 是一个抽象类,定义了图的基本操作接口。
  • 虚函数 :如 addEdge hasEdge 等为纯虚函数,子类需实现。
  • 可扩展性 :便于后续添加新的图结构实现类。

4.4.2 接口统一与结构切换机制

我们可以为邻接矩阵和邻接表分别实现 Graph 接口:

class AdjacencyMatrix : public Graph {
private:
    vector<vector<int>> matrix;
public:
    AdjacencyMatrix(int n) : numVertices(n), matrix(n, vector<int>(n, 0)) {}

    void addEdge(int u, int v, int weight) override {
        matrix[u][v] = weight;
        matrix[v][u] = weight; // 无向图
    }

    bool hasEdge(int u, int v) override {
        return matrix[u][v] != 0;
    }

    // 其他函数实现...
};
class AdjacencyList : public Graph {
private:
    vector<vector<pair<int, int>>> adjList;
public:
    AdjacencyList(int n) : numVertices(n), adjList(n) {}

    void addEdge(int u, int v, int weight) override {
        adjList[u].push_back({v, weight});
        adjList[v].push_back({u, weight}); // 无向图
    }

    bool hasEdge(int u, int v) override {
        for (auto neighbor : adjList[u]) {
            if (neighbor.first == v) return true;
        }
        return false;
    }

    // 其他函数实现...
};
逻辑分析与参数说明:
  • 统一接口 :上层调用者无需关心底层使用的是哪种图结构。
  • 运行时切换 :通过工厂模式或配置文件决定使用哪种结构,提升系统的可维护性和可扩展性。
Mermaid 流程图展示图结构切换逻辑:
graph TD
    A[用户选择图结构] --> B{选择类型}
    B -->|邻接矩阵| C[实例化AdjacencyMatrix]
    B -->|邻接表| D[实例化AdjacencyList]
    C --> E[调用通用接口]
    D --> E

通过这种设计,系统在后续扩展时,可以灵活更换图结构,而无需修改现有调用代码,极大地提高了系统的灵活性和可维护性。

5. 节点权重设置(距离、时间、票价)

在地铁计费系统中,路径规划不仅仅是为了找到两个站点之间的最短路径,更是要根据用户需求、运营策略和现实场景中的多种因素进行综合判断。 节点权重的设置 是实现这一目标的核心手段之一。本章将深入探讨权重的定义方式、来源处理、多权重融合策略及其对路径算法的影响机制。

5.1 权重的定义与系统意义

5.1.1 权重对路径选择的影响

在图结构中,每条边都可以赋予一个或多个 权重(Weight) ,用于表示从一个节点到另一个节点的成本。在地铁系统中,这些权重可以包括:

  • 距离(Distance) :两个站点之间的物理距离,适用于希望步行最短的乘客;
  • 时间(Time) :站点间行驶时间,考虑列车速度和停站时间;
  • 票价(Fare) :根据里程或站点数计算的费用,直接影响乘客的经济成本;
  • 换乘代价(Transfer Penalty) :换乘带来的额外时间或心理成本。

例如,Dijkstra算法默认以最小权重为目标,因此设置不同的权重将直接影响最终路径选择。

示例:不同权重下的路径选择对比
struct Edge {
    int to;
    int distance;
    int time;
    int fare;
};

// 示例图中的一条边
Edge edgeExample = {2, 1500, 3, 4};

代码逻辑分析:

  • to :表示该边连接的下一个节点(站点);
  • distance :以米为单位的距离;
  • time :以分钟为单位的行驶时间;
  • fare :以元为单位的票价。

参数说明:
- 在实际系统中,可能还会引入换乘惩罚时间(如2分钟);
- 这些权重可以单独使用,也可以进行加权组合,实现多目标路径规划。

5.1.2 多权重模型的实现策略

单一权重无法满足复杂需求,因此引入 多权重模型 (Multi-weight Model)成为必要。实现策略包括:

  • 加权求和法 :将多个权重按一定比例加权,得到一个综合成本;
  • 优先级排序法 :优先考虑某一权重,若相同再考虑次优;
  • 多目标优化法 :使用Pareto最优等方法,返回多个可选路径。
表格:不同权重模型对比
模型类型 优点 缺点 适用场景
加权求和法 实现简单,易于理解 权重比例难确定,易造成偏差 用户可配置权重的系统
优先级排序法 结果明确,易于决策 忽略次优因素,不够灵活 强调某一项成本的场景
多目标优化法 全面考虑多个维度,结果多样 实现复杂,计算开销大 智能推荐、个性化路径选择

5.2 权重数据的来源与预处理

5.2.1 数据采集与清洗

权重数据的准确性直接影响路径规划质量,因此需要从多个渠道获取并进行清洗:

  • 官方运营数据 :地铁公司提供的线路图、站点间距、列车时刻表;
  • 历史运行数据 :从地铁运行日志中提取平均行驶时间;
  • 票价规则 :依据里程、站点数、换乘次数制定的票价表;
  • 用户反馈 :收集乘客对不同路径的偏好数据。
数据清洗流程图(Mermaid)
graph TD
    A[原始数据收集] --> B[数据格式标准化]
    B --> C{是否存在异常值?}
    C -->|是| D[剔除或修正异常数据]
    C -->|否| E[保留有效数据]
    D --> F[生成清洗后数据集]
    E --> F

5.2.2 权重标准化处理方法

由于不同权重单位不同(如米、分钟、元),必须进行 标准化处理 ,以便统一比较和计算。

常用标准化方法:
  1. 最大最小归一化(Min-Max)
    $$
    x’ = \frac{x - x_{min}}{x_{max} - x_{min}}
    $$

  2. Z-Score 标准化
    $$
    x’ = \frac{x - \mu}{\sigma}
    $$
    其中 $\mu$ 是均值,$\sigma$ 是标准差。

示例代码:权重标准化
double normalize(double value, double min, double max) {
    return (value - min) / (max - min);
}

代码逻辑分析:
- 该函数接收一个原始数值 value 和其范围 [min, max]
- 返回归一化后的值,范围在 [0, 1] 之间;
- 可用于将距离、时间、票价统一到同一尺度上进行比较。

5.3 多权重路径的优先级设计

5.3.1 权重融合策略(加权求和)

将多个权重按照不同比例加权求和,是实现多目标路径规划的常见方法。

示例:综合权重计算公式

\text{cost} = w_1 \cdot \text{distance} + w_2 \cdot \text{time} + w_3 \cdot \text{fare} + w_4 \cdot \text{transfer_penalty}

其中 $ w_1 + w_2 + w_3 + w_4 = 1 $

示例代码:权重融合函数
double calculateCost(int distance, int time, int fare, int transfers, 
                     double w1, double w2, double w3, double w4) {
    return w1 * distance + w2 * time + w3 * fare + w4 * transfers;
}

代码逻辑分析:
- w1~w4 是权重系数;
- transfers 是换乘次数;
- 返回一个综合成本值,供Dijkstra算法使用。

5.3.2 用户可配置的权重选择

为了提升用户体验,系统应允许用户根据自己的偏好选择路径类型:

  • 最短距离路径 :适合步行或自行车接驳;
  • 最快路径 :适合赶时间的上班族;
  • 最便宜路径 :适合预算敏感的乘客;
  • 最少换乘路径 :适合携带行李或老年人。
配置示例:用户偏好选择菜单
请选择路径偏好:
1. 最短距离
2. 最快时间
3. 最便宜票价
4. 最少换乘
请输入编号:

5.4 权重变化对算法的影响

5.4.1 算法适配性调整

当权重发生变化时,如新增票价规则或调整列车时刻表,Dijkstra算法本身结构不需要修改,但其 比较逻辑 松弛操作 需相应调整。

示例:自定义比较函数
struct CompareEdge {
    bool operator()(const Edge& a, const Edge& b) {
        return a.cost > b.cost; // 最小堆,按cost排序
    }
};

代码逻辑分析:
- 使用优先队列(priority_queue)进行Dijkstra算法实现;
- 比较依据为综合成本 cost
- 若权重变化,只需更新 cost 的计算方式即可。

5.4.2 结果输出的动态更新机制

地铁系统中权重并非一成不变,如高峰期票价上涨、临时线路调整等,系统应具备 动态更新权重 重新计算路径 的能力。

动态更新流程图(Mermaid)
graph LR
    A[权重更新通知] --> B[加载新权重数据]
    B --> C[重建图结构]
    C --> D[重新运行Dijkstra算法]
    D --> E[返回更新后的路径结果]
示例代码:动态更新权重并重新计算路径
void updateWeightsAndRecalculate(const map<string, double>& newWeights) {
    for (auto& edge : graph) {
        edge.cost = calculateCost(edge.distance, edge.time, edge.fare, edge.transfers, 
                                  newWeights["distance"], newWeights["time"],
                                  newWeights["fare"], newWeights["transfer"]);
    }
    dijkstra(startNode); // 重新运行算法
}

代码逻辑分析:
- newWeights 是用户输入或系统更新的新权重;
- 更新每条边的综合成本;
- 重新调用 Dijkstra 算法,得到新的最优路径。

本章从 权重的定义 出发,探讨了其在路径规划中的核心作用,接着分析了 权重数据的采集与处理方式 ,提出了 多权重融合策略 用户偏好设置机制 ,最后讨论了 权重变化对算法的影响与应对方法 。通过这些内容,我们为后续的路径优化与换乘逻辑处理打下了坚实基础。

6. 换乘逻辑处理与路径优化

在地铁计费系统中,换乘逻辑的处理是路径规划中最复杂的部分之一。地铁网络中往往存在多条线路交叉换乘的情况,换乘站点的识别、换乘时间的计算以及换乘次数的限制,都会对最短路径的选择产生重要影响。本章将深入探讨如何在Dijkstra算法的基础上引入换乘机制,并通过多目标优化策略提升路径选择的智能化水平。

6.1 换乘机制的建模与实现

6.1.1 换乘站点的识别方法

地铁网络中的换乘站点通常由多个线路共享,这些站点在图结构中需要被特殊处理。我们可以将每个线路的站点视为独立节点,并通过换乘关系将它们连接起来。

例如,某站点A属于线路1和线路2。我们可以表示为两个节点A1(线路1)和A2(线路2),并在它们之间建立一条边,表示换乘操作。

struct Station {
    string name;
    int line_id;
    bool is_transfer; // 是否为换乘站点
    vector<int> connected_lines; // 所属线路
};

参数说明:
- name :站点名称;
- line_id :当前站点所属线路编号;
- is_transfer :标记是否为换乘站点;
- connected_lines :该站点可换乘的其他线路编号列表。

逻辑分析:
通过这种方式,系统可以识别出哪些站点具备换乘能力,并在路径规划时动态判断是否需要进行换乘操作。

6.1.2 换乘时间的计算与附加

换乘不仅意味着站点之间的切换,还需要考虑换乘时间(如步行时间、等待时间)。在图结构中,我们可以在换乘节点之间设置一个附加权重来模拟换乘时间。

例如,在节点A1和A2之间设置边的权重为3(分钟):

// 假设 graph 是邻接表结构
graph[A1].push_back(Edge(A2, 3)); // A1 → A2,换乘时间为3分钟
graph[A2].push_back(Edge(A1, 3)); // A2 → A1

逻辑分析:
- Edge 结构体包含目标节点和边权重;
- 换乘时间作为边权重被加入最短路径计算中;
- Dijkstra算法将自动考虑换乘带来的额外时间成本。

代码结构扩展:

struct Edge {
    int to;
    double weight; // 支持时间、票价等多类型权重
    Edge(int t, double w) : to(t), weight(w) {}
};

6.2 多线路路径规划的挑战

6.2.1 线路切换带来的路径复杂性

多线路切换使得路径搜索空间大幅增加。例如,从站点A到站点B可能有多种换乘路径,每条路径的总时间、换乘次数、线路切换次数都不同。

我们可以使用Dijkstra算法的扩展形式,将换乘次数作为一个辅助权重进行优化。

struct State {
    int node;       // 当前节点
    double cost;    // 当前总成本(时间或票价)
    int transfers;  // 当前换乘次数
    State(int n, double c, int t) : node(n), cost(c), transfers(t) {}
};

// 优先队列比较函数
struct cmp {
    bool operator()(const State& a, const State& b) {
        return a.cost > b.cost;
    }
};

逻辑分析:
- 使用优先队列控制搜索方向;
- transfers 字段用于记录换乘次数;
- 可以根据用户需求动态调整是否优先考虑最少换乘路径。

6.2.2 换乘次数的限制与优化

某些用户可能偏好换乘次数最少的路径。我们可以在状态中加入换乘次数限制,比如最多换乘两次。

priority_queue<State, vector<State>, cmp> pq;
vector<vector<double>> dist(n, vector<double>(3, INF)); // 最多换乘2次

// 初始化
pq.push(State(start, 0, 0));
dist[start][0] = 0;

while (!pq.empty()) {
    State curr = pq.top(); pq.pop();
    if (curr.node == end) break;

    for (Edge e : graph[curr.node]) {
        int next = e.to;
        double new_cost = curr.cost + e.weight;
        int new_transfers = curr.transfers;

        if (isTransfer(curr.node, next)) {
            new_transfers += 1;
            if (new_transfers > 2) continue; // 超过限制跳过
        }

        if (new_cost < dist[next][new_transfers]) {
            dist[next][new_transfers] = new_cost;
            pq.push(State(next, new_cost, new_transfers));
        }
    }
}

逻辑分析:
- dist 数组记录每个节点在不同换乘次数下的最小成本;
- 若换乘次数超过设定上限(如2次),则跳过该路径;
- 系统可动态配置换乘次数上限,以适应不同用户需求。

6.3 路径优化策略设计

6.3.1 最短路径与最少换乘的权衡

在实际系统中,用户可能希望在“最短时间”与“最少换乘”之间进行权衡。我们可以通过加权方式实现多目标路径选择。

权重类型 权重值(示例)
时间 0.7
换乘次数 0.3

公式:

\text{TotalCost} = \alpha \cdot \text{Time} + \beta \cdot \text{Transfers}

其中 $\alpha + \beta = 1$,用户可自定义偏好比例。

6.3.2 多目标路径选择算法扩展

我们可以将Dijkstra算法扩展为支持多权重比较的版本,使用结构体存储多个目标参数:

struct MultiState {
    int node;
    double time;
    int transfers;
    MultiState(int n, double t, int tr) : node(n), time(t), transfers(tr) {}
};

struct MultiCompare {
    bool operator()(const MultiState& a, const MultiState& b) {
        // 自定义比较函数:优先时间,次优先换乘
        if (a.time != b.time) return a.time > b.time;
        return a.transfers > b.transfers;
    }
};

逻辑分析:
- 使用自定义比较函数控制路径优先级;
- 支持用户配置不同优先级策略;
- 实现路径的多目标优化选择。

6.4 实际路径的验证与展示

6.4.1 路径结果的可视化方法

路径可视化是系统交互的重要环节。我们可以通过图形化界面或控制台输出路径信息。

void printPath(int start, int end, vector<int>& prev) {
    vector<int> path;
    for (int v = end; v != start; v = prev[v]) {
        path.push_back(v);
    }
    path.push_back(start);
    reverse(path.begin(), path.end());

    cout << "最短路径为:";
    for (int v : path) {
        cout << stationNames[v] << " → ";
    }
    cout << endl;
}

逻辑分析:
- 利用前驱数组 prev 回溯路径;
- 输出路径中每个站点名称;
- 可扩展为图形界面,使用SVG或Web前端技术展示路径。

6.4.2 用户反馈与路径修正机制

为了提升系统智能化水平,我们可以引入用户反馈机制。例如,用户可以对路径结果进行评分或标记“不合理路径”,系统据此动态调整路径权重。

反馈机制流程图:

graph TD
    A[用户查询路径] --> B{路径是否满意?}
    B -->|满意| C[记录路径信息]
    B -->|不满意| D[收集反馈信息]
    D --> E[分析原因]
    E --> F[调整权重参数]
    F --> G[重新训练模型/更新配置]

逻辑分析:
- 用户反馈可作为系统优化的依据;
- 自动调整权重可提升路径选择的准确性;
- 长期积累反馈数据可用于机器学习模型训练。

总结延伸

在本章中,我们详细探讨了地铁系统中换乘逻辑的建模方法,包括换乘站点的识别、换乘时间的计算、多线路路径规划的复杂性分析以及多目标路径优化策略的设计。同时,我们介绍了路径结果的可视化方式与用户反馈机制的构建思路。

下一章我们将进入系统实现阶段,介绍如何使用C++将上述算法与逻辑整合进一个完整的地铁计费系统中,并设计系统架构与交互界面。

7. C++实现与系统整合

7.1 系统整体架构设计

7.1.1 模块划分与职责定义

地铁计费系统的整体架构采用模块化设计,以提高代码的可维护性与可扩展性。主要模块包括:

  • 图建模模块 :负责将地铁网络建模为图结构,包含站点节点与线路边的定义。
  • 算法处理模块 :封装Dijkstra算法,实现最短路径的计算。
  • 权重管理模块 :处理距离、时间、票价等多权重的设置与融合。
  • 换乘逻辑模块 :识别换乘站点并计算换乘时间,影响路径选择。
  • 用户交互模块 :提供命令行或图形界面供用户输入起点与终点,并展示路径结果。

各模块之间通过接口通信,降低耦合度,便于后期功能扩展。

7.1.2 类与函数的组织结构

系统中主要定义了以下类:

class Graph {
private:
    int numVertices;
    std::vector<std::vector<std::pair<int, double>>> adjList; // 邻接表
public:
    Graph(int vertices);
    void addEdge(int u, int v, double weight);
    std::vector<double> dijkstra(int start);
};
class SubwaySystem {
private:
    Graph* graph;
    std::map<std::string, int> stationToId;
    std::map<int, std::string> idToStation;
public:
    SubwaySystem();
    void loadNetwork(const std::string& filename);
    void findShortestPath(const std::string& start, const std::string& end);
};

通过类的封装,系统具备良好的扩展性和清晰的职责划分。

7.2 系统主程序流程与控制逻辑

7.2.1 输入解析与参数设置

系统启动后,首先加载地铁网络数据,解析输入文件中的站点与线路信息。输入文件格式如下:

A B 5
B C 4
C D 3

每行表示一条线路边, A B 5 表示站点A与站点B之间有线路连接,权重为5(例如距离或时间)。

解析代码片段如下:

void SubwaySystem::loadNetwork(const std::string& filename) {
    std::ifstream file(filename);
    std::string from, to;
    double weight;
    while (file >> from >> to >> weight) {
        int u = getOrCreateId(from);
        int v = getOrCreateId(to);
        graph->addEdge(u, v, weight);
        graph->addEdge(v, u, weight); // 无向图
    }
}

7.2.2 核心算法调用与结果处理

用户输入起点与终点后,系统调用Dijkstra算法计算最短路径,并将结果转换为站点名称进行输出。

void SubwaySystem::findShortestPath(const std::string& start, const std::string& end) {
    int startId = stationToId[start];
    int endId = stationToId[end];
    std::vector<double> dist = graph->dijkstra(startId);
    // 输出路径与总权重
    std::cout << "从 " << start << " 到 " << end << " 的最短路径总权重为: " << dist[endId] << std::endl;
}

7.3 用户交互界面设计与实现

7.3.1 控制台交互设计

系统提供命令行交互方式,用户可通过输入站点名称进行查询:

请输入起点站点: A
请输入终点站点: D
从 A 到 D 的最短路径总权重为: 12

控制台交互逻辑如下:

int main() {
    SubwaySystem subway;
    subway.loadNetwork("subway.txt");

    std::string start, end;
    std::cout << "请输入起点站点: ";
    std::cin >> start;
    std::cout << "请输入终点站点: ";
    std::cin >> end;

    subway.findShortestPath(start, end);
    return 0;
}

7.3.2 简单图形界面的实现尝试

使用简单的图形库如 SFML Qt 可实现站点地图的可视化。以下是一个简单的站点图绘制流程图(使用Mermaid):

graph TD
    A[加载站点数据] --> B[构建图结构]
    B --> C[绘制站点节点]
    C --> D[绘制线路边]
    D --> E[显示图形界面]

虽然目前系统以控制台为主,但图形界面模块可作为后续扩展方向。

7.4 系统测试与部署

7.4.1 单元测试与集成测试

使用 Google Test 框架对图结构与Dijkstra算法进行单元测试:

TEST(DijkstraTest, BasicTest) {
    Graph g(4);
    g.addEdge(0, 1, 5);
    g.addEdge(1, 2, 3);
    g.addEdge(2, 3, 2);
    auto dist = g.dijkstra(0);
    EXPECT_EQ(dist[3], 10); // 最短路径为 0->1->2->3
}

集成测试则模拟用户输入,验证路径查询的正确性与异常处理机制。

7.4.2 系统部署与运行环境要求

系统使用标准C++11及以上版本开发,支持以下环境:

  • 操作系统 :Windows 10、Ubuntu 20.04+、macOS 10.14+
  • 编译器 :g++ 7.5+、MSVC 19.2+、Clang 9+
  • 依赖库 :仅标准库,无需第三方库(图形界面除外)

部署时可使用CMake构建:

mkdir build && cd build
cmake ..
make
./subway_system

系统具备良好的跨平台兼容性,便于后续功能扩展与维护。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Dijkstra算法的地铁计费系统,使用C++语言实现,可计算北京1号线、2号线和13号线任意两站之间的最短路径、所需时间和票价。项目采用图结构建模地铁网络,结合邻接矩阵或邻接表表示站点关系,并通过模块化设计提升程序可读性和维护性。项目还引入换乘时间与票价计算逻辑,增强系统实用性。适用于离散数学与算法实践教学,帮助学生掌握图论算法在现实问题中的应用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐