算法训练营 图的应用(最短路径)

最短路径

  • 图的一些经典应用,包括最短路径、最小生成树、拓扑排序和关键路径。

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 VS中的所有节点i,都初始化dist[i] = G.Edge[u][i]。如果从源点u到节点i有边相连,则初始化p[i] = u,否则p[i] = -1
  • 找最小。在集合 V − S V - S VS中查找dist[]最小的节点t,即dist[t] = min(dist[j]|j属于集合V - S),则节点t就是集合 V − S V - S VS中距离源点u最近的节点。
  • 判结束。如果集合 V − S V - S VS为空,则算法结束,否则转向下一步。
  • 借东风。在找最小步骤中已经找到了从源点到节点t的最短路径,那么对集合 V − S V-S VS中节点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

算法优化

  1. 优先队列优化。在集合 V − S V-S VS中寻找距离源点u最近的节点t,如果穷举,需要O(n)时间。如果采用优先队列,则寻找一个最近节点需要O(logn)时间。时间复杂度为O(logn)
  2. 数据结构优化,邻接矩阵存储,访问一个节点的所有邻接点需要执行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
  • 插点。其实就是在节点ij之间插入节点k,看是否可以缩短节点ij之间的距离。如果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 1n。找到从1号(Hugo的地方)到 n n n号(客户的地方)可以运输的最大重量。假设至少有一条路径,所有街道都是双向的。

输入:第1行包含测试用例数量。每个测试用例的第1行都包含 n n n 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1n1000)和 m m m,分别表示街道交叉口的数量和街道的数量。以下 m m m行,每行都包含3个整数(正数且不大于 1 0 6 10^6 106),分别表示街道的开始、结束和承重。在每队交叉点之间最多有一条街道。

输出:对每个测试用例,输出都以包含“Scenario #i:”的行开头,其中 i i i是从1开始的测试用例编号。然后单行忽视抽揩油运输给客户的最大承重。在测试用例之间有一个空行。

算法设计

  1. 将所有街道都采用链式前向星存储,每个街道都是双向的。
  2. 将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 1N。对每个交换点都用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 1SN100 1 ≤ M ≤ 100 1 \leq M \leq 100 1M100 V V V是实数, 0 ≤ V ≤ 1 0 3 0 \leq V \leq 10^{3} 0V103

输出:如果尼克可以增加他的财富,则输入“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 100 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(1F5),表示农场的数量。每个农场的第1行有3个整数 N N N M M M W W W,表示编号为 1 ∼ N 1\sim N 1N N ( 1 ≤ N ≤ 500 ) N(1 \leq N \leq 500) N(1N500)块田、 M ( 1 ≤ M ≤ 2500 ) M(1 \leq M \leq 2500) M(1M2500)条路径和 W ( 1 ≤ W ≤ 200 ) W(1 \leq W \leq 200) W(1W200)个虫洞。第 2 ∼ M + 1 2 \sim M+1 2M+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+2M+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 2M+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 1N1000 1 ≤ X ≤ N 1 \leq X \leq N 1XN 1 ≤ M ≤ 100000 1 \leq M \leq 100000 1M100000 1 ≤ T i ≤ 100 1 \leq T_{i} \leq 100 1Ti100

输出:单行输出母牛必须花费的时间的最大值。

算法设计

  • 因为母牛来回走的都是最短路径,所以先求每个节点从出发到聚会地点来回的最短路径之和,然后求最大值即可。
  1. i i i号农场到聚会地点 X X X,相当于在反向图中从 X X X i i i
  2. 从聚会地点 X X X返回到 i i i号农场,相当于在正向图中从 X X X i i i
  3. 创建正向图和反向图,都把 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值