数据结构学不好,c++就到后面会很迷,数据结构真滴很重要啊,上机题一定要认真做,紧密的和实际操作的代码联系在一起是最好的。
在学图这一章时一定要与前面的知识紧密联合,做做比较。
今天就来总结一下图吧,开心~
图与线性表、树的区别
1.图更复杂哈哈哈废话?
2.在线性表中,数据元素之间仅有线性关系,每个数据元素只有一个前驱与一个后继;
在树形结构中呢,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关(孩子节点);
but!!!
在图形结构中,节点之间的关系可以是任意的,图中的任意两个元素之间都可能有关。
名词解释:
-
图(Graph)
在图中的数据元素,我们称之为顶点(Vertex)
1.按方向分,图可分为有向图,无向图。
无向图由顶点和边组成,有向图由顶点和弧构成。弧有弧尾和弧头之分,带箭头一端为弧头。
图上的边或弧带有权则称为网。
2.按边或弧的个数大小,图可分为稀疏图和稠密图。
如果图中的任意两个顶点之间都存在边叫做完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
边=1/2n(n-1)的无向图称为 完全图
边
-
度
顶点(v)的度(Degree)是和v相关联的边的数目(可分为入度,出度)
公式:总边数=1/2总度数
-
路径
简单路径:序列中顶点不重复出现的路径。
环(回路):第一个顶点和最后一个顶点有相同的路径。
若 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,可叫做简单回路(环)。
-
连通分量
指的是无向图中的极大连通子图。
极大连通子图的条件就是:1.是子图。2.尽可能大(emmm这不废话吗...)
强连通图:每一个顶点都有进有出。
强连通分量:有向图的极大强连通子图。
生成树:极小连通子图。
一颗有n个顶点的生成树有且仅有n-1条边
若小于n-1:非连通图
若大于n-1,则一定有回路(环)
图的存储结构
-
邻接矩阵
图的邻接矩阵的表示方式需要两个数组来表示图的信息,一个一维数组表示每个数据元素的信息,一个二维数组(邻接矩阵)表示图中的边或者弧的信息。
如果图有n个顶点,那么邻接矩阵就是一个n*n的方阵,若考虑无向图的邻接矩阵的对称性,则可采用压缩存储的方式只存入矩阵的上三角(or下三角)元素。
对于有权值的网,二维数组中的元素不再是0,1表示是否存在边,而是把元素值表示为权值。不存在的边,权值记录为∞;对角线上的权值为0.
- 结论:
- 无向图的邻接矩阵都是沿对角线对称的
- 要知道无向图中某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
- 对于有向图,要知道某个顶点的出度,其实就是这个顶点vi在邻接矩阵中第i行的元素之和,如果要知道某个顶点的入度,那就是第i列的元素之和。
*邻接矩阵代码的实现(虽长但有注释不难滴)
#include
using namespace std;
enum Graphkind{ DG, DN, UDG, UDN }; //{有向图,无向图,有向网,无向网}
typedef struct Node
{
int * vex; //顶点数组
int vexnum; //顶点个数
int edge; //图的边数
int ** adjMatrix; //图的邻接矩阵
enum Graphkind kind;
}MGraph;
void createGraph(MGraph & G,enum Graphkind kind)
{
cout << "输入顶点的个数" << endl;
cin >> G.vexnum;
cout << "输入边的个数" << endl;
cin >> G.edge;
//输入种类
//cout << "输入图的种类:DG:有向图 DN:无向图,UDG:有向网,UDN:无向网" << endl;
G.kind = kind;
//为两个数组开辟空间
G.vex = new int[G.vexnum];
G.adjMatrix = new int*[G.vexnum];
cout << G.vexnum << endl;
int i;
for (i = 0; i < G.vexnum; i++)
{
G.adjMatrix[i] = new int[G.vexnum];
}
for (i = 0; i < G.vexnum; i++)
{
for (int k = 0; k < G.vexnum; k++)
{
if (G.kind == DG || G.kind == DN)
{
G.adjMatrix[i][k] = 0;
}
else {
G.adjMatrix[i][k] = INT_MAX;
}
}
}
/*//输入每个元素的信息,这个信息,现在还不需要使用
for (i = 0; i < G.vexnum; i++)
{
cin >> G.vex[i];
}*/
cout << "请输入两个有关系的顶点的序号:例如:1 2 代表1号顶点指向2号顶点" << endl;
for (i = 0; i < G.edge; i++)
{
int a, b;
cin >> a;
cin >> b;
if (G.kind == DN) {
G.adjMatrix[b - 1][a - 1] = 1;
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == DG)
{
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == UDG)
{
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
else {
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[b - 1][a - 1] = weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
}
}
void print(MGraph g)
{
int i, j;
for (i = 0; i < g.vexnum; i++)
{
for (j = 0; j < g.vexnum; j++)
{
if (g.adjMatrix[i][j] == INT_MAX)
cout << "∞" << " ";
else
cout << g.adjMatrix[i][j] << " ";
}
cout << endl;
}
}
void clear(MGraph G)
{
delete G.vex;
G.vex = NULL;
for (int i = 0; i < G.vexnum; i++)
{
delete G.adjMatrix[i];
G.adjMatrix[i] = NULL;
}
delete G.adjMatrix;
}
int main()
{
MGraph G;
cout << "有向图例子:" << endl;
createGraph(G, DG);
print(G);
clear(G);
cout << endl;
cout << "无向图例子:" << endl;
createGraph(G, DN);
print(G);
clear(G);
cout << endl;
cout << "有向图网例子:" << endl;
createGraph(G, UDG);
print(G);
clear(G);
cout << endl;
cout << "无向图网例子:" << endl;
createGraph(G, UDN);
print(G);
clear(G);
cout << endl;
return 0;
邻接矩阵优缺点:
优点:
直观、容易理解,可以很容易的判断出任意两个顶点是否有边,最大的优点就是很容易计算出各个顶点的度。
缺点:
当我么表示完全图的时候,邻接矩阵是最好的表示方法,但是对于稀疏矩阵,由于它边少,但是顶点多,这样就会造成空间的浪费。
-
邻接表
邻接表是图的一种链式存储结构。主要是应对于邻接矩阵在顶点多边少的时候,浪费空间的问题。它的方法就是声明两个结构。
用c++来表示如下:
typedef char Vertextype;
//表结点结构
struct ArcNode {
int adjvex; //某条边指向的那个顶点的位置(一般是数组的下标)。
ArcNode * nextarc; //指向下一个表结点
int weight; //这个只有网图才需要使用。普通的图可以直接忽略
};
//头结点
struct Vnode
{
Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用)
ArcNode * firstarc; //指向第一条依附在该顶点边的信息(表结点)
};
想要整体的实现邻接表的代码,没有什么比直接看代码更清楚的了。
因为的今天的上机实验题就类似这个,不过是用邻接表递归地深度优先遍历无向网G,比下面的代码要难嘻嘻嘻,不过我们慢慢来~
平常写代码我从来不标注释,但是标注释真滴是一个非常好的习惯~复习起来很方便
#include
#include
using namespace std;
typedef string Vertextype;
//表结点结构
struct ArcNode {
int adjvex; //某条边指向的那个顶点的位置(一般是数组的下标)。
ArcNode * nextarc; //指向下一个表结点
int weight; //这个只有网图才需要使用。
};
//头结点
struct Vnode
{
Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用)
ArcNode * firstarc; //指向第一条依附在该顶点边的信息(表结点)
};
//
struct Graph
{
int kind; //图的种类(有向图:0,无向图:1,有向网:2,无向网:3)
int vexnum; //图的顶点数
int edge; //图的边数
Vnode * node; //图的(顶点)头结点数组
};
void createGraph(Graph & g,int kind)
{
cout << "请输入顶点的个数:" << endl;
cin >> g.vexnum;
cout << "请输入边的个数(无向图/网要乘2):" << endl;
cin >> g.edge;
g.kind = kind; //决定图的种类
g.node = new Vnode[g.vexnum];
int i;
cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息
for (i = 0; i < g.vexnum; i++)
{
cin >> g.node[i].data;
g.node[i].firstarc=NULL;
}
cout << "请输入每条边的起点和终点的编号:" << endl;
for (i = 0; i < g.edge; i++)
{
int a;
int b;
cin >> a; //起点
cin >> b; //终点
ArcNode * next=new ArcNode;
next->adjvex = b - 1;
if(kind==0 || kind==1)
next->weight = -1;
else {//如果是网图,还需要权重
cout << "输入该边的权重:" << endl;
cin >> next->weight;
}
next->nextarc = NULL;
//将边串联起来
if (g.node[a - 1].firstarc == NULL) {
g.node[a - 1].firstarc=next;
}
else
{
ArcNode * p;
p = g.node[a - 1].firstarc;
while (p->nextarc)//找到该链表的最后一个表结点
{
p = p->nextarc;
}
p->nextarc = next;
}
}
}
void print(Graph g)
{
int i;
cout << "图的邻接表为:" << endl;
for (i = 0; i < g.vexnum; i++)
{
cout << g.node[i].data<<" ";
ArcNode * now;
now = g.node[i].firstarc;
while (now)
{
cout << now->adjvex << " ";
now = now->nextarc;
}
cout << endl;
}
}
int main()
{
Graph g;
cout << "有向图的例子" << endl;
createGraph(g,0);
print(g);
cout << endl;
cout << "无向图的例子" << endl;
createGraph(g, 1);
print(g);
cout << endl;
return 0;
}
- 邻接表的优缺点:
优点:
对于,稀疏图,邻接表比邻接矩阵更节约空间。
缺点:
不容易判断两个顶点是有关系(边),顶点的出度容易,但是求入度需要遍历整个邻接表。
接下来迎来重难点了咳咳:
-
图的遍历
抛出概念 :
图的遍历:从图中某一个顶点出发遍历途中其余顶点,每一个顶点仅被访问一次。
基本思路:
(1)树有四种遍历方式,因为根节点只有一个。而图的复杂情况是的顺着一个点向下寻找,极有可能最后又找到自己,形成回路导致死循环。
(2)所以要设置一个数组voisited[n],n是图中顶点个数,初值为0,当该顶点被遍历后,修改数组元素的值为1
(3)基于此,形成了2中遍历方案:
深度优先遍历&广度优先遍历
-
深度优先遍历(DFS)
如下图所示,我们进行深度遍历,一个原则就是,每当我们发现有多个出度时,选择右手边的出度作为下一个遍历的顶点路径。
(1)从A出发,发现出度为B,F。选择右手边的B。A->B
(2)从B出发,出度为C,I,G,选择右手边的C
(3)从C出发,出度为I,D,选择右手边的D
(4)从D出发,出度为I,G,H,E,选择右手边的E
(5)从E出发,出度为H,F,选择右手边的F
(6)从F出发,出度为A,G,选择右手边的A,但发现A已经被遍历过,所以选择G
(7)从G出发,出度为B,D,H,B,D访问过了,选择H
(8)从H出发,出度为D,F,均被访问过了。但此时图中的节点并没有遍历完全,因此我们要按原路返回,去找没走过的路
(9)回退到G,发现所连接的BDFH均被访问;
(10)回退到F,没有通道;回退到E,没有通道,回退到D,发现一个点I,进行标记(若此时与D相邻的还有其他顶点,则在此时一起进行标记);然后继续回退到A,走完整个路。
- 在深度优先遍历图时,对图中的每个顶点至多调用一次DFS函数。
- 遍历图过程的实质是对每个顶点查找其邻接点的过程。
- 其耗费的时间取决于所采用的存储结构:
- 当是邻接矩阵的存储结构时,查找每个顶点的邻接点所需时间为O(n平方)
(n为顶点数)
- 当是邻接表的存储结构时,找邻接点所需的时间是O(e) (其中e是无向图中的边数or有向图中弧的数,由此,当邻接表作存储结构时,深度优先搜索遍历图的时间复杂度为O(n+e)
嘻嘻嘻如下是邻接矩阵深度遍历的代码:(可以放到vs上看看运行结果,感受一下)
int visited[MAXVEX] = {0};
void DFS(MGraphy g,int i){
visited[i] = 1;
printf("%c,\t",g.vexs[i]);
for (int j = 0; j < g.vnum; j++) {
if(g.arc[i][j]!=0 && g.arc[i][j]!=IUNFINITY && !visited[j]){
DFS(g,j);
}
}
}
void DFSTraverse(MGraphy g){
printf("deep first search begin.\n");
for (int i = 0; i < g.vnum; i++) {
if(!visited[i]){
DFS(g,i);
}
}
}
int main() {
MGraphy g ;
createGraphy(&g);
printf("graphy create success ! ! !\n");
DFSTraverse(g);
}
说完深度遍历的邻接矩阵 怎么能不写写邻接表的呢 对不对
给你代码 自行体会吧
邻接矩阵的深度遍历:
int visited[MAXVEX] = {0};
void DFS(Graph g, int i){
printf("%c",g.vset[i].name);
visited[i] = 1;
EdgeNode *edgeNode = g.vset[i].firstedgeNode;
while(edgeNode!=NULL){
if(!visited[edgeNode->index])
DFS(g,edgeNode->index);
edgeNode = edgeNode->next;
}
}
void DFStraverse(Graph g){
for (int i = 0; i < g.vNum; i++) { // 用于不同连通分量
if(!visited[i])
DFS(g,i);
}
}
int main() {
Graph g;
createGraphy(&g);
printf("create graphy success ! ! !\n");
DFStraverse(g);
}
-
广度优先遍历(BFS)
书上讲的比我好 我做大自然的搬运工:
广度优先遍历类似输的层次遍历
(1)先入队列一个元素
(2)弹出队列顶端的1个元素打印,并把它连接的顶点入队
(3)重复以上过程,直到队列为空
-
每个顶点至多进一次队列,广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两者不同之处仅仅在于对顶点访问的顺序不同。
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
广度优先遍历的邻接矩阵:
/**
* 邻接矩阵遍历图
* @param g
*/
void BFSTraverse(MGraphy g){
SeqQueue *queue;
initQueue(queue); // 顺序表实现的队列,先初始化
int visited[] = {0}; // 初始化每个结点对应为未访问
int a;
for(int i=0;i
广度优先遍历的邻接表:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct EdgeNode{
int adjvex; /* 邻接点域,该顶点对应的下标 */
EdgeType weight;
EdgeNode *next; /* 链,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode{ /* 顶点表结点 */
VertexType data; /* 节点名字 */
EdgeNode *firstedge; /* 边表头节点 */
}VertexNode;
typedef struct{
VertexNode adjList[MAXVEX]; /* 顶点表是一个结构体数组,数组中的元素是Vertex节点 */
int vnum,enumber; /* 图中当前顶点和边数 */
}GraphyAdjList;
/**
* 广度优先遍历邻接表
* @param g
*/
void BFSTraverse2(GraphyAdjList *g){
SeqQueue *queue;
initQueue(queue);
int a;
int visited[g->vnum] = {0};
for (int i = 0; i < g->vnum; ++i) {
if(visited[i] == 0){
visited[i] = 1;
printf("%c",g->adjList[i].data); // 打印定点
enQueue(queue,i);
while(queueLength(queue)!=0){
deQueue(queue,&a);
EdgeNode *p = g->adjList[i].firstedge; // 进入结点的邻接表
while (p!=NULL){
if(visited[p->adjvex] != 0){
visited[p->adjvex] == 1;
printf("%c\n",g->adjList[p->adjvex].data);
enQueue(queue,p->adjvex);
}
p = p->next;
}
}
}
}
}
-
最小生成树
-
最小生成树的概念
(1)一个带权值的图:网。所谓最小成本,就是用n-1条边把n个顶点连接起来,且连接起来的权值最小。
(2)我们把构造联通网的最小代价生成树称为最小生成树
(3)普里姆算法和克鲁斯卡尔算法
-
普里姆算法
如下图,普利姆的最小生成树过程为:用Vs存储已经遍历的点,用Es存储已经遍历的边
(1)选择D为起点,加入Vs,与D连接的边中,权值最小的边为5,连接的点为A,因此将A加入到Vs,路径DA加入到Es。
(2)此时Vs中存在D和A。与DA连接的边中,权值最小的为6,连接的点为F,因此F加入到Vs,边DF加入到Es。
(3)此时Vs中存在DAF,与DAF连接的边中最小权值为7,连接的点为B,因此B加入Vs,路径AB加入Es
(4)重复以上过程,知道Vs中加入了所有的点
(普里姆算法的代码 我老师说不考,不想看的可以忽略下面滴代码)
#include
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
/**
* 普里母最小生成树:邻接表表示,时间复杂度为O(n方)
* @param g
*/
void miniSpanTree_Prim(MGraphy *g){
int adjVetex[MAXVEX] = {0}; // 保存相关定点下标
int lowcost[MAXVEX]; // 保存相关顶点间的权值
lowcost[0] = 0;
for (int i = 1; i < g->vnum; ++i) // 循环除下标为0外的全部结点
lowcost[i] = g->arc[0][i]; // 初始化lowcost数组,每一个元素的值为0点和给点边的权值
for (int i = 1; i < g->vnum; ++i) { // 循环除下标为0外的全部结点
int min = IUNFINITY; // 初始化最小权值为无穷
int j=1,k=0;
while(jvnum){
if(lowcost[j] != 0 && lowcost[j]vnum; ++j) {
if(lowcost[j] != 0 && g->arc[k][j] < lowcost[j]){
lowcost[j] = g->arc[k][j]; // 将较小边的权值并入lowcast
adjVetex[j] = k;
}
}
}
}
-
克鲁斯卡尔算法
克鲁斯卡尔算法从边的集合中挑选权值最小的,加入到选择的边集合中。如果这条边,予以选择的边构成了回路,则舍弃这条边。
如下图所示,克鲁斯卡尔的方法为:
(1)选择权值最小为7的边V7-V4
(2)选择权值最小为8的边V2-V8
(3)选择权值最小为10的边V1-V0
(4)选择权值最小为11的边V0-V5
(5)选择全职最小为12的边V1-V8,但是发现V1和V8全部是已经访问的点,所以会构成回路,舍弃
(6)选择权值最小为16的边V1-V6
(7)选择权值最小为16的边V3-V7
(8)。。。。
/* 科鲁斯卡尔最小生成树的边的结构体 */
typedef struct{
int begin;
int end;
int weight;
}Edge;
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
/**
* 查找连线顶点的尾部下标
*/
int find(int *parement,int f){
while (parement[f] > 0)
f= parement[f];
return f;
}
void miniSpan_Kruskal(MGraphy *g){
Edge edges[g->edgenum]; // 定义边集数组
int parement[g->vnum] = {0}; // 定义一个数组,用来判断是否形成回路
/**
* 此处省略将邻接矩阵g转化为边集数组edges,并按照权值由大到小排序的过程
*/
for(int i=0;iedgenum;i++){
int n = find(parement,edges[i].begin);
int m = find(parement,edges[i].end);
if(n!=m){ // n != m说明没有形成环路
parement[n] = m; // 将此边的为节点放入到下标为起点的parement数组中
printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight);
}
}
}
-
对两个算法的总结:
1.普里姆算法的时间复杂度为O(n平方),与网中的边数无关,因此适用于求边稠密的网的最小生成树。
2.克鲁斯卡尔算法恰恰相反,时间复杂度为O(e loge) (其中e为网中边的数目),因此更适合求边稀疏的网的最小生成树。
-
拓扑排序
有向无环图,称为DAG图。
一. 拓扑排序的概念
拓扑排序是对AOV网输出的一个序列
AOV网(Active on Vertex Network):在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系。这样的图称为活动的网。
二. 拓扑排序的算法
步骤:
从AOV网中选择一个入度为0(没有前驱)的顶点然后删除此顶点,并删除以此顶点为尾的弧。重复此步骤,直到输出全部顶点或AOV网中不存在入度为0的顶点为止。
若存在有前驱的顶点,则说明有向图中存在环。
拓扑排序中顶点的数据结构:
(1)前面求最小生成树和最短路径时,都是使用邻接矩阵,但由于拓扑排序中,需要删除顶点,所以使用邻接表方便。
(2)因为拓扑排序中,需要删除入度为0的顶点,所以在原先的顶点数据结构中,加入入度域in。使顶点接都变为
算法的代码不考,考如何进行拓扑排序的选择题。
但是看看代码也没啥坏处嘻嘻嘻
#include
#define MAXVEX 100
typedef struct EdgeNode{ /* 边表 */
int adjvex; /* 顶点下标 */
int weight; /* 权值 */
struct EdgeNode *next; /* 边表中的下一节点 */
}EdgeNode;
typedef struct VertexNode{ /* 定点表 */
int in;
int data;
EdgeNode *firstEdge;
}VertexNode,AdjList[MAXVEX];
typedef struct{
AdjList adjList;
int numVertexes,numEdges;
}graphAdjList,* GraphAdjList;
/**
* 拓扑排序
* @param gl :链表
* @return :若GL无回路,则输出排序序列并返回1;若有回路则返回-1
*/
int topologicalSort(GraphAdjList gl){
int *stack = (int *)malloc(gl->numVertexes * sizeof(int)); // stack用于存储入度为0的节点
int top = 0; // stack栈顶指针
int count; // 加入到栈中节点个数
for (int i = 0; i < gl->numVertexes; ++i)
if(gl->adjList[i].in == 0)
stack[++top] = i;
while(top!=0){
int gettop = stack[top--];
printf("%d -> ",gl->adjList[gettop].data);
count ++;
for(EdgeNode *e=gl->adjList[gettop].firstEdge; e ; e=e->next){
int k = e->adjvex; // 顶点的下标
if( ! (-- gl->adjList[k].in)) // 将k号顶点入度减1
stack[++top] = k; // 如果发现入度为0,则把该顶点加入到栈中
}
}
int res = (count < gl->numVertexes) ? -1 : 1; // 如果最后遍历到的个数小于图的总定点数,则说明有环存在,返回-1
return res;
}
-
关键路径
一. 概念
拓扑排序是解决一个工程能否顺序进行的问题,
当需要计算一个工程完成的最短时间,就需要用关键路径。
拓扑排序使用的是AOV网(定点表示活动)。关键路径使用AOE网(边表示活动)。AOV网只能表示活动之间的制约关系,而AOE网可以用变得权值表示活动的持续时间。所以AOE网是建立在活动之间制约关系没有矛盾的基础上,再来分析整个工程需要多少时间。
路径长度:路径上各个活动持续的时间之和
关键路径:从源点到汇点具有的最大长度的路径
关键活动:关键路径上的活动
二. 关键路径算法
关键路径算法中需要的变量
(1)事件最早开始时间etv(earlist time of vertex):顶点vk
的最早发生时间
(2)事件最晚开始时间ltv(latest time of vertex) :顶点vk的最晚发生时间,超过此时间,会延误整个工期
(3)活动最早开始时间(earlist time of edge):弧ak的最早发生时间
(4)活动最晚开始时间(latest time of edge) :弧a
的最晚发生时间,就是不推迟工期的最晚开始时间
int *etv,*ltv; /* 事件最早,最晚发生时间 */
int *stack2; /* 用于存储拓扑排序的栈 */
int top2 = 0; /* stack2的栈顶指针 */
int topologicalSort2(GraphAdjList gl){
int *stack = (int *)malloc(gl->numVertexes * sizeof(int)); /* 建栈将入度为0的顶点入栈 */
int top = 0;
for (int i = 0; i < gl->numVertexes; ++i) {
if(0 == gl->adjList[i].in)
stack[++top] = i;
}
etv = (int *)malloc(gl->numVertexes * sizeof(int)); /* 时间最早开时间数组 */
for (int i = 0; i < gl->numVertexes; ++i) /* 初始化最早开始时间数组全0 */
etv[i] = 0;
int count = 0;
stack2 = (int *)malloc(gl->numVertexes * sizeof(int));
while (top !=0 ){
int gettop = stack[top--];
count ++;
stack2[++top2] = gettop; /* 将弹出的顶点序号压入拓扑排序的栈 */
for (EdgeNode *e = gl->adjList[gettop].firstEdge; e ; e = e->next) {
int k = e->adjvex;
if( !(-- gl->adjList[k].in) )
stack[++top] = k;
if( (etv[gettop] + e->weight) > etv[k] ) /* 求各点事件最早发生时间值 */
etv[k] = etv[gettop] + e->weight;
}
}
if(count < gl->numVertexes)
return -1;
else
return 1;
}
void criticalPath(GraphAdjList gl){
topologicalSort2(gl);
ltv = (int *) malloc (gl->numVertexes * sizeof(int)); /* 事件最晚发生时间 */
for (int i = 0; i < gl->numVertexes; ++i)
ltv[i] = etv[gl->numVertexes -1]; /* 初始化ltv */
int k;
while(top2 != 0){
int gettop = stack2[top2--];
for(EdgeNode *e=gl->adjList[gettop].firstEdge ; e ; e=e->next){
k = e->adjvex;
if(ltv[k] - e->weight < ltv[gettop])
ltv[gettop] = ltv[k] - e->weight;
}
}
for (int j = 0; j < gl->numVertexes; ++j) {
for (EdgeNode *e = gl->adjList[j].firstEdge; e ; e = e->next) {
k = e->adjvex;
int ete = etv[j]; /* 活动最早发生时间 */
int lte = ltv[k] - e->weight; /* 活动最迟发生时间 */
if(ete == lte)
printf(" length: %d",gl->adjList[j].data,gl->adjList[k].data,e->weight);
}
}
}
-
最短路径
一. 迪杰斯特拉
迪杰斯特拉算法
(1)迪杰斯特拉,计算的是一个点到其余所有点的最短路径。
(2)它的基本思想:
如果点 i 到点 j 的最短路径经过k,则ij路径中,i到k的那一段一定是i到k的最短路径。
查找方法:
(1)声明2个一维数组:一个用来标识当前顶点是否已经找到最短路径。另一个数组用来记录v0到该点的最短路径中,该点的前一个顶点是什么。
(2)比较:计算v0
到vi的最短路径时,比较v0vi与v0vk+vkvi的大小,而v0vk与vkvi的值是暂时得出的记录在数组中的最短路径。
算法实现:基于邻接矩阵
#include "graphy/graphy.c" // 邻接矩阵
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatrix[MAXVEX]; //存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX]; //存储到各点最短路径的权值和
/**
* 迪杰斯特拉:求有向图G的V[0]到其余各点的最短路径及带权长度
* @return
*/
void shortestPath_Dijkstra(MGraphy *g,int v0,Pathmatrix *p,ShortPathTable *sptable){
int final[MAXVEX] = {0};
*p = {0}; // 初始化最短路径数组为0
for (int i = 0; i < g->vnum; ++i)
(*sptable)[i] = g->arc[v0][i]; //初始化sptable:让最短路径为图中v0和其余各顶点的权值
(*sptable)[v0] = 0; // sptable记录v0到v0的权值为0
final[v0] = 1; // final数组,记录以求得v0到v0的最短路径
/* 每次循环求得v0到顶点v的最短路径 */
for (int i = 0; i< g->vnum ; ++i) {
int min = INFINITY;
int k;
for (int j = 0; j < g->vnum; ++j) { // 循环每个顶点
if(! final[j] && (*sptable)[j] < min){
k = j; // 这个k只是把j的作用域扩大出去,供后面计算a
min = (*sptable)[j]; // 让min=当前被遍历顶点与v0点的边的权值
}
final[k] = 1;
for (int w = 0; w < g->vnum ; ++w) {
int a = min+g->arc[k][w]; // 上面让k=j,所以a=(*sptable)[j] + g->arc[j][w]。也就是:比如计算a0到a2,就比较a0a1+a1a2 与邻接矩阵中的a0a2边的权值
if(! final[w] && a < (*sptable)[w]) {
(*sptable)[w] = a;
(*p)[w] = k; // 这个k就是:假设该等式角标与程序无关,计算 a[i][j] > a[i][k]+a[k][j],记录i到j的最短路径中,j前面的节点
}
}
}
}
}
二. 弗洛伊德算法
弗洛伊德与迪杰斯特拉的区别
(1)它们都是基于比较v0vi与v0vk+vkvi的大小的基本算法。
(2)弗洛伊德三次循环计算出了每个点个其他点的最短路径,迪杰斯特拉算法用2次循环计算出了一个点到其他各点的最短路径 。
(3)如果要计算出全部的点到其他点的最短路径,他们都是O(n2)
typedef int Pathmatrix_Floyd[MAXVEX][MAXVEX]; //存储最短路径下标的数组
typedef int ShortPathTable_Floyd[MAXVEX][MAXVEX]; //存储到各点最短路径的权值和
void shortPath_Floyd(MGraphy *g,Pathmatrix_Floyd *p,ShortPathTable_Floyd *D){
for (int i = 0; i < ; ++i) {
for (int j = 0; j < g->vnum; ++j) {
(*D)[i][j] = g -> arc[i][j];
(*p)[i][j] = j;
}
}
for (int i = 0; i < g->vnum; ++i) {
for (int j = 0; j < g->vnum; ++j) {
for (int k = 0; k < g->vnum; ++k) {
if((*D)[j][k] > (*D)[j][i]+(*D)[i][k]){
(*D)[j][k] = (*D)[j][i]+(*D)[i][k];
(*p)[j][k] = (*p)[j][i];
}
}
}
}
}
-
度
顶点(v)的度(Degree)是和v相关联的边的数目(可分为入度,出度)
公式:总边数=1/2总度数
-
路径
简单路径:序列中顶点不重复出现的路径。
环(回路):第一个顶点和最后一个顶点有相同的路径。
若 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,可叫做简单回路(环)。
-
连通分量
指的是无向图中的极大连通子图。
极大连通子图的条件就是:1.是子图。2.尽可能大(emmm这不废话吗...)
强连通图:每一个顶点都有进有出。
强连通分量:有向图的极大强连通子图。
生成树:极小连通子图。
一颗有n个顶点的生成树有且仅有n-1条边
若小于n-1:非连通图
若大于n-1,则一定有回路(环)
图的存储结构
-
邻接矩阵
图的邻接矩阵的表示方式需要两个数组来表示图的信息,一个一维数组表示每个数据元素的信息,一个二维数组(邻接矩阵)表示图中的边或者弧的信息。
如果图有n个顶点,那么邻接矩阵就是一个n*n的方阵,若考虑无向图的邻接矩阵的对称性,则可采用压缩存储的方式只存入矩阵的上三角(or下三角)元素。
对于有权值的网,二维数组中的元素不再是0,1表示是否存在边,而是把元素值表示为权值。不存在的边,权值记录为∞;对角线上的权值为0.
- 结论:
- 无向图的邻接矩阵都是沿对角线对称的
- 要知道无向图中某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
- 对于有向图,要知道某个顶点的出度,其实就是这个顶点vi在邻接矩阵中第i行的元素之和,如果要知道某个顶点的入度,那就是第i列的元素之和。
*邻接矩阵代码的实现(虽长但有注释不难滴)
#include
using namespace std;
enum Graphkind{ DG, DN, UDG, UDN }; //{有向图,无向图,有向网,无向网}
typedef struct Node
{
int * vex; //顶点数组
int vexnum; //顶点个数
int edge; //图的边数
int ** adjMatrix; //图的邻接矩阵
enum Graphkind kind;
}MGraph;
void createGraph(MGraph & G,enum Graphkind kind)
{
cout << "输入顶点的个数" << endl;
cin >> G.vexnum;
cout << "输入边的个数" << endl;
cin >> G.edge;
//输入种类
//cout << "输入图的种类:DG:有向图 DN:无向图,UDG:有向网,UDN:无向网" << endl;
G.kind = kind;
//为两个数组开辟空间
G.vex = new int[G.vexnum];
G.adjMatrix = new int*[G.vexnum];
cout << G.vexnum << endl;
int i;
for (i = 0; i < G.vexnum; i++)
{
G.adjMatrix[i] = new int[G.vexnum];
}
for (i = 0; i < G.vexnum; i++)
{
for (int k = 0; k < G.vexnum; k++)
{
if (G.kind == DG || G.kind == DN)
{
G.adjMatrix[i][k] = 0;
}
else {
G.adjMatrix[i][k] = INT_MAX;
}
}
}
/*//输入每个元素的信息,这个信息,现在还不需要使用
for (i = 0; i < G.vexnum; i++)
{
cin >> G.vex[i];
}*/
cout << "请输入两个有关系的顶点的序号:例如:1 2 代表1号顶点指向2号顶点" << endl;
for (i = 0; i < G.edge; i++)
{
int a, b;
cin >> a;
cin >> b;
if (G.kind == DN) {
G.adjMatrix[b - 1][a - 1] = 1;
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == DG)
{
G.adjMatrix[a - 1][b - 1] = 1;
}
else if (G.kind == UDG)
{
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
else {
int weight;
cout << "输入该边的权重:" << endl;
cin >> weight;
G.adjMatrix[b - 1][a - 1] = weight;
G.adjMatrix[a - 1][b - 1] = weight;
}
}
}
void print(MGraph g)
{
int i, j;
for (i = 0; i < g.vexnum; i++)
{
for (j = 0; j < g.vexnum; j++)
{
if (g.adjMatrix[i][j] == INT_MAX)
cout << "∞" << " ";
else
cout << g.adjMatrix[i][j] << " ";
}
cout << endl;
}
}
void clear(MGraph G)
{
delete G.vex;
G.vex = NULL;
for (int i = 0; i < G.vexnum; i++)
{
delete G.adjMatrix[i];
G.adjMatrix[i] = NULL;
}
delete G.adjMatrix;
}
int main()
{
MGraph G;
cout << "有向图例子:" << endl;
createGraph(G, DG);
print(G);
clear(G);
cout << endl;
cout << "无向图例子:" << endl;
createGraph(G, DN);
print(G);
clear(G);
cout << endl;
cout << "有向图网例子:" << endl;
createGraph(G, UDG);
print(G);
clear(G);
cout << endl;
cout << "无向图网例子:" << endl;
createGraph(G, UDN);
print(G);
clear(G);
cout << endl;
return 0;
邻接矩阵优缺点:
优点:
直观、容易理解,可以很容易的判断出任意两个顶点是否有边,最大的优点就是很容易计算出各个顶点的度。
缺点:
当我么表示完全图的时候,邻接矩阵是最好的表示方法,但是对于稀疏矩阵,由于它边少,但是顶点多,这样就会造成空间的浪费。-
邻接表
邻接表是图的一种链式存储结构。主要是应对于邻接矩阵在顶点多边少的时候,浪费空间的问题。它的方法就是声明两个结构。
用c++来表示如下:
typedef char Vertextype;
//表结点结构
struct ArcNode {
int adjvex; //某条边指向的那个顶点的位置(一般是数组的下标)。
ArcNode * nextarc; //指向下一个表结点
int weight; //这个只有网图才需要使用。普通的图可以直接忽略
};
//头结点
struct Vnode
{
Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用)
ArcNode * firstarc; //指向第一条依附在该顶点边的信息(表结点)
};
想要整体的实现邻接表的代码,没有什么比直接看代码更清楚的了。
因为的今天的上机实验题就类似这个,不过是用邻接表递归地深度优先遍历无向网G,比下面的代码要难嘻嘻嘻,不过我们慢慢来~
平常写代码我从来不标注释,但是标注释真滴是一个非常好的习惯~复习起来很方便
#include
#include
using namespace std;
typedef string Vertextype;
//表结点结构
struct ArcNode {
int adjvex; //某条边指向的那个顶点的位置(一般是数组的下标)。
ArcNode * nextarc; //指向下一个表结点
int weight; //这个只有网图才需要使用。
};
//头结点
struct Vnode
{
Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用)
ArcNode * firstarc; //指向第一条依附在该顶点边的信息(表结点)
};
//
struct Graph
{
int kind; //图的种类(有向图:0,无向图:1,有向网:2,无向网:3)
int vexnum; //图的顶点数
int edge; //图的边数
Vnode * node; //图的(顶点)头结点数组
};
void createGraph(Graph & g,int kind)
{
cout << "请输入顶点的个数:" << endl;
cin >> g.vexnum;
cout << "请输入边的个数(无向图/网要乘2):" << endl;
cin >> g.edge;
g.kind = kind; //决定图的种类
g.node = new Vnode[g.vexnum];
int i;
cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息
for (i = 0; i < g.vexnum; i++)
{
cin >> g.node[i].data;
g.node[i].firstarc=NULL;
}
cout << "请输入每条边的起点和终点的编号:" << endl;
for (i = 0; i < g.edge; i++)
{
int a;
int b;
cin >> a; //起点
cin >> b; //终点
ArcNode * next=new ArcNode;
next->adjvex = b - 1;
if(kind==0 || kind==1)
next->weight = -1;
else {//如果是网图,还需要权重
cout << "输入该边的权重:" << endl;
cin >> next->weight;
}
next->nextarc = NULL;
//将边串联起来
if (g.node[a - 1].firstarc == NULL) {
g.node[a - 1].firstarc=next;
}
else
{
ArcNode * p;
p = g.node[a - 1].firstarc;
while (p->nextarc)//找到该链表的最后一个表结点
{
p = p->nextarc;
}
p->nextarc = next;
}
}
}
void print(Graph g)
{
int i;
cout << "图的邻接表为:" << endl;
for (i = 0; i < g.vexnum; i++)
{
cout << g.node[i].data<<" ";
ArcNode * now;
now = g.node[i].firstarc;
while (now)
{
cout << now->adjvex << " ";
now = now->nextarc;
}
cout << endl;
}
}
int main()
{
Graph g;
cout << "有向图的例子" << endl;
createGraph(g,0);
print(g);
cout << endl;
cout << "无向图的例子" << endl;
createGraph(g, 1);
print(g);
cout << endl;
return 0;
}
- 邻接表的优缺点:
优点:
对于,稀疏图,邻接表比邻接矩阵更节约空间。
缺点:
不容易判断两个顶点是有关系(边),顶点的出度容易,但是求入度需要遍历整个邻接表。
接下来迎来重难点了咳咳:
-
图的遍历
抛出概念 :
图的遍历:从图中某一个顶点出发遍历途中其余顶点,每一个顶点仅被访问一次。
基本思路:
(1)树有四种遍历方式,因为根节点只有一个。而图的复杂情况是的顺着一个点向下寻找,极有可能最后又找到自己,形成回路导致死循环。
(2)所以要设置一个数组voisited[n],n是图中顶点个数,初值为0,当该顶点被遍历后,修改数组元素的值为1
(3)基于此,形成了2中遍历方案:
深度优先遍历&广度优先遍历
-
深度优先遍历(DFS)
如下图所示,我们进行深度遍历,一个原则就是,每当我们发现有多个出度时,选择右手边的出度作为下一个遍历的顶点路径。
(1)从A出发,发现出度为B,F。选择右手边的B。A->B
(2)从B出发,出度为C,I,G,选择右手边的C
(3)从C出发,出度为I,D,选择右手边的D
(4)从D出发,出度为I,G,H,E,选择右手边的E
(5)从E出发,出度为H,F,选择右手边的F
(6)从F出发,出度为A,G,选择右手边的A,但发现A已经被遍历过,所以选择G
(7)从G出发,出度为B,D,H,B,D访问过了,选择H
(8)从H出发,出度为D,F,均被访问过了。但此时图中的节点并没有遍历完全,因此我们要按原路返回,去找没走过的路
(9)回退到G,发现所连接的BDFH均被访问;
(10)回退到F,没有通道;回退到E,没有通道,回退到D,发现一个点I,进行标记(若此时与D相邻的还有其他顶点,则在此时一起进行标记);然后继续回退到A,走完整个路。
- 在深度优先遍历图时,对图中的每个顶点至多调用一次DFS函数。
- 遍历图过程的实质是对每个顶点查找其邻接点的过程。
- 其耗费的时间取决于所采用的存储结构:
- 当是邻接矩阵的存储结构时,查找每个顶点的邻接点所需时间为O(n平方)
(n为顶点数)- 当是邻接表的存储结构时,找邻接点所需的时间是O(e) (其中e是无向图中的边数or有向图中弧的数,由此,当邻接表作存储结构时,深度优先搜索遍历图的时间复杂度为O(n+e)
嘻嘻嘻如下是邻接矩阵深度遍历的代码:(可以放到vs上看看运行结果,感受一下)
int visited[MAXVEX] = {0};
void DFS(MGraphy g,int i){
visited[i] = 1;
printf("%c,\t",g.vexs[i]);
for (int j = 0; j < g.vnum; j++) {
if(g.arc[i][j]!=0 && g.arc[i][j]!=IUNFINITY && !visited[j]){
DFS(g,j);
}
}
}
void DFSTraverse(MGraphy g){
printf("deep first search begin.\n");
for (int i = 0; i < g.vnum; i++) {
if(!visited[i]){
DFS(g,i);
}
}
}
int main() {
MGraphy g ;
createGraphy(&g);
printf("graphy create success ! ! !\n");
DFSTraverse(g);
}
说完深度遍历的邻接矩阵 怎么能不写写邻接表的呢 对不对
给你代码 自行体会吧
邻接矩阵的深度遍历:
int visited[MAXVEX] = {0};
void DFS(Graph g, int i){
printf("%c",g.vset[i].name);
visited[i] = 1;
EdgeNode *edgeNode = g.vset[i].firstedgeNode;
while(edgeNode!=NULL){
if(!visited[edgeNode->index])
DFS(g,edgeNode->index);
edgeNode = edgeNode->next;
}
}
void DFStraverse(Graph g){
for (int i = 0; i < g.vNum; i++) { // 用于不同连通分量
if(!visited[i])
DFS(g,i);
}
}
int main() {
Graph g;
createGraphy(&g);
printf("create graphy success ! ! !\n");
DFStraverse(g);
}
-
广度优先遍历(BFS)
书上讲的比我好 我做大自然的搬运工:
广度优先遍历类似输的层次遍历
(1)先入队列一个元素
(2)弹出队列顶端的1个元素打印,并把它连接的顶点入队
(3)重复以上过程,直到队列为空
每个顶点至多进一次队列,广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两者不同之处仅仅在于对顶点访问的顺序不同。
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
广度优先遍历的邻接矩阵:
/**
* 邻接矩阵遍历图
* @param g
*/
void BFSTraverse(MGraphy g){
SeqQueue *queue;
initQueue(queue); // 顺序表实现的队列,先初始化
int visited[] = {0}; // 初始化每个结点对应为未访问
int a;
for(int i=0;i
广度优先遍历的邻接表:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct EdgeNode{
int adjvex; /* 邻接点域,该顶点对应的下标 */
EdgeType weight;
EdgeNode *next; /* 链,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode{ /* 顶点表结点 */
VertexType data; /* 节点名字 */
EdgeNode *firstedge; /* 边表头节点 */
}VertexNode;
typedef struct{
VertexNode adjList[MAXVEX]; /* 顶点表是一个结构体数组,数组中的元素是Vertex节点 */
int vnum,enumber; /* 图中当前顶点和边数 */
}GraphyAdjList;
/**
* 广度优先遍历邻接表
* @param g
*/
void BFSTraverse2(GraphyAdjList *g){
SeqQueue *queue;
initQueue(queue);
int a;
int visited[g->vnum] = {0};
for (int i = 0; i < g->vnum; ++i) {
if(visited[i] == 0){
visited[i] = 1;
printf("%c",g->adjList[i].data); // 打印定点
enQueue(queue,i);
while(queueLength(queue)!=0){
deQueue(queue,&a);
EdgeNode *p = g->adjList[i].firstedge; // 进入结点的邻接表
while (p!=NULL){
if(visited[p->adjvex] != 0){
visited[p->adjvex] == 1;
printf("%c\n",g->adjList[p->adjvex].data);
enQueue(queue,p->adjvex);
}
p = p->next;
}
}
}
}
}
-
最小生成树
-
最小生成树的概念
(1)一个带权值的图:网。所谓最小成本,就是用n-1条边把n个顶点连接起来,且连接起来的权值最小。
(2)我们把构造联通网的最小代价生成树称为最小生成树
(3)普里姆算法和克鲁斯卡尔算法
-
普里姆算法
如下图,普利姆的最小生成树过程为:用Vs存储已经遍历的点,用Es存储已经遍历的边
(1)选择D为起点,加入Vs,与D连接的边中,权值最小的边为5,连接的点为A,因此将A加入到Vs,路径DA加入到Es。
(2)此时Vs中存在D和A。与DA连接的边中,权值最小的为6,连接的点为F,因此F加入到Vs,边DF加入到Es。
(3)此时Vs中存在DAF,与DAF连接的边中最小权值为7,连接的点为B,因此B加入Vs,路径AB加入Es
(4)重复以上过程,知道Vs中加入了所有的点
(普里姆算法的代码 我老师说不考,不想看的可以忽略下面滴代码)
#include
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
/**
* 普里母最小生成树:邻接表表示,时间复杂度为O(n方)
* @param g
*/
void miniSpanTree_Prim(MGraphy *g){
int adjVetex[MAXVEX] = {0}; // 保存相关定点下标
int lowcost[MAXVEX]; // 保存相关顶点间的权值
lowcost[0] = 0;
for (int i = 1; i < g->vnum; ++i) // 循环除下标为0外的全部结点
lowcost[i] = g->arc[0][i]; // 初始化lowcost数组,每一个元素的值为0点和给点边的权值
for (int i = 1; i < g->vnum; ++i) { // 循环除下标为0外的全部结点
int min = IUNFINITY; // 初始化最小权值为无穷
int j=1,k=0;
while(jvnum){
if(lowcost[j] != 0 && lowcost[j]vnum; ++j) {
if(lowcost[j] != 0 && g->arc[k][j] < lowcost[j]){
lowcost[j] = g->arc[k][j]; // 将较小边的权值并入lowcast
adjVetex[j] = k;
}
}
}
}
-
克鲁斯卡尔算法
克鲁斯卡尔算法从边的集合中挑选权值最小的,加入到选择的边集合中。如果这条边,予以选择的边构成了回路,则舍弃这条边。
如下图所示,克鲁斯卡尔的方法为:
(1)选择权值最小为7的边V7-V4
(2)选择权值最小为8的边V2-V8
(3)选择权值最小为10的边V1-V0
(4)选择权值最小为11的边V0-V5
(5)选择全职最小为12的边V1-V8,但是发现V1和V8全部是已经访问的点,所以会构成回路,舍弃
(6)选择权值最小为16的边V1-V6
(7)选择权值最小为16的边V3-V7
(8)。。。。
/* 科鲁斯卡尔最小生成树的边的结构体 */
typedef struct{
int begin;
int end;
int weight;
}Edge;
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define IUNFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /* 顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵 */
int vnum,edgenum; /*定点的个数和边的个数*/
}MGraphy;
/**
* 查找连线顶点的尾部下标
*/
int find(int *parement,int f){
while (parement[f] > 0)
f= parement[f];
return f;
}
void miniSpan_Kruskal(MGraphy *g){
Edge edges[g->edgenum]; // 定义边集数组
int parement[g->vnum] = {0}; // 定义一个数组,用来判断是否形成回路
/**
* 此处省略将邻接矩阵g转化为边集数组edges,并按照权值由大到小排序的过程
*/
for(int i=0;iedgenum;i++){
int n = find(parement,edges[i].begin);
int m = find(parement,edges[i].end);
if(n!=m){ // n != m说明没有形成环路
parement[n] = m; // 将此边的为节点放入到下标为起点的parement数组中
printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight);
}
}
}
-
对两个算法的总结:
1.普里姆算法的时间复杂度为O(n平方),与网中的边数无关,因此适用于求边稠密的网的最小生成树。
2.克鲁斯卡尔算法恰恰相反,时间复杂度为O(e loge) (其中e为网中边的数目),因此更适合求边稀疏的网的最小生成树。
-
拓扑排序
有向无环图,称为DAG图。
一. 拓扑排序的概念
拓扑排序是对AOV网输出的一个序列
AOV网(Active on Vertex Network):在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系。这样的图称为活动的网。
二. 拓扑排序的算法
步骤:
从AOV网中选择一个入度为0(没有前驱)的顶点然后删除此顶点,并删除以此顶点为尾的弧。重复此步骤,直到输出全部顶点或AOV网中不存在入度为0的顶点为止。
若存在有前驱的顶点,则说明有向图中存在环。
拓扑排序中顶点的数据结构:
(1)前面求最小生成树和最短路径时,都是使用邻接矩阵,但由于拓扑排序中,需要删除顶点,所以使用邻接表方便。
(2)因为拓扑排序中,需要删除入度为0的顶点,所以在原先的顶点数据结构中,加入入度域in。使顶点接都变为
算法的代码不考,考如何进行拓扑排序的选择题。
但是看看代码也没啥坏处嘻嘻嘻
#include
#define MAXVEX 100
typedef struct EdgeNode{ /* 边表 */
int adjvex; /* 顶点下标 */
int weight; /* 权值 */
struct EdgeNode *next; /* 边表中的下一节点 */
}EdgeNode;
typedef struct VertexNode{ /* 定点表 */
int in;
int data;
EdgeNode *firstEdge;
}VertexNode,AdjList[MAXVEX];
typedef struct{
AdjList adjList;
int numVertexes,numEdges;
}graphAdjList,* GraphAdjList;
/**
* 拓扑排序
* @param gl :链表
* @return :若GL无回路,则输出排序序列并返回1;若有回路则返回-1
*/
int topologicalSort(GraphAdjList gl){
int *stack = (int *)malloc(gl->numVertexes * sizeof(int)); // stack用于存储入度为0的节点
int top = 0; // stack栈顶指针
int count; // 加入到栈中节点个数
for (int i = 0; i < gl->numVertexes; ++i)
if(gl->adjList[i].in == 0)
stack[++top] = i;
while(top!=0){
int gettop = stack[top--];
printf("%d -> ",gl->adjList[gettop].data);
count ++;
for(EdgeNode *e=gl->adjList[gettop].firstEdge; e ; e=e->next){
int k = e->adjvex; // 顶点的下标
if( ! (-- gl->adjList[k].in)) // 将k号顶点入度减1
stack[++top] = k; // 如果发现入度为0,则把该顶点加入到栈中
}
}
int res = (count < gl->numVertexes) ? -1 : 1; // 如果最后遍历到的个数小于图的总定点数,则说明有环存在,返回-1
return res;
}
-
关键路径
一. 概念
拓扑排序是解决一个工程能否顺序进行的问题,
当需要计算一个工程完成的最短时间,就需要用关键路径。
拓扑排序使用的是AOV网(定点表示活动)。关键路径使用AOE网(边表示活动)。AOV网只能表示活动之间的制约关系,而AOE网可以用变得权值表示活动的持续时间。所以AOE网是建立在活动之间制约关系没有矛盾的基础上,再来分析整个工程需要多少时间。
路径长度:路径上各个活动持续的时间之和
关键路径:从源点到汇点具有的最大长度的路径
关键活动:关键路径上的活动
二. 关键路径算法
关键路径算法中需要的变量
(1)事件最早开始时间etv(earlist time of vertex):顶点vk
的最早发生时间
(2)事件最晚开始时间ltv(latest time of vertex) :顶点vk的最晚发生时间,超过此时间,会延误整个工期
(3)活动最早开始时间(earlist time of edge):弧ak的最早发生时间
(4)活动最晚开始时间(latest time of edge) :弧a
的最晚发生时间,就是不推迟工期的最晚开始时间
int *etv,*ltv; /* 事件最早,最晚发生时间 */
int *stack2; /* 用于存储拓扑排序的栈 */
int top2 = 0; /* stack2的栈顶指针 */
int topologicalSort2(GraphAdjList gl){
int *stack = (int *)malloc(gl->numVertexes * sizeof(int)); /* 建栈将入度为0的顶点入栈 */
int top = 0;
for (int i = 0; i < gl->numVertexes; ++i) {
if(0 == gl->adjList[i].in)
stack[++top] = i;
}
etv = (int *)malloc(gl->numVertexes * sizeof(int)); /* 时间最早开时间数组 */
for (int i = 0; i < gl->numVertexes; ++i) /* 初始化最早开始时间数组全0 */
etv[i] = 0;
int count = 0;
stack2 = (int *)malloc(gl->numVertexes * sizeof(int));
while (top !=0 ){
int gettop = stack[top--];
count ++;
stack2[++top2] = gettop; /* 将弹出的顶点序号压入拓扑排序的栈 */
for (EdgeNode *e = gl->adjList[gettop].firstEdge; e ; e = e->next) {
int k = e->adjvex;
if( !(-- gl->adjList[k].in) )
stack[++top] = k;
if( (etv[gettop] + e->weight) > etv[k] ) /* 求各点事件最早发生时间值 */
etv[k] = etv[gettop] + e->weight;
}
}
if(count < gl->numVertexes)
return -1;
else
return 1;
}
void criticalPath(GraphAdjList gl){
topologicalSort2(gl);
ltv = (int *) malloc (gl->numVertexes * sizeof(int)); /* 事件最晚发生时间 */
for (int i = 0; i < gl->numVertexes; ++i)
ltv[i] = etv[gl->numVertexes -1]; /* 初始化ltv */
int k;
while(top2 != 0){
int gettop = stack2[top2--];
for(EdgeNode *e=gl->adjList[gettop].firstEdge ; e ; e=e->next){
k = e->adjvex;
if(ltv[k] - e->weight < ltv[gettop])
ltv[gettop] = ltv[k] - e->weight;
}
}
for (int j = 0; j < gl->numVertexes; ++j) {
for (EdgeNode *e = gl->adjList[j].firstEdge; e ; e = e->next) {
k = e->adjvex;
int ete = etv[j]; /* 活动最早发生时间 */
int lte = ltv[k] - e->weight; /* 活动最迟发生时间 */
if(ete == lte)
printf(" length: %d",gl->adjList[j].data,gl->adjList[k].data,e->weight);
}
}
}
-
最短路径
一. 迪杰斯特拉
迪杰斯特拉算法
(1)迪杰斯特拉,计算的是一个点到其余所有点的最短路径。
(2)它的基本思想:
如果点 i 到点 j 的最短路径经过k,则ij路径中,i到k的那一段一定是i到k的最短路径。
查找方法:
(1)声明2个一维数组:一个用来标识当前顶点是否已经找到最短路径。另一个数组用来记录v0到该点的最短路径中,该点的前一个顶点是什么。
(2)比较:计算v0
到vi的最短路径时,比较v0vi与v0vk+vkvi的大小,而v0vk与vkvi的值是暂时得出的记录在数组中的最短路径。
算法实现:基于邻接矩阵
#include "graphy/graphy.c" // 邻接矩阵
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatrix[MAXVEX]; //存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX]; //存储到各点最短路径的权值和
/**
* 迪杰斯特拉:求有向图G的V[0]到其余各点的最短路径及带权长度
* @return
*/
void shortestPath_Dijkstra(MGraphy *g,int v0,Pathmatrix *p,ShortPathTable *sptable){
int final[MAXVEX] = {0};
*p = {0}; // 初始化最短路径数组为0
for (int i = 0; i < g->vnum; ++i)
(*sptable)[i] = g->arc[v0][i]; //初始化sptable:让最短路径为图中v0和其余各顶点的权值
(*sptable)[v0] = 0; // sptable记录v0到v0的权值为0
final[v0] = 1; // final数组,记录以求得v0到v0的最短路径
/* 每次循环求得v0到顶点v的最短路径 */
for (int i = 0; i< g->vnum ; ++i) {
int min = INFINITY;
int k;
for (int j = 0; j < g->vnum; ++j) { // 循环每个顶点
if(! final[j] && (*sptable)[j] < min){
k = j; // 这个k只是把j的作用域扩大出去,供后面计算a
min = (*sptable)[j]; // 让min=当前被遍历顶点与v0点的边的权值
}
final[k] = 1;
for (int w = 0; w < g->vnum ; ++w) {
int a = min+g->arc[k][w]; // 上面让k=j,所以a=(*sptable)[j] + g->arc[j][w]。也就是:比如计算a0到a2,就比较a0a1+a1a2 与邻接矩阵中的a0a2边的权值
if(! final[w] && a < (*sptable)[w]) {
(*sptable)[w] = a;
(*p)[w] = k; // 这个k就是:假设该等式角标与程序无关,计算 a[i][j] > a[i][k]+a[k][j],记录i到j的最短路径中,j前面的节点
}
}
}
}
}
二. 弗洛伊德算法
弗洛伊德与迪杰斯特拉的区别
(1)它们都是基于比较v0vi与v0vk+vkvi的大小的基本算法。
(2)弗洛伊德三次循环计算出了每个点个其他点的最短路径,迪杰斯特拉算法用2次循环计算出了一个点到其他各点的最短路径 。
(3)如果要计算出全部的点到其他点的最短路径,他们都是O(n2)
typedef int Pathmatrix_Floyd[MAXVEX][MAXVEX]; //存储最短路径下标的数组
typedef int ShortPathTable_Floyd[MAXVEX][MAXVEX]; //存储到各点最短路径的权值和
void shortPath_Floyd(MGraphy *g,Pathmatrix_Floyd *p,ShortPathTable_Floyd *D){
for (int i = 0; i < ; ++i) {
for (int j = 0; j < g->vnum; ++j) {
(*D)[i][j] = g -> arc[i][j];
(*p)[i][j] = j;
}
}
for (int i = 0; i < g->vnum; ++i) {
for (int j = 0; j < g->vnum; ++j) {
for (int k = 0; k < g->vnum; ++k) {
if((*D)[j][k] > (*D)[j][i]+(*D)[i][k]){
(*D)[j][k] = (*D)[j][i]+(*D)[i][k];
(*p)[j][k] = (*p)[j][i];
}
}
}
}
}