夕拾算法进阶篇:31)最短路径Dijkstra(图论)

Dijkstra(迪杰斯特拉)算法,用来解决单源最短路问题,即给定图G和起点s,通过算法得到s到其他每个顶点的最短距离。

Dijkstra的策略:

(1)记V为图的所有顶点集合,S为已访问过的顶点集合。每次从集合V-S(未访问的顶点)中选择一个与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。

(2)之后,令u为中介点,优化起点与所有从u能到达的顶点v之间的最短距离。

Dijkstra的具体实现:

由于Dijkstra的策略偏于理论,因此为了方便编写代码,需要想办法实现策略中比较关键的东西,即集合S的实现、起点到顶点Vi(0<=i<=n-1)的最短距离。

(1)集合S可以使用一个bool数组vis[]来实现,即当vis[i]为true时表示顶点Vi已经被访问过,当Vi为false时表示顶点Vi未被访问。

(2)令int数组d[]表示起点s到顶点Vi的最短距离,初始时除了起点s的d[s]赋值为0,其余顶点都赋值为一个很大的数(建议赋值为0x3fffffff,不要设置最大的整数0x7fffffff,再加一个数就越界了)来表示Inf,即不可达到。

如下给出Dijkstra的算法伪代码:

//G为图,数组d为源点到各顶点的最路径长度,s为起点
Dijsktra(G,d[],s){
	初始化
	for(循环n次){//n个顶点
		u=使d[u]最小的还未被访问的顶点的标号
		记u已经被访问过
		for(从u出发能到达的所有顶点v){
			if(v未被访问&&以u为中介点使s到顶点v的最短距离更优)
				优化d[v]
		}
	}
}

邻接矩阵实现,时间复杂度0(V^2):

const int M=505; //最大的顶点数
const int Inf=0x3fffffff; //无穷大
int map[M][M],d[M]; //map表示图,d表示起点s到顶点Vi的最短距离
bool vis[M]; //访问标记

void dijkstra(int s){ //s=起点
	fill(d,d+M,Inf);//将整个距离数组设置为Inf 
	d[s]=0;//设置起点到自身的距离为0 
	for(int i=0;i<n;i++){//n个结点循环n次 
		int min_=Inf,u=-1;//u使d[u]最小,min_存放最小的d[u]值 
		for(int j=0;j<n;j++){//找到未访问的顶点中d[]最小的 
			if(!vis[j]&&min_>d[j]){
				min_=d[j]; u=j;
			}
		}
		vis[u]=true;//设置顶点u被访问 
		for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更优 
			if(!vis[v]&&d[v]>d[u]+map[u][v]){
				d[v]=d[u]+map[u][v];
		} 
	}
}

邻接表阵实现,时间复杂度O(V^2 + E):

const int M=505; //最大的顶点数
const int Inf=0x3fffffff; //无穷大
int d[M],n; //map表示图,d表示起点s到顶点Vi的最短距离
bool vis[M]; //访问标记

struct  Node{
	int v,dis; //v表示边可以到达的下一个顶点,dis为边权 
};

vector<Node> adj[M]; //图G,adj[u]存放从u出发可以到达的所有顶点 

void dijkstra(int s){ //s=起点
	fill(d,d+M,Inf);//将整个距离数组设置为Inf 
	d[s]=0;//设置起点到自身的距离为0 
	for(int i=0;i<n;i++){//n个结点循环n次 
		int min_=Inf,u=-1;//u使d[u]最小,min_存放最小的d[u]值 
		for(int j=0;j<n;j++){//找到未访问的顶点中d[]最小的 
			if(!vis[j]&&min_>d[j]){
				min_=d[j]; u=j;
			}
		}
		vis[u]=true;//设置顶点u被访问 
		/*****只有下面的for循环和邻接矩阵不一样****/
		for(int j=0;j<adj[u].size();j++){
			int v=adj[u][j].v;//通过邻接表之间获得u能到的的顶点v 
			if(!vis[v]&&d[v]>d[u]+adj[u][j].dis){//v未访问且以u为中介点可以让d[v]更有 
				d[v]=d[u]+adj[u][j].dis;
			} 
		}
	}
}

以上都是求起点到其他顶点的最短路径的长度,如果要打印其路径,哪该怎么做?注意到伪代码中有这么一段:

if(v未被访问&&以u为中介点使s到顶点v的最短距离更优)
	优化d[v]

也就是说,使d[v]变得更小的方案就是让u作为v的前一个结点(即s...->u->s),因此在该处可以使用数组pre[v]保存v的前驱结点u

for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更有 
	if(!vis[v]&&d[v]>d[u]+map[u][v]){
		d[v]=d[u]+map[u][v];
		pre[v]=u;
} 

这样从终点逆向打印出来的顶点就是从终点到起点的路径,此次可以使用递归来实现路径打印,当然你也可以使用数组保存,然后逆序输出(目前只能打印一条路径)。

void dfs(int s,int v){//s为起点,v终点 
	if(v==s){//达到起点s 
		printf("%d\n",s);
		return; 
	} 
	dfs(s,pre[v]);//递归访问v的前驱结点
	printf("%d\n",v);//回溯时打印路径结点 
} 

通常最短路径不会考得这么“裸”,比如从起点到终点的最短路径不止一条。而在最短路径的基础上题目会给出一些附加组合,常见有下面三种组合:

 

(1)给出每条边在增加一个边权(比如说花费),然后求在最短路径有多条是求路径上的花费之和最小(如果边权是其他含义,也可以是最大)

记cost[u][v]表示u->v的花费(由题目给出),从起点s到达顶点u的最少花费为c[u],初始化时只有c[s]为0,其余c[u]皆为Inf。这样就可以在d[u]+G[u][v]<d[v]时更新d[v]和c[v],而当d[u]+G[u][v]==d[v]且c[v]<c[u]+cost[u][v](最短距离相同)时更新c[v]

for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更优
	if(!vis[v]&&d[v]>d[u]+map[u][v]){
		d[v]=d[u]+map[u][v];
		c[v]=c[u]+cost[u][v];
	}else if(d[v]==d[u]+map[u][v]&&c[v]<c[u]+cost[u][v]){
		c[v]=c[u]+cost[u][v];//最短距离相同时,看能否让c[v]更优
	}
} 

(2)新增点权。给每个顶点增加一个点权(例如每个城市可以收集的物资),然后在最短路径多条时,要求路径的点权之和最大(最小也可)。用weight[u]表示城市u中的物资数目(由题目给出),并添加一个数组w[],令从起点s到顶点u可以收集的最大物资为w[u],初始化时只有w[s]=weight[s],其余的w[u]均为0。这样就可以在d[u]+G[u][v]<d[v]时更新d[v]和w[v],而当d[u]+G[u][v]==d[v](最短距离相)且w[v]<w[u]+weight[v]时更新w[v]。

for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更优
	if(!vis[v]&&d[v]>d[u]+map[u][v]){
		d[v]=d[u]+map[u][v];
		w[v]=w[u]+weight[v];
	}else if(d[v]==d[u]+map[u][v]&&w[v]<w[u]+weight[v];){
		w[v]=w[u]+weight[v];//最短距离相同时,看能否让w[v]更优
	}
} 

(3)直接问有多少条路径。只需添加一个数组num[],令起点s到顶点u的最短路径条数为num[u],初始时只有num[s]=1,其余皆为0。这样就可以在d[u]+G[u][v]<d[v]时更新d[v]并让num[v]继承num[u],而当d[u]+G[u][v]==d[v](最短距离相同)将num[u]累加到num[v]上。

for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更优
	if(!vis[v]&&d[v]>d[u]+map[u][v]){
		d[v]=d[u]+map[u][v];
		num[v]=num[u];
	}else if(d[v]==d[u]+map[u][v]){
		num[v]+=num[u];
	}
} 

 

下面给出一个例子

As an emergency rescue team leader of a city, you are given a special map of your country. The map shows several scattered cities connected by some roads. Amount of rescue teams in each city and the length of each road between any pair of cities are marked on the map. When there is an emergency call to you from some other city, your job is to lead your men to the place as quickly as possible, and at the mean time, call up as many hands on the way as possible.
Input
Each input file contains one test case. For each test case, the first line contains 4 positive integers: N (<= 500) - the number of cities (and the cities are numbered from 0 to N-1), M - the number of roads, C1 and C2 - the cities that you are currently in and that you must save, respectively. The next line contains N integers, where the i-th integer is the number of rescue teams in the i-th city. Then M lines follow, each describes a road with three integers c1, c2 and L, which are the pair of cities connected by a road and the length of that road, respectively. It is guaranteed that there exists at least one path from C1 to C2.
Output
For each test case, print in one line two numbers: the number of different shortest paths between C1 and C2, and the maximum amount of rescue teams you can possibly gather.
All the numbers in a line must be separated by exactly one space, and there is no extra space allowed at the end of a line.
Sample Input
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1
Sample Output
2 4

题目的大概意思:给出N个城市,M条无向边。每个城市中都有一定数目的救援小组,所有边的边权已知。现在给出起点和终点,求起点到终点的最短路径条数及最短路径上的救援小组数目之和。如果最短路径有多条,则输出救援队数目最多的(注意无向图为双向边)

该题就是上面的(2)和(3)的组合,直接上代码:

#include <iostream>
#include <algorithm>
using namespace std;

const int M=505;
const int Inf=0x3fffffff;  

int map[M][M],d[M];
bool vis[M];

int n,m,c1,c2,c[M],num[M],w[M];

void dijkstra(int s){ //s=起点
	fill(d,d+M,Inf);//将整个距离数组设置为Inf 
	d[s]=0;//设置起点到自身的距离为0 
	num[s]=1;
	w[s]=c[s];
	for(int i=0;i<n;i++){//n个结点循环n次 
		int min_=Inf,u=-1;//u使d[u]最小,min_存放最小的d[u]值 
		for(int j=0;j<n;j++){//找到未访问的顶点中d[]最小的 
			if(!vis[j]&&min_>d[j]){
				min_=d[j]; u=j;
			}
		}
		vis[u]=true;//设置顶点u被访问 
		for(int v=0;v<n;v++){//v未访问且以u为中介点可以让d[v]更有 
			if(!vis[v]&&d[v]>d[u]+map[u][v]){
				d[v]=d[u]+map[u][v];
				w[v]=w[u]+c[v];
				num[v]=num[u]; 
			}else if(d[v]==d[u]+map[u][v]){
				num[v]+=num[u];
				if(w[v]<w[u]+c[v]){//最短距离相同且可获得更多的救援队 
					w[v]=w[u]+c[v];
				}
			}
		} 
	}
}

int main(){
	int x,y,dis;
	cin>>n>>m>>c1>>c2;
	fill(map[0],map[0]+M*M,Inf);
	for(int i=0;i<n;i++){
		cin>>c[i];
	}
	for(int i=0;i<m;i++){
		cin>>x>>y>>dis;
		map[y][x]=map[x][y]=dis; //注意无向图为双边
	}
	dijkstra(c1);
	cout<<num[c2]<<" "<<w[c2]<<endl;
} 

题目来源:https://www.patest.cn/contests/pat-a-practise/1003

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值