浅谈最短路求法

\(Floyd\)算法

\(Floyd\)算法应该算是所有最短路求法中最简单的一种,但是也是最暴力的,它只能用于求全源最短路,基本思路是先枚举中转点,再计算以该点为中转点时两点之间的距离,并更新,复杂度为\(O(n^3)\)

代码:

void Floyd()
{
    for(int k = 1 ; k <= n ; ++k)
        for(int i = 1 ; i <= n ; ++i)
            for(int j = 1 ; j <= n ; ++j)
                dis[i][j] = min(dis[i][j] , dis[i][k] + dis[k][j]) ;
}

\(Bellman-Ford\)算法

\(Bellman-Ford\)算法的基本思路是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

因为松弛操作最多进行\(n - 1\)次,每次需要枚举\(m\)条边,所以复杂度是\(O(nm)\)的。

代码:

void bellmanford(int sta)
{
    for(int i = 1 ; i <= n ; ++i)
        dis[i] = inf ;
    dis[sta] = 0 ;
    bool flag = false ;
    for(int i = 1 ; i <= n ; ++i)
    {
        flag = false ;
        int siz = edge.size() ;
        for(int j = 0 ; j < siz ; ++j)
        {
            int fro = edge[j].fro , to = edge[j].to , val = edge[j].val ;
            if(dis[fro] == inf)
                continue ;
            if(dis[to] > dis[fro] + val)
            {
                dis[to] = dis[fro] + val;
                flag = true;
            }
        }
        if(!flag)
            break ;
    }
}

\(SPFA\)算法

众所周知,这是一种已经死了的算法,它是一种使用了队列进行优化的\(Bellman-Ford\)

我们考虑\(SPFA\)的基本思想,就是我们在进行\(Bellman-Ford\)的松弛操作时不需要那么多无用的操作,因为显然,只有上一次被松弛了的点所连的边,才会引发下一次的松弛操作,所以我们可以使用队列来维护当前被松弛了的点,从而只访问必要的边。

大多数情况下\(SPFA\)的复杂度是\(O(kn)\)的,其中\(k\)是一个大于\(1\)的常数,但是在最劣情况下,其复杂度会退化为\(O(nm)\),而且很容易卡,考试时要避免使用。

代码:

void spfa(int sta)
{
	memset(dis , 0x3f , sizeof dis) ;
	memset(use , 0 , sizeof use) ;
	queue<int> Q ;
	dis[sta] = 0 ;
	Q.push(sta) ;
	while(!Q.empty())
	{
		int t = Q.front() ;
		Q.pop() ;
		use[t] = false ;
		int siz = edge[t].size() ;
		for(int i = 0 ; i < siz ; ++i)
		{
			int to = edge[t][i].to , val = edge[t][i].val ;
			if(dis[t] + val < dis[to])
			{
				dis[to] = dis[t] + val ;
				if(!use[to])
				{
					use[to] = true ;
					Q.push(to) ;
				}
			}
		}
	}
}

\(Dijkstra\)算法

\(Dijkstra\)算法是一种基于贪心的算法,它每次会选择当前未确定最短路的点集中最短路长度最小的点加入到已选点集中并进行松弛操作。

\(Dijkstra\)算法一般有两种实现方式,分别是朴素的\(O(n^2)\)与堆优化的\(O(m \log m)\)

朴素实现代码:

void dijkstra(int sta)
{
    memset(dis , 0x3f , sizeof dis) ;
    dis[sta] = 0 ;
    for (int i = 1 ; i <= n ; ++i)
    {
        int u = 0 , mindis = inf ;
        for(int j = 1 ; j <= n ; ++j)
            if(!vis[j] && dis[j] < mindis)
                u = j , mindis = dis[j] ;
        vis[u] = true ;
        int siz = edge[u].size() ;
        for(int i = 0 ; i < siz ; ++i)
        {
            int to = edge[u][i].to , val = edge[u][i].val ;
            if(dis[u] + val < dis[to])
                dis[to] = dis[u] + val ;
        }
    }
}

堆优化版代码:

struct edg
{
    int to , val ;
    bool operator<(const edg& b) const
    {
        return val > b.val ;
    }
} ;
vector<edg> edge[N] ;
void dij(int sta)
{
    memset(dis , 0x3f , sizeof dis) ;
    dis[sta] = 0 ;
    priority_queue<edg> Q ;
    Q.push((edg){sta , 0}) ;
    while(!Q.empty())
    {
        int t = Q.top().to ;
        Q.pop() ;
        if(vis[t])
            continue ;
        vis[t] = true ;
        int siz = edge[t].size() ;
        for(int i = 0 ; i < siz ; ++i)
        {
            int to = edge[t][i].to , val = edge[t][i].val ;
            if(!vis[to] && dis[t] + val < dis[to])
                dis[to] = dis[t] + val , Q.push((edg){to , dis[to]}) ;
        }
    }
}

\(Dijkstra\)算法算法在大部分情况下的表现都要远优于\(SPFA\),所以如果是正权图,一般都使用\(Dijkstra\)算法,但是,堆优化的\(Dijkstra\)算法在稠密图下由于边数过多复杂度会很劣,一般这时使用朴素的\(Dijkstra\)算法。

另外要特别注意的就是\(Dijkstra\)算法只适用于正权图,在负权图中会得出错误答案,不要使用。

势能\(Dijkstra\)\(Johnson\)全源最短路

我们知道,\(Dijkstra\)算法不能用于负权图的最短路求解,所以我们考虑将负权图的边进行处理,让他们变得非负。

一种容易想到的方法是给所有边的边权同时加上一个正数\(x\),从而让所有边的边权均非负。如果新图上起点到终点的最短路经过了\(k\)条边,则将最短路减去\(kx\)即可得到实际最短路。

但是这种方法是错误的,因为路径的边数不定!假设之前的最短路长度为\(s_1\),边数为\(n_1\);另一条路径的长度为\(s_2\),边数为\(n_2\),且\(s_2 < s_1\)\(n_2 < n_1\)。处理过后的最短路长度为\(s_1+n_1x\),另外那条路径长度为\(s_2+n_2x\),这两者大小关系不定,原来的最短路不一定是现在的最短路,因此此方法不成立。

我们考虑对每一个点设一个势能\(h\),碰到一条从\(u\)\(v\)的边权为\(w\)的边,则将边权重新设置为\(w+h_u-h_v\),这样求出一条路径后,假设起点为\(s\),终点为\(e\),那么我们最终求得的最短路则为\(dis_{s,e}+h_s-h_e\),因此路径长度的改变量只与首尾的点权有关,这保证了算法的正确性,然后在求出新图的最短路之后,加上\(h_e - h_s\)即可。

但是考虑单独的势能\(Dijkstra\)其实并没有什么用,因为我们要求出每个点的势能,为了保证每两个点之间的势能差能使所有边权非负,我们需要先求出每个点的势能,所以现在考虑势能的求法。

我们可以新建一个虚拟源点,将其与其它所有点连边,新连的边权均为零,然后再跑一遍从该点出发的单源最短路,就可以求出每个点的势能。

现在,我们只需要证明新图中这样边权均非负即可。根据三角形不等式,图上任意一边\((u , v)\)上两点满足:\(h_v\leq h_u + w(u , v)\),该边在新图中的边权为\(w'(u , v) = w(u , v) + h_u - h_v \geq 0\),这样,我们就证明了新图上的边权均非负。

但是,我们为了求出势能,会先跑一遍\(SPFA\)\(Bellman-Ford\),如果只是为了跑一遍单源的势能\(Dijkstra\),根本没有必要,所以我们一般是用这种方法解决负权图上的全源最短路问题,也就是\(Johnson\)全源最短路算法。

\(Johnson\)全源最短路就是将势能求出后跑\(n\)\(Dijkstra\),如果使用了堆优化,复杂度就是\(O(n m \log m)\)的,因为考虑最开始的负权图最短路只需要跑一遍,不是时间瓶颈所在。

\(Johnson\)模板题:P5905 【模板】全源最短路(Johnson) - 洛谷 | 计算机科学教育新生态

参考代码:

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
using namespace std ;
namespace IO
{
	namespace Read
	{
		#define isdigit(ch) (ch >= '0' && ch <= '9')
		#define SIZE (1 << 16)
		char buf[SIZE] , *_now = buf , *_end = buf ;
		char getchar()
		{
			if(_now == _end)
			{
				_now = _end = buf ;
				_end += fread(buf , 1 , SIZE , stdin) ;
				if(_now == _end)
					return EOF ;
			}
			return *(_now++) ;
		}
		template<typename T>
		void read(T& w)
		{
			w = 0 ;
			short f = 1 ;
			char ch = getchar() ;
			while(!isdigit(ch))
			{
				if(ch == '-')
					f = -1 ;
				ch = getchar() ;
			}
			while(isdigit(ch))
				w = (w << 1) + (w << 3) + (ch ^ 48) , ch = getchar() ;
			w *= f ;
		}
		void read(char& c)
		{
			char tmp = getchar() ;
			while(tmp == ' ' || tmp == '\n')
				tmp = getchar() ;
			c = tmp ;
		}
		#define sb(ch) (ch == ' ' || ch == '\n')
		void read(char* s)
		{
			char ch = getchar() ;
			while(sb(ch))
				ch = getchar() ;
			int len = 0 ;
			while(!sb(ch))
				s[len++] = ch , ch = getchar() ;
			s[len] = '\0' ;
		}
		void read(string& s)
		{
			s.clear() ;
			char ch = getchar() ;
			while(sb(ch))
				ch = getchar() ;
			while(!sb(ch))
				s.push_back(ch) , ch = getchar() ;
		}
		void read(double& w)
		{
			w = 0 ;
			short f = 1 ;
			char ch = getchar() ;
			while(!isdigit(ch))
			{
				if(ch == '-')
					f = -1 ;
				ch = getchar() ;
			}
			while(isdigit(ch))
				w = w * 10 + (ch ^ 48) , ch = getchar() ;
			if(ch == '.')
			{
				ch = getchar() ;
				int len = 0 ;
				while(isdigit(ch))
					w = w * 10 + (ch ^ 48) , ch = getchar() , ++len ;
				while(len)
					w /= 10 , --len ;
			}
			w *= f ;
		}
		void read(float& w)
		{
			w = 0 ;
			short f = 1 ;
			char ch = getchar() ;
			while(!isdigit(ch))
			{
				if(ch == '-')
					f = -1 ;
				ch = getchar() ;
			}
			while(isdigit(ch))
				w = w * 10 + (ch ^ 48) , ch = getchar() ;
			if(ch == '.')
			{
				ch = getchar() ;
				int len = 0 ;
				while(isdigit(ch))
					w = w * 10 + (ch ^ 48) , ch = getchar() , ++len ;
				while(len)
					w /= 10 , --len ;
			}
			w *= f ;
		}
		void read(long double& w)
		{
			w = 0 ;
			short f = 1 ;
			char ch = getchar() ;
			while(!isdigit(ch))
			{
				if(ch == '-')
					f = -1 ;
				ch = getchar() ;
			}
			while(isdigit(ch))
				w = w * 10 + (ch ^ 48) , ch = getchar() ;
			if(ch == '.')
			{
				ch = getchar() ;
				int len = 0 ;
				while(isdigit(ch))
					w = w * 10 + (ch ^ 48) , ch = getchar() , ++len ;
				while(len)
					w /= 10 , --len ;
			}
			w *= f ;
		}
		class qistream
		{
			public:
			template<typename T>
			qistream& operator>>(T& a)
			{
				read(a) ;
				return *this ;
			}
			qistream& operator>>(char* s)
			{
				read(s) ;
				return *this ;
			}
		} qcin ;
	}
	namespace Write
	{
		#define SIZE (1 << 16)
		char buf[SIZE] , *p = buf ;
		template<typename T>
		struct pres
		{
			T num ;
			int len ;
		} ;
		template<typename T>
		pres<T> make_lf(T num , int len)
		{
			pres<T> ret ;
			ret.num = num , ret.len = len ;
			return ret ;
		}
		void flush()
		{
			fwrite(buf , 1 , p - buf , stdout) ;
			p = buf ;
		}
		void putchar(char ch)
		{
			if(p == buf + SIZE)
				flush() ;
			*p = ch ;
			++p ;
		}
		class Flush{public:~Flush(){flush() ;};}_;
		template<typename T>
		void write(T x)
		{
			char st[50] ;
			int len = 0 ;
			if(x < 0)
				putchar('-') , x = -x ;
			do
			{
				st[++len] = x % 10 + '0' ;
				x /= 10 ;
			} while(x) ;
			while(len)
				putchar(st[len--]) ;
		}
		void write(char c)
		{
			putchar(c) ;
		}
		void write(const char* s)
		{
			int siz = strlen(s) ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		void write(char* s)
		{
			int siz = strlen(s) ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		void write(string& s)
		{
			int siz = s.size() ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		void write(double x , int len = 6)
		{
			if(x < 0)
				write('-') , x = -x ;
			long long sb = (long long)(x) ;
			x -= sb ;
			int spar = 0 ;
			for(int i = 0 ; i <= len ; ++i)
			{
				x *= 10 ;
				if(int(x) == 0)
					++spar ;
			}
			long long ssb = (long long)(x) ;
			if(ssb % 10 >= 5)
				ssb += 10 ;
			ssb /= 10 ;
			write(sb) , write('.') ;
			for(int i = 1 ; i <= spar ; ++i)
				write('0') ;
			write(ssb) ;
		}
		void write(float x , int len = 6)
		{
			if(x < 0)
				write('-') , x = -x ;
			int sb = int(x) ;
			x -= sb ;
			int spar = 0 ;
			for(int i = 0 ; i <= len ; ++i)
			{
				x *= 10 ;
				if(int(x) == 0)
					++spar ;
			}
			int ssb = int(x) ;
			if(ssb % 10 >= 5)
				ssb += 10 ;
			ssb /= 10 ;
			write(sb) , write('.') ;
			for(int i = 1 ; i <= spar ; ++i)
				write('0') ;
			write(ssb) ;
		}
		void write(long double x , int len = 6)
		{
			if(x < 0)
				write('-') , x = -x ;
			__int128 sb = __int128(x) ;
			x -= sb ;
			int spar = 0 ;
			for(int i = 0 ; i <= len ; ++i)
			{
				x *= 10 ;
				if(int(x) == 0)
					++spar ;
			}
			__int128 ssb = __int128(x) ;
			if(ssb % 10 >= 5)
				ssb += 10 ;
			ssb /= 10 ;
			write(sb) , write('.') ;
			for(int i = 1 ; i <= spar ; ++i)
				write('0') ;
			write(ssb) ;
		}
		class qostream
		{
			public:
			template<typename T>
			qostream& operator<<(T x)
			{
				write(x) ;
				return *this ;
			}
			qostream& operator<<(const char* s)
			{
				write(s) ;
				return *this ;
			}
			template<typename T>
			qostream& operator<<(const pres<T> x)
			{
				write(x.num , x.len) ;
				return *this ;
			}
		} qcout ;
	}
	using Read::qcin ;
	using Write::qcout ;
	using Write::make_lf ;
}
using namespace IO ;
#define N 10010
#define inf 1000000000
int n = 0 , m = 0 ;
struct edg
{
    int to , val ;
    bool operator<(const edg& b) const
    {
        return val > b.val ;
    }
} ;
edg make_edg(int to , int val)
{
    edg ret ;
    ret.to = to , ret.val = val ;
    return ret ;
}
vector<edg> edge[N] ;
int vis[N] = {} , cnt[N] = {} ;
long long h[N] = {} , dis[N] = {} ;
void spfa()
{
    memset(h , 0x3f , sizeof h) ;
    memset(vis , 0 , sizeof vis) ;
    h[0] = 0 , vis[0] = 1 ;
    queue<int> Q ;
    Q.push(0) ;
    while(!Q.empty())
    {
        int t = Q.front() ;
        Q.pop() ;
        vis[t] = 0 ;
        int siz = edge[t].size() ;
        for(int i = 0 ; i < siz ; ++i)
        {
            int to = edge[t][i].to , val = edge[t][i].val ;
            if(h[to] > h[t] + val)
            {
                h[to] = h[t] + val ;
                cnt[to] = cnt[t] + 1 ;
                if(cnt[to] > n)
                {
                    printf("-1\n") ;
                    exit(0) ;
                }
                if(!vis[to])
                {
                    Q.push(to) ;
                    vis[to] = 1 ;
                }
            }
        }
    }
}
void dij(int s)
{
    priority_queue<edg> Q ;
    for(int i = 1 ; i <= n ; ++i)
        dis[i] = inf ;
    memset(vis , 0 , sizeof(vis)) ;
    dis[s] = 0 ;
    Q.push(make_edg(s , 0)) ;
    while(!Q.empty())
    {
        int t = Q.top().to ;
        Q.pop() ;
        if(vis[t])
            continue ;
        vis[t] = true ;
        int siz = edge[t].size() ;
        for(int i = 0 ; i < siz ; ++i)
        {
            int to = edge[t][i].to , val = edge[t][i].val ;
            if(dis[to] > dis[t] + val)
            {
                dis[to] = dis[t] + val ;
                if(!vis[to])
                    Q.push(make_edg(to , dis[to])) ;
            }
        }
    }
}
int main()
{
    qcin>>n>>m ;
    for(int i = 0 ; i < m ; ++i)
    {
        int a = 0 , b = 0 , c = 0 ;
        qcin>>a>>b>>c ;
        edge[a].push_back(make_edg(b , c)) ;
    }
    for(int i = 1 ; i <= n ; ++i)
        edge[0].push_back(make_edg(i , 0)) ;
    spfa() ;
    for(int u = 1 ; u <= n ; ++u)
    {
        int siz = edge[u].size() ;
        for(int i = 0 ; i < siz ; ++i)
            edge[u][i].val += h[u] - h[edge[u][i].to] ;
    }
    for(int i = 1 ; i <= n ; ++i)
    {
        dij(i) ;
        long long ans = 0 ;
        for(int j = 1 ; j <= n ; ++j)
        {
            if(dis[j] == inf)
                ans += (long long)j * inf ;
            else
                ans += (long long)j * (dis[j] + h[j] - h[i]) ;
        }
        qcout<<ans<<'\n' ;
    }
    return 0 ;
}

关于负环的判定

\(Bellman-Ford\)

在最短路存在的情况下,一次松弛操作会使最短路的边数至少增加\(1\),而最短路的边数最多为\(n-1\),所以对于最短路存在的图,松弛操作最多只会执行\(n-1\)轮,若第\(n\)轮仍能松弛,说明图中存在负环。

\(SPFA\)

\(SPFA\)中,我们只需记录最短路经过了多少条边,当经过了至少\(n\)条边时,说明图中存在负环。

\(Dijkstra\)

\(Dijkstra\)只适用于正权图,因此也不能用于判负环

\(Johnson\)

在前面用\(SPFA\)\(Bellman-Ford\)求势能时判即可。

完结★,°:.☆( ̄▽ ̄)/$:.°★

posted @ 2024-11-28 22:05  Torrentolf  阅读(172)  评论(0)    收藏  举报