1. 单源最短路问题(Dijkstra 算法)
算法代码
伪代码
//G为图,一般设为全局变量,数组d为源点到达各个点的最短路径长度,s为起点
Dijkstra(G, d[], s) {
初始化;
for(循环n次) {
u = 使d[u]最小且还未被访问的顶点的标号; //暴力搜索 or 堆结构
标记u已被访问;
for(从u出发能到达的所有顶点v) {
if (v未被访问 && 以u为中介点 使 s到顶点v的最短距离d[v]更优) {
优化d[v];
//可以在此处保存路径 把u保存为v的前驱即可
pre[v] = u;
}
}
}//for
}//Dijkstra
实现差异: 主要区别主要在于如何枚举从u出发到达的顶点v上;邻接矩阵需要枚举所有结点查看顶点v能否到达u,而邻接表则可以直接得到这些顶点v;
注: 若题目所给为无向图,把它转化为两条有向边即可!
const int maxn = 1010;
const int INF = 0x7fffffff;
int n; //结点数
int d[maxn]; //起点到各顶点的最短距离
bool vis[maxn] = {false}; //标记数组,标记是否已经访问
int pre[maxn]; //保存结点前驱,用于获取最短路径
邻接矩阵版(O(n^2))
int G[maxn][maxn]; //顶点数,图
//邻接矩阵版本
void Dijkstra(int s) {
fill(d, d + maxn, INF); //初始为不可达(慎用memset)
d[s] = 0;
for(int i = 0; i < n; i++) { //循环n次(第一次找到的肯定是起点本身,正好完成初始化)
//找到未访问结点中d[]最小的
int u = -1, MIN = INF;
for(int j = 0; j < n; j++) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
if(u == -1) //找不到小于INF的d[u],说明剩下的顶点和起点s不连通
return;
vis[u] = true; //标记已访问
for(int v = 0; v < n; v++) { //更新
//如果v未访问 && u能到达v && 以u为中介点可以是d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) {
d[v] = G[u][v] + d[u];
pre[v] = u;
}
}
}//for - i
}//Dijkstra
邻接表版(O(V^2+E))
struct node{
int v, dis; //v为边的目标结点,dis为边权
};
vector<node> Adj[maxn]; //邻接表; Adj[u]保存从u出发能到达的所有顶点(结构体中还保存了其间的边权)
//邻接表版本
void Dijkstra(int s) {
fill(d, d + maxn, INF);
d[s] = 0;
for(int i = 0; i < n; i++) {
int u = -1, MIN = INF;
for(int j = 0; j < n; j++) {
if(vis[j] == false && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
if(u == - 1) return;
vis[u] = true;
for(int j = 0; j < Adj[u].size(); j++){ //注意vector<>保存的是结构体
int v = Adj[u][j].v;
if(vis[v] == false && Adj[u][j].dis + d[u] < d[v]){
d[v] = d[u] + Adj[u][j].dis;
pre[v] = u;
}
}
}//for - i
}//Dijkstr
注意点
(1)路径保存
新增一个数组pre[]
,pre[v]
表示最短路径上v
的前驱;
每次在更新优化时 把u
保存为v
的前驱即可(见伪代码);
等Dijkstra()
结束后,从目标点DFS()
回溯即可得到最短路径;
void DFS(int s, int v) {
if(v == s){ //如果已经到达起点,则输出并返回
printf("%d\n", s);
return;
}
DFS(s, pre[v]); //回溯
printf("%d\n", v); //等返回后在输出
}
(2)算法优化
Dijkstra
优化
最外层的循环O(V)
是无法避免的,但是寻找最小距离d[u]
可以用堆结构优化,是内部复杂度降到O(logV)
,整体复杂度可以到O(VlogV + E)
;
【堆结构可以直接用STL
的priority_queue
实现】
(3)算法变体
很多时候最短路径不止一条,就需要题目所给的其他条件选择其中一条;一般有一下三种考法:
1、给每条边再增加一个边权(比如花费),然后要求最短路径有多条时,选择花费之和最小的;
2、给每个点增加一个点权,有多条最短路径时,选择点权之和最大(最小)的;
3、直接问有多少条最短路径;
这三种出法都只需增加一个数组,存放新增的边权或点权或最短路径条数然后在Dijkstra()中修改 更新d[v] 的那一步操作即可;其他无需改变;
//考法一:边增加花费
int cost[maxn][maxn]; //存储边的额外信息
int c[maxn]; //存储到每个点最短路径的累计花费
for(int v = 0; v < n; v++) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
c[v] = cost[u][v] + c[u];
} else if (d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) { //最短距离相同时,若花费更小,则更新c[v]
c[v] = c[u] + cost[u][v];
}
}
}
//考法二:顶点增加权值
int weight[maxn]; //存储每个点的权值
int w[maxn]; //存储到每个点最短路径的累计权重
for(int v = 0; v < n; v++) {
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
w[v] = weight[v] + w[u];
} else if (d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]) {
w[v] = w[u] + weight[v]; //最短距离相同时,若权重更大,则更新w[v]
}
}
}
//考法三:输出最短路径条数
int num[maxn]; //记录到每个点的最短路径条数
for(int v = 0; v < n; v++) {
//如果v未访问 && u能到达v && 以u为中介点可以是d[v]更优
if(vis[v] == false && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = G[u][v] + d[u];
num[v] = num[u];
} else if (d[u] + G[u][v] == d[v]) {
num[v] += num[u]; //最短距离相同时,累加num!!!
}
}
}
DIJKSTRA+DFS模版(A(1030))
用Dijkstra + DFS组合解题的情景是解脱出在Dijikstra时要处理的逻辑较为复杂,这里的说的复杂逻辑不是DIjkstra本身,Dijkstra的框架是非常简洁优美且代码好写的。
如果只在Dijkstra中记录所有最短路径,然后再在DFS中根据其他标尺求出最优路径,这种做法也非常符合关注点分离的思想。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 505;
vector<int> pre[maxn];
int G[maxn][maxn],C[maxn][maxn], d[maxn];
bool vis[maxn] = {false};
int n, m, si, di;
int INF = 1e8;
int opt = INF;
vector<int> path,temp;
void dij(int s) {
fill(d, d+maxn, INF);
d[s] = 0;
for(int i = 0; i < n; i++) { //遍历n次,每次取一个点
//选择最近点
int u = -1, MIN = INF;
for(int j = 0; j < n; j++) {
if(!vis[j] && d[j] < MIN) {
u = j;
MIN = d[j];
}
}
if(u == -1) return;
vis[u] = true;
//更新最短距离 & 花费
for(int v = 0; v < n; v++) {
if(!vis[v] && G[u][v] != INF) {
if(d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
pre[v].clear();
pre[v].push_back(u);
} else if(d[u] + G[u][v] == d[v]) {
pre[v].push_back(u);
}
}
}//for - v
}//for - i
}//Dijkstra
void DFS(int v){
if(v == si){ // 这里s是路径的起点
temp.push_back(v);
int value = 0;
//(1)求边权和
for(int i=temp.size()-1; i>0; i--) {//tempPath路径是倒的
int id = temp[i], idNext = temp[i-1];
value += C[id][idNext];
}
if(value < opt){
opt = value;
path = temp;
}
temp.pop_back();
return;
}
temp.push_back(v);
for(int i = 0;i < pre[v].size();i++){
DFS(pre[v][i]);
}
temp.pop_back();
}
int main() {
cin >> n >> m >> si >> di;
int q, w, e, r;
fill(G[0], G[0] + maxn * maxn, INF);
fill(C[0], C[0] + maxn * maxn, INF);
for(int i = 0; i < m; i++){
cin >> q >> w >> e >> r;
G[q][w] = e;
G[w][q] = e;
C[q][w] = r;
C[w][q] = r;
}
dij(si);
DFS(di);
for(int i = path.size() - 1; i >= 0 ; i--){
cout << path[i] << " ";
}
cout << d[di] << " " << opt;
return 0;
}
应用练习
2. 单源最短路问题(SPFA算法)(O(ke))
很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。
但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。
实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N,如果出现了负权环,则求解失败。
#include "bits/stdc++.h"
using namespace std;
const int maxN = 200010;
struct Edge {
int to, next, w;
} e[maxN];
int n, m, cnt, p[maxN], Dis[maxN];
int In[maxN];
bool visited[maxN];
void Add_Edge(const int x, const int y, const int z) {
e[++cnt].to = y;
e[cnt].next = p[x];
e[cnt].w = z;
p[x] = cnt;
return;
}
bool Spfa(const int S) {
int i, t, temp;
queue<int> Q;
memset(visited, 0, sizeof(visited));
memset(Dis, 0x3f, sizeof(Dis));
memset(In, 0, sizeof(In));
Q.push(S);
visited[S] = true;
Dis[S] = 0;
while (!Q.empty()) {
t = Q.front();
Q.pop();
visited[t] = false;
for (i = p[t]; i; i = e[i].next) {
temp = e[i].to;
if (Dis[temp] > Dis[t] + e[i].w) {
Dis[temp] = Dis[t] + e[i].w;
if (!visited[temp]) {
Q.push(temp);
visited[temp] = true;
if (++In[temp] > n)return false;
}
}
}
}
return true;
}
int main() {
int S, T;
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1; i <= m; ++i) {
int x, y, _;
scanf("%d%d%d", &x, &y, &_);
Add_Edge(x, y, _);
}
if (!Spfa(S)) printf("FAIL!\n");
else printf("%d\n", Dis[T]);
return 0;
}
3. 全源最短路问题(Floyd算法)(O(n^3))
算法理论
伪代码
//伪代码
枚举顶点 k ∈ [1, n]
以顶点k作为中介点,枚举所有顶点对i和j (i∈[1,n], j∈[1,n])
如果dis[i][k] + dis[k][j] < dis[i][j]成立
赋值dis[i][j] = dis[i][k] + dis[k][j]
邻接矩阵版
const int INF = 10000000;
const int MAXV = 200;
int n, m; //n为顶点数,m为边数
int dis[MAXV][MAXV]; //dis[i][j]表示顶点i和顶点j的最短距离
void Floyd(){
for(int k=0; k<n; k++){
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(dis[i][k]!=INF && dis[k][j]!=INF && dis[i][k] + dis[k][j] < dis[i][j]){//*****
dis[i][j] = dis[i][k] + dis[k][j]; //找到更短的路径
}
}
}
}
}
//最后输出dis[][]数组
需要注意的是,对于Floyd来说,k必须放在最外面。