1. 项目概述
今天想和大家分享两个来自KamaCoder平台的经典算法题目——"47 参加科学大会"和"94 城市间货物运输 Ⅰ"。这两个题目都涉及到图论中的最短路径问题,是算法学习路上的重要里程碑。作为刷过300+算法题的过来人,我特别理解这类题目给初学者带来的困惑,所以决定用最直白的方式拆解解题思路。
这两个题目看似不同,实则内核一致:都需要在加权图中寻找最优路径。前者需要考虑时间成本,后者则关注运输费用。在实际开发中,这类算法应用广泛,比如导航软件路线规划、物流配送优化、网络路由选择等场景。掌握它们,你就能解决一大类现实中的优化问题。
2. 核心算法解析
2.1 Dijkstra算法精要
Dijkstra算法是解决非负权图单源最短路径的经典方法。它的核心思想是贪心策略:每次从尚未确定最短路径的顶点中,选取距离起点最近的一个,然后更新其邻接点的距离。
算法步骤如下:
- 初始化:起点距离为0,其他顶点距离为∞
- 选择当前距离最小的未处理顶点u
- 对u的所有邻接点v进行松弛操作:
dist[v] = min(dist[v], dist[u] + weight(u,v)) - 标记u为已处理
- 重复2-4直到所有顶点处理完毕
重要提示:Dijkstra不能处理负权边!如果图中存在负权,需要使用Bellman-Ford算法。
2.2 Bellman-Ford算法要点
Bellman-Ford算法可以处理带负权边的图,还能检测负权环。它的基本思路是通过对所有边进行V-1轮松弛操作(V是顶点数),逐步逼近最短路径。
算法流程:
- 初始化:起点距离为0,其他顶点距离为∞
- 进行V-1轮松弛操作
- 检查是否存在可继续松弛的边,若有则说明存在负权环
时间复杂度O(VE),比Dijkstra的O(V^2)或O(ElogV)要慢,但适用性更广。
3. 题目实战解析
3.1 KamaCoder 47 参加科学大会
题目大意:科学家需要参加n个会议,每个会议有固定时间段,场地间移动需要时间,求最多能参加多少个会议。
解题思路:
- 将会议按结束时间排序
- 构建有向图:若会议i结束后能赶到会议j,则建立边i→j,权重为1(表示可以参加)
- 问题转化为求DAG上的最长路径,可以用拓扑排序+动态规划解决
关键代码片段(Python):
python复制def maxMeetings(n, meetings, travel):
meetings.sort(key=lambda x: x[1]) # 按结束时间排序
dp = [1]*n
for i in range(n):
for j in range(i):
if meetings[j][1] + travel[j][i] <= meetings[i][0]:
dp[i] = max(dp[i], dp[j]+1)
return max(dp)
3.2 KamaCoder 94 城市间货物运输 Ⅰ
题目描述:在n个城市间运输货物,有些道路是双向的,有些是单向的,可能有负权边(表示有补贴的运输路线),求从城市1到城市n的最低成本。
解决方案:
- 因为有负权边,必须使用Bellman-Ford算法
- 需要额外检测负权环,若存在则说明可以无限降低成本
- 使用邻接表存储图结构更高效
实现示例(C++):
cpp复制struct Edge {
int from, to, cost;
};
int bellman_ford(vector<Edge>& edges, int n, int start, int end) {
vector<int> dist(n+1, INT_MAX);
dist[start] = 0;
for(int i=1; i<n; ++i) {
for(auto& e : edges) {
if(dist[e.from] != INT_MAX && dist[e.to] > dist[e.from] + e.cost) {
dist[e.to] = dist[e.from] + e.cost;
}
}
}
// 检查负权环
for(auto& e : edges) {
if(dist[e.from] != INT_MAX && dist[e.to] > dist[e.from] + e.cost) {
return -1; // 存在负权环
}
}
return dist[end] == INT_MAX ? -1 : dist[end];
}
4. 算法优化与比较
4.1 Dijkstra的堆优化
原始Dijkstra时间复杂度O(V^2),使用优先队列可优化到O(ElogV):
python复制import heapq
def dijkstra_heap(graph, start):
n = len(graph)
dist = [float('inf')] * n
dist[start] = 0
heap = [(0, start)]
while heap:
current_dist, u = heapq.heappop(heap)
if current_dist > dist[u]:
continue
for v, weight in graph[u]:
if dist[v] > dist[u] + weight:
dist[v] = dist[u] + weight
heapq.heappush(heap, (dist[v], v))
return dist
4.2 SPFA算法
SPFA是Bellman-Ford的队列优化版本,平均时间复杂度O(E),最坏情况仍为O(VE):
java复制int SPFA(List<List<Edge>> graph, int start, int end) {
int n = graph.size();
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
Queue<Integer> queue = new LinkedList<>();
boolean[] inQueue = new boolean[n];
queue.offer(start);
inQueue[start] = true;
while (!queue.isEmpty()) {
int u = queue.poll();
inQueue[u] = false;
for (Edge e : graph.get(u)) {
if (dist[e.to] > dist[u] + e.cost) {
dist[e.to] = dist[u] + e.cost;
if (!inQueue[e.to]) {
queue.offer(e.to);
inQueue[e.to] = true;
}
}
}
}
return dist[end];
}
5. 常见问题与调试技巧
5.1 典型错误排查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 结果比预期大 | 未初始化距离数组 | 确保dist数组初始值为∞,起点为0 |
| 程序陷入死循环 | 存在负权环 | 实现负权环检测逻辑 |
| 结果不正确 | 图的存储方式错误 | 检查邻接表/矩阵构建是否正确 |
| 堆优化版本出错 | 未处理重复节点 | 添加dist[u] < current_dist判断 |
5.2 调试心得
- 小规模测试:先用3-5个节点的简单图验证基本逻辑
- 打印中间结果:在松弛步骤后打印dist数组变化
- 边界检查:特别注意n=0或1的情况
- 性能分析:对于大规模图,使用优先队列优化是必须的
实战技巧:在竞赛中,可以先写Bellman-Ford确保正确性,再优化为SPFA。日常工程中,Dijkstra的堆优化实现更常用。
6. 实际应用扩展
最短路径算法在现实中有广泛应用:
- 导航系统:计算两点间最快路线
- 网络路由:数据包传输路径选择
- 物流配送:最优运输路线规划
- 社交网络:人际关系链分析
- 游戏开发:NPC寻路算法
以物流系统为例,假设我们要开发一个货物运输调度系统:
python复制class LogisticsSystem:
def __init__(self, cities):
self.graph = defaultdict(list)
self.city_index = {city: i for i, city in enumerate(cities)}
def add_route(self, from_city, to_city, cost, bidirectional=False):
i = self.city_index[from_city]
j = self.city_index[to_city]
self.graph[i].append((j, cost))
if bidirectional:
self.graph[j].append((i, cost))
def calculate_cost(self, origin, destination):
start = self.city_index[origin]
end = self.city_index[destination]
return self.dijkstra(start)[end]
def dijkstra(self, start):
# 实现略
pass
7. 进阶学习建议
掌握了基础的最短路径算法后,可以继续深入以下方向:
- A*算法:带启发式的最短路径搜索
- Floyd-Warshall:全源最短路径算法
- 差分约束系统:用最短路径建模约束条件
- 双标准最短路径:同时优化两个指标(如时间和成本)
- 动态路由算法:OSPF等网络协议中的实现
推荐练习题:
- LeetCode 743. Network Delay Time
- LeetCode 787. Cheapest Flights Within K Stops
- POJ 3259 Wormholes (检测负权环)
- Codeforces 20C Dijkstra? (堆优化实现)
我在最初学习时,曾在负权环的判断上栽过跟头。后来发现一个实用技巧:在Bellman-Ford完成后,再对所有边检查一次,如果还能松弛,就说明存在负权环。这个经验帮我解决了不少类似问题。
