简介
最短路问题一般分为两类,单源最短路和多源汇最短路。
单源最短路是求一个点到其他点的最短距离,单源最短路又可以分为两大类,所有边权都是正数和存在负权边两类。
所有边权都是正数:朴素Dijkstra算法,时间与边数无关,适合稠密图。和堆优化版的Dijkstra算法,适合稀疏图。
存在负权边:Bellman-Ford算法。SPEA算法(效率高一些,但是如果有限制经过的边数就不能用)
多源汇最短路就是起点和终点不固定求最短路径,只有一种Floyd算法。
朴素Dijkstra算法
基本思路
首先初始化距离,我们用一个dis数组表示每个点到起点的距离,dis[1]就表示1号点到起点的距离(其实1号点就是起点)。令dis[1] = 0,别的为正无 穷(用一个很大的数代替就行),然后声明一个s来存已经确定最短距离的点。
然后循环n次,在循环中,我们找一个不在s中的,距离最近的点t。然后把t加入s中,再用t更新各点的距离(假如更新的距离比原距离小就覆盖,大就略过)
这样我们每次循环就能确定一个点的最短距离,n次就能确定所有点了。
(这个的感觉其实就是,先慢慢从起点往外延申,起点延申出来的好几条中最短的路径,这一条肯定是最短的,延申出来的这个点又可以当一个新的起点,假如说这个起点到一个点的距离有点大了,我们就会用老起点延申出来的第二短的路继续找。有一种更新起点的机制,同时又避免了绕路)
这个适用于稠密图(用邻接矩阵存),不用太过关注是有向图还是无向图,无向图就是一种特殊的有向图(两个方向都通),所以一律当作有向图来做就行
代码
const int N = 510;
int n,m;
int g[N][N]; //邻接矩阵存图
int dis[N];
bool st[N]; //dis中的距离是不是最短的
int dijkstra()
{
//初始化
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for (int i = 0; i < n; i++)
{
int t = -1;
//找最短距离
for (int j = 1; j <= n; j++)
{
if (!st[j] && (t == -1 || dis[t] > dis[j]))
t = j;
}
st[t] = true; //加入s
//利用t更新最短距离
for (int j = 1; j <=n; j++)
{
dis[j] = min(dis[j], dis[t] + g[t][j]);
}
}
//返回距离
if (dis[n] == 0x3f3f3f) return -1;
return dis[n]
}
堆优化版的Dijkstra算法
基本思路
在上面的朴素版本里面,最慢的一步是找出最小距离,我们就可以用之前学过的堆结构来优化它。我们直接用c++的优先队列就行。
然后因为是稀疏图,我们要用邻接表的形式
代码
typedef pair<int, int> PII;
const int N = 100010;
int n,m;
//h存链表头指针
//w存边的权重
//e存终点
//ne存链表指针
int h[N], w[N], e[N], ne[N], idx;
int dis[N];
bool st[N]; //dis中的距离是不是最短的
void dijkstra()
{
//初始化
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
//队列初始化
//这三个模板的意思是数据类型,容器类型,比较方式(这个是PII升序的意思)
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({ 0,1 });
while (heap.size())
{
//这样就相当于是得到最短的了
auto t = heap.top();
heap.pop();
//ver是对应的位置,distance是距离
int ver = t.second, distance = t.first;
if (st[ver]) continue;
//遍历所有路径
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
//更新距离,加入队列
if (dis[j] > distance + w[i])
{
dis[j] = distance + w[i];
heap.push(dis[j], j);
}
}
}
//返回距离
if (dis[n] == 0x3f3f3f) return -1;
return dis[n]
}
Bellman-Ford算法
基本思路
迭代n次,每次迭代遍历所有边。每次循环用当前边更新距离(第二重遍历的时候用已经找到的最短距离更新最短距离)。这个算法对数据结构无要求,我们可以直接用结构体数组存。
我们第一重迭代的次数其实有实际意义,第一重迭代的次数就是经过几条边找到的最短路径。
代码
const int N = 510, M = 10010;
int n, m, k;
int dis[N], backup[N];
struct Edge
{
int a, b, w;
}edges[M];//Edge是边,a,b是起点终点,w是权值
int bellman_ford()
{
//初始化
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
//这个n可以改成边数限制的次数
for (int i = 0; i < n; i++)
{
//这个相当于是将上一次的结果拷贝一下,确保我们更新只用上一次的结果
memcpy(backup, dis, sizeof dis);
//遍历边,更新距离
for (int j = 0; j < m; j++)
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dis[b] = min(dis[b], backup[a] + w);
}
}
if (dis[n] > 0x3f3f3f3 / 2) return -1; //除2是为了防止在负路径里面套导致更新距离
return dis[n];
}
SPEA算法
基本思路
这个算法是对Bellman-Ford算法的一个优化,因为Bellman-Ford算法其中有很多不需要做的操作,比如如果一个点没有被更新过,那么它后面的点也不需要更新,就相当于是跳过了一些无用功。
我们用类似宽搜的思想来优化它,把更新过的点放进一个队列,再然后取出更新放入新点。
(长得特别像Dijkstra算法)
代码
typedef pair<int, int> PII;
const int N = 100010;
int n, m;
//h存链表头指针
//w存边的权重
//e存终点
//ne存链表指针
int h[N], w[N], e[N], ne[N], idx;
int dis[N];
bool st[N]; //dis中的距离是不是最短的
int SPFA()
{
//初始化
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
queue<int> q;
q.push(1);
st[1] = false;
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dis[j] > dis[t] + w[i])
{
dis[j] = dis[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
if (dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
Floyd算法
基本思路
基于动态规划的思想,用邻接矩阵存图。
首先需要初始化一个二维数组dis,dis[i][j]表示从i到j的距离。初始化时直接相连的赋值,不直接相连的用正无穷代替。
算法将由i到j换成由i到k的距离再加上由k到j的距离,如果更小就替换。
需要使用三重循环,第一重循环k,使算法经过每一个点,第二重循环i,第三重循环j,然后用一个if判断需不需要改距离。
更适合稠密图。
代码
void floyd()
{
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}