“校园地图设计及其应用”这题目,我第一次见到还以为是让交一张平面图,后来才反应过来——它其实是一道典型的 C++ 数据结构课程设计题,本质是把一所学校抽象成一张带权无向图,然后用最短路径算法回答“从宿舍到图书馆怎么走最近”。这类题目几乎是国内高校数据结构课设的常客,因为它的规模刚好卡在一个很舒服的区间:顶点数量不多(通常 10 到 30 个),但足够覆盖图存储、遍历、最短路径、文件读写、交互菜单这几大块核心知识点,写完之后你能把课本上那些抽象的“邻接矩阵”“权值”“路径数组”全部落到看得见的地名和距离上。这篇文章适合三类人看:正在被课设卡住、不知道从哪下手的同学;已经把代码写出来但输出结果总是对不上的同学;以及想把这份作业再往前推一步、做出点差异化亮点的同学。下面我不讲课本概念复述,只讲我在实际写这类项目时的取舍、踩坑和可复现的完整方案。
1. 把校园抽象成图:先想清楚在存什么
1.1 从真实校园到顶点与边的映射
这一步看起来简单,但决定了后面所有代码的结构。我的习惯是先拿一张学校平面图,圈出 10 到 20 个有代表性的地点,比如校门、宿舍区、第一食堂、图书馆、主教学楼、体育馆、校医院、快递点、实验楼、行政楼。每个地点就是一个顶点,然后用直线把它们之间实际走得通的路连起来,每条路带一个权值,通常用米或者“步行分钟数”表示。注意这里是“走得通”而不是“直线距离”,因为在校园里两点之间往往隔着建筑或者绿化带,直线距离是没意义的。
有人会问,权值用距离还是用时间?这取决于你的应用场景。如果只是做导航,距离最直观,也最好在文档里解释;如果想做得更实用一点,可以用步行时间作为权值,因为校园里上下坡、过桥、绕行的时间差别挺大,用时间算出来的“最短路径”反而更符合人的真实感受。我一般会先在代码里统一用整数距离(单位米),然后在输出的时候顺手换算成“约 X 分钟”,这样既有严谨的数据支撑,读起来又贴近生活。这个换算只是除以一个步行速度常量,比如 1.2 米每秒,不涉及任何浮点精度陷阱。
需要提醒的是,校园地图必然是无向图。从宿舍到图书馆是 300 米,从图书馆回宿舍也是 300 米,不存在单行道的问题(除非你专门要模拟某条单行通道,那就是有向图,但绝大多数课设不需要这么复杂)。所以你在建边的时候,一定要记得对称赋值,这是后面问题排查里出现频率最高的 bug 之一。
1.2 邻接矩阵还是邻接表,别在这纠结太久
这是每份课设报告里都要写一段的“技术选型”。我的结论很直接:校园地图这个规模,闭眼选邻接矩阵。原因有三个,都很实在。
第一,顶点数太少。10 到 30 个顶点,邻接矩阵撑死 900 个 int,占几 KB 内存,根本不需要考虑空间优化。邻接表的省内存优势在这个量级下完全体现不出来,反而让你多写一堆链表节点、多处理一堆指针,出错概率直线上升。
第二,也是最关键的,Floyd 算法需要矩阵。校园地图有一个高频需求是“任意两点之间最短路径”,如果你用 Floyd 一次性把所有点对都算出来,后续查询就是 O(1) 查表,体验非常好。而 Floyd 的核心就是三重循环操作一个二维距离矩阵,你用邻接表根本没法直接跑,还得先转成矩阵,纯属绕路。
第三,邻接矩阵让你可以直接用graph[u][v]这种写法判断两点是否相邻,一行代码搞定。换成邻接表,你得遍历链表,代码量和调试成本都上去了。所以别被“邻接表更高级”这种说法带偏,工具是拿来解决问题的,不是拿来炫技的。等以后你处理几万个节点的路网,再去考虑邻接表加堆优化的 Dijkstra,那才是它的战场。
1.3 功能清单决定了你要拆几个模块
动手写代码之前,我建议先花十分钟把功能列清楚,因为功能清单直接决定你要写几个函数、几个全局数组。一个能拿得出手的校园地图,我通常会包含下面这些功能:查看所有景点信息(编号、名称、简介)、查询任意两点最短距离和具体路线、查询经过某个景点的所有可达路线、从某个起点出发遍历全图(这个用 DFS)、增加或删除一个地点、修改某条路的距离、把地图数据保存到文件并支持下次加载。
前面四个是算法核心,后面三个是“应用”部分的体现,也是很多人容易忽略的地方。课设评分里,“有没有交互”“数据能不能持久化”“有没有增删改”往往是拉开差距的地方。你把这些功能列成一张表,左边写功能,右边写用到的数据结构和算法,你会发现整份代码的骨架一下子就清楚了:矩阵负责存图,Dijkstra 和 Floyd 负责算路,DFS 负责遍历,文件流负责存档,主菜单用一个while循环套switch包起来。就这么简单。
提示:功能不要贪多。我见过有人一口气加了十来个功能,结果每个都是半成品,查询输出都跑不通。宁可保五个功能做到输出准确、边界不漏,也不要凑十个半吊子功能。
2. 数据结构与关键变量怎么定
2.1 顶点、边和那几个必须有的常量
先定基础。我会用两个整型记录规模,一个存距离矩阵,一个存地名。顶点编号从 1 开始,不从 0 开始,这一点很多人觉得无所谓,但其实很关键——因为 0 经常被拿来当“不存在”或者“未访问”的标记,混在一起特别容易出错。地名用string数组存,索引就是顶点编号,这样输入输出的时候可以直接name[i]拿到地名,不用再写一个查找函数。
#include <iostream> #include <fstream> #include <string> #include <vector> #include <limits> using namespace std; const int MAXV = 30; // 最大顶点数,按你学校规模调 const int INF = 0x3f3f3f3f; // 代表“不可达”,不要用 INT_MAX int graph[MAXV][MAXV]; // 距离矩阵 string name[MAXV]; // 编号 -> 地名 string intro[MAXV]; // 编号 -> 景点简介 int vNum = 0; // 当前顶点数 int eNum = 0; // 当前边数这里重点说INF 为什么用 0x3f3f3f3f 而不是 INT_MAX。这是图论代码里的一个经典细节。如果用INT_MAX,在做松弛判断dist[u] + graph[u][v]的时候,一旦dist[u]是INT_MAX,加一个正数就直接整型溢出,变成负数,然后这个负的“距离”会被当成更短的路径写进数组,后面整张图的结果全部崩掉。而0x3f3f3f3f大概是 10.6 亿,两个它相加等于 21.2 亿左右,还在 32 位 int 的正数范围(约 21.47 亿)内,不会溢出。这个技巧我第一次见的时候觉得挺妙,后来一直用到现在。
初始化矩阵的时候要分两种值:自己到自己距离是 0,不可达是 INF。千万别图省事用memset(graph, 0, sizeof(graph))一把梭,那样所有不存在的边都变成了 0 距离,Dijkstra 会算出一条“瞬移路径”,输出结果诡异到你想砸键盘。
2.2 数组开多大,全局还是局部
MAXV这个常量我一般开 30 到 50,比实际顶点数大一圈,留出增删地点的余量。有些同学喜欢用vector动态扩容,这当然更“现代”,但在课设场景里反而增加了复杂度,尤其是当你需要把二维数组传给函数的时候,vector<vector<int>>的传参和初始化都比原生数组啰嗦。我的建议是:规模固定的用原生二维数组,规模可能变的用 vector 存地名,这样兼顾简洁和灵活。
至于全局还是局部,这里我明确站全局。理由很简单:graph、name、vNum这几个变量几乎每个函数都要用,如果全部靠参数传递,你会写出dijkstra(graph, name, vNum, start, end, dist, pre)这种又长又容易传错顺序的函数签名。而放在全局区,函数内部直接访问,代码干净很多。当然,从工程规范角度讲全局变量不好维护,但这是课设,不是百万行项目,可读性和开发效率优先。如果你确实介意,可以把它们塞进一个struct CampusMap里,然后传引用,这也是一种很体面的写法。
二维数组传参这里有个坑必须提前说:int dist[][MAXV]这种写法,第二维的尺寸必须是编译期常量,不能是变量。所以你在写 Floyd 和路径还原函数的时候,形参必须写成int dist[][MAXV],写成int**或者int dist[][]都编译不过。很多人卡在这里半天,以为是算法写错了,其实是参数声明的问题。
2.3 地名和编号的对应关系要一次定死
这个词看着不起眼,但它是“校园地图”区别于普通图论练习的地方。纯图论题里节点叫 1、2、3,没人关心它代表什么;而校园地图里,输出必须是人能看懂的“三食堂 → 图书馆 → 主楼”,所以地名和编号的绑定关系必须在一开始就定好,而且全程不改。
我的做法是用文件存这个映射。文件第一行是顶点数和边数,紧接着是每个顶点的名字和简介,最后是边的列表。这样一来,程序每次启动只要读文件就能恢复整张地图,你也不用在代码里硬编码一堆字符串。好处是显而易见的:想改地名,改文件就行,不用重新编译;想演示一个不同规模的地图,复制一份文件改改数字也能跑。这个小设计在答辩的时候挺加分,因为它体现了“数据与代码分离”的意识。
需要留意的是,如果地名里带空格,比如“第一 教学楼”,那用cin >> name[i]读就会断在空格处,读进来一半。所以要么约定地名不带空格,要么老老实实用getline配合丢弃换行符。我一般用后者,稳妥。
3. 核心算法落地:最短路径与全路径遍历
3.1 单源最短路:Dijkstra 的标准写法
Dijkstra 是校园地图里最核心的算法,答“从 A 到 B 怎么走最近”全靠它。它的思路用一个生活化的比喻就是:从起点开始,每次挑出当前“已知距离最小且还没确定”的那个点,把它标记为“已确定”,然后拿它去更新所有邻居的距离。这个“贪心”策略之所以正确,是因为所有边的权值都是正数——校园里的路不可能是负数米。
void dijkstra(int start, int end, int dist[], int pre[]) { bool visited[MAXV] = {false}; for (int i = 1; i <= vNum; ++i) { dist[i] = graph[start][i]; pre[i] = (graph[start][i] < INF) ? start : -1; } dist[start] = 0; pre[start] = -1; visited[start] = true; for (int round = 1; round < vNum; ++round) { int u = -1, best = INF; for (int i = 1; i <= vNum; ++i) { if (!visited[i] && dist[i] < best) { best = dist[i]; u = i; } } if (u == -1) break; // 剩下的点都不可达,提前收工 visited[u] = true; for (int v = 1; v <= vNum; ++v) { if (!visited[v] && graph[u][v] < INF && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; pre[v] = u; } } } }这段代码里有三个细节值得单独拎出来讲。第一,pre数组记录的是“这个点是从哪个点走过来的”,它是后面还原路径的唯一依据,必须在松弛成功的那一刻同步更新,漏了就会出现“距离对了但路线断了”的情况。第二,if (u == -1) break;这行看着可有可无,实际上在处理不连通图的时候能省掉一堆无意义的循环,而且能避免best保持 INF 时做出错误选择。第三,dist[u] + graph[u][v]这里的dist[u]已经在前面被证明是起点到 u 的最短距离了,这是 Dijkstra 正确性的基础,也解释了为什么它不能处理负权边。
有同学问,为什么不用优先队列优化?可以,但校园地图这个规模(30 个点)下,朴素 O(n²) 的写法反而更快也更简单,堆优化的那点性能优势完全体现不出来,还多引入一个<queue>和priority_queue的知识点,出错概率更高。我建议课设就用朴素版,报告里可以提一句“规模增大时可换堆优化”,显得你有延伸思考。
3.2 Floyd:一次算完所有点对的最短路
如果你希望用户连着查好几次路线,每次都跑一遍 Dijkstra,其实也没问题,但体验上会略慢,而且实现多路径查询的时候反而不方便。这时候 Floyd 就更合适——三重循环一次性把所有点对的最短距离和路径都算出来,之后每次查询只是查表。
void floyd(int dist[][MAXV], int path[][MAXV]) { for (int i = 1; i <= vNum; ++i) for (int j = 1; j <= vNum; ++j) { dist[i][j] = graph[i][j]; path[i][j] = (graph[i][j] < INF && i != j) ? i : -1; } for (int k = 1; k <= vNum; ++k) for (int i = 1; i <= vNum; ++i) { if (dist[i][k] == INF) continue; // 剪枝,顺便防溢出 for (int j = 1; j <= vNum; ++j) { if (dist[i][k] + dist[k][j] < dist[i][j]) { dist[i][j] = dist[i][k] + dist[k][j]; path[i][j] = path[k][j]; } } } }Floyd 最反直觉的地方是那句path[i][j] = path[k][j]。为什么不是path[k][j]或者path[i][k]?因为path[i][j]的定义是“从 i 到 j 的路径上,j 的前一个点是谁”。当发现绕道 k 更短时,i 到 j 的新路径实际上是“i 到 k 的路径”接上“k 到 j 的路径”,那么 j 的前一个点就变成了原本 k 到 j 路径上 j 的前一个点,也就是path[k][j]。这个点想通了,路径还原就不会出错;想不通,输出就会五花八门。
还有那个if (dist[i][k] == INF) continue;也别省。它一方面能剪掉大量无效计算,另一方面能防止INF + INF这种看起来危险、虽然用 0x3f3f3f3f 不至于溢出但会让代码显得不严谨的情况。
3.3 DFS 遍历:把所有可行路线都翻出来
“应用”部分最能出彩的就是全路径查询。用户问“从校门到体育馆,一共有几条路可以走”,这时候最短路只给一条答案,不够看。DFS 就是干这个的:从起点出发,沿着所有还通的路往下走,走到终点就打印一条路径,然后回退,换下一条。
int visited[MAXV] = {0}; vector<int> route; void dfsAllPath(int cur, int end) { if (cur == end) { for (size_t i = 0; i < route.size(); ++i) cout << (i ? " -> " : "") << name[route[i]]; cout << "\n"; return; } for (int v = 1; v <= vNum; ++v) { if (graph[cur][v] < INF && graph[cur][v] > 0 && !visited[v]) { visited[v] = 1; route.push_back(v); dfsAllPath(v, end); route.pop_back(); // 回溯:撤销选择 visited[v] = 0; } } }这里两个条件必须同时写:graph[cur][v] < INF表示这条路存在,graph[cur][v] > 0排除掉对角线上的 0,否则你会从当前点“走回自己”,进而无限递归直到栈溢出。另外visited数组和route的 push/pop 必须严格配对,这是回溯法的铁律,少一句pop_back就会得到一堆乱七八糟的路径。
注意:当图中的边比较密、顶点数超过 20 时,全路径 DFS 的搜索空间会爆炸式增长,输出可能刷屏几分钟。演示时建议把起点终点选得远一点,或者限制路径长度上限。
3.4 路径还原,这一步最容易写错
Dijkstra 用pre数组还原路径,Floyd 用二维path数组还原路径,两者做法不同,但都容易写错。Dijkstra 的路径是“从终点往回倒着找”,而 Floyd 的路径更适合用递归正着输出。
void printPathFloyd(int path[][MAXV], int i, int j) { if (i == j) { cout << name[i]; return; } if (path[i][j] == -1) { cout << "不可达"; return; } printPathFloyd(path, i, path[i][j]); cout << " -> " << name[j]; }递归写法的好处是天然正序,不用压栈再弹栈,代码也短。但要小心两个终止条件缺一不可:i == j是正常结束,path[i][j] == -1是不可达的兜底。如果只写前者,遇到不可达的点对就会无限递归。同理,Dijkstra 版本用栈倒序输出时,也要记得在最后把起点补上,因为pre的链条到起点就断了,很多人的输出会莫名其妙少了出发地。
4. 从零跑通:建图、菜单与交互细节
4.1 建图与数据持久化
建图有两种方式:代码里硬编码和从文件读。我强烈建议两种都写,硬编码那份当“初始化模板”,文件那份当“存档”。第一次运行如果没有存档文件,就用硬编码数据初始化并写出一份文件;之后每次启动都优先加载文件。这样既保证了程序首次运行就能用,又体现了持久化能力。
bool loadMap(const string& file) { ifstream in(file); if (!in) return false; in >> vNum >> eNum; in.ignore(); for (int i = 1; i <= vNum; ++i) getline(in, name[i]); for (int i = 1; i <= vNum; ++i) for (int j = 1; j <= vNum; ++j) graph[i][j] = (i == j) ? 0 : INF; for (int k = 0; k < eNum; ++k) { int u, v, w; in >> u >> v >> w; graph[u][v] = graph[v][u] = w; // 无向图,必须对称 } return true; }注意in.ignore();那一行。因为前面读eNum用的是>>,输入流里还留着一个换行符,如果不丢弃它,紧接着的getline就会读到一个空字符串。这是 C++ 流输入里最经典的“坑”,和那堆“error: Microsoft Visual C++ 14.0 is required”的报错一样,属于新手阶段必然会撞上的东西。
保存就更简单了,把刚才的读法反过来写一遍,注意graph是对称矩阵,存边的时候只存u < v的那一半,避免重复。这样文件体积小一半,读回来的时候再对称赋值,逻辑也干净。
4.2 主菜单循环与输入健壮性
交互部分最大的敌人不是逻辑,是用户乱输入。用户在菜单里输入一个字母a,cin >> choice会失败,choice保持原值不变,于是switch又执行了一遍上一次的操作,屏幕上无限刷同样的内容,这就是传说中的菜单死循环。解决办法是每次读取失败后清理状态并丢弃缓冲区。
int readInt(const string& tip) { int x; while (true) { cout << tip; if (cin >> x) return x; cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); cout << "输入非法,请重新输入一个数字。\n"; } }cin.clear()清除错误状态位,cin.ignore(..., '\n')把当前行剩下的垃圾字符全部丢掉,两者配合才能彻底恢复输入流。把这段封装成一个readInt函数,菜单里所有需要读数字的地方都调它,代码会清爽很多,也再也不会出现死循环。
菜单本身我用while(true)加switch,每个选项对应一个函数调用,最后有一个“退出”选项break出循环。注意break在switch里只跳出switch,跳不出while,要退整个程序得用return或者设置一个bool running = false的标志位。这个坑每年都有人踩。
4.3 一份能直接跑的代码骨架
把前面的东西拼起来,主函数大概长这样:
int main() { if (!loadMap("campus.txt")) initDefaultMap(); while (true) { cout << "\n===== 校园地图导航系统 =====\n"; cout << "1. 查看所有地点\n"; cout << "2. 查询最短路径\n"; cout << "3. 查询所有可行路线\n"; cout << "4. 修改道路距离\n"; cout << "5. 保存地图\n"; cout << "0. 退出\n"; int op = readInt("请选择: "); if (op == 0) break; switch (op) { case 1: showAllSpots(); break; case 2: queryShortest(); break; case 3: queryAllRoutes();break; case 4: modifyEdge(); break; case 5: saveMap("campus.txt"); cout << "已保存\n"; break; default: cout << "没有这个选项\n"; } } cout << "已退出。\n"; return 0; }整个项目写到这里,行数大概在 300 到 500 行之间,正好是一个人两三天能做完、又能体现完整工程能力的量。如果你想让代码更漂亮,可以把graph、name、vNum打包成一个结构体,把各个功能函数写成它的成员函数,但那是锦上添花,核心功能跑通之前不要动。
4.4 编译环境配置,别在这卡半天
环境这件事,我得单独说一段,因为它耗费的时间往往比写代码还多。Windows 下我推荐VS Code + MinGW-w64组合,轻量、免费、够用。装完 MinGW 之后,把它的bin目录加进系统环境变量Path,然后在终端敲g++ --version,能输出版本号就说明配置成功。
接下来在 VS Code 里建三个配置文件。c_cpp_properties.json管头文件路径和语法提示,tasks.json管编译,launch.json管调试。编译命令我通常写成这样:
{ "version": "2.0.0", "tasks": [ { "label": "build-campus-map", "type": "shell", "command": "g++", "args": [ "-g", "-std=c++17", "-fexec-charset=GBK", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "group": { "kind": "build", "isDefault": true } } ] }其中-fexec-charset=GBK是关键的一行。Windows 终端默认用 GBK 编码显示中文,而 VS Code 保存的源文件多半是 UTF-8,不加这个参数直接跑,中文地名会变成一堆问号或者方块。加上它之后,编译器会把字符串常量转成 GBK 输出,显示就正常了。如果你用的是较新的 Windows Terminal 并已经设置chcp 65001,也可以去掉这个参数,两种方案二选一,不要同时用,否则会重复转换反而乱码。
提示:换电脑或者换机房演示之前,一定先在自己机器上把整个项目拷过去跑一遍。我见过太多人在答辩现场发现环境不一样、生成的 exe 打不开的尴尬场面。
5. 调试现场:那些年踩过的坑
5.1 输入缓冲区引发的连锁反应
这个坑前面提过,但它值得单独展开,因为它能引发一连串诡异现象。典型症状是:输入一个非数字之后,程序开始疯狂刷屏,或者后面的getline全部读到空字符串,导致地名全空。根因都一样——输入流进入了错误状态,或者缓冲区里残留了未被消费的字符。
判断方法很简单:在读取前后各打印一次变量的值,如果发现第二次读进来的东西和你输入的完全对不上,八成就是缓冲区问题。解决套路也固定:cin.clear()恢复状态,cin.ignore()清空残留。把这两个动作封装进readInt,全项目统一调用,这类问题就绝迹了。
5.2 中文乱码的三种成因
中文乱码在校园地图这个项目里几乎必然遇到,因为地名全是中文。它的成因主要有三种,得分开治。第一种是源码编码和编译器执行编码不一致,解法就是上面说的-fexec-charset=GBK或者把终端切成 UTF-8。第二种是文件读写时用了文本模式但编码不匹配,比如你手写的campus.txt是 UTF-8,程序按 GBK 读,地名就是乱码,解法是让文件和源码保持同一编码。第三种最隐蔽:用char数组存中文字符串,长度算错导致截断。中文在 GBK 下占 2 字节,在 UTF-8 下占 3 字节,char name[10]在 UTF-8 下只能放 3 个汉字。所以地名我坚持用std::string,长度自适应,不给自己找麻烦。
5.3 路径输出不对的对照表
路径类 bug 的排查,我总结了一张对照表,基本能覆盖九成以上的情况。
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 距离正确但路线断成两截 | pre数组没在松弛时同步更新 | 检查松弛分支里是否同时写了pre[v] = u |
| 输出“不可达”但实际有路 | 建边时忘了对称赋值 | 打印矩阵,检查graph[u][v]和graph[v][u]是否都等于权值 |
| 最短路绕了远路 | 初始化时把无边设成了 0 | 检查初始化,无边必须是 INF |
| 路径里出现同一个点两次 | DFS 忘了标记或忘了回溯 | 检查visited[v] = 1和= 0是否成对 |
| 距离变成一个大负数 | 用了 INT_MAX 做 INF,加法溢出 | 换成 0x3f3f3f3f |
| 输出到终点后多一个点 | pre链条没在起点终止 | 检查pre[start]是否设为 -1 |
这张表我建议直接贴在报告附录里,答辩的时候被问到“你怎么排查问题”,拿出来就是现成的答案,比空口说“我会用断点调试”有说服力得多。
5.4 数组越界与栈溢出
最后说两个“程序直接崩”的元凶。数组越界大多出在顶点编号上:如果你习惯从 0 开始循环,但顶点编号是从 1 开始的,那么graph[0][...]这一行永远读不到有用数据,或者某处i <= vNum写成了i <= MAXV,把未使用的下标也扫进去,输出就会掺进垃圾值。判断方法是在所有循环边界处打印一下vNum和循环变量的范围,对不上就说明越界了。
栈溢出的元凶基本锁定在 DFS 上。要么是漏了visited判断导致无限递归,要么是图里有自环(graph[v][v]不为 0),要么是路径空间太大。校园地图这个规模,正常的 DFS 深度最多几十层,绝对不会爆栈;一旦爆了,就是逻辑错误,别去调栈大小,去查代码。
6. 让这个课设再往前一步
6.1 从“最短”到“最优”的加权改造
基础版的最短路只考虑距离,但真实的校园导航其实可以更细腻。一个很容易实现又很出效果的改造是多因素加权:把每条路的代价定义为距离 * w1 + 拥挤度 * w2,其中拥挤度可以用 1 到 5 的整数手动标注。这样你就能回答“现在这个点去食堂,走哪条路不容易堵”这种问题。实现上完全不用改算法,Dijkstra 和 Floyd 照样跑,只是把邻接矩阵里的值从纯距离换成一个综合代价。这个思路在报告里写成“算法与业务解耦”的论述,会显得你对算法本质理解得比较透。
另一个方向是加入方向性,把无向图改成有向图,模拟某一时段只允许单向通行的路段。改动量很小,只是建边的时候不再对称赋值,但要注意 Dijkstra 在有向图上依然成立,因为权值仍然是正的。
6.2 展示和交互层面的小提升
功能对之外,体验也是可以打磨的。比如输出路径时,不光打印地名,顺便把每一段的距离和总距离一起打出来,用户一眼就知道哪一段最长。比如给每个景点加一句简介,查询的时候顺带显示,整个项目立刻从“算法练习”变成“校园导览”。再比如把查询历史记录到一个日志文件里,或者在控制台里用\t对齐输出所有地点,让它看起来像一张整洁的表格而不是一坨文字。
我自己在这个项目里最后加的一件事,是把地名和简介做成可编辑的数据文件,然后在报告里附了一张修改前后的对比截图,说明整个系统的数据是可配置的。这个小细节让我在评分时拿到了“工程完整性”那一项的分。写代码这事儿,功能跑通只是及格线,能不能让人一眼看出你想过“别人怎么用它”,才是拉开差距的地方。
关于扩展我最后再补一句实在话:所有扩展都应该在核心查询稳定之后再做。我见过太多人一开始就想着加语音播报、加图形界面,结果最基础的路径都算不对,最后交上去的东西四不像。先把那 300 行核心代码跑通、跑准、跑稳,剩下的都是加法,随时可以往上叠。