图的定义
· 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示
为:G(V,E)。其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
-在图中数据元素称之为顶点(Vertex)。
-顶点集合要有穷非空。
-任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以空。
· 无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Egde),
用无序偶(Vi,Vj)来表示。
· 上图G1是一个无向图,G1={V1,E1},其中
-V1={A,B,C,D},
-E1={(A,B),(B,C),(C,D),(D,A),(A,C)}
· 有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),
用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。
· 上图G2是一个有向图,G2={V2,E2},其中
-V2={A,B,C,D}
-E2={<B,A>,<B,C>,<C,A>,<A,D>}
· 简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复
出现 ,则称这样的图为简单图。
· 以下两个则不属于简单图:
· 无向完全图:在无向完全图中,如果任意两个顶点之间都存在边,则称
该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。
· 有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的
两天弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。
· 稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言,通常认为边或
弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。
· 有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),
带权的图通常称为网(Network)。
· 假设有两个图G1=(V1,E1)和G2=(V2,E2),如果V2属于V1,E2属于E1,则
称G2为G1的子图(Subgraph)。
图的顶点与边之间的关系
· 对于无向图G=(V,E),如果边(V1,V2)属于E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相
邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说(V1,V2)与顶点V1和V2相关联。
· 顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,
边(A,B)依附于顶点A与B上,顶点A的度为3。
· 对于有向图G=(V,E),如果有<V1,V2>属于E,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。
· 以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为
V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V)
· 下图顶点A的入度是2,出度是1,所以顶点A的度是3。
路径
· 无向图G=(V,E)中从顶点V1到顶点V2的路径。
· 下图用红线列举了从顶点B到顶点D的四种不同路径:
· 如果G是有向图,则路径也是有向的。
· 下图用红线列举顶点B到顶点D的两种路径,而顶点A到顶点B就不存在路径:
· 路径的长度是路径上的边或弧的数目。
· 第一个顶点到最后一个顶点相同的路径称为回路或者环(Cycle)。
· 序列中顶点不重复出现的路径称为 简单路径,除了第一个顶点和最后一个顶点
之外,其余顶点不重复出现的回路,称为简单回炉或简单环。
· 下图左侧是简单环,右侧 不是简单环:
连通图
· 在无向图G中,如果匆匆顶点V1到顶点V2有路径,则称V1和V2是联通的,如果
对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图。
· 无向图中的极大连通子图称为连通分量。
· 注意以下概念:
-首先要是子图,并且子图是要连通的。
-连通子图含有极大顶点数。
-具有极大顶点数的连通子图包含依附于这些顶点的所有边。
· 在有向图G中,如果对于每一队Vi到Vj都存在路径,则称G是强连通图。
· 有向图中的极大强连通子图称为有向图的强连通分量。
· 下图左侧并不是强连通图,右侧是。并且右侧是左侧的极大强连通子图,
也是左侧的强连通分量。
· 最后我们再来看连通图的生成树定义。
· 所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个
顶点,但只有足以构成一棵树的n-1条边。
· 如果一个有向图恰有一个顶点入度为0,其余顶点入度均为1,则是一棵有向树。
左边的有向图,能拆分成右边两棵有向树。
图的存储结构
· 因为任意两个顶点之间都可能村长联系,因此无法以数据元素在内存中的物理
位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。
· 如果用多重链表来描述倒是可以做到,但是纯粹用多重链表导致的浪费是无法
想象的(如果各个顶点的度数相差太大,就会造成巨大的浪费)。
邻接矩阵(无向图)
· 考虑到图是由顶点和边或弧两部分组成,自然会用到两个结构体来存储。
· 顶点因为不区分大小、主次,所以用一个一维数组来存储。
· 边或弧由于是顶点与顶点之间的关系,可以考虑二维数组来存储。
· 图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点
信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
· vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为对称矩阵(由于无向图不分先后)
0表示不存在顶点间的边,1表示顶点间存在边。
· 对称矩阵:所谓对称矩阵就是n阶矩阵的元满足a[i][j]=a[j][i](0<=i,j<=n)。
· 有了这个二维数组组成的对称矩阵,可以很容易地知道图中的信息:
-要判定任意两顶点是否有边无边就非常容易。
-某个顶点的度就为这个顶点Vi在邻接矩阵中第i行(列)的元素之和。
-顶点Vi的所有邻接点就是矩阵中第i行元素扫描一遍,为1就是邻接点。
邻接矩阵(有向图)
· 可见顶点数组vertex[4]={V0,V1,V2,V3},弧树组arc[4][4]也是一个矩阵,但因为是有
向图,所以这个矩阵并不对称,例如由V1到V0有弧,得到arc[1][0]=1,而V0到V1
没有弧,因此arc[0][1]=0。
· 关于入度和出度,顶点V1的入度为1,整合是第V1列各数之和,
顶点V1的出度为2,正好是第V1行的各数之和。
邻接矩阵(网)
· 当i=j时书上是用∞表示,有的地方用0表示
· 这里“∞”表示一个计算机允许的,大于所有边上权值的值(int最大值65535)。
图的数组表示法,结构体:
#define INFINITY 65535 //表示无穷大-->在带权的图中用到,即网
#define MAX_VERTEX_NUM 20 //图的最大顶点数
#define MAX_INFO 20 //最大信息数
typedef char InfoType; //附加信息类型
typedef int VRType; //顶点关系类型
typedef int VertexType; //顶点数据类型
//图的种类:有向图,有向网,无向图,无向网
typedef enum {
DG, DN, UDG, UDN
} GraphKind;
typedef struct ArcCell {
VRType adj; //顶点关系类型,对无权图用1或0表示是否相邻
//对带权图,则为权值类型
InfoType *info; //附加信息指针
} ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct {
VertexType vexs[MAX_VERTEX_NUM];//顶点向量
AdjMatrix arcs; //邻接矩阵
int vexnum, arcnum; //当前顶点数和弧数
GraphKind kind; //图的种类
} MGraph;
邻接表(无向图)
· 我们可以发现,对于边数相对较少的图,上面的邻接矩阵是存在巨大浪费的。如下图
· 因此可以采用数组和链表组合起来存储,这里称为邻接表。
· 邻接表的处理方法是这样的:
-图中顶点用一个一维数组存储。
-图中每个顶点Vi的所有邻接点构成一个线性表,由于个数不确定,所以用单链表存储。
邻接表(有向图)
· 类似地,下面是把顶点当弧尾建立的邻接表,这样容易得到每个顶点的出度。
· 有时为了确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表。
邻接表(网)
· 对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可。
图的邻接表存储,结构体:
#define MAX_VERTEX_NUM 20
typedef char InfoType; //附加信息类型
typedef int VertexType; //顶点数据类型
typedef struct ArcNode{
int adjvex; //该弧所指向的顶点位置
struct ArcNode *nextarc;//下个结点
InfoType *info; //当前结点(弧)的信息
}ArcNode;
typedef struct VNode{
VertexType data;//顶点信息
ArcNode *firstarc;//指向第一个依附于该顶点弧的指针
}VNode,*AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;
int vexnum,arcnum;//图的当前顶点数和弧数
int kind; //图的种类标志
}ALGraph;
十字链表(是对于有向图来说的,关心出入度问题,所以把两个邻接表结合一起)
· 邻接表固然优秀,但也有不足,例如对有向图的处理上,有时需要再建立一个逆邻接表。
· 这时候就出现了十字链表。
· 为此重新定义顶点表结构结点结构:
· 接着重新定义边表结点结构:
· 可以发现,蓝线是邻接表的指向,红线是逆邻接表的指向,是把他们合起来了。
· 十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到Vi为
尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。
· 十字链表除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的。
因此,在有向图的应用中,十字链表也是非常好的数据结构模型。
//十字链表
#define MAX_VERTEX_NUM 20
typedef char InfoType; //附加信息类型
typedef int VertexType; //顶点数据类型
typedef struct ArcBox{
int tailvex,headvex; //该弧的尾和头顶点的位置
struct ArcBox *hlink,*tlink;//分别为弧头相同回合弧尾相同的弧的链域
InfoType *info; //该弧相关信息的指针
}ArcBox;
typedef struct VexNode{
VertexType data;
ArcBox *firstin,*firstout;//分别指向该顶点第一条弧和出弧
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM];//表头向量
int vexnum,arcnum; //有向图的当前顶点数和弧数
}OLGraph;
邻接多重表(对无向图而言)
· 如果在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但如果更挂关注的是边的操作,
比如对已经访问过的边做标记,或者删除某一条边等操作,邻接表就显得不方便了(如下图)
· 因此,这里仿造十字链表的方式,对边表结构进行改装,重新定义的边表结构如下:
· 其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。
iLink指向依附点iVex的下一条边,jLink指向依附顶点jVex的下一条边。
· 也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。
//无向图的邻接多重表存储结构
#define MAX_VERTEX_NUM 20
typedef char InfoType; //附加信息类型
typedef int VertexType; //顶点数据类型
typedef enum {unvisited,visited}VisitIf;
typedef struct EBox{
VisitIf mark; //访问标记
int ivex,jvex; //该边依附的两个顶点的位置
struct EBox *ilink,*jlink;//分别指向依附这两个顶点的下一条边
InfoType *info; //该边信息指针
}EBox;
typedef struct VexBox{
VertexType data;
EBox *firstedge;//指向第一条依附该顶点的边
}VexBox;
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum;//无向图的当前顶点数和边数
}AMLGraph;
图的遍历(这里以邻接矩阵表示)
· 对于图的遍历,因为它的任一顶点都可以和其余的所有顶点相邻接,
因此极有可能村长重复走过某个顶点或漏了某个顶点的遍历过程。
· 如果要避免这种情况,就需要合理科学地遍历,常见的有两种遍历:
深度优先遍历和广度优先遍历。
深度优先遍历
· 也称为深度优先搜索,简称为DFS。
· 从图的某一点按照某个原则进行深度遍历,把遍历过的点作一个标记,在一个分支上
遍历到底后,逐个顶点退回,遍历其他分支,直到退回到原点,遍历完毕。
· 类似于树的先根遍历,是树的先根遍历的推广。
假设先从A遍历
代码实现:
int FirstAdjVex(MGraph G, int v) {
//返回v(序号)的第一个相邻节点(序号)
if (v > G.vexnum || v < 0)
return -1;
int i, j;
j = 0;
//如果是网
if (G.kind == DN || G.kind == UDN)
j = INFINITY;
for (i = 0; i < G.vexnum; i++)
if (G.arcs[v][i].adj != j)
return i;
return -1;
}
int NextAdjVex(MGraph G, int v, int w) {
//w是v的相邻节点,返回v相对w的下一个节点的序号,否则返回-1
int i, j;
j = 0;
//如果为网
if (G.kind == DN || G.kind == UDN)
j = INFINITY;
//两顶点不相邻
if (G.arcs[v][w].adj == j)
return -1;
//从w之后的结点开始
for (i = w + 1; i < G.vexnum; i++)
if (G.arcs[v][i].adj != j)
return i;
return -1;
}
int visited[MAX_VERTEX_NUM];//访问标记数组
void DFS(MGraph G, int v) {
//从第v个顶点出发递归地深度优先遍历图G
visited[v] = 1;
printf("%d", G.vexs[v]);//访问第v个顶点
for (int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
if (!visited[w])//对未访问的邻接顶点w递归调用DFS
DFS(G, w);
}
void DFSTraverse(MGraph G) {
for (int i = 0; i < G.vexnum; ++i) {
visited[i] = 0; //访问标志数组初始化
}
for (int j = 0; j < G.vexnum; ++j) {
if (!visited[j])
DFS(G, j); //对未访问的顶点调用DFS
}
}
int main() {
MGraph G;
CreateGraph(G);
DFSTraverse(G);
return 0;
}
广度优先遍历
· 又称为广度优先搜索,简称BFS。
· 要实现广度优先遍历,可以利用队列来实现。
· 访问A后,把与A相邻的结点入队列,A标记已入过列。访问B,把B相邻的C,I,G入列
并标记已入列。访问F,把F相邻的G,E入列,因为G已被标记,所以只要E入列,依次类推。
代码实现:
void BFSTraverse(MGraph G) {
//按广度优先非递归遍历图G,使用辅助队列Q和访问标志数组visited
int v, w;
VertexType u;
LinkQueue Q;
for (v = 0; v < G.vexnum; ++v)//标志数组初始化
visited[v] = 0;
IniteQueue(Q); //生成队列
for (v = 0; v < G.vexnum; ++v)
if (!visited[v]) {
visited[v] = 1;
printf("%d", G.vexs[v]);
EnQueue(Q, v); //v入队列
while (!QueueEmpty(Q)) {
DeQueue(Q, u); //队头元素出队并至为u
for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
if (!visited[w]) { //w为u的尚未访问的邻接点
visited[w] = 1;
printf("%d", G.vexs[w]);
EnQueue(Q, w);
}
}
}
}
最小生成树
· 普里姆算法:寻找起始顶点邻接的最短边,并入集合,再寻找邻接的最短边 ,不能有环。
· 克鲁斯卡尔算法,寻找最短边,不能有环。
直接上两种算法的代码:
//普里姆算法
struct {
VertexType adjvex;
VRType lowcost;
}closedge[MAX_VERTEX_NUM];
int minmum(MGraph G){
int min=INFINITY;
int index=-1;
for (int i = 0; i < G.vexnum; ++i) {
//最小值大于 该边的权值 且 该边的权值不为 0
if(min>closedge[i].lowcost&&closedge[i].lowcost!=0){
min=closedge[i].lowcost;
index=i;
}
}
return index;
}
void MiniSpanTree_PRIM(MGraph G,VertexType u){
//书本P174表格
int k=LocateVex(G,u);//该顶点位置 k=0
for (int j = 0; j < G.vexnum; ++j)//辅助数组初始化
if(j!=k)//相当于u顶点 边的信息
closedge[j]={u,G.arcs[k][j].adj};//{adjvex,lowcost}
closedge[k].lowcost=0;//初始,U={u},把u并入集合
for (int i = 1; i < G.vexnum; ++i) {//选择其余G.vexnum-1个顶点
k=minmum(G); //求出T的下一个结点,第k顶点
printf("%d-->%d\n",closedge[k].adjvex,G.vexs[k]);//输出 生成树边
closedge[k].lowcost=0;//第k顶点并入U集
for(int j=0;j<G.vexnum;++j)
if(G.arcs[k][j].adj<closedge[j].lowcost)
//新顶点并入U后重新选择最小边
closedge[j]={G.vexs[k],G.arcs[k][j].adj};
}
}
//克鲁斯卡尔算法,自己写的
typedef struct {
int beginNum; //存放顶点下标
int endNum;
VertexType begin;//顶点名称
VertexType end;
VRType weight;//权重
}Edge[MAX_EDGE_NUM];//边集数组
void MiniSpanTree_Kruskal(MGraph G){
Edge edge[G.arcnum];
int set[G.vexnum];
//初始化辅助数组
for (int i = 0; i < G.vexnum; ++i) {
set[i]=i;//{0,1,2,3,4,5,....}
}
int k=0;
//得到边集数组
for (int i = 0; i < G.vexnum; ++i)
for (int j = 1; j < G.vexnum; ++j)
if (i<j&&G.arcs[i][j].adj!=INFINITY){
edge[k]->beginNum=i;
edge[k]->endNum=j;
edge[k]->begin=G.vexs[i];
edge[k]->end=G.vexs[j];
edge[k]->weight=G.arcs[i][j].adj;
k++;
}
VertexType temp;
VRType adj;
//边集数组,按权重从小到大
for (int i = 0; i < G.arcnum; ++i)
for (int j = i+1; j < G.arcnum; ++j)
if(edge[i]->weight>edge[j]->weight){
temp=edge[i]->begin;
edge[i]->begin=edge[j]->begin;
edge[j]->begin=temp;
temp=edge[i]->end;
edge[i]->end=edge[j]->end;
edge[j]->end=temp;
adj=edge[i]->weight;
edge[i]->weight=edge[j]->weight;
edge[j]->weight=adj;
k=edge[i]->beginNum;
edge[i]->beginNum=edge[j]->beginNum;
edge[j]->beginNum=k;
k=edge[i]->endNum;
edge[i]->endNum=edge[j]->endNum;
edge[j]->endNum=k;
}
int a,b;
for (int i = 0; i < G.arcnum; ++i) {
if(set[edge[i]->beginNum]!=set[edge[i]->endNum]){
//如果辅助数组中,对应数字不同
//输出该边,修改辅助数组中数字
//如果set[edge[i]->beginNum]为a
//set[edge[i]->endNum]为b
//那么把辅助数组中所有为b的改为a
printf("%d->%d weight:%d\n",edge[i]->begin,edge[i]->end,edge[i]->weight);
a=set[edge[i]->beginNum];
b=set[edge[i]->endNum];
for (int j = 0; j < G.vexnum; ++j)
if(set[j]==b)
set[j]=a;
}
}
}
· 普里姆算法的时间复杂度为O(n^2),与网中的边数无关,适用于求边稠密的最小生成树。
· 库鲁斯卡尔的时间复杂度为O(eloge),e为网中边的数目,适合于求边稀疏的最小生成树。
拓扑排序
· 一个无环的有向图称为无环图(Directed Acyclic Graph),简称DAG图。
· 所有的工程或者某种流程都可以分为若干个小的工程或者阶段,
称这些小的工程或阶段为"活动"。
· 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,
这样的有向图为顶点表示活动的网,称之为AOV网。
· AOV网中的弧表示活动之间存在某种制约关系,且不能存在回路。
· 拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,...,Vn
满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则称
这样的顶点序列为拓扑序列。
· 拓扑排序:就是对一个有向图构造拓扑序列的过程。
· 拓扑序列(其中一种):
1,13,4,8,15,5,2,3,10,11,12,7,6,9
注:前面的必须指向后面的。
· 对AOV网进行拓扑排序的方法和步骤如下:
-从AOV网中选择一个没有前驱的顶点(入度为0)并输出他。
-从网中删去该顶点,并且删去从该顶点发出的全部有向边。
-重复上述两步,直到剩余网中不再存在没有前驱的顶点为止。
· 由刚才我们那幅AOV网图,我们可以用邻接表(因为需要删除顶点,所以
选择邻接表会更加方便)数据结构表示:
代码实现:
int TopologicalSort(ALGraph G){
//有向图G采用邻接表存储结构
//若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
SqStack S;
ArcNode *p;
int i,k;
InitStack(S);
for ( i = 0; i < G.vexnum; ++i) {
if(G.vertices[i]->in==0)
Push(S,i); //入度为0的顶点下标入栈
}
int count=0; //对输出顶点计数
while (!StackEmpty(S)){
Pop(S,i); //输出i号顶点,并计数
printf("%d,%d\n",i,G.vertices[i]->data);
++count;
for(p=G.vertices[i]->firstarc;p;p=p->nextarc){
k=p->adjvex; //对i号顶点的每个邻接点的入度减1
if(!(--G.vertices[k]->in)) //若入度为0,则入栈
Push(S,k);
}
}
if(count<G.vexnum)
return 0;
else
return 1;
}
算法时间复杂度:
-对一个具有n个 顶点,e条边的网来说,初始建立入度为零的顶点栈,
要检查所有顶点一次,执行时间为O(n)。
-排序中,若AOV网无回路,则每个顶点入出栈各一次次,每个表结点
被检查一次,因而执行时间是O(n+e)。
-所以整个算法时间复杂度是O(n+e)。
关键路径
· AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,
用边上的权值表示活动的持续时间,这种有向图的边表示活动的网称为AOE网。
· 把AOE网中入度为零的顶点称为始点或源点,出度为零的顶点称为终点或汇点。
· 路径长度最长的路径叫做关键路径。
-etv(Earliest Time Of Vertex):事件最早发生时间,就是顶点的最早发生时间。
-ltv(Latest Time Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚
需要开始的时间,如果超出此时间将会延误整个工期。
从后面往前看,减去这个工时就为最晚发生时间
例如:C8的etv为12,C6的ltv为12-4=8
-ete(Earliest Time Of Edge):活动的最早开工时间,就是弧的最早发生时间。
-lte(Latest Time Of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间。
类似ltv算法,也要倒着过来。
代码实现:
//拓扑排序
int TopologicalSort(ALGraph G) {
//有向图G采用邻接表存储结构
//若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
SqStack S;
ArcNode *p;
int i, k;
InitStack(S);
for (i = 0; i < G.vexnum; ++i) {
if (G.vertices[i]->in == 0)
Push(S, i); //入度为0的顶点下标入栈
}
int count = 0; //对输出顶点计数
while (!StackEmpty(S)) {
Pop(S, i); //输出i号顶点,并计数
printf("%d,%d\n", i, G.vertices[i]->data);
++count;
for (p = G.vertices[i]->firstarc; p; p = p->nextarc) {
k = p->adjvex; //对i号顶点的每个邻接点的入度减1
if (!(--G.vertices[k]->in)) //若入度为0,则入栈
Push(S, k);
}
}
if (count < G.vexnum)
return 0;
else
return 1;
}
//关键路径
int ve[];//顶点最早开始时间
int vl[];//顶点最晚开始时间
int TopologicalOrder(ALGraph G, SqStack &T) {
//有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)
//T为拓扑序列顶点栈,S为零入度顶点栈
//若G无回路,则用栈T返回G的一个拓扑序列,返回1,否则0
SqStack S;
ArcNode *p;
int i, k, j;
InitStack(S);
InitStack(T);
for (i = 0; i < G.vexnum; ++i) {
if (G.vertices[i]->in == 0)
Push(S, i); //入度为0的顶点下标入栈
ve[i] = 0; //初始化
}
int count = 0;
while (!StackEmpty(S)) {
Pop(S, j); //j号顶点入T栈并计数
Push(T, j);
++count;
for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
k = p->adjvex; //对j号顶点的每个邻接点的入度减1
if (!(--G.vertices[k]->in)) //若入度减为0,则入栈
Push(S, k);
//当前出栈j号顶点的 最早发生时间+ 当前循环连接k号顶点的权值 >k号顶点最早 发生时间
if (ve[j] + *(p->info) > ve[k])
//k号顶点的最早发生时间=出栈顶点号的最早发生时间+与之相连边的权值
ve[k] = ve[j] + *(p->info);
}
}
if (count < G.vexnum) //该有向网有回路
return 0;
else
return 1;
}
int CriticalPath(ALGraph G) {
//G为有向网,输出G的各项关键活动
int j, k, dut, ee, el;
char tag;
SqStack T;
ArcNode *p;
if (!TopologicalOrder(G, T))
return 0;
for (int i = 0; i < G.vexnum; ++i) //初始化顶点事件的最迟发生时间
vl[i] = ve[G.vexnum - 1];
while (!StackEmpty(T)) //按拓扑逆序求各顶点的vl值
//依附于j号顶点的第一条边(包含边权重和指向的顶点)
for (Pop(T, j), p = G.vertices[j]->firstarc; p; p = p->nextarc) {
//该弧指向顶点的位置k
k = p->adjvex;
//该弧权重
dut = *(p->info);
//k的最迟发生时间-该弧权重<初始顶点最迟发生时间
if (vl[k] - dut < vl[j])
vl[j] = vl[k] - dut;
}
for (j = 0; j < G.vexnum; ++j)
for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
k = p->adjvex;
dut = *(p->info);
//注意ve,vl是顶点的最早和最晚发生时间
//ee,el才是弧的最早和最晚发生时间
//关键路径是对弧而言
ee = ve[j];//该弧最早发生时间=该弧起始顶点最早发生时间
el = vl[k] - dut;//该弧最晚发生时间=该弧结束顶点最晚发生时间-权重
//带* 为关键
tag = (ee == el) ? '*' : '';
printf("起始顶点:%d 结束顶点:%d 该弧权重:%d 最早开始时间:%d 最晚开始时间:%d 是否关键:%c", j, k, dut, ee, el, tag);
}
}
· 这两种算法的时间复杂度均为O(n+e),前一种算法的常数因子要小些。由于计算弧的活动
最早开始时间和最迟开始时间的复杂度为O(e),所以总的求关键路径的时间复杂度O(n+e)
最短路径
· 在网图和非网图中,最短路径的含义是不同的。
-网图是两顶点经过的边上权值之和最少的路径。
-非网图是两顶点之间经过的边数最少的路径。可以看成网图,权值都为1。
· 把路径起始的第一个顶点称为源点,最后一个顶点称为终点。
· 关于最短路径,分为两种算法:
迪杰斯特拉算法
git图演示
-初始所有顶点路径长为∞,设源点为0。
-搜索该源顶点的路径,依次标上花费路径长度,然后归入S集(表示已遍历)
-逐个搜索除S集合外所有已经标有花费路径长度的顶点,无需遍历归入S集顶点
遍历到其他顶点,若花费小于其标值,则修改之,遍历完成后,并入S集。
-重复上步步骤。
//最短路径,迪杰斯特拉算法
typedef int Patharc[MAX_VERTEX_NUM];//用于存储最短路径下标的数组
typedef int ShortPathTable[MAX_VERTEX_NUM];//用于存储到各点最短路径的权值和
void ShortestPath_DIJ(MGraph G, int V0, Patharc &P, ShortPathTable &D) {
int v, w, min, k = NULL;
int final[MAX_VERTEX_NUM]; //final[w]=1 表示已经求得顶点V0到Vw的最短路径
//初始化数据
for (v = 0; v < G.vexnum; v++) {
final[v] = 0; //全部顶点初始化为未找到最短路径
D[v] = G.arcs[V0][v].adj; //将与V0点有连线的顶点加上权值
P[v] = 0; //初始化路径数组P为0
}
D[V0] = 0; //V0至V0路径为0
final[V0] = 1;//V0至V0不需要求路径
//开始主循环,每次求得V0到某个v顶点的最短路径
for (v = 1; v < G.vexnum; v++) {
min = INFINITY;
//循环后得到一个已知最短路径的顶点,作为发散修正的顶点
for (w = 0; w < G.vexnum; w++)
if (!final[w] && D[w] < min) {
k = w;
min = D[w];
}
final[k] = 1; //将目前找到的最近的顶点置1
//修正当前最短路径 及距离
//从该顶点发散出去的各个顶点距离修正
for (w = 0; w < G.vexnum; w++)
//如果经过v顶点的路径比现在这条路径的长度短的话,更新
if (!final[w] && (min + G.arcs[k][w].adj < D[w])) {
D[w] = min + G.arcs[k][w].adj;//修改当前路径长度
P[w] = k; //存放前驱顶点
}
}
}
· 第一个FOR循环的时间复杂度是O(n),第二个FOR循环共进行n-1次,
每次执行时间是O(n),所以总的时间复杂度是O(n^2)。
· 如果用带权的邻接表作为有向图的存储结构,则虽然修改D的时间可以减少,
但由于在D向量中选择最小分量的时间不变,所以总时间仍为O(n^2)。
· 如果只希望找到源点到某一个特定终点的最短路径,这个问题和求
源点到其他所有顶点的最短路径一样复杂,其时间复杂度也是O(n^2)。
佛洛依德算法
· 从任意阶段i到任意节点j的最短路径只有2种可能,一种是直接从i到j另一种是
从i经过若干个节点k到j。所以,假设Dis(i,j)为节点u到节点v的最短路径的距离,
对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,
证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),
这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
· 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
· 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。
十字交叉法
方法:两条线,从左上角开始计算一直到右下角如图所示
给出矩阵,其中矩阵A是邻接矩阵,而矩阵Path记录u,v两点之间最短路径所必须经过的点。
算法实现:
//最短路径,佛洛依德算法
typedef int PathMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef int DistancMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
void ShortestPath_FLOYD(MGraph G, PathMatrix &P, DistancMatrix &D) {
int v, w, k;
//初始化D和P
for (v = 0; v < G.vexnum; v++)
for (w = 0; w < G.vexnum; w++) {
D[v][w] = G.arcs[v][w].adj;
P[v][w] = -1;
}
//算法核心
for (k = 0; k < G.vexnum; k++)
for (v = 0; v < G.vexnum; v++)
for (w = 0; w < G.vexnum; w++)
if (D[v][w] > D[v][k] + D[k][w]) {
D[v][w] = D[v][k] + D[k][w];
P[v][w] = P[v][k];
}
}
· 此算法时间复杂度为O(n^3)。