最短路
序
在这篇文章中,n代表点的数量 ,m代表边的数量。
单源无负权最短路
戴克斯特拉
适用于稠密图,且图中无负权的情况 。
时间复杂度:O(n2)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std ;
const int N = 505 ;
const int inf = 0x3f3f3f3f ;
int W[N][N] ;
int d[N] ;
bool vis[N] ;
void dijkstra(int n){
d[1] = 0 ;
for(int i = 1 ; i <= n ; i ++ ){
int minn = inf , k = 0 ;
for(int j = 1 ; j <= n ; j ++ ){
if(d[j] < minn && !vis[j] ){
minn = d[j] ;
k = j ;
}
}
vis[k] = true ;
for(int j = 1 ; j <= n ; j ++){
if( d[j] > d[k] + W[k][j]) {
d[j] = d[k] + W[k][j] ;
}
}
}
}
int main(){
memset(W , 0x3f , sizeof W) ;
memset(d , 0x3f , sizeof d);
int n , m ;
cin >> n >> m ;
for(int i = 1 ; i <= m ; i ++){
int a ,b ,c;
cin >> a >> b >> c ;
W[a][b] = min(W[a][b] , c ) ;
}
for(int i = 1 ; i <= n ; i ++){
W[i][i] = 0 ;
}
dijkstra(n) ;
if(d[n] == inf ) puts("-1");
else cout << d[n] << endl ;
return 0 ;
}
堆优化戴克斯特拉
适用于稀疏图 ,且图中没有负权边的情况。
时间复杂度:O(m logn)
但在稠密图的情况下,m趋近于n2,回导致时间复杂度成为O(n2 logn),反而没有朴素dijkstra算法高校。
此算法的思想主要是优化在朴素版中的寻找最小d的过程。
我们采用小根堆实现一个优化过程,让每一次有值更新时都采取入对并且对堆造成影响。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 150005 ;
typedef pair <int ,int > PII ;
int h[N] , e[N] ,w[N] , ne[N] , idx ;
int d[N] ;
bool vis[N] ;
void add (int a ,int b ,int c){
e[++idx] = b , w[idx] = c ;
ne[idx] = h[a] ;
h[a] = idx ;
}
void dijkstra() {
memset(d , 0x3f ,sizeof d);
priority_queue<PII ,vector<PII> ,greater<PII> > q;
q.push({0 , 1});
d[1] = 0 ;
while(q.size()){
auto t = q.top();
q.pop();
int ver = t.second , ww = t.first ;
if(vis[ver]) continue ;
vis[ver] = true ;
for(int i = h[ver] ; i != -1 ; i = ne[i] ){
int jv = e[i] , jw = w[i] ;
if(!vis[jv] && d[jv] > jw + ww){
d[jv] = jw + ww ;
q.push({d[jv] , jv}) ;
}
}
}
}
int main(){
memset(h , -1 , sizeof h ) ;
int n ,m ;
cin >> n >> m ;
for(int i = 1 ; i <= m ; i ++){
int a , b , c ;
cin >> a >> b >> c ;
add(a ,b ,c) ;
}
dijkstra();
if (d[n] == 0x3f3f3f3f ) puts("-1" ) ;
else cout << d[n] << endl ;
return 0 ;
}
单源有负权最短路
Bellman-Ford
在有负权边的情况下很有可能求不出最短路:
若从起点到终点上存在负权回路,我们是无法求出最短路径的。
负权回路:此回路上的权值总和小于0
但如果负权回路不在起点与终点的路径上,我们是可以求出最短路径的。
bellman-ford算法才去的思想主要是动态规划。
算法中的迭代k次代表的是最多不超过k条边。
有一个注意的点是最后判断某个点(n)是否能够到达不能直接判断d[n] == 0x3f3f3f3f ,原因是0x3f3f3f3f并不是数学意义上的无穷,所依某个并不能到达的点可能会被更新。所依我们要判断的条件是是否大于0x3f3f3f3f-m*(权值下限)
d
[
n
]
>
i
n
f
−
m
×
权
值
下
限
d[n] > inf - m \times 权值下限
d[n]>inf−m×权值下限
时间复杂度: O(nm)
使用场景:有负权边且求最多不超过k条边的最短路径问题,若SPFA被卡也可尝试这个。
#include <iostream>
#include <cstring>
using namespace std ;
const int N = 100005 ;
typedef struct {
int a,b,w;
}Node ;
Node edge[N] ;
int d[505] ,backup[505];
void Bellman_Ford(int m , int k){
memset(d , 0x3f , sizeof d) ;
d[1] = 0 ;
for(int i = 1 ; i <= k ; i ++ ){
memcpy(backup , d , sizeof d) ;//数据污染,所以要将未被污染的copy
for(int j = 1 ; j <= m ; j ++){
int a = edge[j].a , b = edge[j].b , w = edge[j].w ;
d[b] = min (d[b] , backup[a] + w) ;
}
}
}
int main(){
int n ,m ,k ;
cin >> n >> m >> k ;
for (int i = 1 ; i <= m ; i ++){
int a, b ,c;
cin >> a >> b >> c ;
edge[i] = {a , b , c};
}
Bellman_Ford(m,k);
if (d[n] > 0x3f3f3f3f / 2) puts("impossible");
else cout << d[n] << endl ;
return 0 ;
}
SPFA
SPFA算法的是利用队列将bellman-ford算法进行的优化。在队列中的点(除了起始点是一开始就入队)都是因为这个点发生了更新,我们才让他入队。
也就是说,我们每次取队头,遍历他的边,若与之相连的点发生了更新,我们就让这个点入队。
时间复杂度 :一般O(m) ,最坏O(nm)
适用场景:有负权边的最短路径问题,判断负环。
最短路径问题
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 100005;
int h[N] ,e[N] ,w[N] ,ne[N] ,idx ;
bool vis[N] ;
int d[N] ;
void add (int a, int b ,int c){
e[++idx] = b , w[idx] = c ;
ne[idx] = h[a] ;
h[a] = idx ;
}
void SPFA(){
memset(d , 0x3f ,sizeof d) ;
d[1] = 0 ;
queue<int> q;
q.push(1) ;
vis[1] = true ;
while(q.size()){
auto t = q.front();
q.pop();
vis[t] = false ;
for(int i = h[t] ; i != -1 ; i = ne[i] ){
int jv = e[i] , jw = w[i];
if(d[jv] > jw + d[t] ){
d[jv] = jw + d[t] ;
if(!vis[jv]){
q.push(jv) ;
vis[jv] = true ;
}
}
}
}
}
int main(){
memset(h ,-1 , sizeof h);
int n,m ;
cin >> n >> m ;
for(int i = 1 ; i <= m ; i ++ ){
int a , b , c ;
cin >> a >> b >> c ;
add(a , b , c);
}
SPFA() ;
if (d[n] == 0x3f3f3f3f ) puts("impossible");
else cout << d[n] << endl ;
return 0 ;
}
判断负环
spfa算法判断负环利用了鸽巢原理。我们让cnt[i]代表从原点到i点所经历的负权边数量。每当有一条从i到j的边更新时,我们就可以理解为这一条是负权边,所以我们让cnt[j] = cnt[i] + 1 。当某个点从起点到终点的数量达到n时,代表到达这个点所经历了n条负权边。但图中一共只有n个点,所以必定存在一个环。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std ;
const int N = 100005 ;
int h[N] ,e[N] ,w[N] ,ne[N] ,idx ;
int d[N] ,cnt[N] ;
bool vis[N] ;
void add(int a ,int b ,int c){
e[++idx] = b , w[idx] = c ;
ne[idx] = h[a];
h[a] = idx;
}
bool SPFA(int n){
queue<int > q;
for(int i = 1 ; i <= n ; i ++){
q.push(i) ;
vis[i] = true ;
}
while(q.size()){
auto t = q.front();
q.pop();
vis[t] = false ;
for(int i = h[t] ; i != -1 ; i = ne[i]){
int jv = e[i] , jw = w[i] ;
if(d[jv] > d[t] + jw){
d[jv] = d[t] + jw ;
cnt[jv] = cnt[t] + 1;
if(cnt[jv] >= n) return true ;
if(!vis[jv]) {
q.push(jv);
vis[jv] = true ;
}
}
}
}
return false ;
}
int main(){
memset(h , -1 , sizeof h);
int n , m ;
cin >> n >> m ;
for(int i = 1 ; i <= m ; i ++){
int a , b , c ;
cin >> a >> b >> c ;
add(a ,b ,c );
}
bool tag = SPFA(n);
if(tag) puts("Yes");
else puts("No");
return 0 ;
}
多源汇最短路
Floyd
此算法主要思想利用了动态规划,一句话概括就是,比较存起来的从i到j的距离和从i到k再从k到j的距离,然后取小者。
#include <iostream>
#include <cstring>
using namespace std ;
const int N = 205 ;
int d[N][N] ;
void floyd(int n){
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]);
}
int main(){
int n, m ,q ;
cin >> n >> m >> q ;
memset(d , 0x3f , sizeof d) ;
for(int i = 1 ; i <= n ; i ++){
d[i][i] = 0 ;
}
while(m -- ){
int a, b ,c;
cin >> a >> b >> c ;
d[a][b] = min (d[a][b] , c) ;
}
floyd(n) ;
while( q -- ){
int a ,b ;
cin >> a >> b ;
if ( d[a][b] > 0x3f3f3f3f / 2 ){
puts("impossible");
}
else {
cout << d[a][b] << endl ;
}
}
return 0 ;
}