例题:最短路
Problem Description
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt。但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的!所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗?
Input
输入包括多组数据。每组数据第一行是两个整数N、M(N<=100,M<=10000),N表示成都的大街上有几个路口,标号为1的路口是商店所在地,标号为N的路口是赛场所在地,M则表示在成都有几条路。N=M=0表示输入结束。接下来M行,每行包括3个整数A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口A与路口B之间有一条路,我们的工作人员需要C分钟的时间走过这条路。
输入保证至少存在1条商店到赛场的路线。
Output
对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间
Sample Input
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
Sample Output
3
2
一、Floyd算法:任意两点间的最短路径问题
- 时间复杂度为O(N^3),因此在大部分机试题的时间允许范围内,它要求被求解的图的大小不大于200个结点,否则容易引起超时。
- Floyd算法利用一个二维矩阵来进行相关运算,因此当图使用邻接矩阵表示时较为方便。
- Floyd算法完成后,图中所有结点之间的最短路径都将被确定。
核心代码:
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (path[i][k] == 无穷 || path[k][j] == 无穷) {
continue;
}
if (path[i][j] == 无穷 || path[i][j] > path[i][k] + path[k][j]) {
path[i][j] = path[i][k] + path[k][j];
}
}
}
}
#include<iostream>
using namespace std;
int path[101][101];
void init(int n) {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
path[i][j] = -1;//我们用-1代表无穷
}
path[i][i] = 0;//自己到自己的距离为0
}
}
int main() {
int n, m;
while (cin >> n >> m) {
if (n == 0 && m == 0) { break; }
init(n);
while (m--) {
int a, b, c;
cin >> a >> b;
cin >> path[a][b];
path[b][a] = path[a][b];//无向图
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (path[i][k] == -1 || path[k][j] == -1) {
continue;
}
if (path[i][j] == -1 || path[i][j] > path[i][k] + path[k][j]) {
path[i][j] = path[i][k] + path[k][j];
}
}
}
}
cout << path[1][n] << endl;
}
return 0;
}
二、Dijkstra算法(迪杰斯特拉算法):求得某个特定结点到其它所有结点的最短路径长度(即单源最短路径问题)
我们将按照
最短路径长度递增
的顺序确定每一个结点的最短路径长度,即先确定的结点的最短路径长度不大于后确定的结点的最短路径长度。这样有一个好处,当确定一个结点的最短路径长度时,该最短路径上所有中间结点的最短路径长度必然已经被确定了(中间路径的最短路径长度必小于这个结点的最短路径长度)。不妨设有结点1 到 N,我们将要求从结点 1 出发到其它所有结点的最短路径长度。
初始时
,设结点 1 到其它所有点的最短路径长度均为无穷(或不确定的)
,我们立即确定由结点 1 到结点 1 的最短路径长度距离为 0。设已经确定最短路径长度的结点集合为集合 K,于是我们将结点 1 加入该集合。将问题一般化,假设集合 K 中已经保存了最短路径长度最短的前 m 个结点,它们是 P1,P2……Pm,并已经得出它们的最短路径长度。那么第 m+1 近的结点与结点 1 的最短路径上的中间结点一定全部属于集合 K
,这是因为若最短路径上中间有一个不属于集合 K 的结点,则它的最短路径距离一定小于第 m+1 近的结点的最短路径长度,与距离小于第m+1 近的结点的最短路径已经全部确定、这样的结点全部属于集合 K 矛盾。那么第 m+1 近结点的最短路径必是由以下两部分
组成,从结点 1 出发经由已经确定的最短路径到达集合 K 中的某结点 P2,再由 P2 经过一条边到达该结点。
我们遍历与集合 K 中结点直接相邻的边,设其为(U,V,C),其中 U 属于集合
K,V 不属于集合 K,计算由结点 1 出发经过已经确定的最短路到达结点 U,再
由结点 U 经过该边到达结点 V 的路径长度。该路径长度为已经确定的结点 U 的
最短路径长度+C。所有与集合 K 直接相邻的非集合 K 结点中,该路径长度最短
的那个结点即确定为第 m+1 近结点,并将该点加入集合 K。如此往复,直到结
点 1 到所有结点的最短路径全部确定。
核心代码:
//本题起始点为结点1
dis[1] = 0;
mark[1] = true;
int newNode = 1;//集合K中新加入的结点为1
for (int i = 1; i < n; i++) {
// 循环n- 1次, 按照最短路径递增的顺序确定其他n - 1个点的最短路径长度
for (int j = 0; j < edge[newNode].size(); j++) {
int t = edge[newNode][j].next;
int c = edge[newNode][j].c;
if (mark[t] == true) continue;
if (dis[t] == -1 || dis[t] > dis[newNode] + c) {
dis[t] = dis[newNode] + c;
}
}
int min = 1e5 + 8;//最小值初始化为一个较大的数
for (int j = 1; j <= n; j++) {
//遍历所有结点
if (mark[j] == true || dis[j] == -1) continue;
if (dis[j] < min) {
min = dis[j];
newNode = j;//新加入的点
}
}
mark[newNode] = true;
}
完整代码:
#include<iostream>
#include<vector>
using namespace std;
struct Node {
int next;//代表相邻结点
int c;//代表该边的权值
};
vector<Node> edge[101];//邻接链表
bool mark[101];//为true时则代表该结点已经加入到集合K
int dis[101];//存放距离
void init(int n) {
for (int i = 1; i <= n; i++) {
mark[i] = false;
dis[i] = -1;//所有距离为-1,即不可达
edge[i].clear();//居然有clear()函数! 注意使用前要先清空vector容器中的对象元素
//while (!edge[i].empty()) { edge[i].pop_back(); }//注意使用前要先清空vector容器中的对象元素
}
}
int main() {
int n, m;
while (cin >> n >> m) {
if (n == 0 && m == 0) { break; }
init(n);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
Node tmp;
tmp.c = c;
//无向图
tmp.next = b;
edge[a].push_back(tmp);
tmp.next = a;
edge[b].push_back(tmp);
}
//本题起始点为结点1
dis[1] = 0;
mark[1] = true;
int newNode = 1;//集合K中新加入的结点为1
for (int i = 1; i < n; i++) {
// 循环n- 1次, 按照最短路径递增的顺序确定其他n - 1个点的最短路径长度
for (int j = 0; j < edge[newNode].size(); j++) {
int t = edge[newNode][j].next;
int c = edge[newNode][j].c;
if (mark[t] == true) continue;
if (dis[t] == -1 || dis[t] > dis[newNode] + c) {
dis[t] = dis[newNode] + c;
}
}
int min = 1e5 + 8;//最小值初始化为一个较大的数
for (int j = 1; j <= n; j++) {
//遍历所有结点
if (mark[j] == true || dis[j] == -1) continue;
if (dis[j] < min) {
min = dis[j];
newNode = j;//新加入的点
}
}
mark[newNode] = true;
}
cout << dis[n] << endl;
}
return 0;
}
在该代码中,使用了邻接表来保存图信息,可以发现Dijstra算法很好的支持邻接链表,同时也可以用于邻接矩阵。这里使用邻接链表,也是为了熟悉一下用vector模拟邻接链表的方法(注意使用前要先清空vector容器中的对象元素)。
下面是使用邻接矩阵的代码,代码结构大致相同,就是要先判断其它不属于集合K的结点与新加入集合K的结点之间是否存在边。
#include<iostream>
using namespace std;
int path[101][101];
bool mark[101];
void init(int n) {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
path[i][j] = -1;//我们用-1代表无穷
mark[i] = false;
}
path[i][i] = 0;
}
}
int main() {
int n, m;
while (cin >> n >> m) {
if (n == 0 && m == 0) { break; }
init(n);
while (m--) {
int a, b, c;
cin >> a >> b;
cin >> path[a][b];
path[b][a] = path[a][b];
}
mark[1] = true;
int newNode = 1;
//循环n-1次
for (int i = 1; i < n; i++) {
for (int j = 1; j <= n; j++) {
//更新距离
if (mark[j] == true || path[newNode][j] == -1) continue;
if (path[1][j] == -1 || path[1][j] > path[1][newNode] + path[newNode][j]) {
path[1][j] = path[1][newNode] + path[newNode][j];
}
}
//寻找最小值加入集合
int min = 1e5 + 8;
for (int j = 1; j <= n; j++) {
if (mark[j] == true || path[1][j] == -1) continue;
if (path[1][j] < min) {
min = path[1][j];
newNode = j;
}
}
mark[newNode] = true;
}
cout << path[1][n] << endl;
}
return 0;
}
若是想用该算法求解多个结点对之间的最短路径长度问题,即全源最短路问题,可以在Dijstra算法的最外层加入一层循环,即将本例中的起始结点1换成任意节点k
核心代码:
for (int k = 1; k <= n; k++) {
mark[k] = true;
int newNode = k;
//循环n-1次
for (int i = 1; i < n; i++) {
for (int j = 1; j <= n; j++) {
//更新距离
if (mark[j] == true || path[newNode][j] == -1) continue;
if (path[k][j] == -1 || path[k][j] > path[k][newNode] + path[newNode][j]) {
path[k][j] = path[k][newNode] + path[newNode][j];
}
}
//寻找最小值加入集合
int min = 1e5 + 8;
for (int j = 1; j <= n; j++) {
if (mark[j] == true || path[k][j] == -1) continue;
if (path[k][j] < min) {
min = path[k][j];
newNode = j;
}
}
mark[newNode] = true;
}
}