思路:从i
到j
点最多经过n-1
条边,因此对这n-1
条边进行遍历,对于每条边找能更新的,即每次对当前的m
条边进行松弛
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
int n, m, s;
int u[maxn], v[maxn], w[maxn];//u,v,w分别记录一条边的起点、终点和权值
int d[maxn];//距离数组,维护起点到每个点的距离
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) cin >> u[i] >> v[i] >> w[i];
//初始化
fill(d, d + maxn, inf);
d[s] = 0;
for (int i = 1; i <= n - 1; ++i) {
int flag = 0;//若某次遍历没有更新数组d,则后面也不会更新d(因为是用当前的d更新d的其他部分),直接break
for (int j = 1; j <= m; ++j)
if (d[v[j]] > d[u[j]] + w[j]) {
flag = 1;
d[v[j]] = d[u[j]] + w[j];
}
if (!flag) break;
}
//输出
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}
思路:对 Bellman–Ford 进行改进,方法一搜寻边时过于盲目。只有在上一次数组d
改变的点才会造成下一次d
的改变,因此使用队列仅将队首相邻的结点入队列,防止盲目搜索。
SPFA 与 BFS 十分相似,区别是 SPFA 可以重复入队列,在出队列后要将inq
数组置0,表明可以重新入队列。
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
vector<pair<int, int> >E[maxn];//存每条边的边集,first为边终点,second为边权
int n, m, s;
int d[maxn];
bool vis[maxn];
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) {
int u, v, w; cin >> u >> v >> w;
E[u].push_back({v, w});
}
//初始化
fill(d, d + maxn, inf);
queue<int>q;
q.push(s), d[s] = 0, vis[s] = 1;
while (!q.empty()) {
int now = q.front(); q.pop(); vis[now] = 0;//可重复入队列
for (int i = 0; i < E[now].size(); ++i) {
int v = E[now][i].first;//边终点
if (d[v] > d[now] + E[now][i].second) {//需要松弛
d[v] = d[now] + E[now][i].second;
if (vis[v]) continue;//已在队列则不入队
vis[v] = 1; q.push(v);
}
}
}
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}
思路:使用 SLF(Small Label First) 对 SPFA 进行优化。将队列改为双端队列,对要加入队列的点now
,如果d[now]
代码与 SPFA 相同,仅在第 30 行有改动
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
vector<pair<int, int> >E[maxn];//存每条边的边集,first为边终点,second为边权
int n, m, s;
int d[maxn];
bool vis[maxn];
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) {
int u, v, w; cin >> u >> v >> w;
E[u].push_back({v, w});
}
//初始化
fill(d, d + maxn, inf);
deque<int>q;
q.push_back(s), d[s] = 0, vis[s] = 1;
while (!q.empty()) {
int now = q.front(); q.pop_front(); vis[now] = 0;//可重复入队列
for (int i = 0; i < E[now].size(); ++i) {
int v = E[now][i].first;//边终点
if (d[v] > d[now] + E[now][i].second) {//需要松弛
d[v] = d[now] + E[now][i].second;
if (vis[v]) continue;//已在队列则不入队
vis[v] = 1;
//SLF优化:若当前结点距离<队首距离,将当前结点放在队首
if (!q.empty() && d[v] < d[q.front()]) q.push_front(v);
else q.push_back(v);
}
}
}
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}
思路:使用 LLL(Large Label Last) 对 SPFA 进行优化。对每个要出队的队首元素u
,比较d[u]
和队列中点的d
的平均值,如果 d[u]
更大,将其弹出放到队尾,再取队首元素进行相同操作,直到队首元素的 d
≤ ≤ ≤ 平均值。
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
vector<pair<int, int> >E[maxn];//存每条边的边集,first为边终点,second为边权
int n, m, s;
int d[maxn];
bool vis[maxn];
int sum;//队列中点的d数组之和
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) {
int u, v, w; cin >> u >> v >> w;
E[u].push_back({v, w});
}
//初始化
fill(d, d + maxn, inf);
queue<int>q;
q.push(s), d[s] = 0, vis[s] = 1;
sum = d[s];
while (!q.empty()) {
int now = q.front();
//LLL优化
while (q.size() * d[now] > sum) {//队首元素的d高于平均值,将其放到队尾
q.pop(); q.push(now);
now = q.front();
}
q.pop(); vis[now] = 0;//可重复入队列
sum -= d[now];
for (int i = 0; i < E[now].size(); ++i) {
int v = E[now][i].first;//边终点
if (d[v] > d[now] + E[now][i].second) {//需要松弛
d[v] = d[now] + E[now][i].second;
if (vis[v]) continue;//已在队列则不入队
vis[v] = 1; q.push(v); sum += d[v];
}
}
}
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}
思路:这两种优化方法并不相互干扰,因此可以同时使用
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
vector<pair<int, int> >E[maxn];//存每条边的边集,first为边终点,second为边权
int n, m, s;
int d[maxn];
bool vis[maxn];
int sum;//队列中点的d数组之和
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) {
int u, v, w; cin >> u >> v >> w;
E[u].push_back({v, w});
}
//初始化
fill(d, d + maxn, inf);
deque<int>q;
q.push_back(s), d[s] = 0, vis[s] = 1;
sum = d[s];
while (!q.empty()) {
int now = q.front();
//LLL优化
while (q.size() * d[now] > sum) {//队首元素的d高于平均值,将其放到队尾
q.pop_front(); q.push_back(now);
now = q.front();
}
q.pop_front(); vis[now] = 0;//可重复入队列
sum -= d[now];
for (int i = 0; i < E[now].size(); ++i) {
int v = E[now][i].first;//边终点
if (d[v] > d[now] + E[now][i].second) {//需要松弛
d[v] = d[now] + E[now][i].second;
if (vis[v]) continue;//已在队列则不入队
vis[v] = 1; sum += d[v];
//SLF优化
if (!q.empty() && d[v] < d[q.front()]) q.push_front(v);
else q.push_back(v);
}
}
}
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}
以洛谷 P3371 【模板】单源最短路径(弱化版)为例比较优化结果。
朴素的 SPFA:
SLF 优化的 SPFA:
LLL 优化的 SPFA:
SLF+LLL 优化的 SPFA:
思路:用优先队列优化 SPFA,不需要vis
队列,将队列修改为优先队列。注意 Dijkstra 算法每次遍历找到的都为最短路径,则已经更新过的点无需更新。时间复杂度为 O ( m l o g n ) O(mlogn) O(mlogn)
#include
using namespace std;
const int maxn = 1e6;
const int inf = 0x7fffffff;
vector<pair<int, int> >E[maxn];//存每条边的边集,first为边终点,second为边权
int n, m, s;
int d[maxn];
bool vis[maxn];
int main() {
cin >> n >> m >> s;
for (int i = 1; i <= m; ++i) {
int u, v, w; cin >> u >> v >> w;
E[u].push_back({v, w});
}
fill(d, d + maxn, inf);
priority_queue<pair<int,int> >q;
d[s] = 0;
q.push({-d[s], s});
while (!q.empty()) {
int now = q.top().second; q.pop();
if (vis[now]) continue;
vis[now] = true;
for (int i = 0; i < E[now].size(); ++i) {
int v = E[now][i].first;//边终点
if (d[v] > d[now] + E[now][i].second) {//需要松弛
d[v] = d[now] + E[now][i].second;
q.push({-d[v],v});
}
}
}
for (int i = 1; i <= n; ++i) cout << d[i] << ' ';
return 0;
}