最短路问题
图的存储
- 邻接矩阵存储: 用二维数组存图,
g[i][j]
代表节点i到节点j有一条有向边,其元素值为边的权重- 邻接表存储(vector实现):使用C++的STL库中的
vector
与结构体实现
struct edge{
int from, to, w; //边:起点 from, 重点 to, 权值 w
edge(){}
edge(int a, int b, int c){from = a; to = b; w = c;}
};
vector<edge> e[N]; //e[i] 存第i个节点连接的所有边
//边的插入操作 from, to, w
e[from].push_back(edge(from, to, w));
//访问节点a的所有出边
for (int i = 0; i < e[a].size(); i ++ ){
int from = e[a][i].from;
int to = e[a][i].to;
int w = e[a][i].w;
}
- 邻接表存储(数组模拟链表)
int h[N], e[N], w[N], ne[N], idx;
// 初始化
memset(h, -1, sizeof h); //为h数组赋值为1
idx = 1;
//插入边 a, b, c (a到b的一条有向边,权重为c)
e[idx] = b; ne[idx] = h[a]; h[a] = idx; w[idx ++ ] = c;
//访问节点a的所有出边
for (int i = h[a]; ~i; i = ne[i]){
int b = e[i], c = w[i]; //b为a指向的节点,c为权重
}
单源最短路(无负权)
解决无负权的单源最短路通常使用
dijkstra
算法,其思想为维护一个最短路节点集合S
,和一个距源点的最短距离数组dis[N]
,dis
数组元素初始化为无穷大.
- 设源点为
x
,并标记dis[x] = 0
- 选择不在
S
中,且其dis
值最小的节点a
放入S
中- 更新节点a的所有出边指向的节点的dis值:
dis[b] = min(dis[b], dis[a] + weight(a, b))
- 重复步骤2、3,直到所有节点都在S中
朴素版dijkatra模板,时间复杂度
int dis[N], g[N][N]; //dis[i]记录节点i距离目标节点的最短距离, g[N][N]存图
bool st[N]; //st[i]为true代表节点i在集合S中
void dijkstra(){ //计算节点n到节点1的最短路
memset(dis, 0x3f, sizeof dis); //初始化距离为无穷大
dis[1] = 0;
for (int i = 1; i <= n; i ++ ){ //n次循环,每次将一个节点加入集合S
int t = -1;
for (int j = 1; j <= n; j ++ ){ // 找到dis值最小的节点(即距离集合S末端节点最近的节点)
if (!st[j] && (t == -1 || dis[t] > dis[j])) t = j;
}
st[t] = true; //将节点t放入集合S中
for (int j = 1; j <= n; j ++ ){ //用dis[t]更新t连接的节点的最短路距离
if (g[t][j]) dis[j] = min(dis[j], dis[t] + g[t][j]);
}
}
return dis[n];
}
优先队列优化版dijkstra模板
typedef pair<int, int> pii;
int h[N], e[N], ne[N], w[N], idx;
bool st[N];
void dijkstra(){ //计算节点n到节点1的最短距离
memset(dis, 0x3f, sizeof dis); //初始化距离为无穷大
dis[1] = 0;
priority_queue<pii, vector<pii>, greater<pii> > q; //定义优先队列,存放节点的dis值与节点编号,按dis值升序排序
q.push({dis[1], 1});
while(!q.empty()){
pii u = q.top(); //取出队头元素
q.pop();
int a = u.first, b = u.second; // 取出距离与节点编号
if (st[b]) continue; //若节点最短路已经确定,则继续循环
st[b] = true; //标记节点,代表节点的最短路已经确定
for (int i = h[b]; ~i; i = ne[i]){ //遍历节点b的所有出边,更新最短路
int x = e[i];
if (dis[x] > a + w[i]){ //若节点x的最短路距离可以被b的最短距离更新,则执行更新与入队操作
dis[x] = a + w[i];
q.push({dis[x], x});
}
}
}
if (dis[n] == 0x3f3f3f3f) return -1; //若节点的dis值仍为无穷大,则不存在最短路径
return dis[n];
}
单源最短路(有负权)
Bellman-ford算法
Bellman-ford
算法主要用于解决有边权限制的最短路算法,如到目标节点做多不超过步的最短路径,其算法思想为,对所有边进行次松弛,用矩阵存储每次松弛后每个节点到达目标节点的最短距离,在每次松弛操作时,用上一次的矩阵更新当前边缘末端节点的最短距离。
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m, k, dis[N], st[N];
struct edge{ //结构体存储图
int a, b, c;
}e[M];
void bellman-ford(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for (int i = 0; i < k; i ++ ){ //进行k次松弛
memcpy(st, dis, sizeof dis); // 拷贝上次松弛的结果
for (int j = 0; j < m; j ++ ) { // 遍历每条边,对其进行松弛操作
int a = e[j].a, b = e[j].b, c = e[j].c;
dis[b] = min(dis[b], st[b] + c); //松弛操作
}
}
if (dis[n] > 0x3f3f3f3f / 2) return -1; //判断最短路是否存在
else return dis[n];
}
int main(){
cin >> n >> m >> k;
int a, b, c;
for (int i = 0; i < m; i ++ ){
cin >> a >> b >> c;
e[i] = edge({a, b, c});
}
cout << bellman-ford() << endl;
}
SPFA算法 一般,最坏
spfa
算法是bellman-ford
算法的优化,其思想为,在bellman-ford
算法对每条边进行松弛的过程中,并不是所有节点距目标节点的最短距离都会发生变化,但只有最短距离变小的节点才会对其他节点的最短距离产生影响。spfa
算法与dijkstra
算法类似,具体步骤如下:
- 声明队列
Q
,最短距离列表dis[N]
初始化为无穷大,节点状态列表st[N]
,判断节点是否在队列中。将起始节点s
入队,并设置dis[s] = 0
,st[i] = true
,若队列不为空,则执行以下操作.- 队头元素
u
出队,并标记st[u] = false
.- 遍历节点
u
的所有出边所指向的节点x
判断节点x
的最短距离是否能被节点u
更新,若能更新,则将节点u
入队,并标记st[x]
及更新dis[x]
const int N = 1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], w[M], idx;
int n, m, st[N], dis[N];
int spfa(){ //计算1号节点到n号节点的最短距离
queue<int> q;
dis[1] = 0; //存储1号节点到所有节点的最短距离
st[1] = 1; //标记1号节点在队列中
q.push(1);
while(!q.empty()){
int u = q.fornt(); //取出队头元素
q.pop();
st[u] = 0;
for (int i = h[u]; ~i; i = ne[i]){ //遍历节点u的所有邻居
int x = e[i]; //取出邻居x
if (dis[x] > dis[u] + w[i]){ //若1号点到x的最短距离可以被更新,则更新并将x入队
dis[x] = dis[u] + w[i];
if (!st[x]){ //若x不在队列中
q.push(x);
st[x] = 1;
}
}
}
}
if (dis[n] > 0x3f3f3f3f / 2) return -1; // 判断最短路是否存在
else return dis[n];
}
void addedge(int a, int b, int c){ //节点的插入操作
e[idx] = b; ne[idx] = h[a]; w[idx] = c; h[a] = idx ++;
}
void init(){ //初始化
memset(h, -1, sizeof h);
memset(dis, 0x3f, sizeof dis);
idx = 1;
}
int main(){
ios::sync_with_stdio(false); //优化cin写入速度
cin.tie(0); //优化cin写入速度
init();
cin >> n >> m;
int a, b, c;
for (int i = 0; i < m; i ++ ){
cin >> a >> b >> c;
addedge(a, b, c); //插入连边
}
int ans = spfa();
if (ans < 0) puts("impossible");
else cout << ans << endl;
负权回路
Bellman-ford判断负环
Bellman-ford
求最短路的思想是对每条边均进行k次松弛操作,假设1号节点到n号节点存在最短路且不存在负环,则在进行n次松弛操作后,一定能求的1到n的最短路。若其路径中存在负环,则在1到n路径上的某个点,1号点距其最短距离在进行n次松弛操作后,必定能继续变小,以此来判断负环是否存在。
const int N = 1e5 + 10, M = 2e5 + 10;
int dis[N], st[N];
struct Edge{
int a, b, c;
}e[M];
bool bellman_ford(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
bool ans = false;
for (int i = 0; i < n; i ++ ){
memcpy(st, dis, sizeof dis); //拷贝上次的结果
for (int j = 0; j < m; j ++ ){
int a = e[j].a, b = e[j].b, c = e[j].c;
dis[b] = min(dis[b], st[a] + c); //用上次的结构更新本次松弛
}
}
for (int i = 0; i < m; i ++ ){ //在进行n次松弛后判断是否能继续松弛,若能,则负环存在,否则不存在
int a = e[i].a, b = e[i].b, c = e[i].c;
if (dis[i] > dis[i] + c) {
ans = true; //表示存在负环
break;
}
}
return ans;
spfa判断负环
由于
spfa
是bellman-ford
算法的变体,因此其判断负环的方式与bellman-ford
算法类似,即在进行n次松弛之后,若某节点仍可继续松弛,则存在负环。但由于spfa
通过维护一个队列结构来进行松弛操作,某一节点被松弛后,即进入队列,因此可以开辟一个数组判断某个节点进入队列的次数,若大于n次,则必定存在负环。
int cnt[N]; //记录节点进入队列的次数
bool spaf(){
queue<int> q;
for (int i = 1; i <= n; i ++ ) {
dis[i] = 0;
st[i] = 1; //编辑节点是否在队列中
cnt[i] = 1; //统计节点进入队列的次数
q.push(i);
}
while(!q.empty()){
int u = q.front();
q.pop();
st[u] = 0;
for (int i = h[u]; ~i; i = ne[i]){
int a = e[i];
if (dis[a] > dis[u] + w[i]){
dis[a] = dis[u] + w[i]; //松弛操作
if (!st[a]) { //若a不在队列中,则入队
st[a] = 1;
q.push(a);
cnt[a] ++;
if (cnt[a] > n) return true; //若入队次数大于n,存在负环
}
}
}
}
return false;
}
多源汇最短路
多源汇最短路可计算两两节点之间的最短路,通常用
floyd
算法解决,其思想为:
遍历每个节点k,视其为中间节点。
遍历所有节点对
(i, j)
, 若i可指向节点k,节点k可指向j,则做如下操作
dis(i, j) = min(dis(i, j), dis(i, k) + dis(k, j))
int g[N][N]; //邻接矩阵存储节点间的距离
const int INF = 0x3f3f3f3f;
int n, m, k;
void floyd(){
for (int k = 1; k <= n; k ++ ){
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j <= n; j ++ ){
g[i][j] = min(g[i][j], g[i][k] + g[k][j]); //利用中间节点k来更新节点i与j之间的最短路
}
}
}
}
int main(){
cin >> n >> m >> k; //n表示节点数,m表示边数,k表示查询次数
int a, b, c;
for (int i = 1; i <= n; i ++ ){ //初始化,两两不同节点间的距离初试化为无穷大
for (int j = 1; j <= n; j ++ ){
g[i][j] = INF;
if (i == j) g[i][j] = 0;
}
}
for (int i = 0; i < m; i ++ ) {
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c);
}
floyd();
while(k -- ){ //k次查询,查询两个节点间的最短距离
cin >> a >> b;
if (g[a][b] > INF / 2) puts("impossible");
else cout << g[a][b] << endl;
}
}