北邮22信通一枚~
跟随课程进度每周更新数据结构与算法的代码和文章
持续关注作者 解锁更多邮苑信通专属代码~
获取更多文章 请访问专栏:
北邮22信通_青山如墨雨如画的博客-CSDN博客
一. 算法核心思想
Dijkstra算法是用来求取图中两个结点之间最短路径的算法。已知一个始发结点,如何求取到某节点的最短路径呢?
Dijkstra算法给出了这样的思想:加法原则。先取最少量事件,一次加入一个事件,直到所有事件被加入进去为止。对某一次加入的事件:如果事件加入后能够优化上一次的结果,那么更新结果为被优化的结果,否则保持原样不动。
具体过程如下(书上给出的过程+小编批注):
(1)找出从源点能够直接到达的顶点的所有路径,并从中选出一条最短路径;
(2)以这条已选出的最短路径作为转发路径/*批注:先取最少量事件*/,找出经过这条路径转发后到达其他顶点的路径,从中选出一条最短的路径;需要注意的是,如果这条路径转发后到达目的结点比直接从源点到达目的结点的结点路径长,就不用转发了;
/*批注:看加入的事件能否优化原先的结果,有优化则优化,不能优化保持原样;*/
/*又批注:这里需要有一个函数,能够找出这条最短路径。*/
/*又批注:这里需要一个辅助数组(path),数据类型为整型,长度为结点个数,用来记录源顶点到顶点i的路径。这个数组是最妙的,我们放在“测试”部分讲解。*/
(3)重复执行(2),直到找到所有顶点的路径位置。/*批注:需要一个辅助数组,数据类型为布尔型,长度为结点个数,用来记录某个顶点是否被添加*/
实际上步骤二是一个迭代算法,具体过程如下:
(1)记录所有直接与源点相连的顶点的路径,找出其中与源点最近的顶点vi,在辅助数组中将其标注为已经被用过;
(2)将vi作为转发结点,重新计算从v经过vi可以到达的顶点路径disk[ 1…n ],对于任意结点j,若经过vi转发后的路径disk[ j ]小于之前的路径长度,则更新disk[ j ],否则保留原来的值,更新完毕后,找出disk[ 1…n ]中路径最短的顶点,在辅助数组中将其标注为已经被用过;
(3)重复执行(2)每次都将上次新标记的顶点作为转发结点,重新计算disk[ 1…n ]的值,直到最后一个顶点被标记,算法结束。
二.几点说明
2.1存储结构
本篇文章采用邻接矩阵作为存储结构来实现算法。
#include<iostream>
#include<fstream>
#include<queue>
using namespace std;
class student
{
private:
int ID;
string name;
public:
student()
{
this->ID = 0;
this->name = "un_known name";
}
student(int ID, string name)
{
this->ID = ID;
this->name = name;
}
void print()
{
cout << "ID:" << this->ID << " name:" << this->name << endl;
}
friend fstream& operator>>(fstream& fin, student& s)
{
fin >> s.ID;
fin >> s.name;
return fin;
}
};
const int maxsize = 10;
template<class temp>
class m_graph
{
private:
temp vertex[maxsize];
int arc[maxsize][maxsize];
int vnum, arcnum;
public:
m_graph(ifstream& fin);
m_graph(temp a[], int arc[][maxsize], int vnum, int arcnum);
};
template<class temp>
m_graph<temp>::m_graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
m_graph<temp>::m_graph(temp a[], int arc[][maxsize], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
student stu[6] =
{
{1001,"zhang"},{2002,"wang"},{3003,"li"},
{4004,"zhao"},{5005,"liu"},{6006,"yao"}
};
int arc[maxsize][maxsize] =
{
{0,1,1,0,0,1},
{1,0,1,0,1,0},
{1,1,0,1,0,0},
{0,0,1,0,0,0},
{0,1,0,0,0,0},
{1,0,0,0,0,0},
};
int main()
{
system("color 0A");
m_graph<student>mm(stu, arc, 6, 12);
return 0;
}
2.2测试用图
本篇文章讲解的算法采用书上182页图5-34作为例图,用来测试结果。
2.3一个注意事项
使用邻接矩阵作为存储结构时要注意,某结点到自己的距离必须标注为MAX而不是0,不然算法对其修改的时候会出现逻辑错误,导致结果不对。
2.4设置辅助数据结构
三.3个核心函数讲解&&书上代码订正
3.1 short_path函数
这个函数是Dijkstra算法的核心部分,其他两个函数都是为了辅助或者修饰这个函数的。
函数传入两个参数,一个是类操作对象,一个是源点值。源点值是:用户使用Dijkstra算法时要计算到哪个结点的最短距离?这个源点值是用户自定的,因此传参时必不可少。
3.1.1初始化
首先我们应该对辅助数组s进行初始化。s数组每个元素都应更新为false。
*注意*:
有同学会问:既然s数组已经被设置为全局变量了,为什么在这里还要重新初始化一遍呢?
回答:这里s数组的初始化是很必要的。源点的选择是用户自定的,在现实测试中,用户不可能只使用一遍Dijkstra算法,因此不可能只调用一遍short_path函数,因此当调用次数大于等于2次时,如果s数组没有重新初始化,上一次的结果将会影响下一次的运算,所以这里s数组的初始化必不可少。
然后我们应该对disk数组和path数组初始化。根据用户传参的源点不同,disk数组传入 到该源点所有结点的权值,也就是二维数组arc中某一行(列)。path数组初始化的方法就是,除非和源点没有直接相连,否则全部初始化为源结点的下标。
初始化部分的代码:
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;//v是传参,源点值,也就是源点下标
disk[v] = 0;
3.1.2迭代更新
按下标遍历所有结点,对某次遍历:
寻找离源点最近的结点,将下标值赋值给传参v,将v标记为访问过,将v作为转发结点:
按下标遍历所有结点,如果遍历到某个结点vi,加入了转发结点之后,
源点到该结点的 路径权值和 比原先的disk[ i ]小,
那就更新disk[ i ]为这个最小值,同时将path数组该结点的位置的元素标记为v。
解释一下最后一行话的意思:将path[ i ]标记为v代表什么意思?
答:就是,加入了转发结点v之后,到源点到下标为i的结点路径更短了。因为有了v的出现,优化了结果,vi就想记录一下,从家到我这儿,经过v这个地方,比原先路程更短了呢。
迭代更新部分代码为:
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
{
print(graph.vnum);
return;
}
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
3.1.3一个错误
书上代码这样写:
template<class temp>
void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
return;
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
print(graph.vnum);
}
我们可以看到,第二个for循环的循环结束条件是v==-1,执行return ; 语句,但是如果这样,函数直接结束,无法执行最后一行的print函数了。
修改方法1:
template<class temp>
void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
{
print(graph.vnum);//放到这里来
return;
}
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
}
修改方法2:将return;改为break;
template<class temp>
void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
break;
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
print(graph.vnum);
}
3.1.3short_path函数整体代码
:
template<class temp>
void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
{
print(graph.vnum);
return;
}
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
}
3.2 find_min函数
此函数的功能是:在disk[ 1…n ]中寻找路径最小值,即离源点最近的结点。
比较简单,过程不赘述了,代码如下:
template<class temp>
int m_graph<temp>::find_min(m_graph<temp>graph)
{
int k = 0;
int min = MAX;
for (int i = 0; i < graph.vnum; i++)
{
if ((!s[i]) && (min > disk[i]))
{
min = disk[i];
k = i;
}
}
if (min == MAX)
return -1;//表示所有顶点都无法到达源点了
return k;
}
3.3打印路径
打印路径主要利用的是path这个辅助数组。path数组的核心思想是,下标和存储数据的妙用。每个下标都存储一个数据,对其中某一组数据+下标(第i组),其代表的真正含义如下:从源点到第i个结点,第一步要走过的结点的下标是path[ i ]。
这里也是一个迭代思想的体现:如果已知某条路径最短,那么这条路径的子路径也最短。打比喻说:已知始发站到终点站路程最短,那么到途经站的路程也是最短的。
因此,循环的终止条件是,最终查找到源点自己头上。过程是,从下标0开始,遍历path数组,对某一次遍历:
取出存储在这个地方的值,将这个值作为下标,找path[ 下标 ]的值,再按照前面的步骤迭代……直到查找到源点自己头上。
打印函数的代码如下:
template<class temp>
void m_graph<temp>::print(int n)
{
for (int i = 0; i < n; i++)
if (disk[i] != MAX)
{
cout << "V" << i << ":" << disk[i] << "\t{V" << i;
int pre = path[i];
while (pre != -1)
//如果某个存储的元素是-1,
//则代表这个元素的下标对应的结点是源点
{
cout << "V" << pre;
pre = path[pre];
}
cout << "}" << endl;
}
}
四.通过测试了解核心函数对各种数据的处理
以源点为v0为例,测试书上182页图5-34,对每次数据修改,都将重新输出一遍所有的辅助数据和辅助数组。我们来对比书上的分析图看运行结果:
4.1有向带权图:
4.2数字化为二维数组:
4.3 Dijkstra算法分析:
横坐标是操作第i次,纵坐标是终点。
4.4测试结果:
4.5 测试结果对比分析:
每次操作输出中:
操作第i次
path
下标0~5
每个下标里存储的元素
disk数组
遍历展现
min是:find_min函数每次操作寻找的最小权值
k是:find_min函数返回值,最小权值对应的终点下标,如果k=5然后k=0,说明最短路径找完了。
后面为数据优化更新部分。
执行第一次:
执行第二次:
执行第三次:
执行第四次:
执行第五次:
最终结果
4.6举例分析
通过一系列操作,我们最终确定了path数组中各个元素。现在我们以从v0到v5为例,利用path数组,追溯print算法,看看到底怎么用的。
最终确定的path数组如上图所示,
path[0]=-1,path[1]=-1,path[2]=0,path[3]=4,path[4]=0,path[5]=3。
取i=5;
cout<< V5 : 60 {V
pre=path[5]=3;
cout<<3;
pre=path[path[5]]=path[3]=4;
cout<<V4;
pre=pre[4]=0;
cout<<V0;
pre=pre[0]=-1;
cout<<};
结束。
五.总体代码
5.1含测试代码的总体代码
5.1.1代码效果图
5.1.2代码部分
#include<iostream>
#include<fstream>
using namespace std;
//邻接矩阵使用的辅助数组
const int maxsize = 10;//规定图结点最大值
bool visited[maxsize];//用于记录是否已经访问过
int MAX = 1e5;
//Dijkstra算法设置的辅助数据结构
bool s[maxsize];//记录顶点i是否被添加
int disk[maxsize];//记录顶点到顶点i的路径长度
int path[maxsize];//path[i]记录源顶点到顶点i的路径
//模板类的实例化
class student
{
private:
int ID;
string name;
public:
student()
{
this->ID = 0;
this->name = "un_known name";
}
student(int ID, string name)
{
this->ID = ID;
this->name = name;
}
void print()
{
cout << "ID:" << this->ID << " name:" << this->name << endl;
}
friend fstream& operator>>(fstream& fin, student& s)
{
fin >> s.ID;
fin >> s.name;
return fin;
}
friend ostream& operator<<(ostream& output, student& s)
{
cout << "ID:" << s.ID << " name:" << s.name;
}
};
template<class temp>
class m_graph
{
private:
temp vertex[maxsize];
int arc[maxsize][maxsize];
int vnum, arcnum;
public:
m_graph(ifstream& fin);
m_graph(temp a[], int arc[][maxsize], int vnum, int arcnum);
void short_path(m_graph<temp> graph, int v);
int find_min(m_graph<temp>graph);
void print(int n);
};
template<class temp>
m_graph<temp>::m_graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
m_graph<temp>::m_graph(temp a[],
int arc[][maxsize], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
template<class temp>
inline void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
//测试1
cout << "执行第"<<i+1<<"次" << endl;
cout << "path" << endl;
cout << "0 1 2 3 4 5" << endl;
for (int i = 0; i < graph.vnum; i++)
cout << path[i] << " ";
cout << endl;
cout << "disk" << endl;
for (int i = 0; i < graph.vnum; i++)
cout << disk[i] << " ";
cout << endl;
//
v = find_min(graph);
if (v == -1)
{
print(graph.vnum);
return;
}
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
//测试2
cout << "上次disk[" << j << "]=" << disk[j] << endl;
cout << "此时graph.arc[v][j] + disk[v]=" << graph.arc[v][j] + disk[v] << endl;
//
disk[j] = graph.arc[v][j] + disk[v];
//测试3
cout << "修改disk[" << j << "]为" << disk[j] << endl;
cout << "此时的v为" << v << endl;
cout << "修改path[" << j << "]为" << v << endl << endl;
//
path[j] = v;
}
}
}
template<class temp>
int m_graph<temp>::find_min(m_graph<temp>graph)
{
int k = 0;
int min = MAX;
for (int i = 0; i < graph.vnum; i++)
{
if ((!s[i]) && (min > disk[i]))
{
min = disk[i];
k = i;
}
}
//检测
cout << "min=" << min << endl;
cout << "k=" << k << endl;
//
if (min == MAX)
return -1;
return k;
}
template<class temp>
void m_graph<temp>::print(int n)
{
for (int i = 0; i < n; i++)
{
if(disk[i]!=MAX)
{
cout << "V" << i << ":" << disk[i] << "\t{V" << i;
int pre = path[i];
while (pre != -1)
{
cout << "V" << pre;
pre = path[pre];
}
cout << "}" << endl;
}
}
}
student stu[6] =
{
{1001,"zhang"},{2002,"wang"},{3003,"li"},
{4004,"zhao"},{5005,"liu"},{6006,"yao"}
};
int arc1[maxsize][maxsize] =
{
{0,1e5,10,1e5,30,100},//如果到自己不改的话那就一直是0,无法跳出循环
{1e5,0,5,1e5,1e5,1e5},
{1e5,1e5,0,50,1e5,1e5},
{1e5,1e5,1e5,0,1e5,10},
{1e5,1e5,1e5,20,0,60},
{1e5,1e5,1e5,1e5,1e5,0}
};
int arc2[maxsize][maxsize] =
{
{1e5,1e5,10,1e5,30,100},
{1e5,1e5,5,1e5,1e5,1e5},
{1e5,1e5,1e5,50,1e5,1e5},
{1e5,1e5,1e5,1e5,1e5,10},
{1e5,1e5,1e5,20,1e5,60},
{1e5,1e5,1e5,1e5,1e5,1e5}
};
int main()
{
system("color 0A");
m_graph<student>m5(stu, arc2, 6, 12);
m5.short_path(m5, 0);
return 0;
}
5.1.3代码运行结果
5.2不含测试代码的总体代码(书上代码)
5.2.1代码效果图
5.2.2代码部分
#include<iostream>
#include<fstream>
using namespace std;
//邻接矩阵使用的辅助数组
const int maxsize = 10;//规定图结点最大值
bool visited[maxsize];//用于记录是否已经访问过
int MAX = 1e5;
//Dijkstra算法设置的辅助数据结构
bool s[maxsize];//记录顶点i是否被添加
int disk[maxsize];//记录顶点到顶点i的路径长度
int path[maxsize];//path[i]记录源顶点到顶点i的路径
//模板类的实例化
class student
{
private:
int ID;
string name;
public:
student()
{
this->ID = 0;
this->name = "un_known name";
}
student(int ID, string name)
{
this->ID = ID;
this->name = name;
}
void print()
{
cout << "ID:" << this->ID << " name:" << this->name << endl;
}
friend fstream& operator>>(fstream& fin, student& s)
{
fin >> s.ID;
fin >> s.name;
return fin;
}
friend ostream& operator<<(ostream& output, student& s)
{
cout << "ID:" << s.ID << " name:" << s.name;
}
};
template<class temp>
class m_graph
{
private:
temp vertex[maxsize];
int arc[maxsize][maxsize];
int vnum, arcnum;
public:
m_graph(ifstream& fin);
m_graph(temp a[], int arc[][maxsize], int vnum, int arcnum);
void short_path(m_graph<temp> graph, int v);
int find_min(m_graph<temp>graph);
void print(int n);
};
template<class temp>
m_graph<temp>::m_graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
m_graph<temp>::m_graph(temp a[],
int arc[][maxsize], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
template<class temp>
inline void m_graph<temp>::short_path(m_graph<temp>graph, int v)
{
for (int i = 0; i < graph.vnum; i++)
{
s[i] = false;//必要性
disk[i] = graph.arc[v][i];
if (disk[i] != MAX)
path[i] = v;
else
path[i] = -1;
}
s[v] = 1;
disk[v] = 0;
for (int i = 0; i < graph.vnum; i++)
{
v = find_min(graph);
if (v == -1)
{
print(graph.vnum);
return;
}
s[v] = true;
for (int j = 0; j < graph.vnum; j++)
if ((!s[j]) && (disk[j] > graph.arc[v][j] + disk[v]))
{
disk[j] = graph.arc[v][j] + disk[v];
path[j] = v;
}
}
}
template<class temp>
int m_graph<temp>::find_min(m_graph<temp>graph)
{
int k = 0;
int min = MAX;
for (int i = 0; i < graph.vnum; i++)
{
if ((!s[i]) && (min > disk[i]))
{
min = disk[i];
k = i;
}
}
if (min == MAX)
return -1;
return k;
}
template<class temp>
void m_graph<temp>::print(int n)
{
for (int i = 0; i < n; i++)
if (disk[i] != MAX)
{
cout << "V" << i << ":" << disk[i] << "\t{V" << i;
int pre = path[i];
while (pre != -1)
{
cout << "V" << pre;
pre = path[pre];
}
cout << "}" << endl;
}
}
student stu[6] =
{
{1001,"zhang"},{2002,"wang"},{3003,"li"},
{4004,"zhao"},{5005,"liu"},{6006,"yao"}
};
int arc1[maxsize][maxsize] =
{
{0,1e5,10,1e5,30,100},//如果到自己不改的话那就一直是0,无法跳出循环
{1e5,0,5,1e5,1e5,1e5},
{1e5,1e5,0,50,1e5,1e5},
{1e5,1e5,1e5,0,1e5,10},
{1e5,1e5,1e5,20,0,60},
{1e5,1e5,1e5,1e5,1e5,0}
};
int arc2[maxsize][maxsize] =
{
{1e5,1e5,10,1e5,30,100},
{1e5,1e5,5,1e5,1e5,1e5},
{1e5,1e5,1e5,50,1e5,1e5},
{1e5,1e5,1e5,1e5,1e5,10},
{1e5,1e5,1e5,20,1e5,60},
{1e5,1e5,1e5,1e5,1e5,1e5}
};
int main()
{
system("color 0A");
m_graph<student>m5(stu, arc2, 6, 12);
m5.short_path(m5, 0);
return 0;
}