最短路


假设n为图的点,m为连接的边,若m远小于n*n的图称为稀疏图,而m相对较大的图称为稠密图。稀疏图适合用vector数组保存,还有最近比较流行的邻连接表保存。在这种表示法中,每个结点i都有一个链表,里面保存着从i出发的所有边,对于无向图来说,每条边会在连接表中出现两次。

Floyd算法:

求出每两点之间的最短路,关键代码:
在这之前要先初始化,d[i][i] = 0 ,其他d为无穷大INF,注意INF不能开的太大,否则在d[i][k] + d[k][j] 的时候有可能会溢出,但是太小又有可能成为短路的一部分,一般设置大一点点就可以,例如:有m条边,每条边的长度不超过1000,那么INF可以设置成1000001,或者是在相加前加一个判断(d[i][k] < INF && d[k][j] < INF)

for(int k=1; k<=n; k++)         //枚举分割点
        for(int i=1; i<=n; i++)
			if(graph[i][k] != INF)      
            for(int j=1; j<=n; j++)  枚举从i到j的路线
                if(graph[i][j] > graph[i][k] + graph[k][j])
                   graph[i][j] = graph[i][k] + graph[k][j];
// graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);  

三层循环时间复杂度为:n * n * n ,所以只能处理 n < 200 的情况。
但是floyd程序简单,可以一次性求出所有结点之间的最短路,能够处理有负圈的边。
负圈:带负权的边,图中可能有环路边上的权值之和为负数,这就是负圈,每一次走到这个负圈内,总权值会变小导致陷在圈里出不来,用floyd判断负圈只要判断是否存在g[i][i] < 0 即可,因为这是i从外面绕了一圈回来的最小路径,如果小于0,则说明存在负圈。

Dijkstra算法:

适用于边权为正的情况。可计算正权图上的单源最短路(即从单个远点出发,到所有结点的最短路),该算法适用于有向图和无向图。
方法实现: 利用优先队列的性质,每次弹出距离起点最近的点来处理(松弛)。
详细解说移步大佬博客:https://blog.csdn.net/qq_35644234/article/details/60870719
模板题:hdu2544

#include <cstdio>
#include <vector>
#include <queue>
using namespace std ; 
const int N = 105 ; 
const int INF = 1e6 ; 
struct edge{	//边的终点,权值 
	int to , w ; 
	edge(int a,int b){to = a ; w = b ;} ; 
} ;
struct node{
	int id , dis ; //结点的编号和距离起点的距离
	node(int a,int b){
		id = a ; dis = b ; 
	} 
	bool operator < (const node & c)
	const{
		return dis > c.dis ; 
	}
};
vector<edge> e[N] ; //用于存图 
int n , m ; 
void dijkstra(int s){	//起点 
	int d[N] ; 	//存每个结点到起点的距离 
	bool done[N] ;	//每个结点到起点的最短距离是否已经找到 
	for (int i = 1 ; i <= n ; ++ i ){	//初始化 
		d[i] = INF ; 
		done[i] = false ;  
	}
	d[s] = 0 ;
	priority_queue<node> q ;
	q.push (node(s,d[s])) ; 
	while(!q.empty()) {
		node u = q.top() ;
		q.pop() ;
		if (done[u.id])	continue ; 
		done[u.id] = true ; 
		for (int i = 0 ; i < e[u.id].size() ; ++ i){
			edge y = e[u.id][i] ; 
			if (done[y.to])		continue ; 	//已找到最短路径,跳过 
			if (d[y.to] > u.dis + y.w){
				d[y.to] = u.dis + y.w ; 
				q.push(node(y.to ,d[y.to])) ; //扩展新邻居入队 
			}
		} 
	}
	printf ("%d\n",d[n]) ;
}
int main(){
	while(~scanf ("%d%d",&n,&m)){
		if (m == 0 && n == 0)	break ; 
		for (int i = 1 ; i <= n ; ++ i)
			e[i].clear() ; 
		while(m --){	//建图 
			int a ,  b , c ; 
			scanf ("%d%d%d",&a,&b,&c) ;
			e[a].push_back(edge(b,c)) ; 
			e[b].push_back(edge(a,c)) ;
		}
		dijkstra(1) ; 
	}
	return 0 ; 
} 

dijsksta的应用

昂贵的聘礼(入口

交换的规则可以转化为最短路,题目要求找到到达点1的最短路(最小花费)并且等级不超过m,可以枚举最低的等级起点,每次以0为起点(0到各个点都可达即为每个点本身的花费),然后每次返回0(起点)到1的最短距离,用ans保存每次进行dijksta的最小结果。

#include <cstdio>
#include <queue>
#include <cstring>
using namespace std ; 
const int N=110 ;
const int INF=0x3f3f3f ; 
int n,m ;
struct node{
	int id,d ; 
	node(int a,int b){id=a,d=b;} ; 
	bool operator < (const node & c)
	const{
		return d > c.d ;
	}
};
int dis[N] ,cost[N][N] , state[N] ; 
int dijksta(int level){
	memset(dis,INF,sizeof(dis)) ; 
	dis[0]=0 ;	//以0为起点(到任何点都可达) 
	priority_queue<node> q ; 
	q.push(node(0,0)) ; 
	while(!q.empty()){
		node x = q.top() ; q.pop() ; 
		if(x.d > dis[x.id])	continue ; 
		for(int i=1 ; i<=n ;++i){
			if(state[i]>=level&&state[i]<=level+m){
				if(dis[i]>dis[x.id]+cost[x.id][i]){
					dis[i] = dis[x.id]+cost[x.id][i] ; 
					q.push(node(i,dis[i])) ; 
				}
			}
		}
	}
	return dis[1] ; 
} 
int main(){
	scanf("%d%d",&m,&n) ;
	for(int i=0 ; i<=n ; ++i){
		for(int j=0 ; j<=n ; ++j){
			if(i==j)	cost[i][j]=0 ; 
			else	cost[i][j]=INF ; 
		}
	}
	for(int i=1 ; i<=n ; ++i){
		int x; scanf("%d%d%d",&cost[0][i],&state[i],&x) ; 
		for(int j=0 ; j<x ; ++j){
			int u,w; scanf("%d%d",&u,&w) ; 
			cost[u][i] = w ; 
		}
	}
	int ans=INF ; 
	for(int i=state[1]-m ; i<=state[1] ; ++i){	//从能交换的最低等级开始枚举 
		ans = min(ans,dijksta(i)) ; 
	}
	printf("%d\n",ans) ;
	return 0 ; 
} 

bellman ford 算法

#include <cstdio>
#include <vector>
using namespace std ; 
const int N = 10010 ;
const int INF = 1e6 ; 
struct edge{
	int v , u , w ; 
}e[N] ;
int n , m ; 
void bellman(){
	int dis[N] ; 
	for (int i = 1 ; i <= n ; ++ i)
		dis[i] = INF ; 
	dis[1] = 0 ;
	for (int i = 1 ; i <= n ; ++ i){	//遍历每一个点 
		for (int j = 1 ; j <= m ; ++ j){	//遍历每一条边 
			if (dis[e[j].u] > dis[e[j].v] + e[j].w)
				dis[e[j].u] = dis[e[j].v] + e[j].w ; 
			if (dis[e[j].v] > dis[e[j].u] + e[j].w)		//无向图两边都要判断 
				dis[e[j].v] = dis[e[j].u] + e[j].w ; 	
		}		 
	}
	printf ("%d\n",dis[n]) ;
} 
int main(){
	while(~scanf ("%d%d",&n,&m)){
		if (n == 0 && m == 0)	break ; 
		for (int i = 1 ; i <= m ; ++ i){
			scanf ("%d%d%d",&e[i].v,&e[i].u,&e[i].w) ;
		}
		bellman() ;
	}
	return 0 ; 
}

spfa(例题:hdu2066)

移步:大佬解说

#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
using namespace std ; 
const int N = 10005 ;
const int INF = 1e6 ; 
struct egde{
	int to , w ; 
	egde(int a , int b){to = a ; w = b ;} ; 
};
vector<egde> e[2*N] ; //要开两倍大不然会WA 
queue<int> q ;
int point[N] , want[N] ; 
int dis[N] ; 
bool done[N] ;
int t , m , d ; 
void spfa(int s){
	memset(dis,INF,sizeof(dis)) ; 
	memset(done,false,sizeof(done)) ; 
	q.push(s) ; 
	dis[s] = 0 ; 
	done[s] = true ; //已在队列中 
	while(!q.empty()){
		int head = q.front() ; 
		q.pop() ; 
		done[head] = false ; //不在队伍中 
		for (int i = 0 ; i < e[head].size() ; ++ i){
			egde v = e[head][i] ; 
			if (dis[v.to] > dis[head] + v.w){
				dis[v.to] = dis[head] + v.w ;
				if (!done[v.to]){
					q.push(v.to) ;
					done[v.to] = true ;	//入队 
				}
			}	 
		}
	}
}
int main(){
	while(~scanf ("%d%d%d",&t,&m,&d)){
		for (int i = 1 ; i <= N ; ++ i)
			e[i].clear() ; 
		for (int i = 1 ; i <= t ; ++ i){
			int u , v , w ; 
			scanf ("%d%d%d",&u,&v,&w) ;
			e[u].push_back(egde(v,w)) ; 
			e[v].push_back(egde(u,w)) ; 
		}
		for (int i = 0 ; i < m ; ++ i)
			scanf ("%d",&point[i]) ;
		for (int i = 0 ; i < d ; ++ i)
			scanf ("%d",&want[i]) ;
		int mins = INF ; 
		for (int i = 0 ; i < m ; ++ i){
			spfa(point[i]) ; 	//搜索每一个起点到个点的最短路 
			for (int j = 0 ; j < d ; ++ j){	//找出想去的城市离起点的最短路 
				mins = mins < dis[want[j]] ? mins : dis[want[j]] ; 
			}
		}	
		printf ("%d\n",mins) ;
	}
	return 0 ; 
} 

spfa判断负圈:

假设有n 个点,如果有点x入队超过n次则证明有负圈
例题

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std ; 
const int N = 510;
const int INF = 0x3f3f3f3f ; 
int g[N][N] ,dis[N] ,cnt[N];
bool done[N] ;
int n,m,p ;
bool spfa(int ss){
	memset(dis,INF,sizeof(dis)) ;
	memset(cnt,0,sizeof(cnt)) ; 
	memset(done,false,sizeof(done)) ;
	queue<int> q ; q.push(ss) ; 
	done[ss]=true ; //入队(在队中)
	dis[ss]=0 ,cnt[ss]++ ; 
	while(!q.empty()){
		int x = q.front() ; q.pop() ; 
		done[x]=false ; //出队(不在队中)
		for(int i=1 ; i<=n ; ++i){
			if(dis[i]>dis[x]+g[x][i]){
				dis[i]=dis[x]+g[x][i] ; 
				if(!done[i]){
					q.push(i),done[i]=true,++cnt[i] ;
					if(cnt[i]>=n)	return true ;//存在负圈 
				}	
			}
		} 
	}
	return false ;	//没有负圈 
}
int main(){
	int t; scanf("%d",&t) ; 
	while(t--){
		scanf("%d%d%d",&n,&m,&p) ;
		for(int i=1 ; i<=n ; ++i){
			for(int j=1 ; j<=n ; ++j){
				if(i==j)	g[i][j]=0 ; 
				else	g[i][j]=INF ; 
			}
		} 
		for(int i=0 ; i<m ; ++i){
			int u,v,w ; scanf("%d%d%d",&u,&v,&w) ;
			g[u][v]=g[v][u]=min(g[v][u],w) ; 
		}
		for(int i=0 ; i<p ; ++i){
			int u,v,w ; scanf("%d%d%d",&u,&v,&w) ; 
			g[u][v]=min(g[v][u],-w) ;//虫洞(单向)  
		}
		if(spfa(1))	printf("YES\n") ; 
		else	printf("NO\n");
		
	} 
	return 0 ;
} 

spfa和dijkstra的区别:

  • 相似:两者很像,都是从起点s出发,逐步扩展邻居结点。

  • 区别:

  • (1)dijkstra每次扩展邻居结点,都从中找一个点,它到s的距离是最小的;
    (2)SPFA并没有找这个点,而是更新这些邻居点的状态(这些点到s的距离),这需要进行多轮更新,等所有点都不能再更新了,就结束了。

  • 因此,dijkstra比SPFA要快。

  • 但是,从上述区别也看出,dikstra不能处理负权边,而SPFA能。
    (1)dijkstra每次扩展邻居结点,都从中找一个点,它到s的距离是最小的;
    (2)SPFA并没有找这个点,而是更新这些邻居点的状态(这些点到s的距离),这需要进行多轮更新,等所有点都不能再更新了,就结束了。

应用例题:

洛谷p1144 最短路的计数
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
using namespace std ; 
const int N = 2e6 + 5 ; 
const int INF = 1e6 ; 
const int MOD = 100003 ; 
vector<int> e[N] ;
int dis[N] , ans[N] ; 
bool done[N] ; 
int n , m ;
void spfa(){
	queue<int> q ; 
	q.push(1) ; 
	memset(dis,INF,sizeof(dis)) ; 
	ans[1] = 1 ; 
	dis[1] = 0 ;
	done[1] = true ; 
	while(!q.empty()){
		int head = q.front() ; 
		q.pop() ; 
		done[head] = false ; 
		for (int i = 0 ; i < e[head].size() ; ++ i){
			int t = e[head][i] ; 
			if (dis[t] > dis[head] + 1){	
				dis[t] = dis[head] + 1 ;
				ans[t] = ans[head] ;	//t和head在同一条路上 ,最短路相同 
				if (!done[t]){
					done[t] = true ; 
					q.push(t) ; 
				}
			} 
			else if (dis[t] == dis[head] + 1){	
				ans[t] += ans[head] ; 
				ans[t] %= MOD ; 
			}
			
		}
	}
} 
int main(){
	scanf ("%d%d",&n,&m) ; 
	while(m --){
		int u , v ;
		scanf ("%d%d",&u,&v) ;
		e[u].push_back(v) ; 
		e[v].push_back(u) ;  
	} 
	spfa() ;
	for (int i = 1 ; i <= n ; ++ i)
		printf ("%d\n",ans[i]) ;
	return 0 ; 
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值