最短路径
- 图的一些经典应用,包括最短路径、最小生成树、拓扑排序和关键路径。
Dijkstra算法
- Dijkstra算法是解决单源最短路径问题的贪心算法,它先求出长度最短的一条路径,再参照该最短路径求出长度次短的一条路径,直到求出从源点到其他各节点的最短路径
- 数据结构。设置地图的邻接矩阵为
G.Edge[][]
,即如果从源点 u u u到节点 i i i有边,就令G.Edge[u][i]
等于<u,i>
的权值,否则G.Edge[u][i] =
∞ \infty ∞(无穷大);采用一维数组dist[i]
记录从源点到节点i
的最短路径长度;采用一维数组p[i]
记录最短路径上节点i
的前驱。 - 初始化。令集合
S
=
u
S = {u}
S=u,对于集合
V
−
S
V - S
V−S中的所有节点
i
,都初始化dist[i] = G.Edge[u][i]
。如果从源点u
到节点i
有边相连,则初始化p[i] = u
,否则p[i] = -1
。 - 找最小。在集合
V
−
S
V - S
V−S中查找
dist[]
最小的节点t
,即dist[t] = min(dist[j]|j属于集合V - S)
,则节点t
就是集合 V − S V - S V−S中距离源点u
最近的节点。 - 判结束。如果集合 V − S V - S V−S为空,则算法结束,否则转向下一步。
- 借东风。在找最小步骤中已经找到了从源点到节点t的最短路径,那么对集合
V
−
S
V-S
V−S中节点t的所有邻接点
j
,都可以借助t
走捷径。如果dist[j] > dist[t]+G.Edge[t][j]
,则dist[j] = dist[t]+G.Edge[t][j]
,记录节点j
的前驱为t
,有p[j] = t
,转向找最小步骤。 - 由此,可求得从源点u到图G的其余各个节点的最短路径及长度,也可通过数组p[]逆向找到最短路径上的节点。
算法实现
#include<iostream>
#include<stack>
using namespace std;
const int MaxVnum=100;
const int INF=0x3f3f3f3f; //无穷大
int dist[MaxVnum],p[MaxVnum];//最短距离和前驱数组
bool flag[MaxVnum]; //如果s[i]等于true,说明顶点i已经加入到集合S;否则顶点i属于集合V-S
typedef string VexType; //顶点的数据类型,根据需要定义
typedef int EdgeType; //边上权值的数据类型,若不带权值的图,则为0或1
typedef struct{
VexType Vex[MaxVnum];
EdgeType Edge[MaxVnum][MaxVnum];
int vexnum,edgenum; //顶点数,边数
}AMGraph;
int locatevex(AMGraph G,VexType x); //查找顶点信息的下标
void CreateAMGraph(AMGraph &G); //初始化(构建)邻接矩阵
void Dijkstra(AMGraph G,int u); //寻求最短路径
void findpath(AMGraph G,VexType u); //输出最短路径
int main(){
AMGraph G;
int st;
VexType u;
CreateAMGraph(G);
cout<<"请输入源点的信息:"<<endl;
cin>>u;
st=locatevex(G,u);//查找源点u的存储下标
Dijkstra(G,st);
cout<<"当前位置:"<<u<<endl;
for(int i=0;i<G.vexnum;i++){
cout<<u<<" - "<<G.Vex[i];
if(dist[i]==INF)
cout<<" sorry,无路可达"<<endl;
else
cout<<" 最短距离为:"<<dist[i]<<endl;
}
findpath(G,u);
return 0;
}
int locatevex(AMGraph G,VexType x){
for(int i=0;i<G.vexnum;i++)
if(x==G.Vex[i])
return i;
return -1;//没找到
}
void CreateAMGraph(AMGraph &G){
int i,j,w;
VexType u,v;
cout<<"请输入顶点数:"<<endl;
cin>>G.vexnum;
cout<<"请输入边数:"<<endl;
cin>>G.edgenum;
cout<<"请输入顶点信息:"<<endl;
for(int i=0;i<G.vexnum;i++)//输入顶点信息,存入顶点信息数组
cin>>G.Vex[i];
for(int i=0;i<G.vexnum;i++)//初始化邻接矩阵为无穷大
for(int j=0;j<G.vexnum;j++)
G.Edge[i][j]=INF;
cout<<"请输入每条边依附的两个顶点及权值:"<<endl;
while(G.edgenum--){
cin>>u>>v>>w;
i=locatevex(G,u);//查找顶点u的存储下标
j=locatevex(G,v);//查找顶点v的存储下标
if(i!=-1&&j!=-1)
G.Edge[i][j]=w; //有向图邻接矩阵
else{
cout<<"输入顶点信息错!请重新输入!"<<endl;
G.edgenum++;//本次输入不算
}
}
}
void Dijkstra(AMGraph G,int u){
for(int i=0;i<G.vexnum;i++){
dist[i]=G.Edge[u][i]; //初始化源点u到其他各个顶点的最短路径长度
flag[i]=false;
if(dist[i]==INF)
p[i]=-1; //源点u到该顶点的路径长度为无穷大,说明顶点i与源点u不相邻
else
p[i]=u; //说明顶点i与源点u相邻,设置顶点i的前驱p[i]=u
}
dist[u]=0;
flag[u]=true; //初始时,集合S中只有一个元素:源点u
for(int i=0;i<G.vexnum;i++){
int temp=INF,t=u;
for(int j=0;j<G.vexnum;j++) //在集合V-S中寻找距离源点u最近的顶点t
if(!flag[j]&&dist[j]<temp){
t=j;
temp=dist[j];
}
if(t==u) return; //找不到t,跳出循环
flag[t]=true; //否则,将t加入集合
for(int j=0;j<G.vexnum;j++)//更新V-S中与t相邻接的顶点到源点u的距离
if(!flag[j]&&G.Edge[t][j]<INF)
if(dist[j]>(dist[t]+G.Edge[t][j])){
dist[j]=dist[t]+G.Edge[t][j];
p[j]=t;
}
}
}
void findpath(AMGraph G,VexType u){
int x;
stack<int>S;
cout<<"源点为:"<<u<<endl;
for(int i=0;i<G.vexnum;i++){
x=p[i];
if(x==-1&&u!=G.Vex[i]){
cout<<"源点到其它各顶点最短路径为:"<<u<<"--"<<G.Vex[i]<<" sorry,无路可达"<<endl;
continue;
}
while(x!=-1){
S.push(x);
x=p[x];
}
cout<<"源点到其它各顶点最短路径为:";
while(!S.empty()){
cout<<G.Vex[S.top()]<<"--";
S.pop();
}
cout<<G.Vex[i]<<" 最短距离为:"<<dist[i]<<endl;
}
}
输入与输出:
请输入顶点数:
5
请输入边数:
8
请输入顶点信息:
1 2 3 4 5
请输入每条边依附的两个顶点及权值:
1 2 2
1 3 5
2 3 2
2 4 6
3 4 7
3 5 1
4 3 2
4 5 4
请输入源点的信息:
1
当前位置:1
1 - 1 最短距离为:0
1 - 2 最短距离为:2
1 - 3 最短距离为:4
1 - 4 最短距离为:8
1 - 5 最短距离为:5
源点为:1
源点到其它各顶点最短路径为:1 最短距离为:0
源点到其它各顶点最短路径为:1--2 最短距离为:2
源点到其它各顶点最短路径为:1--2--3 最短距离为:4
源点到其它各顶点最短路径为:1--2--4 最短距离为:8
源点到其它各顶点最短路径为:1--2--3--5 最短距离为:5
算法优化
- 优先队列优化。在集合
V
−
S
V-S
V−S中寻找距离源点
u
最近的节点t
,如果穷举,需要O(n)
时间。如果采用优先队列,则寻找一个最近节点需要O(logn)
时间。时间复杂度为O(logn)
。 - 数据结构优化,邻接矩阵存储,访问一个节点的所有邻接点需要执行
n
次,总时间复杂度为O(n^2)
。如果采用邻接表存储,则访问一个节点的所有邻接点的执行次数为该节点的出度,所有节点的出度之和为m
(边数),总时间复杂度为O(m)
。
Floyd算法
- Floyd算法可用于求解任意两个节点间的最短路径。Floyd算法又被称为插点法,其算法核心是在节点
i
与节点j
之间插入节点k
,看看是否可以缩短节点i
与节点j
之间的距离。 - 数据结构。设置地图的带权邻接矩阵为
G.Edge[][]
,即如果从节点i
到节点j
有边,则G.Edge[i][j] = <i,j>
的权值,否则G.Edge[i][j] = 无穷大
;采用两个辅助数组:最短距离数组dist[i][j]
,记录从节点i
到节点j
的最短路径长度;前驱数组p[i][j]
,记录从节点i
到节点j
的最短路径上节点j
的前驱。 - 初始化。初始化
dist[i][j] = G.Edge[i][j]
,如果从节点i
到节点j
有边相连,则初始化p[i][j] = i
,否则p[i][j] = -1
。 - 插点。其实就是在节点
i
、j
之间插入节点k
,看是否可以缩短节点i
、j
之间的距离。如果dist[i][j]>dist[i][k]+dist[k][j]
,则dist[i][j] = dist[i][k]+dist[k][j]
,记录节点j
的前驱p[i][j] = p[k][j]
;
算法实现
#include<iostream>
#include<cstring>
using namespace std;
const int MaxVnum=100; //顶点数最大值
const int INF=0x3f3f3f3f; // 无穷大
typedef string VexType; //顶点的数据类型,根据需要定义
typedef int EdgeType; //边上权值的数据类型,若不带权值的图,则为0或1
typedef struct{
VexType Vex[MaxVnum];
EdgeType Edge[MaxVnum][MaxVnum];
int vexnum,edgenum; //顶点数,边数
}AMGraph;
int dist[MaxVnum][MaxVnum],p[MaxVnum][MaxVnum];
int locatevex(AMGraph G,VexType x){
for(int i=0;i<G.vexnum;i++)//查找顶点信息的下标
if(x==G.Vex[i])
return i;
return -1;//没找到
}
void CreateAMGraph(AMGraph &G){//创建无向图的邻接矩阵
int i,j,w;
VexType u,v;
cout<<"请输入顶点数:"<<endl;
cin>>G.vexnum;
cout<<"请输入边数:"<<endl;
cin>>G.edgenum;
cout<<"请输入顶点信息:"<<endl;
for(int i=0;i<G.vexnum;i++)//输入顶点信息,存入顶点信息数组
cin>>G.Vex[i];
for(int i=0;i<G.vexnum;i++)//初始化邻接矩阵所有值为0,若是网,则初始化为无穷大
for(int j=0;j<G.vexnum;j++)
if(i!=j)
G.Edge[i][j]=INF;
else
G.Edge[i][j]=0; //注意i==j时,设置为0
cout<<"请输入每条边依附的两个顶点及权值:"<<endl;
while(G.edgenum--){
cin>>u>>v>>w;
i=locatevex(G,u);//查找顶点u的存储下标
j=locatevex(G,v);//查找顶点v的存储下标
if(i!=-1&&j!=-1)
G.Edge[i][j]=w; //有向图邻接矩阵存储权值
}
}
void Floyd(AMGraph G){ //用Floyd算法求有向网G中各对顶点i和j之间的最短路径
int i,j,k;
for(i=0;i<G.vexnum;i++) //各对结点之间初始已知路径及距离
for(j=0;j<G.vexnum;j++){
dist[i][j]=G.Edge[i][j];
if(dist[i][j]<INF&&i!=j)
p[i][j]=i; //如果i和j之间有弧,则将j的前驱置为i
else p[i][j]=-1; //如果i和j之间无弧,则将j的前驱置为-1
}
for(k=0;k<G.vexnum;k++)
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++)
if(dist[i][k]+dist[k][j]<dist[i][j]){//从i经k到j的一条路径更短
dist[i][j]=dist[i][k]+dist[k][j]; //更新dist[i][j]
p[i][j]=p[k][j]; //更改j的前驱
}
}
void print(AMGraph G){
int i,j;
for(i=0;i<G.vexnum;i++){//输出最短距离数组
for(j=0;j<G.vexnum;j++)
cout<<dist[i][j]<<"\t";
cout<<endl;
}
cout<<endl;
for(i=0;i<G.vexnum;i++){//输出前驱数组
for(j=0;j<G.vexnum;j++)
cout<<p[i][j]<<"\t";
cout<<endl;
}
}
void DisplayPath(AMGraph G,int s,int t){//显示最短路径
if(p[s][t]!=-1){
DisplayPath(G,s,p[s][t]);
cout<<G.Vex[p[s][t]]<<"-->";
}
}
int main(){
VexType start,destination;
int u,v;
AMGraph G;
CreateAMGraph(G);
Floyd(G);
print(G);
cout<<"请依次输入路径的起点与终点的名称:";
cin>>start>>destination;
u=locatevex(G,start);
v=locatevex(G,destination);
DisplayPath(G,u,v);
cout<<G.Vex[v]<<endl;
cout<<"最短路径的长度为:"<<dist[u][v]<<endl;
cout<<endl;
return 0;
}
输入与输出:
请输入顶点数:
4
请输入边数:
8
请输入顶点信息:
0 1 2 3
请输入每条边依附的两个顶点及权值:
0 1 1
0 3 4
1 2 9
1 3 2
2 0 3
2 1 5
2 3 8
3 2 6
0 1 9 3
11 0 8 2
3 4 0 6
9 10 6 0
-1 0 3 1
2 -1 3 1
2 0 -1 1
2 0 3 -1
请依次输入路径的起点与终点的名称:0 2
0-->1-->3-->2
最短路径的长度为:9
SPFA算法
- Dijkstra算法无法处理带有负权边的图,如果有负权边,应采用SPFA算法。
- 创建一个队列,首先源点
u
入队,标记u
在队列中,u
的入队次数加1。 - 松弛操作。取出队头节点
x
,标记x
不在队列中。扫描x
的所有出边i(x,v,w)
,如果dis[v]>dis[x]+e[i].w
,则松弛,令dis[v] = dis[x]+e[i].w
。如果节点v
不在队列中,判断v
的入队次数加1后大于或等于n
,则说明有负环,退出;否则v
入队,标记v
在队列中 - 重复松弛操作,直到队列为空。
算法实现
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=505,maxe=100001;
int n,m,cnt;
int head[maxn],dis[maxn],sum[maxn];
bool vis[maxn];//标记是否在队列中
struct node{
int to,next,w;
}e[maxe];
void add(int u,int v,int w){
e[cnt].to=v;
e[cnt].next=head[u];
e[cnt].w=w;
head[u]=cnt++;
}
bool spfa(int u){
queue<int>q;
memset(vis,0,sizeof(vis));//标记是否在队列中
memset(sum,0,sizeof(sum));//统计入队的次数
memset(dis,0x3f,sizeof(dis));
vis[u]=1;
dis[u]=0;
sum[u]++;
q.push(u);
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=0;
for(int i=head[x];~i;i=e[i].next){
int v=e[i].to;
if(dis[v]>dis[x]+e[i].w){
dis[v]=dis[x]+e[i].w;
if(!vis[v]){
if(++sum[v]>=n)
return true;
vis[v]=1;
q.push(v);
}
}
}
}
return false;
}
void print(){//输出源点到其它节点的最短距离
cout<<"最短距离:"<<endl;
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
cout<<endl;
}
int main(){
cnt=0;
cin>>n>>m;
memset(head,-1,sizeof(head));
int u,v,w;
for(int i=1;i<=m;i++){
cin>>u>>v>>w;
add(u,v,w);
}
if(spfa(1))
cout<<"有负环!"<<endl;
else
print();
return 0;
}
输入:
5 8
1 2 2
1 3 5
2 3 2
2 4 6
3 4 7
3 5 1
4 3 2
4 5 4
输出:
0 2 4 8 5
训练1:重型运输
题目描述
Hugo需要将巨型起重机从工场运输到他的客户所在的地方,经过的所有街道都必须能承受起重机的重量。他已经有了所有街道及其承重的城市规划。不幸的是,他不知道如何找到街道的最大承重能力,以将起重机可以有多重告诉他的客户。
街道(具有重量限制)之间的交叉点编号为 1 ∼ n 1 \sim n 1∼n。找到从1号(Hugo的地方)到 n n n号(客户的地方)可以运输的最大重量。假设至少有一条路径,所有街道都是双向的。
输入:第1行包含测试用例数量。每个测试用例的第1行都包含 n n n( 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1≤n≤1000)和 m m m,分别表示街道交叉口的数量和街道的数量。以下 m m m行,每行都包含3个整数(正数且不大于 1 0 6 10^6 106),分别表示街道的开始、结束和承重。在每队交叉点之间最多有一条街道。
输出:对每个测试用例,输出都以包含“Scenario #i:
”的行开头,其中
i
i
i是从1开始的测试用例编号。然后单行忽视抽揩油运输给客户的最大承重。在测试用例之间有一个空行。
算法设计
- 将所有街道都采用链式前向星存储,每个街道都是双向的。
- 将Dijkstra算法的更新条件变换为最小值最大的更新。
算法实现
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=1005,maxe=1000001;
const int inf=0x3f3f3f3f; //最大值
int T,n,m,w,cnt;
int head[maxn],dis[maxn];
bool vis[maxn];//标记是否已访问
struct node{ //前向星结构
int to,next,w;
}e[maxe];
void add(int u,int v,int w); //添加一条边
void solve(int u); //dijkstra变形算法
int main(){
int p=1;
cin>>T;
while(T--){
cnt=0;
memset(head,-1,sizeof(head)); //将前驱数组置为-1
cin>>n>>m;
int u,v,w;
for(int i=1;i<=m;i++){
cin>>u>>v>>w;
add(u,v,w);//两条边
add(v,u,w);
}
solve(1);
cout<<"Scenario #"<<p++<<":"<<endl;
cout<<dis[n]<<endl<<endl;
}
return 0;
}
void add(int u,int v,int w){
e[cnt].to=v;
e[cnt].next=head[u];
e[cnt].w=w;
head[u]=cnt++;
}
void solve(int u){//dijkstra算法变形,求最小值最大的路径
priority_queue<pair<int,int> >q; //pair模板类型,每个pair对象可以存储两个值
memset(vis,0,sizeof(vis)); //标记矩阵置0
memset(dis,0,sizeof(dis)); //距离矩阵置0
dis[u]=inf;
q.push(make_pair(dis[u],u));//最大值优先
while(!q.empty()){
int x=q.top().second;
q.pop();
if(vis[x])
continue;
vis[x]=1;
if(vis[n])
return;
for(int i=head[x];~i;i=e[i].next){ //遍历节点x的所有邻接点
int v=e[i].to;
if(vis[v])
continue;
if(dis[v]<min(dis[x],e[i].w)){//求最小值最大的
dis[v]=min(dis[x],e[i].w);
q.push(make_pair(dis[v],v));
}
}
}
}
输入:
1
3 3
1 2 3
1 3 4
2 3 5
输出:
Scenario #1:
4
训练2:货币兑换
题目描述
有几个货币兑换点,每个点只能兑换两种特定货币。可以有几个专门针对同一种货币的兑换点。每个兑换点都有自己的汇率,货币 A A A到货币 B B B的汇率是 1 A 1A 1A兑换 B B B的数量。此外,每个交换点都有一些佣金,即必须为交换操作支付的金额。佣金始终以源货币收取
可以处理 N N N种不同的货币。货币编号为 1 ∼ N 1 \sim N 1∼N。对每个交换点都用6个数字来描述:整数 A A A和 B B B(交换的货币类型)。以及 R A B R_{AB} RAB、 C A B C_{AB} CAB、 R B A R_{BA} RBA和 C B A C_{BA} CBA(分别表示交换A到B和B到A时的汇率和佣金)。
尼克有一些货币 S S S,并想知道他是否能在一些交易所操作之后增加他的资本。当然,他最终想要换回货币 S S S。在进行操作时所有金额都必须是非负数。
输入:输入的第1行包含4个数字: N N N表示货币类型的数量, M M M表示交换点的数量, S S S表示尼克拥有的货币类型, V V V表示他拥有的货币数量。以下 M M M行,每行都包含6个数字,表示相应交换点的描述。数字由一个或多个空格分隔。 1 ≤ S ≤ N ≤ 100 1 \leq S \leq N \leq 100 1≤S≤N≤100, 1 ≤ M ≤ 100 1 \leq M \leq 100 1≤M≤100, V V V是实数, 0 ≤ V ≤ 1 0 3 0 \leq V \leq 10^{3} 0≤V≤103
输出:如果尼克可以增加他的财富,则输入“YES”,在其他情况下输出“NO”。
算法设计
- 本题从当前货币出发,走一个回路,赚到一些钱。因为走过的边是双向的,因此能走过去就一定能走回来。只需判断在图中是否都正环,即使这个正环不包含 S S S也没关系,走一次正环就会多赚一些
- Bellman-Ford算法,判断正环。用边松弛
n-1
次后,再执行一次,如果还可以松弛,则说明有环(是正环还是负环,主要取决于松弛条件)。注意:对双向边,边数是2m
或使用边数计数器cnt
算法实现
#include<iostream>
#include<cstring>
using namespace std;
struct node{
int a,b;
double r,c;
}e[210];
double dis[110];
int n,m,s,cnt=0;
double v;
void add(int a,int b,double r,double c); //添加结构体
bool bellman_ford(); //判正环
int main(){
int a,b;
double rab,cab,rba,cba;
cin>>n>>m>>s>>v;
for(int i=0;i<m;i++){
cin>>a>>b>>rab>>cab>>rba>>cba;
add(a,b,rab,cab);
add(b,a,rba,cba);
}
if(bellman_ford())
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
return 0;
}
void add(int a,int b,double r,double c){
e[cnt].a=a;
e[cnt].b=b;
e[cnt].r=r;
e[cnt++].c=c;
}
bool bellman_ford(){
memset(dis,0,sizeof(dis));
dis[s]=v;
for(int i=1;i<n;i++){//执行n-1次
bool flag=false;
for(int j=0;j<cnt;j++)//注意:边数是2m或使用cnt
if(dis[e[j].b]<(dis[e[j].a]-e[j].c)*e[j].r){ //松弛,a、b为边的节点,r、c为汇率和佣金
dis[e[j].b]=(dis[e[j].a]-e[j].c)*e[j].r;
flag=true;
}
if(!flag)
return false;
}
for(int j=0;j<cnt;j++)//再执行1次,还能松弛说明有环
if(dis[e[j].b]<(dis[e[j].a]-e[j].c)*e[j].r)
return true;
return false;
}
输入:
3 2 1 20.0
1 2 1。00 1.00 1.00 1.00
2 3 1.10 1.00 1.10 1.00
输出:
YES
训练3:虫洞
题目描述
在探索许多农场时,约翰发现了一些令人惊奇的虫洞。虫洞是非常奇特的,因为它是一条单向路径,可以将人穿越到虫洞之前的某个时间!约翰想从某个地方开始,穿过有一些路径和虫洞,并在他出发前的一段时间放回起点,也许他将能够见到自己。
输入:第1行是单个整数 F ( 1 ≤ F ≤ 5 ) F(1 \leq F \leq 5) F(1≤F≤5),表示农场的数量。每个农场的第1行有3个整数 N N N、 M M M、 W W W,表示编号为 1 ∼ N 1\sim N 1∼N的 N ( 1 ≤ N ≤ 500 ) N(1 \leq N \leq 500) N(1≤N≤500)块田、 M ( 1 ≤ M ≤ 2500 ) M(1 \leq M \leq 2500) M(1≤M≤2500)条路径和 W ( 1 ≤ W ≤ 200 ) W(1 \leq W \leq 200) W(1≤W≤200)个虫洞。第 2 ∼ M + 1 2 \sim M+1 2∼M+1行,每行都包含3个数字 S S S、 E E E、 T T T,表示穿过 S S S与 E E E之间的路径(双向)需要 T T T秒。两块田可能有多个路径。第 M + 2 ≤ M + W + 1 M+2 \leq M+W+1 M+2≤M+W+1行,每行都包含3个数字 S S S、 E E E、 T T T,表示对从 S S S到 E E E的单向路径,旅行者将穿越 T T T秒。没有路径需要超过10000秒的旅行时间,没有虫洞可以穿越超过10000秒。
输出:对于每个农场,如果约翰可以达到目标,则输出“YES”,否则输出“NO”
算法设计
- 使用SPFA判断负环,值得注意的是:普通道路是双向的,虫洞是单向的,而且时间为负值。
算法实现
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=505,maxe=100001;
const int inf=0x3f3f3f3f;
int T,n,m,w,cnt;
int head[maxn],dis[maxn],sum[maxn];
bool vis[maxn];//标记是否在队列中
struct node{
int to,next,c;
}e[maxe];
void add(int u,int v,int c); //添加一条边
bool spfa(int u); //寻找负环
bool solve();
int main(){
cin>>T;
while(T--){
cnt=0;
cin>>n>>m>>w;
memset(head,-1,sizeof(head));
int u,v,t;
for(int i=1;i<=m;i++){
cin>>u>>v>>t;
add(u,v,t);//两条边
add(v,u,t);
}
for(int i=1;i<=w;i++){
cin>>u>>v>>t;
add(u,v,-t);//一条边
}
if(solve())
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
}
return 0;
}
void add(int u,int v,int c){
e[cnt].to=v;
e[cnt].next=head[u];
e[cnt].c=c;
head[u]=cnt++;
}
bool spfa(int u){
queue<int>q;
memset(vis,0,sizeof(vis));
memset(sum,0,sizeof(sum));
vis[u]=1;
dis[u]=0;
sum[u]++;
q.push(u);
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=0;
for(int i=head[x];~i;i=e[i].next){
if(dis[e[i].to]>dis[x]+e[i].c){
dis[e[i].to]=dis[x]+e[i].c;
if(!vis[e[i].to]){
if(++sum[e[i].to]>=n)
return false;
vis[e[i].to]=1;
q.push(e[i].to);
}
}
}
}
return true;
}
bool solve(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++)
if(dis[i]==inf)//如果已经到达该点没找到负环,则不需要再从该点找
if(!spfa(i))
return 1;
return 0;
}
输入:
2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8
输出:
NO
YES
训练4:最短路径
题目描述
母牛从 N N N个农场中的任一去参加盛大的母牛聚会,聚会地点在 X X X号农场。共有 M M M条单行道分别连接两个农场,且通过路 i i i需要花 T i T_{i} Ti时间。每头母牛都必须参加宴会,并且在宴会结束时回到自己的领地,但是每头母牛都会选择时间最少的方案。来时的路和去时的路可能不一样,因为路是单向的。求所有的母牛中参加聚会来回的最长的时间。
输入:第1行包含3个整数 N N N、 M M M和 X X X。在第 2 ∼ M + 1 2 \sim M+1 2∼M+1行中,第 i + 1 i+1 i+1描述道路i,有3个整数: A i A_{i} Ai、 B i B_{i} Bi和 T i T_{i} Ti,表示从 A i A_{i} Ai号农场到 B i B_{i} Bi号农场需要 T i T_{i} Ti时间。其中, 1 ≤ N 1000 1 \leq N 1000 1≤N1000, 1 ≤ X ≤ N 1 \leq X \leq N 1≤X≤N, 1 ≤ M ≤ 100000 1 \leq M \leq 100000 1≤M≤100000, 1 ≤ T i ≤ 100 1 \leq T_{i} \leq 100 1≤Ti≤100。
输出:单行输出母牛必须花费的时间的最大值。
算法设计
- 因为母牛来回走的都是最短路径,所以先求每个节点从出发到聚会地点来回的最短路径之和,然后求最大值即可。
- 从 i i i号农场到聚会地点 X X X,相当于在反向图中从 X X X到 i i i。
- 从聚会地点 X X X返回到 i i i号农场,相当于在正向图中从 X X X到 i i i。
- 创建正向图和反向图,都把
X
X
X作为源点,分别调用SPFA算法求正向图、反向图中源点到其他各个点的最短时间
dis[i]
和rdis[i]
,求最大和值。
算法实现
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=10005,maxe=100005;
const int inf=0x3f3f3f3f;
int n,m,x,cnt,rcnt;
int head[maxn],rhead[maxn],dis[maxn],rdis[maxn];
bool vis[maxn];//标记是否在队列中
struct node{
int to,next,w;
}e[maxe],re[maxe];
void add(node *e,int *head,int u,int v,int w,int &cnt); //添加一条边
void spfa(node *e,int *head,int u,int *dis); //寻找负环
int main(){
cin>>n>>m>>x;
cnt=rcnt=0;
memset(head,-1,sizeof(head));
memset(rhead,-1,sizeof(rhead));
int u,v,w;
for(int i=1;i<=m;i++){
cin>>u>>v>>w;
add(e,head,u,v,w,cnt);
add(re,rhead,v,u,w,rcnt);//反向图
}
spfa(e,head,x,dis);
spfa(re,rhead,x,rdis);
int ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,dis[i]+rdis[i]);
cout<<ans<<endl;
return 0;
}
void add(node *e,int *head,int u,int v,int w,int &cnt){
e[cnt].to=v;
e[cnt].next=head[u];
e[cnt].w=w;
head[u]=cnt++;
}
void spfa(node *e,int *head,int u,int *dis){
queue<int>q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,maxn*sizeof(int));//数组做参数,不能用sizeof(dis)测量
vis[u]=1;
dis[u]=0;
q.push(u);
while(!q.empty()){
int x=q.front();
q.pop();
vis[x]=0;
for(int i=head[x];~i;i=e[i].next){
if(dis[e[i].to]>dis[x]+e[i].w){
dis[e[i].to]=dis[x]+e[i].w;
if(!vis[e[i].to]){
vis[e[i].to]=1;
q.push(e[i].to);
}
}
}
}
}
输入:
4 8 2
1 2 4
1 3 2
1 4 7
2 1 1
2 3 5
3 1 2
3 4 4
4 2 3
输出:
10