全源最短路模版

/*
多源最短路径 floyd_warshall 复杂度O(n^3) 
传入图的大小和邻接阵
返回各点间最短距离min[][]和路径pre[][]
pre[i][j]记录i到j最短路径上j的父结点
可更改路权类型,路权必须非负!
*/
#define MAXN 200
#define inf 1000000000
typedef int elem_t;

void floyd_warshall(int n, elem_t mat[][MAXN], elem_t min[][MAXN], int pre[][MAXN]){
	int i, j, k;
	for (i = 0; i < n; i++)
		for (j = 0; j < n; j++)
			min[i][j] = mat[i][j], pre[i][j] = (i == j) ? -1 : i;
	for (k = 0; k < n; k++)
		for (i = 0; i < n; i++)
			for (j = 0; j < n; j++)
				if (min[i][k] + min[k][j] < min[i][j])
					min[i][j] = min[i][k] + min[k][j], pre[i][j] = pre[k][j];
}

# P5905 【模板】全源短路(Johnson) ## 题目描述 给定一个包含 $n$ 个结点和 $m$ 条带权边的有向图,求所有点对间的短路径长度,一条路径的长度定义为这条路径上所有边的权值和。 注意: 1. 边权**可能**为负,且图中**可能**存在重边和自环; 2. 部分数据卡 $n$ 轮 SPFA 算法。 ## 输入格式 第 $1$ 行:$2$ 个整数 $n,m$,表示给定有向图的结点数量和有向边数量。 接下来 $m$ 行:每行 $3$ 个整数 $u,v,w$,表示有一条权值为 $w$ 的有向边从编号为 $u$ 的结点连向编号为 $v$ 的结点。 ## 输出格式 若图中存在负环,输出仅一行 $-1$。 若图中不存在负环: 输出 $n$ 行:令 $dis_{i,j}$ 为从 $i$ 到 $j$ 的短路,在第 $i$ 行输出 $\sum\limits_{j=1}^n j\times dis_{i,j}$,注意这个结果可能超过 int 存储范围。 如果不存在从 $i$ 到 $j$ 的路径,则 $dis_{i,j}=10^9$;如果 $i=j$,则 $dis_{i,j}=0$。 ## 输入输出样例 #1 ### 输入 #1 ``` 5 7 1 2 4 1 4 10 2 3 7 4 5 3 4 2 -2 3 4 -3 5 3 4 ``` ### 输出 #1 ``` 128 1000000072 999999978 1000000026 1000000014 ``` ## 输入输出样例 #2 ### 输入 #2 ``` 5 5 1 2 4 3 4 9 3 4 -3 4 5 3 5 3 -2 ``` ### 输出 #2 ``` -1 ``` ## 说明/提示 【样例解释】 左图为样例 $1$ 给出的有向图,短路构成的答案矩阵为: ``` 0 4 11 8 11 1000000000 0 7 4 7 1000000000 -5 0 -3 0 1000000000 -2 5 0 3 1000000000 -1 4 1 0 ``` 右图为样例 $2$ 给出的有向图,红色标注的边构成了负环,注意给出的图不一定连通。 ![](https://cdn.luogu.com.cn/upload/image_hosting/7lb35u4u.png) 【数据范围】 对于 $100\%$ 的数据,$1\leq n\leq 3\times 10^3,\ \ 1\leq m\leq 6\times 10^3,\ \ 1\leq u,v\leq n,\ \ -3\times 10^5\leq w\leq 3\times 10^5$。 对于 $20\%$ 的数据,$1\leq n\leq 100$,不存在负环(可用于验证 Floyd 正确性) 对于另外 $20\%$ 的数据,$w\ge 0$(可用于验证 Dijkstra 正确性) upd. 添加一组 Hack 数据:针对 SPFA 的 SLF 优化
最新发布
07-18
<think>我们正在讨论Johnson算法,用于求解全源短路径问题。Johnson算法适用于稀疏图,特别是当图中包含负权边(但不含负权环)时。它通过重新赋予权值(reweighting)来消除负权边,然后利用Dijkstra算法(要求非负权值)来高效计算所有点对之间的短路径。 Johnson算法的步骤如下: 1. 向图中添加一个新节点(记为s),并添加从s指向所有原图中节点的边,且这些边的权值为0。 2. 以s为源点运行Bellman-Ford算法(或SPFA),计算从s到所有原图节点的短路径,记为h(v)。如果检测到负权环,则算法终止(因为无法计算短路径)。 3. 移除新添加的节点s及其相关边,恢复原图。 4. 对原图的每条边(u, v)进行重新赋权:新的权值为 w(u,v) + h(u) - h(v)。(这个重新赋权保证所有边权非负,且不会改变短路径) 5. 对每个原图中的节点v,运行Dijkstra算法(使用新的权值),得到从v到其他节点的短路径距离。 6. 后,将Dijkstra算法得到的短路径距离还原:实际距离d(u,v) = d'(u,v) - h(u) + h(v),其中d'(u,v)是重新赋权后的图计算出的距离。 现在,我们给出一个C++实现,并逐行解析。注意,我们将使用邻接表表示图,并使用优先队列优化Dijkstra算法。同时,我们使用SPFA(Bellman-Ford的一种优化)来执行第一步中的短路径计算。 代码实现如下: ```cpp #include <iostream> #include <vector> #include <queue> #include <climits> using namespace std; typedef long long ll; typedef pair<ll, ll> pll; const ll INF = 1e18; // 表示无穷大 // 边结构体 struct Edge { ll v, w; Edge(ll v, ll w) : v(v), w(w) {} }; vector<vector<Edge>> graph; // 图的邻接表表示 ll n; // 节点数 // 添加一条从u到v的权值为w的有向边 void addEdge(ll u, ll v, ll w) { graph[u].push_back(Edge(v, w)); } // SPFA算法:计算从源点s到所有点的短路径,结果存储在dist中 bool spfa(ll s, vector<ll>& dist) { dist.assign(n+1, INF); vector<ll> cnt(n+1, 0); vector<bool> inQueue(n+1, false); queue<ll> q; dist[s] = 0; q.push(s); inQueue[s] = true; cnt[s] = 1; while (!q.empty()) { ll u = q.front(); q.pop(); inQueue[u] = false; for (auto& edge : graph[u]) { ll v = edge.v, w = edge.w; if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (!inQueue[v]) { q.push(v); inQueue[v] = true; cnt[v]++; // 如果某个点入队超过n次,说明存在负环 if (cnt[v] > n) { return false; // 存在负环 } } } } } return true; // 不存在负环 } // Dijkstra算法:计算从源点s出发的短路径,结果存储在dist中(使用新权值) void dijkstra(ll s, vector<ll>& dist) { dist.assign(n+1, INF); priority_queue<pll, vector<pll>, greater<pll>> pq; // 小堆 dist[s] = 0; pq.push({0, s}); while (!pq.empty()) { ll d = pq.top().first; ll u = pq.top().second; pq.pop(); if (d != dist[u]) continue; // 跳过旧信息 for (auto& edge : graph[u]) { ll v = edge.v, w = edge.w; if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } } int main() { ll m; cin >> n >> m; graph.resize(n+2); // 多开两个位置,其中一个用于新节点(编号为0,原图节点从1到n) // 添加原图的边 for (ll i = 0; i < m; i++) { ll u, v, w; cin >> u >> v >> w; addEdge(u, v, w); } // 添加新节点0,并添加从0到所有原图节点(1~n)的边,权值为0 for (ll i = 1; i <= n; i++) { addEdge(0, i, 0); } vector<ll> h(n+1, INF); // h[i]表示从0到i的短路径 // 运行SPFA,计算从0出发的短路径 if (!spfa(0, h)) { cout << -1 << endl; // 存在负环 return 0; } // 移除新节点0及其相关的边(这里我们通过重新构建图来移除,但注意我们只移除0相关的边,同时保留原图边) // 注意:我们不需要实际移除,而是重新构建原图(不含0节点)的邻接表,但为了效率,我们直接修改原图的边权 // 但是,我们接下来要运行Dijkstra,所以需要修改图?实际上,我们可以不修改原图,而是在运行Dijkstra时使用新的权值 // 因此,我们修改图:将每条边(u,v)的权值更新为 w + h[u] - h[v] for (ll u = 1; u <= n; u++) { for (auto& edge : graph[u]) { edge.w += h[u] - h[edge.v]; // 更新边权 } } // 现在,对每个节点运行Dijkstra for (ll i = 1; i <= n; i++) { vector<ll> dist; // 存储从i出发的短路径(新权值) dijkstra(i, dist); // 计算实际的短路径:将新权值下的距离还原 for (ll j = 1; j <= n; j++) { if (dist[j] == INF) { cout << "INF "; } else { // 还原:dist[j] - h[i] + h[j] cout << dist[j] - h[i] + h[j] << " "; } } cout << endl; } return 0; } ``` ### 代码解析 1. **数据结构**:使用邻接表`graph`存储图,每个节点是一个`Edge`结构体的列表,包含目标节点和边权。 2. **添加新节点**:在读取图之后,我们添加一个节点0,并添加从0到每个原图节点(1~n)的边,权值为0。 3. **SPFA算法**:以0为源点运行SPFA,计算到所有原图节点的短路径`h`。如果存在负环,则输出-1并退出。 4. **重新赋权**:遍历原图的每条边(注意,新节点0的边已经不需要了,但我们只处理原图节点1~n的出边),将边权更新为`w + h[u] - h[v]`。这个操作保证所有边权非负。 5. **Dijkstra算法**:对每个原图节点运行Dijkstra算法,计算重新赋权后的短路径。注意,这里Dijkstra使用的是非负权值。 6. **还原距离**:将Dijkstra计算出的距离`dist[j]`还原为实际的短路径距离:`dist[j] - h[i] + h[j]`。 ### 优化建议 1. **稀疏图优化**:Johnson算法在稀疏图上表现良好,因为Dijkstra算法使用优先队列优化,时间复杂度为$O((V+E)\log V)$。而SPFA算法在平均情况下为$O(E)$,坏情况下为$O(VE)$。因此,总时间复杂度为$O(VE + V(V+E)\log V)$,对于稀疏图($E=O(V)$)是可行的,但稠密图($E=O(V^2)$)则可能较慢。 2. **负环检测**:使用SPFA检测负环,如果存在负环则立即退出。 3. **内存优化**:在运行Dijkstra时,每次只计算一个源点的短路径,因此我们不需要存储所有点对的距离矩阵(除非需要)。但是,如果图很大,内存可能成为问题。可以考虑分批次处理。 4. **避免重建图**:我们在重新赋权时直接修改了原图的边权,这样在后续运行Dijkstra时就直接使用新权值。这样避免了重建图,但要注意如果原图需要保留,则应该备份。 5. **处理不连通图**:如果图不连通,那么从某些节点可能无法到达其他节点,此时Dijkstra计算出的距离为INF,我们在还原时直接输出INF(或自定义的无穷大符号)即可。 6. **使用更快的Dijkstra**:在C++中,使用`priority_queue`(默认是大堆)时,我们使用`greater`来构建小堆。也可以使用`std::set`或`std::multiset`,但优先队列通常更快。 7. **注意整数溢出**:由于重新赋权和还原过程中涉及加减,如果边权很大,可能需要使用足够大的数据类型(如`long long`)。 8. **避免重复初始化**:在多次运行Dijkstra时,注意每次都要重新初始化`dist`数组。 ### 相关问题 1. Johnson算法中为什么重新赋权不会改变短路径? 2. Johnson算法的时间复杂度是多少?与Floyd-Warshall算法相比有什么优势? 3. 如果图中存在负权环,Johnson算法如何处理? 4. 在重新赋权后,为什么所有边权都非负? 5. 如何修改Johnson算法以输出具体的短路径(而不仅仅是距离)?</think>### Johnson算法实现全源短路径的C++代码解析 Johnson算法是一种高效处理**稠密图中全源短路径**的算法,特别适用于包含负权边但无负权环的图。其核心思想是通过**重新赋权**消除负权边,再利用Dijkstra算法高效计算。以下是完整实现和逐行解析: ```cpp #include <iostream> #include <vector> #include <queue> #include <climits> using namespace std; typedef long long ll; const ll INF = 1e18; // 无穷大常量 // 边结构:目标节点v,边权w struct Edge { ll v, w; }; vector<vector<Edge>> graph; // 邻接表存图 ll n; // 节点数 // SPFA检测负环并计算势能函数h bool spfa(ll s, vector<ll>& h) { vector<ll> cnt(n + 1, 0); // 入队次数 vector<bool> inQueue(n + 1, false); queue<ll> q; h.assign(n + 1, INF); h[s] = 0; q.push(s); inQueue[s] = true; while (!q.empty()) { ll u = q.front(); q.pop(); inQueue[u] = false; for (auto& e : graph[u]) { if (h[u] + e.w < h[e.v]) { h[e.v] = h[u] + e.w; if (!inQueue[e.v]) { // 检测负环:入队次数超过n if (++cnt[e.v] > n) return false; q.push(e.v); inQueue[e.v] = true; } } } } return true; } // Dijkstra求单源短路(使用新边权) void dijkstra(ll s, vector<ll>& dist) { dist.assign(n + 1, INF); priority_queue<pair<ll, ll>, vector<pair<ll, ll>>, greater<>> pq; dist[s] = 0; pq.push({0, s}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d != dist[u]) continue; // 跳过旧数据 for (auto& e : graph[u]) { ll newDist = dist[u] + e.w; // 使用重新赋权后的边权 if (newDist < dist[e.v]) { dist[e.v] = newDist; pq.push({newDist, e.v}); } } } } int main() { ll m, u, v, w; cin >> n >> m; graph.resize(n + 2); // 多分配空间用于虚节点 // 1. 构建原图 for (ll i = 0; i < m; ++i) { cin >> u >> v >> w; graph[u].push_back({v, w}); } // 2. 添加虚节点0和零权边 for (ll i = 1; i <= n; ++i) graph[0].push_back({i, 0}); // 3. SPFA计算势能函数h vector<ll> h(n + 1, INF); if (!spfa(0, h)) { cout << "存在负权环!" << endl; return -1; } // 4. 边权重新赋权:w'(u,v) = w(u,v) + h[u] - h[v] for (ll u = 1; u <= n; ++u) for (auto& e : graph[u]) e.w += h[u] - h[e.v]; // 确保边权非负[^3] // 5. 对每个节点运行Dijkstra for (ll i = 1; i <= n; ++i) { vector<ll> dist; dijkstra(i, dist); // 6. 还原实际距离:d(u,v) = d'(u,v) - h[u] + h[v] for (ll j = 1; j <= n; ++j) { if (dist[j] == INF) cout << "INF "; else cout << dist[j] - h[i] + h[j] << " "; } cout << endl; } return 0; } ``` --- ### 关键步骤解析 1. **虚节点添加**(第43-44行) 添加虚拟节点0,并向所有原节点连接权值为0的边,用于后续势能函数计算。 2. **SPFA检测负环**(第46-50行) 以虚节点为源运行SPFA: - 若检测到负环(`cnt[e.v] > n`),立即终止[^3] - 计算势能函数$h$,满足三角不等式$h(v) \leq h(u) + w(u,v)$ 3. **边权重新赋权**(第53-55行) 对每条边$(u,v)$进行权值更新: $$ w'(u,v) = w(u,v) + h[u] - h[v] $$ 此操作保证所有边权非负,且不改变短路径结构[^2]。 4. **Dijkstra计算新图短路**(第58-62行) 对每个节点运行Dijkstra(使用优先队列优化),时间复杂度$O((V+E)\log V)$。 5. **距离还原**(第68行) 实际短距离计算: $$ d(u,v) = d'(u,v) - h[u] + h[v] $$ 其中$d'$是新图上的计算距离。 --- ### 算法优势与复杂度 - **时间复杂度**:$O(VE + V(V+E)\log V)$ 优于Floyd-Warshall的$O(V^3)$,尤其适合稀疏图[^1] - **空间复杂度**:$O(V+E)$ - **核心优势**: 允许负权边(无负环),复用高效Dijkstra实现 --- ### 优化建议 1. **堆优化**:使用斐波那契堆可将Dijkstra降至$O(E + V\log V)$ 2. **内存优化**:分批次计算避免存储全距离矩阵 3. **负环处理**:增加详细负环定位功能 4. **并行化**:不同源点的Dijkstra可并行计算 5. **稀疏图特化**:对链式前向星存储优化 > 示例输入: > 3 5 > 1 2 2 > 1 3 4 > 2 3 -1 > 3 1 1 > 2 1 3 > 输出矩阵: > 0 2 1 > 3 0 -1 > 1 3 0 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值