Week7作业-最短路专题
TT 的魔法猫
众所周知,TT 有一只魔法猫。
这一天,TT 正在专心致志地玩《猫和老鼠》游戏,然而比赛还没开始,聪明的魔法猫便告诉了 TT 比赛的最终结果。TT 非常诧异,不仅诧异于他的小猫咪居然会说话,更诧异于这可爱的小不点为何有如此魔力?
魔法猫告诉 TT,它其实拥有一张游戏胜负表,上面有 N 个人以及 M 个胜负关系,每个胜负关系为 A B,表示 A 能胜过 B,且胜负关系具有传递性。即 A 胜过 B,B 胜过 C,则 A 也能胜过 C。
TT 不相信他的小猫咪什么比赛都能预测,因此他想知道有多少对选手的胜负无法预先得知,你能帮帮他吗?
思路:
- 考虑弗洛伊德算法可以用来解决传递闭包的大致想法: 弗洛伊德算法最初用于计算图中所有点对之间的最短路,针对每个点,它会不断尝试经过更多的中间节点,判断其是否缩短了距离. 最终当所有点都尝试完毕, 结果即为最短路. 对于关系来说, 这种不断扩展经过中间节点的过程其实就是尝试建立关系的过程.
- 上课时看到题目想到如果
dia[a][b]==0
那么a,b应该就不能判断胜负关系了. 显然这种想法是错误的: 每一个胜负关系是有序的,dis[a][b]==0
只能说明a不能胜过b, 但是对于b能否胜过a是未知的.(后者是由b出发计算闭包得到的结果) - 剪枝: 如果
dis[i][k]==0
那么其实后续的运算是没有意义的.
实现:
#include<iostream>
using namespace std;
int p,n,m,a,b;
bool dis[505][505];
int main()
{
std::ios::sync_with_stdio(false);
cin>>p;
for(int i=0;i<p;++i)
{
int ans=0;
cin>>n>>m;
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
dis[i][j]=0;
for(int k=0;k<m;++k)
{
cin>>a>>b;
dis[a][b]=1;
}
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
if(dis[i][k]!=0)
{//注意剪枝
for(int j=1;j<=n;++j)
dis[i][j]=dis[i][j]|(dis[i][k]&dis[k][j]);
}
for(int i=1;i<=n;++i)
for(int j=i+1;j<=n;++j)
if(dis[i][j]==0 && dis[j][i]==0)ans++;
cout<<ans<<endl;
}
return 0;
}
TT 的旅行日记
众所周知,TT 有一只魔法猫。
今天他在 B 站上开启了一次旅行直播,记录他与魔法猫在喵星旅游时的奇遇。 TT 从家里出发,准备乘坐猫猫快线前往喵星机场。猫猫快线分为经济线和商业线两种,它们的速度与价钱都不同。当然啦,商业线要比经济线贵,TT 平常只能坐经济线,但是今天 TT 的魔法猫变出了一张商业线车票,可以坐一站商业线。假设 TT 换乘的时间忽略不计,请你帮 TT 找到一条去喵星机场最快的线路,不然就要误机了!
思路:
- 这道题应该是无向加权图, 属于最短路问题. 考虑到我们需要从起点到终点耗时最短, 因此使用迪杰斯特拉算法. 比较不同的地方在于, 这道题的边有经济线和商业线两种, 因此需要区分开来. 比较直接的思路是枚举商业线, 记为(u,v,w), 那么我们需要计算从起点到u+v到终点+w,以及起点到v+u到终点+w. 取两者中的较小者,那么我们便得到经过此条商业线的最短路.
- 枚举所有商业线, 再与只乘坐经济线的最短路进行比较,便可得到最终的目标最短路.
- 注意路径的输出. 考虑到在跑迪杰斯特拉的过程中记录每个点的前序节点. 之后对于商业线, 需要注意其前后的路径顺序是不一样的,区分输出即可.
实现:
#include<iostream>
#include<queue>
#include<utility>
#include<algorithm>
#include<string.h>
#include<list>
using namespace std;
const int M=1010;
const int N=1010;
const int K=1010;
const int inf=1e7;
int _N,_S,_E,X,Y,Z;
int eco,bus;//经济线与商业线路段数
int ans_eco,ans_bus;//两种答案,前者代表只有经济线
int targetx,targety;//表示商业线的两个端点
list<int> path;
struct Edge{
int to,next,w;
}e[10100];
int head[N],tot,n,m,vis[N];
int disv[N],disu[N];
int pre_v[N],preu[N];
void add(int x,int y,int w)
{
e[++tot].to=y,e[tot].next=head[x];
e[tot].w=w,head[x]=tot;
}
priority_queue<pair<int,int> >q;
void dijkstra(int s,int *dis,int *pre)
{
while(q.size())q.pop();
memset(vis,0,sizeof vis);
for(int i=1;i<=_N;++i)dis[i]=inf;
dis[s]=0;
pre[s]=-1;
q.push(make_pair(0,s));
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].next)
{
int y=e[i].to,w=e[i].w;
if(dis[y] > dis[x] + w)
{
dis[y] = dis[x] +w;
pre[y] = x;
q.push(make_pair(-dis[y],y));
}
}
}
}
int main()
{
std::ios::sync_with_stdio(false);
bool first=0;
while(cin>>_N>>_S>>_E)
{
if(first==1)cout<<endl;
path.clear();
tot=0;
for(int i=1;i<=_N;++i)
{
head[i]=0;
pre_v[i]=-1;
preu[i]=-1;
}
cin>>eco;
for(int i=0;i<eco;++i)
{
cin>>X>>Y>>Z;
add(X,Y,Z);
add(Y,X,Z);
}
cin>>bus;
dijkstra(_S,disv,pre_v);
dijkstra(_E,disu,preu);
ans_eco=disv[_E];
targetx=-1,targety=-1,ans_bus=ans_eco;
for(int i=1;i<=bus;++i)
{
cin>>X>>Y>>Z;
int templength1 = disv[X]+disu[Y]+Z;
int templength2 = disv[Y]+disu[X]+Z;
if(ans_bus>min(templength1,templength2))
{
ans_bus=min(templength1,templength2);
if(templength1 < templength2)
{
targetx=X;
targety=Y;
}
else
{
targetx=Y;
targety=X;
}
}
}
if(targetx==-1)
{
path.push_front(_E);
int par=pre_v[_E];
while(par!=-1)
{
path.push_front(par);
par=pre_v[par];
}
cout<<_S;
path.pop_front();
while(!path.empty())
{
int tmp=path.front();
path.pop_front();
cout<<" "<<tmp;
}
cout<<endl;
cout<<"Ticket Not Used"<<endl;
cout<<ans_eco<<endl;
}
else
{
//先输出前半部分,后半部分可以直接根据pre进行输出
path.push_front(targetx);
int par = pre_v[targetx];
while(par!=-1)
{
path.push_front(par);
par = pre_v[par];
}
while(!path.empty())
{
cout<<path.front()<<" ";
path.pop_front();
}
cout<<targety;
par = preu[targety];
while(par!=-1)
{
cout<<" "<<par;
par=preu[par];
}
cout<<endl;
cout<<targetx<<endl;
cout<<ans_bus<<endl;
}
first=1;//不是第一组
}
return 0;
}
反思:
- 注意输出的格式. 目标是最后一组之后不输出换行, 但是显然如果不输入
_N,_S,_E
没办法判断是否到达了最后一组数据. 因此我们需要在除第一组之外每组前面输出换行, 进行一个判断即可. - 刚开始考虑到经济线路段数不超过1000, 因此设置Edge数组大小为1010, 但是出现
RE
, 后面才意识到由于是无向图, 每条路段存了两次, 更改大小之后正确. - 需要注意对于商业线(u,v,w)既可能是从S到u也可能是S到v, 这两种情况都需要考虑. 但是一开始没有想到, 助教上课说了才意识到.
TT 的美梦
这一晚,TT 做了个美梦!
在梦中,TT 的愿望成真了,他成为了喵星的统领!喵星上有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。
喵星上共有 M 条有向道路供商业城市相互往来。但是随着喵星商业的日渐繁荣,有些道路变得非常拥挤。正在 TT 为之苦恼之时,他的魔法小猫咪提出了一个解决方案!TT 欣然接受并针对该方案颁布了一项新的政策。
具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每一只喵沿道路从一个商业城市走到另一个商业城市时,TT 都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。
TT 打算测试一下这项政策是否合理,因此他想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法到达请悄咪咪地打出 ‘?’。
思路:
- 分析题目可知可能存在负环, 因此使用SPFA. 对于负环的判断, 如果一个点入队次数达到了n, 说明对应边已经被松弛了n次, 那么必然存在负环. 对于处于负环中并且可以经过负环到达的点, 它们都不存在最短路, 可以通过DFS或者BFS进行搜索标记, 在最后查询时只需判断是否被标记过即可.
实现:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
int INF=1e9,N;
int a[210];
struct Edge{
int to,next,w;
}e[100010];
int head[210],tot,m,vis[210];
void add(int x,int y,int w)
{
e[++tot].to=y,e[tot].next=head[x];
e[tot].w=w,head[x]=tot;
}
bool flag[210]={0};
void dfs(int v)
{
flag[v]=1;
for(int i=head[v];i!=-1;i=e[i].next)
{
if(!flag[e[i].to])
dfs(e[i].to);
}
}
int dis[210],cnt[210];
void SPFA(int s)
{
queue<int> q;
while(!q.empty()) q.pop();
for(int i=1;i<=N;i++)
dis[i]=INF,vis[i]=0,cnt[i]=0,flag[i]=0;
dis[s]=0;
q.push(s);
vis[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(dis[v]>dis[u]+w)
{
cnt[v]=cnt[u]+1;
if(cnt[v]>=N)
{//dfs对于负环可到达的点进行标记
dfs(v);
}
dis[v]=dis[u]+w;
if(!vis[v] && !flag[v])
{
q.push(v);
vis[v]=1;
}
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
int T,M,A,B,Q,P;
cin>>T;
for(int i=0;i<T;++i)
{
tot=0;
cin>>N;
for(int j=1;j<=N;++j)
head[j]=-1;
for(int j=1;j<=N;++j)
cin>>a[j];
cin>>M;
for(int j=0;j<M;++j)
{
cin>>A>>B;
int tmp=pow(a[B]-a[A],3);
add(A,B,tmp);
}
SPFA(1);
cout<<"Case "<<i+1<<":"<<endl;
cin>>Q;
for(int j=0;j<Q;++j)
{
cin>>P;
if(flag[P]==true || dis[P]<3 || dis[P]==INF)
cout<<"?"<<endl;
else cout<<dis[P]<<endl;
}
}
return 0;
}
总结:
- 这周主要学习了最短路的四个算法, 包括迪杰斯特拉算法/弗洛伊德算法/贝尔曼福德算法及其对应的队列优化SPFA. 需要注意其正确性的理解/适用范围/代码实现等.
- 注意每次有多组数据时, 需要注意初始化的过程, 要考虑得比较全面, 这次在这里犯了错误.