给定图G
常用算法有Dijkstra算法、Bellman-Ford算法、SPFA算法和Floyd算法。
迪杰斯特拉算法用来解决单源最短路问题(即给定图G和起点S,求S到其他每个顶点的最短距离)。同时无法处理负权。
设置集合S存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):
① 每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S
② 之后,令u为中介点,更新起点s与所有能经过u到达的顶点v之间的最短距离。
考虑集合S的实现,起点s到达顶点Vi的最短距离的实现
//设G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijkstra (G,d[], s){
初始化;
for(循环n次){
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能够到达所有顶点v){
if(v未被访问 && 以u中转使s到v的最短距离d[v]更小){
更新优化d[v];
}
}
}
}
此时图可以使用邻接矩阵或者邻接表两种形式存储。当顶点数<1000时,使用邻接矩阵(也较为简单)。
枚举所有顶点来查看v是否可由u到达
#include
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;//设INF为一个很大的数
int n,G[MAXV][MAXV]; //n为顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
bool vis[MAXV] = {false}; //标记是否访问的数组
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0;
for(int i=0;i<n;i++){ //循环n次
int u =-1 , MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j =0;j<n;j++){ //找到未被访问的顶点中d[u]最小的
if(vis[j]==false && d[j]<MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return; //找不到更小的顶点
vis[u] = true; //标记u为已访问
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] = d[u]+G[u][v];//优化d[v]
}
}
}
}
可以直接得到u能够到达的顶点v,无须遍历所有顶点,当顶点较多的时候可以使用
#include
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3ffffff;//设INF为一个很大的数
struct Node{
int v,dis; //v为边的目标顶点,dis为边权值
};
vector<Node> Adj[MAXV];//存放图G,Adj[u]存放从顶点u出发的所有可达顶点
int n;//顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
bool vis[MAXV] = {false}; //标记是否访问的数组
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0;
for(int i=0;i<n;i++){ //循环n次
int u =-1 , MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j =0;j<n;j++){ //找到未被访问的顶点中d[u]最小的
if(vis[j]==false && d[j]<MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return; //找不到更小的顶点
vis[u] = true; //标记u为已访问
//只有下面这个for循环与邻接矩阵不同
for(int j = 0;j<Adj[u].size();j++){
int v = Adj[u][j].v;//通过邻接表直接获得u能到达的顶点v
if(vis[v] == false && d[u]+Adj[u][j].dis <d[v]){
//如果v未访问&&以u为中转可以优化d[v]距离
d[v] = d[u]+Adj[u][j].dis;//优化d[v]
}
}
}
}
如果题目给出的无向边而不是有向边,这时只需要把无向边当成两条指向相反的有向边即可。对于邻接矩阵来说,一条u到v之间的无向边,在输入时可以分别对G[u][v] = d[v][u] = value ; 对于邻接表来说,只需要在u的邻接表Adj[u]的末尾添加上v,并在v的邻接表Adj[v]的末尾加上u即可。
设置一个数组pre[ ],pre[v]表示从起点s到顶点v的最短路径上v的前一个顶点的编号。这样,当伪代码中的条件成立时,就可以将u赋值给pre[v],最终就能把最短路径上每一个顶点的前驱顶点记录下来。
for(从u出发能够到达所有顶点v){
if(v未被访问 && 以u中转使s到v的最短距离d[v]更小){
更新优化d[v];
令v的前驱为u;
}
}
#include
using namespace std;
const int MAXV = 1000;//最大顶点数
const int INF = 0x3fffffff;//设INF为一个很大的数
int n,G[MAXV][MAXV]; //n为顶点数
int d[MAXV]; //起点到达各个顶点的最短路径长度
int pre[MAXV]; //pre[v]表示从起点到顶点v的最短路径上v的前一个顶点【新添加】
bool vis[MAXV] = {false}; //标记是否访问的数组
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF);//fill函数将整个d数组赋值为INF(慎用memset)
for(int i =0;i<n;i++) pre[i] = i; //初始状态设每个点的前驱为自身【新添加】
d[s] = 0;
for(int i=0;i<n;i++){ //循环n次
int u =-1 , MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j =0;j<n;j++){ //找到未被访问的顶点中d[u]最小的
if(vis[j]==false && d[j]<MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return; //找不到更小的顶点
vis[u] = true; //标记u为已访问
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] = d[u]+G[u][v];//优化d[v]
pre[v] = u;//记录v的前驱顶点是u【新添加】
}
}
}
}
上述pre[ ]中已经得到了每个顶点的前驱。我们使用递归不断利用pre[ ]的信息来寻找前驱,直至到达起点V1后从递归深处开始输出。
也可以使用Dijkstra+DFS来求,见下
void DFS(int s, int v){ //s为起点编号,v为当前访问的顶点编号(从终点开始递归)
if(v == s){
printf("%d\n",s);
return;
}
DFS(s,pre[v]); //递归访问v的前驱顶点pre[v]
printf("%d\n",v);//从最深处return回来后,输出每一层的顶点号
}
上述是迪杰斯特拉算法的基本用法,如果当从起点到终点的最短距离不止一条时,题目通常会给出第二个条件,要求在所有的最短路径中选择第二个条件最优的一条路径。第二条件常见的是以下三种出题方法:
给每条边再增加一个边权(比如花费),然后要求在最短路径有多条时要求路径上的花费之和最小(或者其他边权,要求最大)。
给每个点增加一个点权(例如每个城市能收集到的物资),然后在最短路径有多条时,要求路径上的点权之和最大(或者其他点权,要求最小)。
直接问有几条最短路径。
上面三种,都需要增加一个数组来存放新增的边权或点权或最短路径条数,然后再Dijkstra算法中修改 优化d[v] 的那个步骤。
以新增的边权代表花费为例,用cost[u][v] 表示u→v的花费(输入),并新增加一个数组c[ ],令从起点到达顶点u的最少花费为c[u],初始化时只有c[s] = 0、其他c[u] = INF。这样就在d[u]+G[u][v] 以新增的点权代表城市中能收集到的物资为例,用weight[u]表示城市u中的物资数目(输入),并增加一个数组w[ ],表示从起点s到达顶点u可以收集到的最大物资为w[u],初始化时只有w[s] = weight[s],其他w[u] = 0。这样就在d[u]+G[u][v] 只需要增加一个数组num[ ],令从起点s到达顶点u的最短路径条数为num[u],初始化时只有num[s] =1、其余num[u] = 0。这样就可以在d[u]+G[u][v] 写DFS的递归函数: 必须要有以下: 递归边界:如果当前访问的结点是叶子结点,那么说明到达了递归边界,此时tempPath存放了一条路径,这是要对这条路径求出第二条件的值value,并和optValue对比,如果更优,则更新optValue并把tempPath覆盖path。 递归式:如果当前访问的结点是v,那么只需要遍历pre[v]中的所有结点并进行递归即可。 如何递归生成tempPath:在访问当前节点v是将v加入tempPath的最后面,然后遍历pre[v]进行递归,等pre[v]的所有结点遍历完毕后再把tempPath最后面的v弹出。叶子结点不能按照上面,需要在访问到叶子结点时临时加入。 DFS: 关于上述如何计算路径tempPath上的value值: 存放在tempPath中的路径结点是逆序的,因此访问结点需要倒着进行。 题意:现有N个城市(0~N-1),M条道路(无向边),并给出M条道路的距离属性和花费属性。现给定起点S和终点D,求从起点到终点的最短路径、最短距离及花费。注意:如果有多条最短路径,则选择花费最小的那条。 思路: 该题除了求最短距离外,还要求两个额外信息:最短路径和最短路径上的最小花费之和。使用Dijkstra或者Dijkstra+DFS都可以。 Dijkstra: 求单源最短路,可以判断有无负权回路(若有,则不存在最短路)算法返回一个bool值,如果其中存在从源点可达的负环,那么翻书返回false,否则返回true,同时数组d中存放的值就是从源点到达各顶点的最短距离。 对图中的边进行v-1轮操作(v个顶点),每轮都遍历图中的所有边:对于每条边u -> v,如果d[u] + length[u->v] < d[v]成立,就用d[u] + length[u->v]更新d[v]。 Bellman-Ford算法: 由于Bellman-Ford算法需要遍历所有边,显然使用邻接表会比较方便,如果用邻接矩阵会使复杂度上升。 当有多个条件的时候,做法与Dijkstra算法中的相同,只有统计最短路径条数不一样。 Bellman-Ford算法的复杂度过高,SPFA对其进行优化。 bellman-Ford算法的每轮操作都需要操作所有边,显然会产生大量无意义的操作。只有当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才可能会被改变。 因此可以进行优化:建立一个队列,每次将队首顶点u取出,然后对从u出发的所有边u -> v进行松弛操作,也就是判断d[u]+weight[u][v] < d[v]是否成立,如果成立,则用d[u]+weight[u][v] 覆盖 d[v],于是d[v]获得更优的值,此时如果v不在队列中,就把v加入队列。这样操作直到队列为空(无负权回路),或者是某个顶点的入队次数超过v-1(存在负权回路)。 SPFA是从bellman-ford算法优化得来的 BFS版: 求多源(全源)最短路、无负权边的最短路。即对给定的图G 如果存在顶点k,使得以k作为中介点时,顶点i和顶点j的当前最短距离缩短,则使用顶点k作为顶点i和顶点j的中介点,即当dis[i][k] + dis[k][j] < dis[i][j]时,令dis[i][j]=dis[i][k] + dis[k][j]。for(int v =0;v<n;v++){
//如果v未访问 && u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u]+G[u][v] < d[v]){ //以u中转使得更优
d[v] = d[u] +G[u][v];
c[v] = c[u] +cost[u][v];
}else if(d[u]+G[u][v] == d[v] && c[u]+cost[u][v] < c[v]){
c[v] = c[u] +cost[u][v]; //最短距离相同时,看花费能否更优
}
}
}
新增点权
for(int v =0;v<n;v++){
//如果v未访问 && u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u]+G[u][v] < d[v]){ //以u中转使得更优
d[v] = d[u] +G[u][v];
w[v] = w[u] +weight[v];
}else if(d[u]+G[u][v] == d[v] && w[u] +weight[v] > w[v]){
w[v] = w[u] +weight[v]; //最短距离相同时,看物资能否更优
}
}
}
求最短路径条数
for(int v =0;v<n;v++){
//如果v未访问 && u能到达v
if(vis[v] == false && G[u][v] != INF){
if(d[u]+G[u][v] < d[v]){ //以u中转使得更优
d[v] = d[u] +G[u][v];
num[v] = num[u];
}else if(d[u]+G[u][v] == d[v]){
num[v] += num[u]; //最短距离相同时,累加num 根本不
}
}
}
使用Dijkstra+DFS来求最短路径
int optValue; //第二条件最优值
vector<int> pre[MAXV]; //存放结点的前驱结点
vector<int> path, tempPath; //最优路径、临时路径
void DFS(int v){ //v为当前访问结点
//递归边界
if(v == st){//如果到达了叶子结点st(即路径的起点)
tempPath.push_back(v); //将起点st加入临时路径tempPATH的最后面
int value; //存放临时路径temp的第二条件的值
计算路径tempPath上的value值;
if(value 优于 optValue){
optValue = value;
path = tempPath;
}
tempPath.pop_back(); //将刚加入的结点删除
return;
}
//递归式
tempPath.push_back(v); //将当前访问结点v加入临时路径tempPATH的最后面
for(int i =0; i< pre[v].size();i++){
DFS(pre[v][i]); //结点v的前驱结点pre[v][i],递归
}
tempPath.pop_back(); //遍历完所有前驱结点,将当前结点v删除
}
以计算路径tempPath上边权值he和点权之和的代码为例://边权之和
int value = 0;
for(int i = tempPath.size(); i>0; i--){ //倒着访问结点
//当前结点id,下一个结点idNext
int id = tempPath[i], idNext = tempPath[i-1];
value += V[id][idNext]; //value增加边 id -> idNext 的边权
}
//点权之和
int value = 0;
for(int i = tempPath.size(); i>0; i--){ //倒着访问结点
//当前结点id
int id = tempPath[i];
value += W[id]; //value增加点 id 的点权
}
例题:
我们使用Dijkstra算法的写法,令cost[MAXV][MAXV]表示顶点间的花费(也即是边权),c[MAXV]存放从起点s到达每个结点u的在最短路径下的最小花费,其中c[s]在初始化时为0。而对于最短路径,可以用int型pre数组存放每个结点的前驱,接下来就是按前面讲解的过程在最短距离的更新过程中同时更新数组c和数组pre。#include
Bellman-Ford算法
策略:
如果图中此时没有从源点可达的负环,那么数组d中的所有值都应当已经达到最优。此时只需要对所有边再进行一轮操作,判断是否有某条边u ->v 仍然满足d[u] + length[u->v] < d[v],如果有,则说明图中有从源点可达的负环,返回false;否则,说明数组d中的所有值都已经达到最优,返回true。for(i = 0;i<n-1;i++){ //执行n-1轮操作,其中n为顶点数
for(each edge u->v){ //每轮操作都遍历所有边
if(d[u] + length[u->v] < d[v]){ //以u为中介点可以使d[v]更小
d[v] = d[u] + length[u->v]; //松弛操作
}
}
}
for(each edge u->v){ //对每条边进行判断
if(d[u] + length[u->v] < d[v]){ //如果仍可以被松弛
return false;
}
}
return true;
具体实现:
struct Node{
int v,dis; //v为邻接边的目标顶点,dis为邻接边的边权
};
vector<Node> Adj[MAXV]; //图G的邻接表
int n; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
bool Bellman(int s){ //s为源点
fill(d,d+MAXV,INF); //fill函数将整个d数组赋为INF
d[s] = 0;
//以下为求解d数组的部分
for(int i =0;i<n-1;i++){ //执行n-1轮操作,n为顶点数
for(int u = 0; u<n; u++){ //每轮操作都遍历所有的边
for(int j =0; j< Adj[u].size(); j++){
int v = Adj[u][j].v; //邻接边的顶点
int dis = Adj[u][j].dis; //邻接边的边权
if(d[u] + dis[u][v] < d[v]){ //以u为中介点可以使d[v]更小
d[v] = d[u] +dis[u][v]; //松弛操作
}
}
}
}
//以下为判断负环的代码
for(int u = 0; u<n; u++){ //对每条边进行判断
for(int j =0; j<Adj[u].size(); j++){
int v = Adj[u][j].v; //邻接边的顶点
int dis = Adj[u][j].dis; //邻接边的边权
if(d[u] + dis[u][v] < d[v]){ //如果仍然可以被松弛
return false; //说明图中有从源点可达的负环
}
}
}
return true; //数组d的所有值已经达到最优
}
需要设置记录前驱的数组set< int > pre[MAXV],当遇到一条和已有最短路径长度相同的路径时,必须重新计算最短路径条数。举例:同上算法的问题
#include
SPFA算法
伪代码:
queue<int> Q;
源点s入队;
while(队列非空){
取出队首元素u;
for(u的所有邻接边u ->v){
if(d[u]+dis <d[v]){
d[v] = d[u]+dis;
if(v不在队列中){
v入队;
if(v入队次数 > n-1){
说明有可达负环,return;
}
}
}
}
}
具体实现:
vector<Node> Adj[MAXV]; //图G的邻接表
int n,d[MAXV],num[MAXV]; //num数组记录顶点的入队次数
bool inq[MAXV]; //顶点是否在队列中
bool SPFA(int s){
//初始化部分
memset(inq,false,sizeof(inq));
memset(num,0,sizeof(num));
fill(d,d+MAXV,INF);
//源点入队部分
queue<int> Q;
Q.push(s); //源点入队
inq[s] = true; //源点已入队
num[s] += 1;//源点入队次数+!
d[s] = 0;
//主体部分
while(!Q.empty()){
int u =Q.front();
Q.pop();//队首顶点出队
inq[u] = false; //设置u不在队列中
//遍历u的所有邻接边v
for(int j = 0;j<Adj[u].size();j++){
int v = Adj[u][j].v;
int dis = Adj[u][j].dis;
//松弛操作
if(d[u] + dis < d[v]){
d[v] = dis+d[u];
if(!inq[v]){ //如果v不在队列中
Q.push(v); //v入队
inq[v] = true;
num[v] += 1;
if(num[v] >= n) return false; //存在负权回路
}
}
}
}
return true; //没有可达负权回路,d可用
}
Floyd算法
策略:
枚举顶点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]
具体实现
#include