n^2
遍深度或广度优先搜索,即对每两个点都进行一次深度或广度优先搜索,便可以求得任意两点之间的最短路径。e[i][1]+e[1][j]
是否比e[i][j]
要小即可。其中,e[i][j]
表示从i到j顶点之间的路程,e[i][1]+e[1][j]
表示的是从i先到1,再从1到j的路程之和。for(i = 1; i <= n; ++i)
for(j = 1; j <= n; ++j)
if(e[i][j] > e[i][1] + e[1][j])
e[i][j] = e[i][1] + e[1][j];
e[i][2]+e[2][j]
是否比e[i][j]
更短?//经过1
for(i = 1; i <= n; ++i)
for(j = 1; j <= n; ++j)
if(e[i][j] > e[i][1] + e[1][j])
e[i][j] = e[i][1] + e[1][j];
//经过2
for(i = 1; i <= n; ++i)
for(j = 1; j <= n; ++j)
if(e[i][j] > e[i][2] + e[2][j])
e[i][j] = e[i][2] + e[2][j];
for(k = 1; k <= n; ++k)
for(i = 1; i <= n; ++i)
for(j = 1; j <= n; ++j)
if(e[i][j] > e[i][k] + e[k][j])
e[i][j] = e[i][k] + e[k][j];
#include
#include
#include
#include
using namespace std;
int main(void)
{
int v = 4; //顶点数
int e = 8; //边数
vector<vector<int>> graph(v, vector<int>(v, INT_MAX));
for (int i = 0; i < v; ++i)
graph[i][i] = 0;
graph[0][1] = 2;
graph[0][2] = 6;
graph[0][3] = 4;
graph[1][2] = 3;
graph[2][0] = 7;
graph[2][3] = 1;
graph[3][0] = 5;
graph[3][2] = 12;
/*
0 2 6 4
∞ 0 3 ∞
7 ∞ 0 1
5 ∞ 12 0
*/
//1~k作为中转点
for (int k = 0; k < v; ++k)
{
for (int i = 0; i < v; ++i)
{
for (int j = 0; j < v; ++j)
{
if (graph[i][k] < INT_MAX && graph[k][j] < INT_MAX
&& graph[i][j] > graph[i][k] + graph[k][j]) //防止相加溢出
graph[i][j] = graph[i][k] + graph[k][j];
}
}
}
for (const auto & line : graph)
{
for (const auto & point : line)
cout << setw(3) << point;
cout << endl;
}
return 0;
}
O(N^3)
。由于Floyd-Warshall算法实现起来非常容易,所以如果时间复杂度要求不高,使用Floyd-Warshall来指定两点之间的最短路径或者指定一个点到其余各个顶点的最短路径也是可行的。如果一个图中带有“负权回路”,那么这个图则没有最短路径。
#include
#include
#include
using namespace std;
int main(void)
{
int v = 6; //顶点数
int e = 9; //边数
vector<vector<int>> graph(v, vector<int>(v, INT_MAX));
vector<bool> flags(v, false);
for (int i = 0; i < v; ++i)
graph[i][i] = 0;
graph[0][1] = 1;
graph[0][2] = 12;
graph[1][2] = 9;
graph[1][3] = 3;
graph[2][4] = 5;
graph[3][2] = 4;
graph[3][4] = 13;
graph[3][5] = 15;
graph[4][5] = 4;
/*
0 2 6 4
∞ 0 3 ∞
7 ∞ 0 1
5 ∞ 12 0
*/
vector<int> dis(v);
//以0为源点,初始化
for (int i = 0; i < v; ++i)
dis[i] = graph[0][i];
flags[0] = true;
//Dijkstra
for (int i = 1; i < v; ++i) //只需v-1次循环
{
//找到目前离源点最近的未标记的点
int min_dis = INT_MAX; //记录最小
int v_index; //最近是哪个点
for (int j = 0; j < v; ++j)
{
if (flags[j] == false && dis[j] < min_dis)
{
min_dis = dis[j];
v_index = j;
}
}
flags[v_index] = true; //标记这个最近的点为确定
//以找到的点所关联的边来进行松弛操作
for (int j = 0; j < graph[v_index].size(); ++j)
{
if (graph[v_index][j] < INT_MAX)
{
//这条边是存在的
if (dis[j] > dis[v_index] + graph[v_index][j])
dis[j] = dis[v_index] + graph[v_index][j];
}
}
}
//单源最短路径结果
for (const auto & i : dis)
cout << i << ' ';
cout << endl;
return 0;
}
O(N^2)
,每次找到离源点最近的点的时间复杂度为O(N)
,可以用堆进行优化,使得O(N)
降低为O(logN)
。O((M+N)logN)
。在最坏情况下M就是N^2,这样(M+N)logN比N^2还大,但是大多数情况下并不会有那么多边,所以通常(M+N)logN要比N^2小很多。
//核心代码
for(k = 1; k <= n - 1; ++k)
for(i = 1; i <= m; ++i)
if(dis[v[i]] > dis[u[i]] + w[i])
dis[v[i]] = dis[u[i]] + w[i];
那需要进行多少轮?
n-1?最短路径中不可能包含回路吗?
#include
#include
#include
using namespace std;
struct Edge
{
int from; //边起点
int to; //边终点
int weight; //边权重
};
int main(void)
{
int v = 5; //顶点数
int e = 5; //边数
const int INT_INF = INT_MAX - 10000; //防止溢出的最大值
vector<int> dis(v);
//以0为源点,初始化
for (int i = 0; i < v; ++i)
dis[i] = INT_INF;
dis[0] = 0;
vector edges = {
{1, 2, 2},
{0, 1, -3},
{0, 4, 5},
{3, 4, 2},
{2, 3, 3}
};
//Bellman-Ford
for (int turn = 0; turn < v - 1; ++turn)
{
vector<int> detect = dis; //备份
for (const auto & edge : edges)
{
if (dis[edge.to] > dis[edge.from] + edge.weight)
dis[edge.to] = dis[edge.from] + edge.weight;
}
if (detect == dis) //如果dis数组没有更新,提前退出循环结束算法
break;
}
for (const auto & point : dis)
cout << point << ' ';
cout << endl;
//检测负权回路
bool isloop = false;
for (const auto & edge : edges)
if (dis[edge.to] > dis[edge.from] + edge.weight)
isloop = true;
cout << (isloop ? "存在" : "不存在") << "回路" << endl;
return 0;
}
O(NM)
(比Dijkstra算法还高?),可以继续优化。在实际操作中,Bellman-Ford算法经常会在未达到n-1轮松弛前就已经计算出最短路径(n-1只是最大值)。因此可以添加一个数组来备份dis,如果在新一轮的松弛中数组dis没有发生变化,则可以提前跳出循环。(优化的前提基于整个数组没有变化)#include
#include
#include
#include
#include
using namespace std;
struct Edge
{
int from; //边起点
int to; //边终点
int weight; //边权重
};
int main(void)
{
int v = 5; //顶点数
int e = 7; //边数
const int INT_INF = INT_MAX - 10000; //防止溢出的最大值
vector<int> dis(v);
queue<int> vex_opt; //bellman-ford优化队列
vector<bool> flags(v, false); //标记点是否在队列中
vector<vector > graph(v); //list和forward_list也行
graph[0].push_back({ 0, 1, 2 });
graph[0].push_back({ 0, 4, 10 });
graph[1].push_back({ 1, 2, 3 });
graph[1].push_back({ 1, 4, 7 });
graph[2].push_back({ 2, 3, 4 });
graph[3].push_back({ 3, 4, 5 });
graph[4].push_back({ 4, 2, 6 });
//以0为源点,初始化
for (int i = 0; i < v; ++i)
dis[i] = INT_INF;
dis[0] = 0;
flags[0] = true;
vex_opt.push(0);
//判断是否存在负权回路
vector<int> loop(v, 0);
++loop[0];
while (!vex_opt.empty() && find(loop.begin(), loop.end(), v) == loop.end())
//队列不为空和没检测出负权回路的时候循环
{
//扫描当前顶点的所有边
for (const auto & edge : graph[vex_opt.front()])
{
//判断是否松弛成功
if (dis[edge.to] > dis[edge.from] + edge.weight)
{
dis[edge.to] = dis[edge.from] + edge.weight;
//判断顶点是否在队列中
if (flags[edge.to] == false)
{
//入队
flags[edge.to] = true;
vex_opt.push(edge.to);
++loop[edge.to];
}
}
}
//出队
flags[vex_opt.front()] = false;
vex_opt.pop();
}
if (vex_opt.empty())
{
for (const auto & point : dis)
cout << point << ' ';
cout << endl;
cout << "不存在负权回路" << endl;
}
else
{
cout << "存在负权回路" << endl;
}
return 0;
}
O(MN)
。通过队列优化的Bellman-Ford算法如何判断一个图是否有负环呢?
Floyd | Dijkstra | Bellman-Ford | 队列优化Bellman-Ford |
---|---|---|---|
空间复杂度 | O(N^2) |
O(M) |
O(M) |
时间复杂度 | O(N^3) |
O((M+N)logN) |
O(MN) |
适用情况 | 稠密图,和顶点关系密切 | 稠密图,和顶点关系密切 | 稀疏图,和边关系密切 |
负权 | 可以解决负权 | 不能解决负权 | 可以解决负权 |