对于最短路问题,可用邻接表、邻接矩阵等形象表述,是图论算法的基础
比如这样的一个【图】(边长一般会有权值)
它可以表示成像左图这样的【邻接表】或向右图这样的【邻接矩阵】
struct EDGE
{
int u,v,w,next;
EDGE() {}
EDGE(int _u,int _v,int _w,int _next)
{
u=_u,v=_v,w=_w,next=_next;
}
}edge[MAXM];
初始化所有的head[i] = INF,当前边总数 edgeCount=0
每读入一条边,调用addEdge(u,v,w)函数如下:
void addEdge(int u,int v,int w)
{
edge[edgeCount]=EDGE(u,v,w,head[u]);
head[u]=edgeCount++;
}
暂时还比较习惯用vector储存邻接表,后续仔细学习链式前向星后再进行运用
可参考大佬博客讲解——前向星和链式前向星
今天我们主要运用一下Floyd弗洛伊德算法和Dijkstra迪杰斯特拉算法
另外两种算法后续仔细学习了再做运用叭φ(>ω<*)
先来康康最短路问题的例题。◕ᴗ◕。
畅通工程续 HZNU19training题源
Background
某省自从实行了很多年的畅通工程计划后,终于修建了很多路。不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多。这让行人很困扰。
现在,已知起点和终点,请你计算出要从起点到终点,最短需要行走多少距离。
Input
本题目包含多组数据,请处理到文件结束。
每组数据第一行包含两个正整数N和M(0接下来是M行道路信息。每一行有三个整数A,B,X(0<=A,B 再接下一行有两个整数S,T(0<=S,T
Output
对于每组数据,请在一行里输出最短需要行走的距离。如果不存在从S到T的路线,就输出-1.
Sample Input
3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2
Sample Output
-1
算法思想图解——构造邻接矩阵(INF表示无穷大)
该算法时间复杂度较大,但思维比较简单
核心代码中【 i 和 j 】表示两点之间的最短路径,外套【 k 】层循环表示其他经过点
也就是说,比如在26个字母中从A走到Z,要找最短路径就是A经过一点从那点到Z
那么具体是哪一点就靠循环k来遍历,如果k循环到起始位置,不用担心,同点的dis提早赋值为0啦
#include
#include
#include
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int n,m,a,b,x,s,t;
int dis[N][N];
void Floyd()//弗洛伊德核心代码-不做优化时,时间复杂度为O(n3)
{
for(int k=0;k<n;k++)
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
dis[i][j]=inf;
if(i==j) dis[i][j]=0;
}
}
for(int i=1,a,b,x;i<=m;i++)
{
scanf("%d %d %d",&a,&b,&x);
x=min(x,dis[a][b]);
dis[a][b]=dis[b][a]=x;
}
Floyd();
scanf("%d %d",&s,&t);
if(dis[s][t]==inf) printf("%d\n",-1);
else printf("%d\n",dis[s][t]);
}
return 0;
}
上文已经提到Floyd算法的时间复杂度较大,一般的OJ系统1s能处理1e7-1e8的数据
那么对于Floyd这样n3的复杂度,只能接受不到1e3的数据,真的是非常非常小啦
所以很多时候我们需要更加快捷的算法,来康康下面这道例题叭。◕ᴗ◕。
一个人的旅行 HZNU19training题源
Background
虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,0),很多事,还能丰富自己的阅历,还可以看美丽的风景……草儿想去很多地方,她想要去东京铁塔看夜景,去威尼斯看电影,去阳明山上看海芋,去纽约纯粹看雪景,去巴黎喝咖啡写信,去北京探望孟姜女……眼看寒假就快到了,这么一大段时间,可不能浪费啊,一定要给自己好好的放个假,可是也不能荒废了训练啊,所以草儿决定在要在最短的时间去一个自己想去的地方!因为草儿的家在一个小镇上,没有火车经过,所以她只能去邻近的城市坐火车(好可怜啊~)。
Input
输入数据有多组,每组的第一行是三个整数T,S和D,表示有T条路,和草儿家相邻的城市的有S个,草儿想去的地方有D个;
接着有T行,每行有三个整数a,b,time,表示a,b城市之间的车程是time小时;(1=<(a,b)<=1000;a,b 之间可能有多条路)
接着的第T+1行有S个数,表示和草儿家相连的城市;
接着的第T+2行有D个数,表示草儿想去地方。
Output
输出草儿能去某个喜欢的城市的最短时间。
Sample Input
6 2 3
1 3 5
1 4 7
2 8 12
3 8 4
4 9 12
9 10 2
1 2
8 9 10
Sample Output
9
这道题如果用第一道例题那样最普通的Floyd算法,大概率就TLE啦
但是有的时候碰碰运气,数据不是特别狠的话,我们对Floyd算法做个小优化还是可以AC哒!
来康康这个写法叭。◕ᴗ◕。
#include
#include
#include
using namespace std;
const int N=1e3+10;
const int inf=0x3f3f3f3f;
int n,t,s,d,a,b,time;
int other[N],pos[N],love[N],dis[N][N],present[N];
void init()
{
for(int i=1;i<=N;i++)
{
for(int j=1;j<=N;j++)
{
if(i==j) dis[i][j]=0;
else dis[i][j]=inf;
}
}
memset(pos,-1,sizeof(pos));
memset(love,-1,sizeof(love));
memset(present,0,sizeof(present));
}
void Floyd()//弗洛伊德核心代码-不做优化时,时间复杂度为O(n3)
{
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
if(dis[other[i]][other[k]]==inf) continue;
//注意这一步能节省时间、优化代码的时间复杂度
for(int j=1;j<=n;j++)
{
dis[other[i]][other[j]]=min(dis[other[i]][other[j]],dis[other[i]][other[k]]+dis[other[k]][other[j]]);
}
}
}
}
int main()
{
while(~scanf("%d %d %d",&t,&s,&d))
{
init();
n=0;
for(int i=1;i<=t;i++)
{
scanf("%d %d %d",&a,&b,&time);
if(present[a]!=1)
{
n++;
other[n]=a;
present[a]=1;
}
if(present[b]!=1)
{
n++;
other[n]=b;
present[b]=1;
}
time=min(time,dis[a][b]);
dis[a][b]=dis[b][a]=time;
}
for(int i=1;i<=s;i++)
{
scanf("%d",&pos[i]);
}
for(int i=1;i<=d;i++)
{
scanf("%d",&love[i]);
}
Floyd();
int Min=inf;
for(int i=1;i<=s;i++)
{
for(int j=1;j<=d;j++)
{
if(dis[pos[i]][love[j]]<Min) Min=dis[pos[i]][love[j]];
}
}
printf("%d\n",Min);
}
}
这段代码最最关键的就是下面这句话啦٩(๑❛ᴗ❛๑)۶
if(dis[other[i]][other[k]]==inf) continue;
因为当数据很大的时候,真的可能遇到很多很多的INF点,也就是路径不通的点,时间距离无穷大
那么三重循环里当我们发现两重循环得到的dis[i][k]已经是INF
又何必浪费时间去做下一层的比较呢,下一层的比较找的是min值呢!
况且无穷大代表路不通,走一条不通的路何必呢哈哈哈哈哈
不过呐,如果出题方特别狠,给的数据在1e3以上,偏巧每条路还都是通的
那么这个投机取巧的优化也就没有用武之地啦,新的算法还是要好好学下哒❥(ゝω・✿ฺ)
算法思想图解——构造邻接矩阵(INF表示无穷大)
这个算法的核心代码就是要有一个两个集合
一个是【node型q优先队列pop出去】的是已经找到最短路径的点集
另一个是还在node型vector的G数组中记录着的待找的各点之间的距离
G数组是二维的
第一维是以某点出发
第二维是个node结构体,记录着到to点的路径w
其中dis数组记录了已经找到的最短路径,同时也用于更新还没被pop的可能更短的路径
主要的思想就是从起点出发,找到到其他点的最短路径
比如起点是A,在其他所有点里面,它到G点的路径最短,那么这个最短路径就是A-G的最短路径
所以这个算法是单源的,必须确定起点然后去找到其他点的最短路径
接着,下一步从G点开始找最短路径的点,比如找到了C点
那么现在得到的最短路径值,就是A-C的最短路径啦~以此类推……
不过有的时候题目会要求输入几种起点的可能性,几种终点的可能性
那么怎么找所有方案的最短路径呢?(比如第二道例题,下附AC代码)
只要输入起点位置的时候,对G数组进行初始化,赋值让0点位和起始点位的距离为0
现在只要在循环初始传入Dijkstra函数的变量是个虚设的起点0
在Dijkstra函数中第一轮循环的时候找到的就是那几个真正的起点
因为它们对于0这个虚设起点来说,路径为0肯定最小
就相当于找的是从0(虚设起点)到真正起点再到其他各点的路径
最后任务完成,虚设的起点当然阔以忽略啦
来啦来啦~快康康下面用Dijkstra算法写的第二道例题叭٩(๑>◡<๑)۶
#include
#include
#include
#include
using namespace std;
const int N=1e3+10,INF=0x3f3f3f3f;
/*
32-bit(位)int类型的最大值是0x7fffffff
0x3f3f3f3f为ACM的无穷大常量=1061109567
好处:无穷大相加依然不会溢出
可以用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f
因为这个数的每个字节都是0x3f
*/
struct Edge//用于传变量,构造vector
//构造一个结构体,里面存有to和w两个变量
/*
to存着这条边指向哪儿;w存着权值
Edge(){}是个用来给变量初始化0的函数
Edge(int to,int w):to(to),w(w){}为结构体赋初值
定义的结构体数组大小开了N<<1即2*N
*/
{
int to,w;
Edge(){}//后面别加分号
Edge(int to,int w):to(to),w(w){}//后面别加分号
} edge[N<<1];//位移操作,往左n个单位,就是乘以2的n次方,往右是除以
/*
Edge(int to, int w) : to(to), w(w) {}相当于像这样赋值
void f()
{
Edge e;
e.to=1,e.w=2;
Edge e2=Edge();
Edge e3=Edge(1,2);
*/
struct qnode//用于传变量,构造优先队列
{
int u,w;
qnode() {}//后面别加分号
qnode(int u,int w):u(u),w(w){}//后面别加分号,此处类似于上面的Edge赋初值
bool operator<(const qnode &other) const
{//这是优先队列里面固定了只能重载小于号!!!return >小的出,return <大的出
return w>other.w;//优先队列默认大的在前,此处意义详见上一行
}
/*
或者定义成这样friend bool operator < (const node &a,const node &b)
下面就要return a.w>b.w; 注意上面的第一个只有w
同时定义的时候,没有friend,但末尾得有const
*/
};
/*
结构体里构造这类函数就是用于
把之后类似于"qnode(s,0)" 的值,按顺序对应赋给"qnode(int u,int w)"的u和w
同时!!!切记,这类函数名必须和结构体类型名一样才能起到这个作用
*/
int n,m;
int S,D,s[N],d[N];//s[N]存起点位置;d[N]存想去的地方
int dis[N];//存着最短时间,被pop了的是已经最短的,还没被pop的是还在另一集合内待更新的
vector<Edge> G[N];//二维结构体可变长数组,是还没找到最短路径的点集
void Dijkstra(int s)//从main函数传入一个变量,后面用的时候叫做s
{////不做优化时,时间复杂度为O(n2)
priority_queue<qnode> q;
memset(dis,INF,sizeof(dis));//dis数组赋初始值均为ACM无穷大常量
dis[s]=0;
q.push(qnode(s,0));//0,0入队列
/*
等于有一个新的qnode类型结构体的变量
它的u=s,w=0,同时被push到了优先队列里面
*/
while(!q.empty())//这个部分类似于BFS广搜
{
int u=q.top().u;
/*
定义一个新的变量u储存队首元素
从起点走到某点目前最短的时间
然后去找走到下一个点的最短时间
*/
q.pop();//储存完毕就移除,后续有push,直到队列无push的时候循环结束
/*
以下代码也可以写成(auto自动识别it迭代器的类型)
for(auto it:G[u])
{
int v=it.to,w=it.w;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
q.push(qnode(v,dis[v]));
}
}
*/
for (int i=0,len=G[u].size();i<len;i++)
{
/*
遍历u到某点的时间,初始是点位0
因为之前记录过邻接城市到0的时间是0min
所以下一个先被找出来的最短时间位置就是起始位置啦
*/
/*
循环找从起点走到u之后的下一个点v的min time
找的时候就是遍历u记录的这个点往下走能走到哪个点
最小的那个肯定也是从起点走到那个点的最小值
因为对于非负的路径,已经是最小的
通过其他路径再加路径长度,不会更小
*/
int v=G[u][i].to,w=G[u][i].w;
/*
从u出发到i的时间,起始位置的w是0
i对应的to值是对于u来说要往下走的下一个点
输入的时候已经用Edge捆绑录入过to和w
如果没连着就是w的值就是INF无穷大
*/
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;//dis[v]记录从起点暂时走到点位v的最短时间
q.push(qnode(v,dis[v]));
/*
每个dis[v]都被放到队列里面
那找被pop的那个,就找到最小的咯
*/
}
}
}
}
/*
传统写法-较慢
void Dijkstra(int s)
{
memset(dis,INF,sizeof(dis));
dis[1]=0;
while(true)
{
int pos=-1,Min=INF;
//找右边点集中最小值
for(int i=1;i
int main()
{
while(~scanf("%d %d %d",&n,&S,&D))
{
for(int i=0;i<N;i++)
{
G[i].clear();//vector二维数组的第二维循环清空
}
for(int i=1,u,v,w;i<=n;i++)//u,v,w相当于a,b,time
{//u,v,w只是int在定义变量,因为循环内部要用
scanf("%d %d %d",&u,&v,&w);
G[u].push_back(Edge(v,w));
/*
G[u]记录点u到点v的时间w,这个v和w用结构体捆绑
且每个能从u走到的点,都会被接在G[u]可变长的第二维里面
*/
G[v].push_back(Edge(u,w));
/*
G[v]记录点v到点u的时间w,这个u和w用结构体捆绑
且每个能从v走到的点,都会被接在G[v]可变长的第二维里面
*/
}
for (int i=1;i<=S;i++)
{//输入可能的起点位置,让它们与0点位时间为0,方便函数里找起始位置
scanf("%d",&s[i]);//&s[i]也可用地址写成s+i
G[0].push_back(Edge(s[i],0));//初始化点位0到邻接城市点s[i]是0min
G[s[i]].push_back(Edge(0,0));//初始化邻接城市点s[i]到点位0是0min
}
Dijkstra(0);//传入s的初始值为0
int res=INF;//输出的要是最短时间,初始值就设为无穷大
for (int i=1;i<=D;i++)//一共有D个想去的地方,就循环找最小时间
{
scanf("%d",&d[i]);//&d[i]也可用地址写成d+i
res=min(res,dis[d[i]]);
/*
这个写法就相当于if新的值比原来的min小
那么min就变成了新的值
也就是遍历想去的城市,找到最短时间最短的那一个
*/
}
printf("%d\n",res);
}
return 0;
}
/*
神奇的操作
for(int i=1;i<=n;i++)
{
printf("%d%c",a[i]," \n"[i==n]);//看\n前面还有一个空格喔
}
这个的意思就是当i==n的时候,对于%c输出的就是\n,否则输出的是空格
[]内的条件成立的时候输出所列内容的后者!!!
这个小技巧在打印类似于到各个点最短路径矩阵的问题时可以用到
*/