最短路及其延伸

最短图

想必最短路大家都很熟悉,那么我们就先对最短路两个重要的算法总结一下;

Prim

Prim其实就是以DP为基础的最短路算法,通过两个节点的中介转折点来更新两点之间的最短路。从而解决任意两点的最短路,时间复杂度是O(n^3);具体实现过程如下:

mp[Maxn][Maxn];//两点之间的最短路 

for(int k=1;k<=n;++k)//k枚举状态所以必须在外层循环 
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			if(mp[i][j]>mp[i][k]+mp[k][j])
				mp[i][j]=mp[i][k]+mp[k][j]; 

那么基本算法讲完了,让我们来加深对Prim的理解,来一道例题:
题目背景
BB地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响。但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车。换句话说,只有连接着两个重建完成的村庄的公路才能通车,只能到达重建完成的村庄。
题目描述
给出BB地区的村庄数N,村庄编号从00到N-1,和所有M条公路的长度,公路是双向的。并给出第i个村庄重建完成的时间t_i,你可以认为是同时开始重建并在第t_i 天重建完成,并且在当天即可通车。若t_i 为0则说明地震未对此地区造成损坏,一开始就可以通车。之后有Q个询问(x, y, t),对于每个询问你要回答在第t天,从村庄x到村庄y的最短路径长度为多少。如果无法找到从x村庄到y村庄的路径,经过若干个已重建完成的村庄,或者村庄x或村庄y在第t天仍未重建完成 ,则需要返回−1。

Prim的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。
(仔细理解这段话,它揭露了这个算法的本质并为本题提供了很好的方法)

到这里我们已经知道,Floyd算法就是一个利用其它点进行中转来求最短路的步骤。

而我们再回头看题意:

所有的边全部给出,按照时间顺序更新每一个可用的点(即修建好村庄),对于每个时间点进行两点之间询问,求对于目前建设的所有村庄来说任意两点之间的最短路

不正好就是Floyd算法中使用前k个节点更新最短路的思维吗?

于是到了这里,我们差不多也就知道这题如何写了。

#include<bits/stdc++.h>
#define maxn 500
using namespace std;

int n,m,q,t[maxn],cnt=0,head[maxn],mp[maxn][maxn];

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n-1;++i)
	scanf("%d",&t[i]);
	for(int i=0;i<=n-1;++i)
		for(int j=0;j<=n-1;++j)
			mp[i][j]=1e9;
	for(int i=0;i<=n-1;++i) mp[i][i]=0;
	
	for(int i=1;i<=m;++i)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		mp[x][y]=mp[y][x]=z;
	}
	scanf("%d",&q);
	int now=0;
	for(int i=1;i<=q;++i)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		while(t[now]<=z&&now<=n-1)
		{
			for(int i=0;i<=n-1;++i)
				for(int j=0;j<=n-1;++j)
				 	if(mp[i][j]>mp[i][now]+mp[now][j])
				 		mp[j][i]=mp[i][j]=mp[i][now]+mp[now][j];
			now++;
		}
		if(t[x]>z||t[y]>z) printf("-1\n");
		else if(mp[x][y]==1e9) printf("-1\n");
		else printf("%d\n",mp[x][y]);
	}
	return 0;
}

Dijkstra

这就是最短路的算法中最重要的一个求单源最短路径的算法,正常情况下它的时间复杂度是O(n^2)在堆优化之后可以完美的降到nlogn;而且这个算法极其稳定(不会像spfa一样被卡),所以在以后的考试中建议大家都用Dijkstra来跑最短路。
实现过程如下:

dis[maxn],vis[maxn];
priority_queue<pair<int ,int > >q;

DJ()
{
	for(int i=1;i<=n;++i)
	dis[i]=0x7fffffff,vis[i]=0;
	dis[1]=0;
	q.push(make_pair(0,1));
	while(q.size())
	{
		int x=q.top().second;q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=head[x];i;i=e[i].pre)
		{
			int y=e[i].to;
			int z=e[i].val;
			if(dis[y]>dis[x]+z)
			{
				dis[y]=dis[x]+z;
				q.push(make_pair(-dis[y],y));
			}
		}
	}
} 

来一道例题:
题目背景
在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士,他是部落的中坚力量
有一天他醒来后发现自己居然到了联盟的主城暴风城
在被众多联盟的士兵攻击后,他决定逃回自己的家乡奥格瑞玛

题目描述
在艾泽拉斯,有n个城市。编号为1,2,3,…,n。
城市之间有m条双向的公路,连接着两个城市,从某个城市到另一个城市,会遭到联盟的攻击,进而损失一定的血量。
每次经过一个城市,都会被收取一定的过路费(包括起点和终点)。路上并没有收费站。
假设1为暴风城,n为奥格瑞玛,而他的血量最多为b,出发时他的血量是满的。
歪嘴哦不希望花很多钱,他想知道,在可以到达奥格瑞玛的情况下,他所经过的所有城市中最多的一次收取的费用的最小值是多少。

这道题一看是有限制的最短路 ,有两个维度,血量,和费用;
我们可能会不知所措,但这题面中,有一个非常显眼的一句话:‘’他所经过的所有城市中最多的一次收取的费用的最小值是多少。‘’
这句话不就是典型的二分答案吗?
想到二分答案,这道题的正解就呼之欲出了。
没错,就是二分答案加Dijkstra检查是否血量充足。

来看一看这道题的核心代码:

bool DJ(int M)
{
	if(f[1]>M||f[n]>M) return false;
	for(int i=1;i<=n;++i)
	{
		dis[i]=INF;
		if(f[i]>M) vis[i]=1;
		else vis[i]=0;
	}
	dis[1]=0;
	q.push(make_pair(0,1));
	while(q.size())
	{
		int x=q.top().second;q.pop();
		if(vis[x]) continue;
		vis[x]=1;
		for(int i=head[x];i;i=e[i].pre)
		{
			int y=e[i].to;
			int z=e[i].val;
			if(dis[y]>dis[x]+z)
			{
				dis[y]=dis[x]+z;
				q.push(make_pair(-dis[y],y));
			}
		}
	}
	if(dis[n]<=b) return true;
	else return false;
}

int main()
{
	int l=1,r=n,ans=c[n];
	while(l<=r)
	{
		int mid=(l+r)/2;
 		if(DJ(c[mid])==true) r=mid-1,ans=c[mid];
		else l=mid+1;
	}
	printf("%d",ans);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值