题解 P1462 【通往奥格瑞玛的道路】
其实吧,个人认为这个题作为一个蓝题是被评高了
核心算法:单源最短路 + 二分答案
(我为什么说评高了:
单源最短路(弱化版):洛谷难度评级:普及/提高-(这题并没有卡SPFA)
标准的二分答案题:【P1182 数列分段Section II】
与【P1316 丢瓶盖】
:洛谷难度评级:普及/提高-
两个黄题合在一起居然蓝了....
不说这个难度,开始讲题。
首先仍然是分析题意。
核心条件:
-
一开始在1号节点,要到达终点n号节点
-
每个点会有一个费用
-
每条道路都有一个对应的损失血量
次要条件(同样重要)
-
道路为双向
-
只有血量会使他无法到达终点
(土豪不会被钱卡住)
题目目的:
求出到达路线中最大收费的最小值
把这些要点提炼出来后,就差不多可以开始写题了。
首先,看见这个问题,就应该想到二分。
所有类似于“求解所有的最大值中的最小值”的问题,都应该先想一想用二分答案的方法来写。
为什么呢?因为二分答案就是用来求解这种问题的,它通过猜测目前的答案+验证目前答案是否成立来求解,这种“假设已经知道答案”的方法比用题目信息求解答案的方法要优秀很多,而且复杂度也仅仅是加了一个log。
但是还要注意一点,除了这种经典问法以外,还要通过答案是否具有单调性来判断是否可以使用二分答案的方法
不具有单调性的问题是不可以使用这种方法的!
回到本题,我们猜测可以使用二分答案后,再验证:由于“我们假设需要的钱”越多,能走的点也就越多,可以走到终点的可能性也就越大,所以,这题是完全可以通过二分答案来写的。
然后,我们发现这里有两个变量:血量和费用。至于费用,我们已经用二分答案解决了,现在只需要解决血量的问题。 我们在上面分析了,损失血量是和边绑定在一起的,所有可以把损失血量看做每条边的长度,进而就到了这题的中心——单源最短路。
单源最短路径就不多说了,这题没有卡spfa,所以想怎么求就可以怎么求。因为我是把堆优化的dijkstra算法完完整整的理解性背了下来,所以直接套的dijkstra。虽然速度可能没有spfa那么快,但不会被卡啊。(我会跟你说我完全不会spfa吗
然后就是具体实现了。
(仍然分开给出):
标准二分模板:
int l=1,r=MAXN,mid=(l+r)>>1;//准备开始二分
int c;
while(l<=r){//终止条件
c=work(mid);//使用work函数来判断当前猜测的答案是否可行
if(c){//如果可以的话
r=mid-1;//改变右端点,看看再少一点是不是也可以
mid=(l+r)>>1;
}
else{
l=mid+1;//当前答案不可行,需要增大答案
mid=(l+r)>>1;
}
}
cout<<l<<endl;//输出找到的答案
}
然后是最短路....
说实话,如果你不习惯用“前向星+小根堆+pair”的话,很可能看不懂我写的什么玩意..建议使用自己熟悉的算法。
说一下最核心的改变,原版最短路为:
while(q.size()!=0){
s1=q.top().second;q.pop();
if(ed[s1])continue;
ed[s1]=1;
s2=head[s1];
while(s2){
if(d[edge[s2].to]>d[s1]+edge[s2].dis&&ed[edge[s2].to]==0){
d[edge[s2].to]=d[s1]+edge[s2].dis;
q.push(make_pair( d[edge[s2].to] , edge[s2].to));
}
s2=edge[s2].next;
}
}
更改一个很小的细节:
while(que.size()){
s1=que.top().second;que.pop();
if(ed[s1])continue;
ed[s1]=1;
s2=head[s1];
while(s2){//只有收费小于x的才能使用,大于x的一律视为没有
//***************增加一个判定条件************************************************
if(f[e[s2].to]<=x&&ed[e[s2].to]==0&&dis[s1]+e[s2].len<dis[e[s2].to])
{dis[e[s2].to]=dis[s1]+e[s2].len;
que.push(make_pair(dis[e[s2].to],e[s2].to));
}//入堆准备下一轮操作
s2=e[s2].next;//去下一条边
}
}
每次枚举答案时把只在符合当前答案的点里面找最短路,然后判断是否成立。
因为我这里是直接套用P4779 【模板】单源最短路径(标准版)的代码,所以代码看上去可能又烦又长...但最总要的是解题思路,如果你能想到使用二分答案并修改求最短路条件的话,两个基本上就是套模板的代码就很好写了。
放上我自己写的AC代码,可以用来参考一下细节(虽然对不会前向星或者堆优化的萌新来说可能不太友好,但我真的没用过spfa或者原版dijkstra...抱歉了)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#define il inline
#define rg register
#define N 10005
#define M 100005
#define MAXN 1000000005//最大收费(二分用
using namespace std;
int f[N];//每个城市的收费额度
bool ed[N];//求最短路使用
int dis[N];//同上
int head[N];//前向星
priority_queue <pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > que;
int n,m,b;
int t;
struct edge{
int to,next,len;
}e[M];
il void add(int x,int y,int z){
e[++t].len=z;
e[t].to=y;
e[t].next=head[x];
head[x]=t;
}
il int read(){
int sum=0;char a=getchar();
while(a<'0'||a>'9')a=getchar();
while(a>='0'&&a<='9'){
sum=(sum<<3)+(sum<<1)+a-'0';
a=getchar();
}
return sum;
}
il bool work(int x){//对于每个最大收费都确定是否可以到达
if(x<f[1])return 0;//起点都过不了走个p
for(int i=1;i<=n;i++){
dis[i]=1e9;//初始化为极大值
}
memset(ed,0,sizeof(ed));
dis[1]=0;
que.push(make_pair(0,1));
int s1,s2;
while(que.size()){
s1=que.top().second;que.pop();
if(ed[s1])continue;
ed[s1]=1;
s2=head[s1];
while(s2){//只有收费小于x的才能使用,大于x的一律视为没有
if(f[e[s2].to]<=x&&ed[e[s2].to]==0&&dis[s1]+e[s2].len<dis[e[s2].to])
{dis[e[s2].to]=dis[s1]+e[s2].len;
que.push(make_pair(dis[e[s2].to],e[s2].to));}//入堆准备下一轮操作
s2=e[s2].next;//去下一条边
}
}
if(dis[n]<b){
return 1;
}
return 0;
}
int main(){
n=read();m=read();b=read();
for(rg int i=1;i<=n;i++)
f[i]=read();//储存费用
int a1,a2,a3;
for(rg int i=1;i<=m;i++){
a1=read();a2=read();a3=read();
add(a1,a2,a3);
add(a2,a1,a3);
}
if(work(MAXN)==0){
cout<<"AFK"<<endl;
return 0;//如果所有都开通任然不满足条件,剩下的肯定也不会满足
}
int l=1,r=MAXN,mid=(l+r)>>1;//准备开始二分
int c;
while(l<=r){
c=work(mid);
if(c){//如果可以的话
r=mid-1;
mid=(l+r)>>1;
}
else{
l=mid+1;
mid=(l+r)>>1;
}
}
cout<<l<<endl;
}
还有什么不理解的可以回复并@我,只要我看见了就会回复的。