1. 什么是单源最短路问题?
- 定义:给定一个图(有向或无向),每条边有一个权重(可以是正数、负数或零),以及一个源点 s s s(起始节点),目标是找到从 s s s到所有其他节点的最短路径长度(即路径上所有边权重之和的最小值)。
- 重要性:
广泛应用于现实世界,如地图导航(求最短行驶距离)、网络路由(求最小延迟路径)等。广泛地出现在NOIP T2/T3 中。 - 关键术语:
- 节点(顶点):图中的点。
- 边:连接节点的线,权重用 w ( u , v ) w(u,v) w(u,v)表示(例如,边 ( u , v ) (u,v) (u,v)的权重)。
- 源点:固定起点 s s s。
- 最短路径长度:从 s s s到节点 v v v的最小权重和,记为 d i s t [ v ] dist[v] dist[v]。
2. 基本概念和前提
- 图类型:可以是稀疏图(边少)或稠密图(边多),权重可正可负。
- 核心思想:通过“松弛”操作更新距离估计。松弛是指:对于边 ( u , v ) (u,v) (u,v),如果 d i s t [ v ] > d i s t [ u ] + w ( u , v ) dist[v] > dist[u] + w(u,v) dist[v]>dist[u]+w(u,v),则更新 d i s t [ v ] dist[v] dist[v]为 d i s t [ u ] + w ( u , v ) dist[u] + w(u,v) dist[u]+w(u,v)。
- 问题分类:
- 非负权图:所有边权重$ \geq 0$,适合Dijkstra算法。
- 含负权图:允许负权重,但需避免负权环(否则无解),适合SPFA算法。
- 新手提示:先从简单例子入手,理解权重和路径的概念。
3. Dijkstra算法(非负权图专用)
- 适用条件:图中所有边权重非负(即 w ( e ) ≥ 0 w(e) \geq 0 w(e)≥0)。如果图有负权,结果可能错误。
- 原理:贪心策略。每次选择当前距离最小的未处理节点,逐步扩展,确保每一步都是局部最优。
- 步骤详解(简单版):
- 初始化:设 d i s t [ s ] = 0 dist[s] = 0 dist[s]=0,其他 d i s t [ v ] = ∞ dist[v] = \infty dist[v]=∞(一个大数),所有节点标记为未访问。
- 循环:从所有未访问节点中选取 d i s t [ u ] dist[u] dist[u]最小的节点 u u u。
- 松弛:对 u u u的每个邻居 v v v,检查是否 d i s t [ v ] > d i s t [ u ] + w ( u , v ) dist[v] > dist[u] + w(u,v) dist[v]>dist[u]+w(u,v),如果是则更新 d i s t [ v ] dist[v] dist[v]。
- 标记 u u u为已访问。
- 重复步骤2-4,直到所有节点访问完。
- 时间复杂度:使用优先队列时为 O ( ( V + E ) log V ) O((V+E)\log V) O((V+E)logV)( V V V为节点数, E E E为边数)。
- 优点:高效、简单。
- 代码示例(C++):
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6+10;
int head[N], dis[N], cnt;
bool vis[N];
int n, m, s;
struct edge { int to, dis, next; } e[N];
inline void add(int u, int v, int d)
{
cnt++;
e[cnt].dis = d;
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
struct node
{
int dis;
int pos;
bool operator < (const node &x)const { return x.dis < dis; }
};
priority_queue <node> q;
void dijkstra()
{
dis[s] = 0;
q.push((node){0, s});
while(!q.empty())
{
node tmp = q.top();
q.pop();
int x = tmp.pos, d = tmp.dis;
if(vis[x]) continue;
vis[x] = 1;
for (int i = head[x]; i; i = e[i].next)
{
int y = e[i].to;
if (dis[y] > dis[x] + e[i].dis)
{
dis[y] = dis[x] + e[i].dis;
if(!vis[y])
q.push(( node ){dis[y], y});
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> s;
for(int i = 1; i <= n; ++i)
dis[i] = INT_MAX;
for( register int i = 0; i < m; ++i )
{
register int u, v, d;
cin >> u >> v >> d;
add( u, v, d );
}
dijkstra();
for( int i = 1; i <= n; i++ )
cout << dis[i] << ' ';
return 0;
}
[Luogu模版提P4779](https://www.luogu.com.cn/problem/P4779)
#### 4. **SPFA算法(含负权图专用)**
*其实并非专用,而是Dijkstra无法解决负环图问题,在一般情况下Dijkstra比SPFA效率高*
- **适用条件**:允许边权重为负,但图中不能有负权环(否则无解)。SPFA是Bellman-Ford算法的优化版。
- **原理**:基于动态逼近。使用队列存储待处理节点,只对距离更新的节点进行松弛,避免重复计算。
- **步骤详解**:
1. 初始化:$dist[s] = 0$,其他$dist[v] = \infty$;队列$q$初始含$s$。
2. 循环:从队列取节点$u$。
3. 松弛:对$u$的每个邻居$v$,如果$dist[v] > dist[u] + w(u,v)$,则更新$dist[v]$,并将$v$加入队列(如果$v$不在队列中)。
4. 重复步骤2-3,直到队列空。
- **负环检测**:如果某个节点入队次数超过$V$次(节点总数),则存在负权环(问题无解)。
- **时间复杂度**:平均$O(E)$,最坏$O(VE)$。
- **优点**:比Bellman-Ford高效,适合网络优化等有负权场景。
- **代码示例(C++)**:
首先定义两个数组
```cpp
int dist[maxn];//dist[i]到i点的最短路长度
bool inq[maxn];//inq[i]代表i点是否在队列中
接下来直接跑一遍SPFA
inline void spfa(int s)
{
// 初始化
memset(dist,0x3f,sizeof(dist));
dist[s]=0;
queue<int> q;//用来存储可能改变其他点最短路的点
q.push(s);
inq[s] = 1;
while (q.size() != 0)
{
// 取出队首
int a = q.front();
q.pop();
inq[a] = false;
for (int i = 0; i < e[a].size(); i++)
{
// 存边
int b = e[a][i].first;
int c = e[a][i].second;
// 松弛
if (dist[b] > dist[a] + c)
{
dist[b] = dist[a] + c;
if (!inq[b])
{
inq[b] = 1;
q.push(b);
}
}
}
}
}
5. 补充:差分约束系统
- 定义:一组线性不等式约束,形式如 x j − x i ≤ b k x_j - x_i \leq b_k xj−xi≤bk(其中 x i , x j x_i, x_j xi,xj是变量, b k b_k bk是常数)。例如,在任务调度中, x j x_j xj表示任务结束时间, x i x_i xi表示开始时间。
- 与单源最短路的联系:差分约束问题可以转化为单源最短路问题。具体步骤:
- 构建图:每个变量对应一个节点;每个约束 x j − x i ≤ b k x_j - x_i \leq b_k xj−xi≤bk对应一条边 ( i , j ) (i,j) (i,j),权重 w ( i , j ) = b k w(i,j) = b_k w(i,j)=bk。
- 添加源点:引入一个虚拟源点 s s s,添加边 ( s , v ) (s,v) (s,v),权重 0 0 0(确保所有点可达)。
- 求解:运行SPFA算法(因可能含负权),得到 d i s t [ v ] dist[v] dist[v]即为变量 x v x_v xv的值。
- 数学解释:约束 x j − x i ≤ b k x_j - x_i \leq b_k xj−xi≤bk等价于 d i s t [ j ] ≤ d i s t [ i ] + b k dist[j] \leq dist[i] + b_k dist[j]≤dist[i]+bk,这正是松弛操作的目标。
- 适用场景:资源分配、时间表规划等。
- 简单示例:
- 约束: x 2 − x 1 ≤ 3 x_2 - x_1 \leq 3 x2−x1≤3, x 3 − x 2 ≤ − 1 x_3 - x_2 \leq -1 x3−x2≤−1。
- 转化图:节点 x 1 , x 2 , x 3 x_1, x_2, x_3 x1,x2,x3;边 ( x 1 , x 2 ) (x_1,x_2) (x1,x2)权重3, ( x 2 , x 3 ) (x_2,x_3) (x2,x3)权重-1;添加源点 s s s到所有点权重0。
- 求解:SPFA输出 d i s t dist dist即变量值。
- 新手提示:先掌握Dijkstra和SPFA,再尝试此应用。
6. 总结与比较
- 算法选择指南:
- 权重全非负?用Dijkstra(更快)。
- 有负权重?用SPFA(需检测负环)。
- 差分约束问题?转化为SPFA求解。
- 常见陷阱:
- 负权环:SPFA中需主动检测。
- 初始化:确保 d i s t [ s ] = 0 dist[s] = 0 dist[s]=0。
- 学习建议:
- 先理解算法步骤,再动手写代码。
- 用简单图测试(如3-5个节点)。
1145

被折叠的 条评论
为什么被折叠?



