链式前向星是一种静态链表存储,用边集数组和邻接表相结合,可以快速访问一个顶点的所有邻接点,在算法竞赛中广泛使用。
——摘自知乎
对于下面这组数据,第一行4个顶点,6条边,接下来的6行是边的起点、终点、权值,比如 1 -> 2 的权值为2
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
链式前向星存的就是每一个顶点为起点的边的集合
//以1为起点的边的集合有
1 2 2
1 3 5
1 4 4
//以2为起点的边的集合有
2 3 2
2 4 1
//以3为起点的边的集合有
3 4 3
我们给上面的6条边进行编号【0,5】。
接下来我们给出链式前向星的存储结构,其中e[i]存第i条边,to代表终点,next代表与这条边相同的上一条边的编号,weight代表权值,head[i]代表以第i号点为起点的最后一条边的编号,通常head[i]初始化为-1,因为没有编号为-1的边。
#define m 10
struct edge {
int to;
int next;
int weight;
}e[m];
int head[m];
现在我们就可以给出以下函数,u为起点,v为终点,w为权值,cnt记录第几条边,m为边数
void create()
{
for (int i = 0; i < m; i++)head[i] = -1;
int cnt = 0;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[cnt].to = v;
e[cnt].next = head[u];
e[cnt].w = w;
head[u] = cnt++;
}
}
比如上面输入的:
第0条边:1 2 2,就是 e[0].to=2; e[0].next=head[1]=-1; e[0].w=2; head[1]=0; cnt=1;
第1条边:2 3 2,就是 e[1].to=3; e[1].next=head[2]=-1; e[1].w=2; head[2]=1; cnt=2;
第2条边:2 4 1,就是 e[2].to=4; e[2].next=head[2]=1; e[2].w=1; head[2]=2; cnt=3;
第3条边:1 3 5,就是 e[3].to=3; e[3].next=head[1]=0; e[3].w=5; head[1]=3; cnt=4;
第4条边:3 4 3,就是 e[4].to=4; e[4].next=head[3]=-1; e[4].w=3; head[3]=4; cnt=5;
第5条边:1 4 4,就是 e[5].to=4; e[5].next=head[1]=3; e[5].w=4; head[1]=5; cnt=6;
这样就把所有的6条边存完了,那么接下来就是如何遍历所有的边了
for (int i = 1; i <= n; i++)//n个点
{
for (int j = head[i]; j != -1; j = e[j].next)//遍历以i为起点的边
{
cout << i << " -> " << e[j].to << " = " << e[j].weight << endl;
}
}
比如当i=1时过程如下:
从上面的数据可以知道以1为起点的边有3条,所以有三次循环
第一次循环:j=head[1]=5; e[j].to=4; e[j].weight=4; e[j].next=3;
第二次循环:j=e[j].next=3; e[j].to=3; e[j].weight=5; e[j].next=0;
第三次循环:j=e[j].next=0; e[j].to=2; e[j].weight=2; e[j].next=-1; 此时退出循环,开始找以2为起点的边
到这链式前向星如何存图以及遍历已经解决了,接下来把它结合到Dijkstra算法中
Dijkstra算法
还是以上面的数据作为案例,我们把它复制下来
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
我们知道这是一个计算单源最短路的算法,单源最短路径就是给出一个源点,到目标点的最短距离,比如我们这里给出源点1,到所有其他点的最短路径
除了上面的链式前向星存储结构,我们还需要三个数组,D[]——记录最小权值,P[]——记录最短路径中经过了哪些点,F[]——记录该点是否完成计算
先看代码
#define INFINITY 2147483647
void findminpath(int x)//x为源点
{
int min,temp,cnt=1;
f[x]=1;
d[x]=0;
p[x]=x;
while(cnt!=n){
min=INFINITY;
for(int j=head[x];j!=-1;j=e[j].next){
if(f[e[j].to]==0&&e[j].w+d[x]<d[e[j].to]){
d[e[j].to]=e[j].w+d[x];
p[e[j].to]=x;
}
}
for(int i=1;i<=n;i++){
if(f[i]==0&&d[i]<min){
min=d[i];
temp=i;
}
}
x=temp;
f[temp]=1;
cnt++;
}
}
我们来分析其过程
首先我们选定源点x=1;并将d[]全部初始化为一个很大的值
将d[x]=0,因为源点到自身的最短路径为0;将f[x]=1,表示源点已经完成计算(0代表还没有计算完成);将p[x]=x,因为源点到自身为x->x;同时以源点作为拐点
第一个for循环我们更新所有与拐点有边并且没有计算完成的点,只要当前边的权值加上d[x]比之前的d[i]小,那么就更新,同时将p[i]全部置为拐点
接着第二个for循环我们找除了已经计算完成的点之外的所有点的d[i]的最小值,将那个点作为下一个拐点,将其赋值给x
第一次循环:与拐点1有边并且没有计算完成的点有2、3、4,遍历到点2时,1->2的权值为2,2+d[1]=2<d[2]=INFINITY,所以更新 d[2]=2+d[1]=2,p[2]=x=1;同理3和4就更新为 d[3]=5,d[4]=4,p[3]=x=1,p[4]=x=1;上述我们已经将d[2]、d[3]、d[4]全部更新为2,5,4;很明显这里的最小值为d[2],所以我们将2作为下一个拐点,令x=2、f[x]=1
D | P | F | |
1 | 0 | 1 | 1 |
2 | 2 | 1 | 1 |
3 | 5 | 1 | 0 |
4 | 4 | 1 | 0 |
第二次循环:d[3]>d[2]+2,所以更新d[3]=4,p[3]=x=2;d[4]>d[2]+1,所以更新d[4]=3,p[4]=x=2;很明显这次4作为下一个拐点,将4置为计算完毕
D | P | F | |
1 | 0 | 1 | 1 |
2 | 2 | 1 | 1 |
3 | 4 | 2 | 0 |
4 | 3 | 2 | 1 |
第三次循环:d[3]<d[4]+3,所以不更新d[3]和p[3],这里只剩下3一个了,所以将3置为计算完毕,循环结束。
D | P | F | |
1 | 0 | 1 | 1 |
2 | 2 | 1 | 1 |
3 | 4 | 2 | 1 |
4 | 3 | 2 | 1 |
所以源点1到其他点的最短路径通过最后一个表格已经出来了
那么如何遍历最短路径经过了哪些点呢,代码如下
tempindex=4;//需要查看的最短路径的目标点,我们这里选4
while (D[tempindex] != 0) //只要D[tempindex]!=0就代表还没有到源点
{
cout << P[tempindex] << "->" << tempindex << endl;
tempindex = P[tempindex];
}
结果为
2->4
1->2
当然你也可以改进一下让它变得更好看
到此我们的基本的Dijkstra就结束了