最短图
想必最短路大家都很熟悉,那么我们就先对最短路两个重要的算法总结一下;
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);
}