当负权存在时,连最短路都不一定存在了。尽管如此,还是有办法在最短路存在的情况下把它求出来。在介绍算法之前,请读者确认这样一个事实:如果最短路存在,一定存在一个不含环的最短路。
理由如下:在边权可正可负的图中,环有零环,正环和负环三种。如果包含零环或正环,去掉以后路径不会边长:如果包含负环,则意味着最短路不存在(为了取得最小值不断在环上走,权值不断减小,但是永远得不到最小的值,最后死循环)。
既然不含环,最短路最多只经过(起点不算)n-1个结点,可以通过n-1轮松弛操作得到.起点默认为0.
void bellman_ford()
{
int i, k, x, y;
for(i = 1; i <= n; i++)
d[i] = INFINITY;
d[1] = 0;
for(k = 0; k < n-1; k++) //迭代n-1次 ,因为每次都有一个结点的最短路确定
for(i = 0; i < m; i++) //检查每条边
{
x = u[i];
y = v[i];
if(d[x] < INFINITY) //松弛
d[y] = min(d[y], d[x] + w[i]);
}
}
不难看出它的时间复杂度为O(nm)。在实践中,常常用FIFO队列来代替上面的循环检查。
边的存储结构定义:
struct Edge
{
int from, to, dist; //起始结点,结束结点,距离
Edge(int u, int v, int d):from(u), to(v), dist(d){}
};
首先给出存储结构:
int n, m;
vector edges;
vector G[maxn]; //边的编号
int d[maxn]; //s到各个点的距离
int p[maxn]; //最短路的上一条弧
初始化:
void init(int n)
{
this->n = n;
for(int i = 0; i < n; i++)
G[i].clear();
edges.clear();
}
增加边:
void AddEdge(int from, int to, int dist)
{
edges.push_back(Edge(from, to, dist));
m = edges.size();
G[from].push_back(m-1);
}
含有负权的最短路bellman_ford算法 复杂度n*m
//含有负权的最短路bellman_ford算法 复杂度n*m
bool bellman_ford(int s)
{
bool inp[maxn]; //是否在队列中
int cnt[maxn]; //统计结点的松弛次数
int u, i;
queue Q;
memset(inp, false, sizeof(inp)); //初始化
memset(cnt, 0, sizeof(cnt));
for(i = 1; i <= n; i++)
d[i] = INFINITY;
d[s] = 0;
inp[s] = true;
Q.push(s);
while(!Q.empty())
{
u = Q.front();
Q.pop();
inp[u] = false;
for(i = 0; i < G[u].size(); i++)
{
Edge &e = edges[G[u][i]];
if(d[u] < INFINITY && d[e.to] > d[u] + e.dist)
{
d[e.to] = d[u] + e.dist;
p[e.to] = G[u][i];
if(!inp[e.to]) //队列中没有
{
inp[e.to] = true;
Q.push(e.to);
if(++cnt[e.to] > n) //发现负圈及时退出 因为总共进行n-1次松弛,所以每个结点最多
return false; //松弛n-1次 然而如果存在负圈,就不断的松弛无限循环,就超出了n
}
}
}
}
return true;
}
该算法最坏情况下需要O(nm)时间,不过在实践中,往往只需要很短的时间就能求出最短路。
上面代码还有一个功能,在发现负圈时及时退出。注意,这只说明s可以到达一个负圈,并不代表s到每个点的最短路都不存在。另外,如果图中有其他负圈但是s无法到达这个负圈,则上面的算法也无法找到。