声明:这里简单聊聊我们Bellman-ford算法的思路,我也查了一些资料来进行辅助了解,我们主要掌握SPFA算法的思现,因为我们Bellman-ford算法的时间复杂度是稳定的O(VE)(其中V是顶点个数,E是边的个数),在大多数算法题目里这个时间复杂度已经很大了(打XCPC应该O(n^2)左右几乎都会卡)。而我们的SPFA算法平均情况下的时间复杂度是O(kE)(k是一个小于2的数),所以在大多数情况下SPFA算法都是比较友好的(针对有负权边的情况),不过少数情况出题人可能会造卡SPFA算法的数据,因为我们SPFA算法最差时间复杂度和Bellman-ford是一样的,在O(VE)。所以本章节内容主要学习掌握SPFA算法的思想和具体实现。
为什么要引入一个新的最短路径算法?我们在前面内容中提到过,Dijkstra主要用于实现没有负权边的算法,所以我们需要一个能够解决含有负权边问题的算法,就是Bellman-ford算法了,这里简单介绍一下Bellman-ford算法的思想。在此之前,我们介绍几个概念:
vector
负权环(负权回路):负权环就是在图中,存在一个环,这个环所有的边的权值和为负数。(我们可以很容易想到,在求最短路径的时候,如果起点经过一个负权环,再到终点,这种情况下是不存在最短路径的,因为我们起点可以经过无限个负权环,使得它从起点到终点的权值无限小。所以在解决带负权边问题时,如果有负权环,那么这个题目是无解的)。
Bellman-ford算法的思想(AI思路):
初始化:将除源点外的所有顶点的最短距离估计值设为正无穷,源点的最短距离设为 0。
迭代求解:对边集进行 | V|-1 次松弛操作(|V | 为顶点数)。每次松弛操作中,遍历每条边,若通过该边能使目标顶点的最短距离变小,则更新目标顶点的最短距离。
检验负权回路:再进行一次边集遍历,若仍能对边进行松弛操作,说明图中存在负权回路,返回 false;否则返回 true,此时得到的即为各顶点到源点的最短距离
代码一:(AI生成版本)
#include
#include
using namespace std;
struct Edge {
int u, v, w;
Edge(int a, int b, int c) : u(a), v(b), w(c) {}
};
bool bellmanFord(int n, vector& edges, int s, vector& dist) {
dist.assign(n, INT_MAX);
dist[s] = 0;
for (int i = 0; i < n - 1; ++i) {
for (const auto& edge : edges) {
int u = edge.u, v = edge.v, w = edge.w;
if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
}
}
}
// 检查负权回路
for (const auto& edge : edges) {
int u = edge.u, v = edge.v, w = edge.w;
if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
return false;
}
}
return true;
}
int main() {
int n = 5; // 顶点数
vector edges = {
Edge(0, 1, -1),
Edge(0, 2, 4),
Edge(1, 2, 3),
Edge(1, 3, 2),
Edge(1, 4, 2),
Edge(3, 2, 5),
Edge(3, 1, 1),
Edge(4, 3, -3)
};
vector dist;
int source = 0;
if (bellmanFord(n, edges, source, dist)) {
for (int i = 0; i < n; ++i) {
cout << "源点到顶点 " << i << " 的最短距离: " << dist[i] << endl;
}
} else {
cout << "图中存在负权回路" << endl;
}
return 0;
}
Bellman-ford算法的思想(个人总结):Bellman-ford算法的实现思路其实是非常简单的。初始时,我们需要构造一个距离数组d[maxn] 和 图结构体 vector
g[maxn](用来存一条边的起点和终点和这个边的边权)。初始时我们令距离数组全为inf。令d[s]=0。 接着我们开始进行n-1次(n是顶点数目)松弛操作。每次松弛操作遍历所有的边,如果加入这条边能够使起点到该顶点更近,那么更新距离数组。
进行完n-1次松弛操作后,再进行一个边的遍历,检查距离数组还能不能被更新,如果不能被更新了,说明图中没有负权环。否则,说明图中有负权环,无法求解。
代码二:(纯手敲模板)
#include
using namespace std;
#define endl '\n'
#define int long long
#define pii pair
#define f first
#define s second
#define inf 0x3f3f3f3f
int n,m,s,t;
vector g[1010];//构造图
int d[1010];
bool bellman_ford(int s){
fill(d,d+n,inf);
d[s]=0;
for(int k=1;kd[i]+di){
d[ti]=d[i]+di; //如果起点从顶点i到ti更近,更新d[ti]
}
}
}
}
//检测有没有负权环
for(int i=0;id[i]+di){
return false;//说明有负权环
}
}
}
return true;//没有负权环
}
signed main(){
cin>>n>>m>>s>>t;
while(m--){
int a,b,w;
cin>>a>>b>>w;
g[a].push_back({b,w});
g[b].push_back({a,w});
}
bool flag=bellman_ford(s);
if(flag)//说明有数据
if(d[t]==inf) cout<<"-1";
else cout<
需要注意的是,bellman-ford算法如果要统计路径条数会比较麻烦!
同Dijkstra的堆优化算法类似,SPFA是bellman-ford的队列优化算法。,通过减少不必要的冗余计算,来提高算法效率。
SPFA算法思想(AI思路):使用一个队列来维护待处理的顶点。初始时将源点加入队列,然后不断从队列中取出顶点,对其相邻顶点进行松弛操作。如果某个相邻顶点的最短距离因松弛而更新,且该顶点不在队列中,则将其加入队列。重复此过程,直到队列为空。如果一个顶点入队次数超过顶点数,说明图中存在负权回路。
平均情况:在平均情况下,SPFA 算法的时间复杂度为 O (kE),k 是一个较小的常数,一般 k 不超过 2。这是因为它避免了 Bellman-Ford 算法中大量不必要的松弛操作,从而提高了运行效率。
最坏情况:在最坏情况下,例如遇到刻意构造的特殊数据(被称为 “卡 SPFA” 的数据),图中每个顶点都频繁入队和出队,SPFA 算法的时间复杂度会退化为 O (VE) ,与 Bellman-Ford 算法相同。
代码一:AI生成代码模板:
#include
#include
#include
#include
using namespace std;
struct Edge {
int to, weight;
Edge(int t, int w) : to(t), weight(w) {}
};
vector> graph;
bool spfa(int n, int s, vector& dist) {
vector inQueue(n, false);
vector count(n, 0);
dist.assign(n, INT_MAX);
dist[s] = 0;
queue q;
q.push(s);
inQueue[s] = true;
count[s]++;
while (!q.empty()) {
int u = q.front();
q.pop();
inQueue[u] = false;
for (const auto& edge : graph[u]) {
int v = edge.to;
int w = edge.weight;
if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inQueue[v]) {
q.push(v);
inQueue[v] = true;
if (++count[v] > n) {
return false;
}
}
}
}
}
return true;
}
int main() {
int n = 5; // 顶点数
graph.resize(n);
graph[0].push_back(Edge(1, -1));
graph[0].push_back(Edge(2, 4));
graph[1].push_back(Edge(2, 3));
graph[1].push_back(Edge(3, 2));
graph[1].push_back(Edge(4, 2));
graph[3].push_back(Edge(2, 5));
graph[3].push_back(Edge(1, 1));
graph[4].push_back(Edge(3, -3));
vector dist;
int source = 0;
if (spfa(n, source, dist)) {
for (int i = 0; i < n; ++i) {
cout << "源点到顶点 " << i << " 的最短距离: " << dist[i] << endl;
}
} else {
cout << "图中存在负权回路" << endl;
}
return 0;
}
SPFA算法思想(个人总结):需要距离数组d[manx]和构造图vector
g[1010];此外需要bool数组b[maxn]来记录这个顶点有没有加入队列中,还有一个计数数组num[maxn]用来记录顶点加入队列次数,如果存在一个顶点加入队列的次数大于n次(超过顶点数),说明图中一定有负权环。 SPFA算法初始时,将顶点s加入队列中,并给b[s]=true赋值为真,num[s]=1,表示顶点放入队列,且放入次数为1。然后while(!q.empty){.......},当队列非空时,一直进行循环操作。在一个循环中,将队列中的顶点取出,遍历这个顶点的所有边,如果这个顶点存在一条边,使得起点到另一个顶点的距离更近,更新这条边。 如果这个顶点不在队列中(if( !b[i] )),就把这个顶点加入队列中,令该顶点的布尔数组对应值为真,且该顶点加入队列次数num[i]++。SPFA算法结束条件时,如果num数组中存在有顶点加入队列次数超过n次,说明存在负权环,返回false。否则当队列为空时,得到最短路径,返回true。
代码二:纯手敲代码模板:
#include
using namespace std;
#define endl '\n'
#define int long long
#define pii pair
#define f first
#define s second
#define inf 0x3f3f3f3f
int n,m,s,t;
vector g[1010];//构造图
int d[1010];
bool b[1010];
int num[1010];
bool spfa(int s){
fill(d,d+n,inf);
d[s]=0;
queue q;
q.push(s);
num[s]++;
b[s]=true;
while(!q.empty()){
int si=q.front();
b[si]=false;
q.pop();
for(int i=0;id[si]+di){ //ti才是被更新的顶点
d[ti]=d[si]+di;
if(!b[ti]){
q.push(ti);
b[ti]=true;
num[ti]++;
if(num[ti]>n) return false;//有负权环
}
}
}
}
return true;
}
signed main(){
cin>>n>>m>>s>>t;
while(m--){
int a,b,w;
cin>>a>>b>>w;
g[a].push_back({b,w});
g[b].push_back({a,w});
}
bool flag=spfa(s);
if(flag)//说明有数据
if(d[t]==inf) cout<<"-1";
else cout<
写在最后:上述个人总结模板,主要根据题目 最短路径 来进行编写。