介绍(关于具体概念看课本)
值得注意的是,有向图的边用<>表示,无向图用()表示
存储结构
邻接矩阵
概念
网就是边有权值的图
代码
c
/*----------------------------------------图的邻接矩阵存储表示---------------------------------------*/
#define MaxInt 32767//极大值,即无穷大
#define MVNum 100//最大顶点数
typedef char VerTexType;//假设顶点类型为字符型
typedef int ArcType;//假设权值类型为整型
typedef struct
{
VerTexType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum, arcnum;//图的当前点数与边数
}AMGraph;
/*----------------------------------------采用邻接矩阵创建无向网---------------------------------------*/
/*
算法步骤
1.输入总顶点与总边数
2.依次输入点的信息并将其存入顶点表中
3.初始化邻接矩阵,使每个权值初始化为最大值
4.构造邻接矩阵
依次输入每条边依附的顶点和权值,确定两个顶点在图中的位置之后,使相应边赋予相应的权值,同时使其对称边赋予相同的权值
*/
int LocateVex(AMGraph G, VerTexType v)
{
for(int i = 0; i < G.vexnum; i++)
if (G.vexs[i] == v)return i;
}
Status CreateUDN(AMGraph& G)
{
cin >> G.vexnum >> G.arcnum;//输入点总数与边总数
for (int i = 0; i < G.vexnum; i++)
{
cin >> G.vexs[i];//输入点
}
for (int i = 0; i < G.vexnum; i++)
{
for (int j = 0; j < G.vexnum; j++)
{
G.arcs[i][j] = MaxInt;//初始化邻接矩阵边的权值为无穷大
}
}
for (int k = 0; k < G.arcnum; k++)
{
int v1, v2, w;
cin >> v1 >> v2 >> w;//输入两个点和其相连边的权值
int i = LocateVex(G, v1);//确定点的下标
int j = LocateVex(G, v2);//确定点的下标
G.arcs[i][j] = w;//设置权值
G.arcs[j][i] = G.arcs[i][j];//对称的边也一样,因为这是以无向图为例;若为有向图,则只有第二个存在
}
return OK;
}
邻接表
概念
代码
c
/*----------------------------------------图的邻接表存储表示---------------------------------------*/
#define MVNum 100//最大顶点数
typedef char VerTexType;//假设顶点类型为字符型
typedef struct ArcNode//边节点
{
int adjvex;//该边所指的顶点位置
struct ArcNode * nextarc;//指向下一条边的指针
OtherInfo info;//和边有关的信息
}ArcNode;
typedef struct VNode//点节点
{
VerTexType data;//数据
ArcNode * fistarc;//指向第一条依附该顶点的指针的边的节点
}VNode, AdjList[MVNum];
typedef struct //邻接表
{
AdjList vertices;//邻接表
int vexnum, arcnum;
}ALGraph;
/*----------------------------------------采用邻接矩阵创建无向网---------------------------------------*/
/*
算法步骤
1. 输入总边数与总点数
2. 依次将信息存入表顶点表中,使每个表节点的指针域(fistarc)初始化为NULL
3. 创建邻接表。依次输入每条边依附的两个顶点,确定其下标i与j后,将此边节点插入两个顶点对应边链表的头部
*/
Status CreateUDG(ALGraph& G)
{
cin >> G.vexnum >> G.arcnum;
for (int i = 0; i < G.vexnum; i++)
{
cin >> G.vertices[i].data;
G.vertices[i].fistarc = NULL;
}
for (int k = 0; k < G.arcnum; k++)
{
int v1, v2;
cin >> v1 >> v2;//输入两个点和其相连边的权值
int i = LocateVex(G, v1);//确定点的下标
int j = LocateVex(G, v2);//确定点的下标
//若为有向图,则只有第一个存在;此处为无向图,所有两个点都要互相插入,先j后i,别搞错了!!!
ArcNode* p1 = new ArcNode;//生成一个边节点
p1->adjvex = j;//边节点的一个点为j
p1->nextarc = G.vertices[i].fistarc;//把边节点的指针域 指向 点节点的指针域第一个边节点
G.vertices[i].fistarc = p1;//把边节点插入到点指针域的第一个点
ArcNode* p2 = new ArcNode;//生成一个边节点
p2->adjvex = i;//边节点的一个点为i
p2->nextarc = G.vertices[j].fistarc;//把边节点的指针域 指向 点节点的指针域第一个边节点
G.vertices[j].fistarc = p2;//把边节点插入到点指针域的第一个点
}
return ok;
}
十字链表
有向图的另外一种链式存储结构,其中弧头为箭头端,另外一端为弧尾,结构如下
tailvex:弧尾顶点 headvex:弧头顶点 hlink:弧头相同的下一条弧 tlink:弧尾相同的下一条弧 info:弧的其他信息
data:点的数据 firstin:第一条指向该顶点为弧头的弧节点 firstout:第一条指向该顶点为弧尾的弧节点
c
#define MAX_VERTEX_NUM 20
typedef struct ArcBox
{
int tailvex, headvex;
struct ArcBox *hlink, *tlink;
InitType info;
}ArcBox;
typedef strcut VexNode
{
VertexType data;
ArcBox *firstin, *firstout;
}VexNode;
typedef struct
{
VexNode xlist[MAX_VERTEX_NUM];//表头向量
int vexnum, arcnum;//有向图的当前顶点数和弧数
}OLGraph;
邻接多重表
是无向图的一种线性存储方式,结构如下
mark:标志域,标记此边是否被搜索过 ivex,jvex:边的两个顶点在图中的位置 ilink:下一条依附于ivex的边 jlink:下一条依附于jvex的边 info:边的其他信息
data:数据 firstedge:第一条依附于该点的边
c
#define MAX_VERTEX_NUM 20
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[MAX_VERTEX_NUM];
int vexnum, edgenum;
}AMLGraph;
遍历
如果涉及到关于谁先,最好选择权值较小的顶点
深度优先搜索(DFS)
先访问到底,再回来上一个,访问下一个未访问的邻接点
样板
c
bool visted[MVNum];
//连通图
void DFS(Graph G, int v)
{//从第v个顶点遍历图G
cout << v;//访问节点
visited[v] = true;//改变标志为已访问
//依次检测v的所有邻接点w
//FirstAdjVex(G, v)表示v的第一个邻接点
//NextAdjVex(G, v, w)则是v对除了w外的下一个邻接点
//不同的存储形式,上面两个函数的内容也不一样,具体得看具体问题
//w >= 0表示存在邻接点
for(int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
if(!visited[w]) DFS(G, w);//递归到该点的邻接点都遍历完,再退回访问上一个节点的下一个邻接点,以此类推重复步骤,直到退到第一层
}
//非连通图,若用上面的算法会有些节点没有访问,故而为了实现整个图的遍历,引入下面算法
//访问每一个顶点集内没访问过的节点,在访问该节点时深度访问到没有邻接点为止
void DFSTraverse(Graph G)
{
for(int v = 0; v < G.vexnum; v++) visited[v] = false;//标志数组初始化
for(int v = 0; v < G.vexnum; v++)
if(!visited[w]) DFS(G, v);//访问未访问的节点
}
//实际使用中两个函数都要一起,因为图可能流通也可能不连通
邻接矩阵存储结构
c
void DFS_AM(AMGraph G, int v)
{
cout << v;//访问节点
visited[v] = true;//改变标志为已访问
for(int w = 0; w < G.vexnum; w++)//依次检查邻接矩阵该点所在的行
if((G.arcs[v][w] != 0) && (!visited[w]))DFS_AM(G, w);//如果邻接矩阵上为1,即v与w相连接;且点w未被访问过,则访问w
}
void DFS_AM_Traverse(Graph G)
{
for(int v = 0; v < G.vexnum; v++) visited[v] = false;//标志数组初始化
for(int v = 0; v < G.vexnum; v++)
if(!visited[w]) DFS_AM(G, v);//访问未访问的节点
}
邻接表存储结构
c
void DFS_AL(AMGraph G, int v)
{
cout << v;//访问节点
visited[v] = true;//改变标志为已访问
ArcNode *p = G.vextices[v].firstarc;//定义边节点p指向访问点的边链表的第一个边节点
while(p)
{
w = p->adjvex;//取出边节点相连点
if(!visited[w])DFS_AM(G, w);//未访问就访问它
p = p->nextarc;//访问链表下一个点
}
}
void DFS_AL_Traverse(Graph G)
{
for(int v = 0; v < G.vexnum; v++) visited[v] = false;//标志数组初始化
for(int v = 0; v < G.vexnum; v++)
if(!visited[w]) DFS_AL(G, v);//访问未访问的节点
}
广度优先搜索(BFS)
访问一个点,就把他未访问的邻接点全访问,再访问邻接点的未访问邻接点,以此循环
c
void BFS(Graph G, int v)
{
cout << v;
visited[v] = true;
queue<int>Q;//定义一个队列存储访问序列
Q.push(v);//将访问节点入队
while(!Q.empty())
{
int u = Q.front();
Q.pop();//取出队头
//依次检测v的所有邻接点w
//FirstAdjVex(G, v)表示v的第一个邻接点
//NextAdjVex(G, v, w)则是v对除了w外的下一个邻接点
//不同的存储形式,上面两个函数的内容也不一样,具体得看具体问题
//w >= 0表示存在邻接点
for(int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
if(!visited[w]) //未访问则访问它
{
cout << w;
visited[w] = true;
Q.push(w);//将其存入队列,等上一个节点的邻接点全部访问完后便会以此访问队列元素
}
}
}
void BFSTraverse(Graph G)
{
for(int v = 0; v < G.vexnum; v++) visited[v] = false;//标志数组初始化
for(int v = 0; v < G.vexnum; v++)
if(!visited[w]) BFS(G, v);//访问未访问的节点
}
//实际使用中两个函数都要一起,因为图可能流通也可能不连通
//上面只是样板,具体存储格式实现可以参考深度优先搜索算法
应用
最小生成树
普里姆算法(加点法)
-
算法思路
-
代码实现
c/* 算法步骤: 1.首先将初始顶点u加入U中,对于其他每一个顶点v,将closedge[j]均初始化为到u的边信息 2.循环n-1次,做下面处理: 。从各组边closedge中选出最小边closedge[k](权值相同就随便选),输出此边 。将k加入U中 。更新剩余的每个小组最小边信息closedge[j],对于V-U中的边,新增一条从k到j的边。如果新边的权值比closedge[j].lowcost小,则将closedge[j].lowcost更新为新边的权值 */ //辅助数组closedge定义 struct { VerTexType adjvex;//最小边在U中的那个顶点(最小边有两个顶点,一个是在U中,一个在V-U) ArcType lowcast;//最小边上的权值 }closedge[MVNum]; void MiniSpanTree_Prim(AMGRaph G, VerTexType u)//无向网、以邻接矩阵存储,从点u出发构造G的最小生成树,输出T的各条边 { int k = LocateVex(G, u);//找出点u的下标 for (int j = 0; j < G.vexnum; j++) if (j != k)closedge[j] = { u, G.arcs[k][j] };//更新V-U中每一个顶点的closedge[j] closedge[k].lowcast = 0;//将u归入U for (int i = 1; i < G.vexnum; i++)//循环n-1次 { k = Min(closedge);//找出权值最小边 VerTexType u0 = closedge[k].adjvex;//把最小边在U中的顶点取出 VerTexType v0 = G.vexs[k];//把最小边在V-U中的顶点取出 cout << u0 << v0;//输出边 closedge[k].lowcast = 0;//将最小边在V-U中的顶点归入U for (int j = 0; j < G.vexnum; j++)//更新边的权值和关联顶点 if (G.arcs[k][j] < closedge[j].lowcast)//如果当前点到其它顶点距离小于其它顶点closedge中的最小权值则更新 closedge[j].lowcast = { G.vexs[k], G.arcs[k][j] }; } }
克鲁斯卡尔算法(加边法)
-
算法思路
-
代码实现
c/* 算法步骤: 1.将数组Edge的元素按权值从小到大排序 2.依次查看数组Edge中的边,循环执行以下操作 。依次从排好序的数组Edge中选出一条边(v1, v2) 。在Vexset中分别查找v1和v2所在的连通分量vs1和vs2进行判断: √ 若vs1和vs2不相等,说明两个顶点不属于同一个连通分量,输出此边,并且合并两个顶点成为同一个连通分量 √ 若vs1和vs2相等,说明两个顶点属于同一个连通分量,舍弃此边继续找下一条权值最小的边 */ //辅助数组Edges定义:存储边的信息,包括边相连的两个顶点和权值 struct { VerTexType head;//边的始点 VerTexType tail;//边的终点 ArcType lowcast;//最小边上的权值 }Edge[MVNum]; //辅助数组Vexset定义:标识各个顶点属于连通分量(顶点相连就为一个连通分量,没连它自己也算一个连通分量),用来看会不会形成回路,初始化为Vexset[i]=i,表示各顶点自成一个连通分量 int Vexset[MVNum]; void MiniGraphTree_Kruskal(AMGraph G)//无向网、邻接矩阵存储,构造G的最小生成树T,输出T的各边 { Sort(Edge);//按权值从小到大排序 for (int i = 0; i < G.vexnum; i++)//初始化数组Vexset Vexset[i] = i; for (int i = 0; i < G.arcnum; i++)//循环每一条边 { int v1 = Locate(G, Edge[i].head);//找头顶点下标 int v2 = Locate(G, Edge[i].tail);//找尾顶点下标 int vs1 = Vexset[v1];//找所属连通分量 int vs2 = Vexset[v2];//找所属连通分量 if (vs1 != vs2)//不相等就输出且并入 { cout << Edge[i].head << Edge[i].tail; for (int j = 0; j < G.vexnum; j++)//若有与vs1相连的点也要重新编号,所以要循环 if (Vexset[j] == vs2)Vexset[j] = vs1; } } }
最短路径
从某个源点到其余各顶点的最短路径(迪杰斯特拉算法)
-
算法思路
-
代码实现
c//数组S用来标记是否已确定最短路径 //数组Path用来标记当前节点最短路径的上一个节点 //数组D用来标记从起点到当前节点的最短路径权值和 /* 算法步骤: 1.初始化 。将源点v0加到S中,即S[v0] = true 。将v0到各个终点的最短路径长度初始化为权值,即D[i] = G.arcs[v0][vi](vi是还没确定路径的顶点V-S) 。如果v0与vi之间有弧,则将vi的前驱设为v0,即Path[i] = v0,否则Path[i] = -1 2.循环n-1次,执行以下操作: 。选择下一条最短路径的终点vk,使得最短路径长度D[k]是数组D中最小的 。将vk加入S,即S[vk] = true 。根据条件更新从v0出发到集合V-S上任一顶点的最短路径的长度,若条件D[k] + G.arcs[k][i] < D[i]成立,则更新 D[i] = D[k] + G.arcs[k][i],同时更改vi的前驱为vk,Path[i] = k */ void ShortestPath_DIJ(AMGraph G, int v0) { //初始化 int n = G.vexnum; for(int v = 0; v < n; v++) { S[v] = false; D[v] = G.arcs[v0][v]; if(D[v] < MaxInt) Path[v] = v0;//与v0有弧则前驱变为v0 else Path[v] = -1;//否则为-1 } S[v0] = true;//将v0放入S D[v0] = 0;//自己到自己为0 //主循环 for(int i = 1; i < n; i++) { int min = MaxInt, v = -1; for(int w = 0; w < n; w++) if(!S[w] && D[w] < min)//选择一条当前的最短路径 { v = w;//记录最短路径的除源点外另一个顶点 min = D[w];//记录最短路径值 } S[v] = true;//将该顶点加入S for(int w = 0; w < n; w++)//更新数组D和Path if(!S[w] && D[v] +G.arcs[v][w] < D[w])//新的中转点到下一个顶点的最短路径值若小于原先顶点的最短路径值 { D[w] = D[v] + G.arcs[v][w];//更新最短路径值 Path[w] = v;//更新前驱为新的中转点 } } }
每一对顶点之间的最短路径(弗洛依德算法)
-
算法思路
-
代码实现
为什么会直接就是环路?
答:因为在数组D中,如果与另外一个点没有通路,dij会是0或者极大值,不会是正常值,所以直接查看是否有正常值即可知道两点间是否有通路
c//二维数组D用来记录vi到vj之间的最短路径长度 //二维数组Path用来记录最短路径上vi的前一个顶点序号 //具体大小看题目 int **D; int **Path; void ShortestPath_Floyd(AMGraph G) { //初始化数组D与Path for(int i = 0; i < G.vexnum; i++) for(int j = 0; j < G.vexnum; j++) { D[i][j] = G.arcs[i][j];//创建D(-1) if(D[i][j] < MaxInt && i != j) Path[i][j] = i;//两个顶点之间有路径(不是同一个顶点)则记录前驱为i else Path[i][j] = -1;//否则记录-1 } for(int k = 0; k < G.vexnum; k++)//创建D(0)~D(vexnum-1),D(vexnum-1)为最终结果,k为中间节点 for(int i = 0; i < G.vexnum; i++)//i为起点 for(int j = 0; j < G.vexnum; j++)//j为终点 if(D[i][k] + D[k][j] < D[i][j])//如果i通过k到j比上一个表更近,则更新 { D[i][j]= D[i][k] + D[k][j];//更新最短路径长度 Path[i][j] = Path[k][j];//更新前驱节点,前驱节点不为中间节点,而在Path中找中间点到终点路径上终点的前驱节点 } }
拓扑排序
AOV-网
- 无环的有向图称为有向无环图,简称DAG图
- 用顶点表示活动,用弧表示活动间的优先关系的有向图称为以顶点表示活动的网,简称AOV-网
- 拓扑排序是将AOV网中所有顶点排成一个线性序列,该序列满足:若在AOV-网中,从vi到vj有一条路径,则该线性序列中vi必在vj前
过程
实现
c
//一维数组indegree[i]:存放各顶点入度,删除顶点及以它为尾的弧的操作,可通过弧头入度减一的办法来实现,无需改变图的结构
//栈S:暂存所有入度为0的顶点
//一维数组topo[i]:记录拓扑序列的顶点序号
/*
算法步骤:
1. 求出各顶点入度并存入数组indegree[i]中,使入度为0的顶点入栈
2. 只有栈不为空,则重复以下操作:
·使栈顶顶点vi出栈并保存在拓扑序列topo中
·对vi的每个邻接点vk入度减1,如果vk入度为0,使vk入栈
3.如果输出的顶点个数少于AOV-网的顶点个数,则存在环,无法进行拓扑排序;否则拓扑排序成功
*/
Status TopologicalSort(ALGraph G, int topo[])
{
FindInDegree(G, indegree);//求各顶点的入度
InitStack(S);//创建一个空栈
for(int i = 0; i < G.vexnum; i++)//遍历所有点,把入度为0的顶点入栈
if(!indegree[i]) Push(S, i);
int m = 0;//对输出顶点计数
while(!StackEmpty(S))
{
Pop(S, i);//弹出栈顶顶点
topo[m++] = i;//放入拓扑序列,并且m加一计数
ArcNode* p = G.vertics[i].firstarc;//vertics为邻接表,遍历当前顶点所有邻接点
while(p)
{
int k = p->adjvex;//将第一个邻接点的顶点提取
--indegree[k];//入度减1
if(indegree[k] == 0) Push(S, k);//如果入度为0则入栈
p = p->nextarc;//遍历下一个邻接点
}
}
if(m < G.vexnum) return ERROR;//小于顶点数则拓扑排序失败
else return OK;//否则成功
}
关键路径
AOE-网
-
与AOV-网类似,以边表示活动的网称为AOE-网
-
其中,顶点表示事件,弧表示活动,权值表示活动持续的时间
-
一般用它来估算工程的完成时间
-
一些概念
求解过程
-
前置描述
-
事件vi的最早发生时间ve(i)
【进入事件vi的每一个活动结束了,vi才会发生,所以记录最长的时间,即下面要记录最大值】
通常把源点的ve设为0(其余顶点也初始化为0),其余顶点的ve为作为弧尾的邻接点的ve加上两个顶点间弧的权值 的最大值
-
事件vi的最晚发生时间vl(i)
【一开始是用汇点的最晚发生时间去推,所以要在这个最后期限前去发生才可用使工程不延误,故记录最小值】
汇点的vl一定与ve一样(其余顶点也初始化为与汇点的vl一样),其余顶点的vl为作为弧头的邻接点的vl减去两个顶点间弧的权值 的最小值
-
活动ai = <vj, vk>的最早开始时间e(i)
等于弧尾点的最早开始时间
-
活动ai = <vj, vk>的最晚开始时间l(i)
等于弧头点 的最晚开始时间减去边的权值
-
-
注:若e(i) = l(i),则为关键活动
-
求解过程(先求顶点再求边)
- 对图中顶点进行排序,在排序过程中按拓扑序列求出每个事件的最早发生时间ve(i)
- 按逆拓扑序列(顺序与拓扑排序相同,不过每次选出度为0的顶点)求出每个事件的最迟发生时间vl(i)
- 求出每个活动a的最早开始时间e(i)
- 求出每个活动a的最晚开始时间l(i)
- 找出e(i) = l(i)的活动a,即关键活动。由关键活动形成的由源点到汇点的每一条路径就是关键路径,关键路径有可能不止一条。
实现
c
//一维数组ve:事件vi的最早发生时间
//一维数组vl:事件vi的最晚发生时间
//一维数组topo:记录拓扑排序的顶点序号
/*
算法步骤:
1. 调用拓扑排序算法,使拓扑排序保存至topo中
2. 将每个事件的最早开始时间初始化为0
3. 根据topo中的值,按从前向后的拓扑次序,依次求每个事件的最早发生时间,循环执行以下操作:
·取拓扑序列中的顶点序号k,k = topo[i]
·用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,依次更新顶点j的最早发生时间ve[j]:
if(ve[j] < ve[k] + p->weight) ve[j] = ve[k] + p->weight;
4. 将每个事件的最迟发生时间vl[i]初始化为汇点的最早发生时间
5. 根据topo中的值,按从后向前的拓扑次序,依次求每个事件的最迟发生时间,循环执行以下操作:
·取拓扑序列中的顶点序号k,k = topo[i]
·用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,依次根据k的邻接点,更新k的最迟发生时间vl[k]:
if(vl[k] > vl[j] - p->weight) vl[k] = vl[j] - p->weight;
6. 判断某一个活动是否为关键活动,循环n次,执行以下操作:
对于每个顶点vi,用指针p依次指向vi的每个邻接点,取得每个邻接顶点的序号j,分别计算活动<vi, vj>最早和最迟开始时间e和l
e = ve[i]; l = vl[j] - p->weight;
如果e和l相等,则活动<vi, vj>为关键活动,输出弧<vi, vj>
*/
Status CriticalPath(ALGraph G)
{
if(!TopologicalOrder(G, topo)) return ERROR;//产生拓扑排序
int n = G.vexnum;
for(int i = 0; i < n; i++)//最早开始时间初始化
ve[i] = 0;
/*--------------------------------------按拓扑次序求每个事件的最早发生时间--------------------------------------*/
for(int i = 0; i < n; i++)
{
int k = topo[i];//根据拓扑排序取出顶点序号
ArcNode* p = G.vertices[k].firstarc;//p指向顶点的第一个边节点
while(p)//遍历所有邻接点
{
int j = p->adjvex;
if(ve[j] < ve[k] + p->weight)//更新最早发生时间
ve[j] = ve[k] + p->weight;
p = p->nextarc;
}
}
for(int i = 0; i < n; i++)//最迟开始时间初始化
vl[i] = ve[n-1];
/*------------------------------------按逆拓扑次序求每个事件的最迟发生时间--------------------------------------*/
for(int i = n - 1; i >= 0; i--)
{
int k = topo[i];//根据逆拓扑排序取出顶点序号
ArcNode* p = G.vertices[k].firstarc;//p指向顶点的第一个边节点
while(p)//遍历所有邻接点
{
int j = p->adjvex;
if(vl[k] > vl[j] - p->weight)//更新最迟发生时间
vl[k] = vl[j] - p->weight;
p = p->nextarc;
}
}
/*-----------------------------------------判断每一活动是否为关键活动------------------------------------------*/
for(int i = 0; i < n; i++)
{
ArcNode* p = G.vertices[k].firstarc;//p指向顶点的第一个边节点
while(p)//遍历所有邻接点
{
int j = p->adjvex;
e = ve[i];//计算活动的最早发生时间
l = vl[j] - p->weight;//计算活动的最晚发生时间
if(e == l)//如果为关键活动,输出
cout << G.vertices[i].data << G.vertices[j].data;
p = p->nextarc;
}
}
}