最短路
1.无权最短路径算法:广度优先搜索
讨论最简单的情况:不带权的图(有向图或无向图)或权值相同的图。如某人从A城到B城,希望选择一条途中中转次数最少的路线。该问题反映到图上就是找一条从顶点A到B所含边数目最少的路径。方法为从顶点A出发对图做广度优先搜索,一旦遇到顶点B就终止。由此所得的广度优先生成树上,从根顶点A到顶点B的路径就是中转次数最少的路径,路径上A与B之间的顶点就是途径的中转站数。
int main() {
int n, e; //5 7
cin >> n >> e;
vector<vector<int>> adjmatrix(n, vector<int>(n, 0));
//输入边(无向图) //0 1 0 2 1 2 1 3 2 3 2 4 3 4
int u, v;
for (int i = 0; i < e; ++i) {
cin >> u >> v; //uv两顶点之间有边
adjmatrix[u][v] = adjmatrix[v][u] = 1;
}
int start, end; //起始点和终点
cin >> start >> end;
vector<bool> vis(n, false); //访问标志数组
queue<int> Q;
int step = 1;
bool flag = false;
//bfs
vis[start] = true;
Q.push(start); //将起点放入
while (!Q.empty()) {
int size = Q.size();
while (size > 0) { //遍历完一层后才能使step+1,这样才能保证step是最短的路径数
int tmp = Q.front();
Q.pop();
for (int i = 0; i < n; ++i) {
if (adjmatrix[tmp][i] == 1 && vis[i] == false) { //tmp和i两点之间有边且i未曾访问
if (i == end) {
cout << step;
exit(0);
}
vis[i] = true;
Q.push(i);
}
}
size -= 1;
}
step++;
}
}
还可以用一个结构体存储每个节点的路径长度(起始点到每个顶点的路径长度),以便计算最短路径长度。
struct node { //使用结构体,可存储每个节点的路径长度,便于计算最短路径
int x; //节点编号
int step; //路径
node(int xx, int s) : x(xx), step(s) {}
};
int main() {
int n, e; //5 7
cin >> n >> e;
vector<vector<int>> adjmatrix(n, vector<int>(n, 0));
//输入边(无向图) //0 1 0 2 1 2 1 3 2 3 2 4 3 4
int u, v;
for (int i = 0; i < e; ++i) {
cin >> u >> v; //uv两顶点之间有边
adjmatrix[u][v] = adjmatrix[v][u] = 1;
}
int start, end; //起始点和终点
cin >> start >> end;
vector<bool> vis(n, false); //访问标志数组
queue<node> Q;
bool flag = false;
//bfs
vis[start] = true;
Q.push(node(start,0)); //将起点放入
while (!Q.empty()) {
node tmp = Q.front();
Q.pop();
for (int i = 0; i < n; ++i) {
if (adjmatrix[tmp.x][i] == 1 && vis[i] == false) { //tmp和i两点之间有边且i未曾访问
if (i == end) {
cout << tmp.step + 1;
exit(0);
}
vis[i] = true;
Q.push(node(i, tmp.step + 1));
}
}
}
}
上面两种代码仅实现了计算出最短路径长度,无法实现将某条最短路径上所有节点输出的功能,下面给出可以输出最短路径所有节点的代码,通过在节点结构体中添加一个指向父节点的指针 (pre), 用于记录每个节点的前一个节点,然后在找到终点之后通过递归回溯的方法,输出整个路径上的节点。但这里只实现了输出其中一条最短路径的节点,若有多条最短路径,没有实现全部输出。
参考:https://blog.csdn.net/weixin_44965308/article/details/104928695
struct node { //使用结构体,可存储每个节点的路径长度,便于计算最短路径
//int pre; //前一个节点编号
node* pre;//前一个节点定义为指针更好
int x; //节点编号
int step; //路径
node() {};
node(node* p, int xx, int s) : pre(p), x(xx), step(s) {}
};
int n;
//vector<node> back(n);//存储找到终点前遍历的所有节点
void print(node* cur) { //递归打印最短路径上的所有节点
if (cur->pre != NULL) {
print(cur->pre);
}
cout << cur->x << ends;
}
int main() {
int e; //5 7
cin >> n >> e;
vector<vector<int>> adjmatrix(n, vector<int>(n, 0));
//输入边(无向图) //0 1 0 2 1 2 1 3 2 3 2 4 3 4
int u, v;
for (int i = 0; i < e; ++i) {
cin >> u >> v; //uv两顶点之间有边
adjmatrix[u][v] = adjmatrix[v][u] = 1;
}
//vector<node> back(n);
int start, end; //起始点和终点
cin >> start >> end;
vector<bool> vis(n, false); //访问标志数组
queue<node> Q;
bool flag = false;
//bfs
vis[start] = true;
Q.push(node(NULL, start, 0)); //将起点放入,-1表示起点没有父节点
//back.push_back(node(-1, start, 0));
while (!Q.empty()) {
node* tmp = new node(Q.front());
Q.pop();
for (int i = 0; i < n; ++i) {
if (adjmatrix[tmp->x][i] == 1 && vis[i] == false) { //tmp和i两点之间有边且i未曾访问
if (i == end) {
cout << tmp->step + 1 << endl;
print(tmp);
cout << end;
exit(0);
}
vis[i] = true;
Q.push(node(tmp, i, tmp->step + 1));
//back.push_back(node(tmp.x, i, tmp.step + 1));
}
}
}
}
应用举例1:
题目描述:
一匹马在一个8*8的棋盘上走着,它的每一步恰好走成一个日字,也就是在x、y两个方向上,如果在一个方向走一步,另一个方向就走两步。假设棋盘的下标左下角是(1,1),右上角是(8,8)。给你马的最初位置(a,b)各最终位置(an,bn),请你编程求出马从最初位置到最终位置所走的最少步数。
Input
先输入一个正整数T表示有T种情况,每一种情况一行,由四个正整数组成,分别表示a、b、an、bn。
Ouput
每种情况先输出“Case :id”,id是从1开始的序号,然后输出马走的最小步数。
struct node {
int x;
int y;
int step;
node(int xx, int yy, int s) :x(xx), y(yy), step(s) {}
};
int main() {
int dn[8][2] = { {1,2},{2,1},{2,-1},{1,-2},{-1,-2},{-2,-1},{-2,1},{-1,2} }; //方向数组
int n, a, b, an, bn;
cin >> n;
for (int k = 0; k < n; ++k) {
cin >> a >> b >> an >> bn;
vector<vector<bool>> vis(9, vector<bool>(9, false));
bool flag = false;
queue<node> Q;
Q.push(node(a, b, 0));
while (!Q.empty()) {
node tmp = Q.front();
Q.pop();
for (int i = 0; i < 8; ++i) {
if (tmp.x + dn[i][0] >= 1 && tmp.x + dn[i][0] <= 8 && tmp.y + dn[i][1] >= 1 && tmp.y + dn[i][1] <= 8
&& vis[tmp.x + dn[i][0]][tmp.y + dn[i][1]] == false) {
if (tmp.x + dn[i][0]==an && tmp.y + dn[i][1]==bn) {
cout << "Case " << k + 1 << ":" << tmp.step + 1 << endl;
flag = true;
break;
}
vis[tmp.x + dn[i][0]][tmp.y + dn[i][1]] = true;
Q.push(node(tmp.x + dn[i][0], tmp.y + dn[i][1], tmp.step + 1));
}
}
if (flag) {
break;
}
}
}
}
例2
迷宫问题
https://blog.csdn.net/qq_40637903/article/details/108429933
2.对于带权有向图,我们要找的最短路径是该路径上边的权值之和最小。下面是两种最常见的最短路径问题。
一. 单源最短路径(single - source shortest path)
基于图的邻接矩阵存储。
单源最短路径是指从一个顶点到其它各顶点之间的最短路径(single - source shortest path)。
迪杰斯特拉(E.W.Dijkstra)于1959年提出了一个寻找单源最短路径的方法。
其基本思想是,设置一个顶点集合S,并不断地作贪心选择来扩充这个集合。
该算法属于算法设计方法中的贪心算法(greedy selector)类——总是作出当前看来最好的选择,通过获取局部最优,最终达到获取整体最优)
时间复杂度O(n^2)。
如果要求任一顶点到其他所有顶点的最短路径可以分别对n个顶点调用Dijkstra算法,算法复杂度O(n^3)。
//cost为图的邻接矩阵,dist存储相应的路径长度,path[i]存储始点到各点的最短路径
//st为已找到最短路径的终点集
void Dijkstra(vector<vector<int>>& cost, int v0, vector<int>& dist, vector<vector<int>>& path, unordered_set<int>& st) {
//初始化源点到各点的距离
for (int i = 1; i < cost.size(); ++i) {
dist[i] = cost[v0][i];
cout << dist[i] << endl;
if (dist[i] < INT_MAX) {
cout << i << endl;
path[i].push_back(v0);
path[i].push_back(i);
}
}
st.insert(v0);
//每次加到st中一个终点,最后剩下一个可不加
int m = 1;
while (m < cost.size() - 1) {
int wm = INT_MAX, u = v0;//u为待选顶点
//选最小的dist[i]
for (int i = 1; i < cost.size(); ++i) {
if (st.find(i) == st.end() && dist[i]<wm) {
u = i;
wm = dist[i];
}
}
st.insert(u);
//新加入一个终点后要重新检查并修改
for (int i = 1; i < cost.size(); ++i) {
//注意防止INT_MAX加一个数大于INT_MAX的情况
if (st.find(i) == st.end() && cost[u][i]!=INT_MAX && dist[u] + cost[u][i] < dist[i]) {//cost[u][i]!=INT_MAX防止INT_MAX加一个数大于INT_MAX的情况
//更新源点到顶点i的路径长度
dist[i] = dist[u] + cost[u][i];
//更新源点到顶点i的路径
path[i].clear();
path[i] = path[u];
path[i].push_back(i);
}
}
m += 1;
}
}
int main() {
int n, e;
cin >> n >>e;
vector<vector<int>> cost(n, vector<int>(n,INT_MAX)); //邻接矩阵
vector<int> dist(n); //v0到i的当前最短路径长度
vector<vector<int>> path(n); //path[i]为相应v0到i
unordered_set<int> st;
int u, v, w;
for (int i = 0; i < e; i++) {
cout << "输入边权:";
cin >> u >> v >> w;
cost[u][v] = w;
}
Dijkstra(cost,0,dist,path,st);
for (int i = 1; i < n; ++i) {
if (dist[i] < INT_MAX) {
cout << "0->" << i << ":";
for (auto& x : path[i]) {
cout << x << " ";
}
}
}
}
二. 多源最短路径(All Pairs Shortest Paths)
Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。
递推产生A(0), A(1), A(2), …, A(k), …, A(n)的过程,就是逐步允许越来越多的顶点作为路径的中间顶点,直到所有的顶点都允许作为中间顶点,这时最短路径就求出来了。
(注意这里 A(k) [i] [j]表示从顶点i到j中途经过索引不大于k的点的最短路径长度,而不是中间经过顶点数量)。
算法执行中A的值被逐渐代之为A(1), A(2), …, A(k), …, A(n); 在算法每步中path[i, j]是从Vi到Vj中间顶点序号不大于k的最短路径上Vj的前一个顶点的序号;算法结束时,由path[i, j]的值追溯, 可以得到从Vi到Vj的最短路径。
状态转移方程:
时间复杂度O(n^3)
const int& INF = 100000000;
//cost为可被更新的邻接矩阵
//dist(k)[i,j]表示从顶点Vi到Vj中间经过顶点序号不大于k的最短路径长度,通过运算,递推地产生一个矩阵序列A(0), A(1), A(2),…, A(k), …,A(n)。
//path[i][j]是从Vi到Vj中间顶点序号不大于k的最短路径上Vj的前一个顶点的序号;
//算法结束时,由path[i,j]的值追溯,可以得到从Vi到Vj的最短路径
void floyd(vector<vector<int>>& cost, vector<vector<int>>& dist, vector<vector<int>>& path) {
//初始化边界值
for (int i = 0; i < cost.size(); ++i) {
for (int j = 0; j < cost.size(); ++j) {
dist[i][j] = cost[i][j];
if (dist[i][j] < INF) {
path[i][j] = i; //path[i][j]是从Vi到Vj中间顶点序号不大于k的最短路径上Vj的前一个顶点的序号
}
}
}
//递推地产生一个矩阵序列A(0), A(1), A(2), …, A(k), …, A(n)。A即dist
for (int k = 0; k < cost.size(); ++k) {
for (int i = 0; i < cost.size(); ++i) {
for (int j = 0; j < cost.size(); ++j) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
//状态转移方程
dist[i][j] = dist[i][k] + dist[k][j];
//path[i][j]更新为从Vi到Vj中间顶点序号不大于k的最短路径上Vj的前一个顶点的序号
path[i][j] = path[k][j];
}
}
}
}
}
int main() {
int n, e; //顶点从0开始编号
cin >> n >>e;
vector<vector<int>> cost(n, vector<int>(n,INF)); //邻接矩阵
//vector<int> dist(n); //v0到i的当前最短路径长度
vector<vector<int>> dist(n, vector<int>(n, INF));//dist[i][j]为从vi到vj的最短路径长度
vector<vector<int>> path(n, vector<int>(n, -1)); //path[i][j]为相应的最短路径
unordered_set<int> st;
int u, v, w;
for (int i = 0; i < n; ++i) {
cost[i][i] = 0;
}
for (int i = 0; i < e; i++) {
cout << "输入边权:";
cin >> u >> v >> w;
cost[u][v] = w;
}
floyd(cost, dist, path);
int beg, end;
vector<int> short_path;
cout << "输入查询始点和终点:";
cin >> beg >> end;
short_path.push_back(end);
//回溯依次找前一个顶点
//例如,从v1到v5的最短路为v1-v2-v3-v4-v5,则path[1][5]=4,由此找path[1][4]=3,...最终找到path[1][2]=1
while (path[beg][end] != beg) {
short_path.insert(short_path.begin(), path[beg][end]);
end = path[beg][end];
}
short_path.insert(short_path.begin(), beg);
for (auto& x : short_path) {
cout << x << " ";
}
cout << endl;
}