第六章 图
1.图的基本结构
1.1 图的基本概念
1)图G (Graph)由顶点集V(Vertex)和边集E(Edge)组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;
E(G)表示图G中顶点之间的关系(边)集合。若V={v1,v2 … , vn},则用|V|表示图G中顶点的个数,也称图G的阶,E= {(u, v)| u∈U, v∈V},用|E|表示图G中边的条数。
注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集,U可以是空集。
2)简单图:不存在重复边;不存在顶点到自身的边。(只考简单图)
多重图:图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图
3)无向图与有向图
i.无向图:若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v, w)或(w, v),因为(w, v) = (v, w),其中v、w是顶点。可以说顶点w和顶点v互为邻接点。边(v,w)依附于顶点w和v,或者说边(v, w)和顶点v、w相关联。
度:顶点v的度是指依附于该顶点的边的条数,记为TD(v);所有定点的度的和为顶点个数的二倍。
ii.有向图:若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v、w是顶点,v称为弧尾,w称为弧头,<v, w>称为从顶点v到顶点w的弧,也称邻按到w,或w邻接自v。<v, w> ≠ <w, v>
入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
入度之和=出度之和=顶点个数
4)顶点-顶点关系描述
路径:顶点a到顶点b之间的一条路径是指一个顶点序列;
回路:第一个顶点和最后一个顶点相同的路径称为回路或环;
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径;
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路;
路径长度:路径上边的数目;
点到真的距离:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离若从u到v根本不存在路径,则记该距离为无穷;
连通:无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的;若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
强连通:有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的;若图中任何一对顶点都是强连通的,则称此图为强连通图。
常考考点:
对于n个顶点的无向图G,若G是连通图,则最少有n-1条边,若G是非连通图,则最多可能有C2n-1条边。
对于n个顶点的有向图,若G是强联通图,则最少有n条边,形成一个回路。
5)研究图的局部
设有两个图G= (V,E)和G’= (v’ ,E’),若v是v的子集,且E’是E的子集,则称G’是G的子图。
若有满足v(G’)= v(G)的子图G’,则称其为G的生成子图。
无向图的极大连通子图称为连通分量,极大连通子图要包含尽可能多得顶点与尽可能多得边。
有向图中的极大强连通子图称为有向图的强连通分量
生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图。n个顶点,n-1条边。
生成森林:在非连通图中,连通分量的生成树构成了非连通图的生成森林。
6)边的权、带权图/网
边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网:边上带有权值的图称为带权图,也称网。
带权路径长度:当图是带权图时,一多路径上所有边的权值之和,称为该路径的带权路径长度。
7)几种特殊形态的图
无向完全图:无向图中任意两个顶点之间都存在边,v = n, e = C2n;
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧,v=n,e=2C2n;
稀疏图与稠密图:一般来说|E<|V| log|V|时,可以将G视为稀疏图,否则为稠密图;
树:不存在回路,且连通的无向图,v=n,e=n-1;森林有k颗树,则e=v-k
有向树:一个顶点的入度为0其余顶点的入度均行1的有向图,称为有向树。
1.2 图的存储
1.2.1 邻接矩阵
表示方法唯一。
无向图是对称矩阵,可以只保存上三角区。
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表
bool Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int Vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
//一维数组与二维数组的对应:(i,j)->k
//k = i * n +j -> i = k / n, j = k % n;
无向图:第i个结点的度=第i行(或第i列)的非零元素个数; O(n);
有向图:第i个结点的出度=第i行的非零元素个数;
入度=第i列的非零元素个数;
第i个度=第i行、第i列的非零元素个数之和
存储带权图:
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 0x3f3f3f3f //宏定义常量"无穷大" 0xc0c0c0c0为无穷小
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex [MaxVertexNum]; //顶点
EdgeType Edge [MaxVertexNum][MaxVertexNum];//边的权
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
//一般只想自己的权记为0;
适合存储稠密图,空间复杂度之和顶点个数有关。
性质:
设图G的邻接矩阵为A(矩阵元素为0/1,n阶),则An的元素A[i][j]等于由顶点i到顶点j的长度为n的路径的数目。举证乘法例如:从1到4得路径长度为2得路径树数如下所示:
考点:
i.如何计算指定顶点的度、入度、出度(分无向图、有向图来考虑);
ii.时间复杂度如何?如何找到与顶点相邻的边(入边、出边)?时间复杂度如何;
iii.如何存储带权图;
iv.空间复杂度为O(|v|2),适合存储稠密图;
v.无向图的邻接矩阵为对称矩阵,如何压缩存储;
vi.设图G的邻接矩阵为A(矩阵元素为0/1),则An的元素A[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
1.2.2 邻接表
顺序存储+链式存储,类似与树的“孩子兄弟”表示法
表示方法不唯一
#define MaxVertexNum 100 //顶点数目的最大值
//"边/弧""
typedef struct(ArcNode{
int adjvex; //边/弧指向哪个结点
struct ArcNode *next; //指向下一条弧的指针
//infoType info; //边权值
}ArcNode;
//顶点
typedef struct VNode {
VertexType data; //顶点信息
ArcNode *first; //第一条边/弧
}VNode, AdjList[MaxVertexNum];
//用邻接表存储的图
typedef struct {
AdjList vertices;
int vexnum, arcnum;
}ALGraph;
在无向图中边节点的数目是2e,整体空间复杂度为O(v+2e);在有向图中边节点的数目是e,整体空间复杂度为O(v+e)
无向图的度和有向图的入度只需遍历该节点的链表即可,求有向图的入度需要遍历整张邻接表。
1.2.3 十字链表
可以沿着tlink指针找出度,沿着hlink找入度,十字链表法只用于存储有向图。
1.2.4 邻接多重表
空间复杂度:O(v+e),删除边和节点等的操作很方便,邻接多重表只适合用于存储无向图。
总结:
6.3 基本操作
有向图用邻接矩阵存储,无向图用邻接表存储。
图的基本操作:
Adjacent(G,x,y):判断图G是否存在边<x, y>或(x, y)。
Neighbors(G,x):列出图G中与结点x邻接的边。
lnsertVertex(G,x):在图G中插入顶点x。
DeleteVertex(G,x):从图G中删除顶点x。
AddEdge(G,x,y):若无向边(x,y)或有向边<x, y>不存在,则向图G中添加该边。
RemoveEdge(G,x,y):若无向边(x,y)或有向边<x, y>存在,则从图G中删除该边。
Get_edge_value(G,x,y):获取图G中边(x, y)或<x, y>对应的权值。
set_edge_value(G,x,yv):设置图G中边(x, y)或<x, y>对应的权值为v。
//注意有向图与无向图的实现区别
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
2.图的遍历
2.1 广度优先遍历(BFS)
树的层次遍历就是一种广度优先遍历。
广度优先遍历(Breadth-First-Search, BFS)要点:
i.找到与一个顶点相邻的所有顶点:FirstNeighbor(G,x),NextNeighbor(G,x,y)
ii.标记哪些顶点被访问过:定义访问标记数组来记录是否被已经被访问。
iii.需要一个辅助队列
bool visited [MAX_VERTEX_NUM];//访问标记数组
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for(i=0; i<G.vexnum; ++i)
visited[i]=FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助队列Q
for(i=0; i<G.vexnum; ++i) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
//广度优先遍历
void BFS(Graph G,int v){ //从顶点v出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0; w=NextNeighbor(G,v,w)){
//检测v所有邻接点
if(!visited[w]){ //w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w); //顶点w入队列
}//fi
}//rof
}//elihw
}
对于无向图,调用BFS函数的次数=连通分量数
空间复杂度:O(v),时间复杂度:邻接矩阵需要O(v2),邻接表需要O(v+e)
广度优先生成树(森林):根据广度优先的过程产生的。
重点:求广度优先遍历序列
2.2 深度优先遍历(DFS)
树的先根遍历就是一种深度优先遍历。
bool visited [MAX_VERTEX_NUM] ; //访问标记数组
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(i=0; i<G.vexnum; ++i)
visited[i]=FALSE; //访问标记数组初始化
for(i=0; i<G.vexnum; ++i) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次DFS
DFS(G,i); //vi未访问过,从vi开始DFS
}
void DFS(Graph G,int v){ //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v); w>=0; w=NextNeighor(G,v,w)){
if (!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}//fi
}//rof
}
空间复杂度:最好O(1),最坏O(v),时间复杂度:邻接矩阵需要O(v2),邻接表需要O(v+e)
重点:求深度优先遍历序列
3.图的应用(重点)
3.1 最小生成树
对于一个带权连通无向图G =(V, E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则7称为G的最小生成树(Minimum-Spanning-Tree,MST)。
注意:
i.最小生成树可能有多个,但边的权值之和总是唯一且最小的
ii.最小生成树的边数=顶点数-1。砍掉一条则不连通,增加一条边则会出现回路
iii.如果一个连通图本身就是—棵树,则其最小生成树就是它本身
iv.只有连通图才有生成树,非连通图只有生成森林
3.1.1 通用算法:
GenericMst(G){
T = Ø;
while(T未形成一颗生成树){
找到一条最小代价边(u,v);
if((u,v)加入T后不会形成环)
T = T ∪ (u,v);
}
}
3.1.2 Prim算法(普里姆)∶
(选点)从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度为:O(v2),适合与边稠密图。
void Prim(G,T){
T = Ø; //初始化空树
U = {w}; //添加任一顶点w
while((V-U) != Ø){ //若树中不含全部的顶点
设(u,v),满足u∈U, v∈(V-U),且是权值最小的边;
T = T ∪ {(u,v)}; //将此边加入生成树中
U = U ∪ {v}; //顶点归于树
}
}
需要定义两个数组:
isJoin[v]标记各节点是否已加入树,
lowCost[v]各节点加入树得最低代价,若不能直接加入则记为(∞)
可以继续优化,mark一下
3.1.3 Kruskal算法(克鲁斯卡尔)
(选边)每次选择—条权值最小的边,使这条边的两头连通〈原本已经连通的就不选)
直到所有结点都连通。
时间复杂度:O(e*log2e),适合边稀疏图。
void Kruskal(V,T){
T = V; //初始化树T,仅含有顶点
numS = n; //连通分量数
while(numS > 1){ //若连通分量大于1
从E中取出权值最小的边(v,u);
if(v和u属于T中不同的连通分量){
T = T ∪ {(u,v)}; //将此边加入生成树中
numS = numS - 1; //连通分量减一
}
}
}
需要对所有的顶点按照权值排序,使用并查集判断是否属于同一连通分量。
3.2 最短路径问题
3.2.1 单源最短路径问题:
BFS算法(无权图)
bool visited [MAX_VERTEX_NUM];//访问标记数组
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for(i=0; i<G.vexnum; ++i)
visited[i]=FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助队列Q
for(i=0; i<G.vexnum; ++i) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
//广度优先遍历
void BFS(Graph G,int v){ //从顶点v出发,广度优先遍历图G
//visit(v); //访问初始顶点v
for(i=0; i<G.vexnum ;++i){ //d[i]表示从u到i结点的最短路径
d[i]=0x3f3f3f3f; //初始化路径长度
path[i]=-1; //最短路径从哪个顶点过来
}
d[v] = 0;
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q)){ //BFS算法核心
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0; w=NextNeighbor(G,v,w)){
//检测v所有邻接点
if(!visited[w]){ //w为v的尚未访问的邻接顶点
//visit(w); //访问顶点w
d[w] = d[v] + 1; //路径长度加一
path[w] = v; //最短路径应从u到w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w); //顶点w入队列
}//fi
}//rof
}//elihw
}
Dijkstra算法(带权图,无权图)(重点)
提出“goto有害理论”―一操作系统,虚拟存储技术
信号量机制PV原语一―操作系统,进程同步
银行家算法――操作系统,死锁
解决哲学家进餐问题――操作系统,死锁
Dijkstra最短路径算法――数据结构大题、小题
算法思路:与Prime算法类似,贪心思想
两个辅助数组:
dist[]:记录从源点v0到其他各顶点的当前最短路径长度;
path[i]:表示从源点到顶点i之间的最短路径的前驱节点
若从V0开始,令final[0]=ture; dist[0]=0; path[0]=-1;
其余顶点final[k]=false; dist[k]=arcs[0][k];
path[k]=arcs[0][k]<INF? 0 ∶-1
循环遍历所有顶点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i]=ture。
并检查所有邻接自Vi的顶点,对于邻接自Vi的顶点Vj,
若final[i]==false且dist[i]+arcs[i][j]< dist[j],
则令dist[j]=dist[i]+arcs[i][i]; path[j]=i。
(注: arcs[i][j]表示Vi到Vj的弧的权值)
#include <iostream>
#define Max 503
#define INF 0x3f3f3f3f //宏定义无穷大
using namespace std;
typedef struct MGraph { //定义图
int vex, arc; //顶点数,边数
int arcs[Max][Max]; //邻接矩阵
}MGraph;
int dist[Max], path[Max]; //dist保存最短路径总权值、path通过保存路径的前驱结点来保存路径
bool final[Max]; //已找到最短路集合
void Dijkstra(MGraph &G) //迪杰斯特拉算法
{
for (int i = 1; i <= G.vex; i++)
{
dist[i] = G.arcs[0][i]; //初始化dist数组
path[i] = dis[i] < INF ? 0 : -1; //初始化路径数组
}
final[0] = true;
dist[0] = 0; //起点初始化
for (int i = 1; i < G.vex; i++) //遍历G.vex-1次
{
int mins = INF, u = 0;
for (int j = 1; j < G.vex; j++) //找到当前没加入集合的最短路的后驱点
{
if (!final[j] && mins > dist[j]) {
mins = dist[j];
u = j;
}
}
final[u] = true; //将该点加入集合
for (int j = 1; j <= G.vex; j++) //遍历所有其他点对其最短路进行更新(松弛操作)
{
if (!final[j] && dist[j] > dist[u] + G.arcs[u][j]) {
dist[j] = dist[u] + G.arcs[u][j]; //更新最短路径值
path[j] = u; //修改j的前驱为u
}
}
}
}
void find(int x) //递归输出最短路径
{
if (path[x] == 1) {
cout << 1;
}
else {
find(path[x]);
}
cout << " -> " << x;
return;
}
void input(MGraph &G) //输入图
{
cin >> G.vex >> G.arc;
for (int i = 1; i <= G.vex; i++) //初始化邻接矩阵
for (int j = 1; j <= G.vex; j++)
G.arcs[i][j] = INF;
for (int i = 1; i <= G.arc; i++)
{
int u, v, w;
cin >> u >> v >> w;
G.arcs[u][v] = w;
}
}
void output(MGraph &G) //输出
{
//cout << "起点 v1 到各点的最短路程为: \n";
for (int i = 1; i < G.vex; i++)
{
cout << dist[i] << " ";
}
cout << dis[G.vex] << endl;
/*for (int i = 2; i <= G.vex; i++)
{
cout << "起点 v1 到 v" << i << " 的路径为: ";
find(i);
cout << endl;
}*/
}
int main()
{
MGraph G;
input(G);
Dijkstra(G);
output(G);
return 0;
}
时间复杂度O(v2)
注意:主要考手算,若路径中存在负权值,则这个算法不适合。
3.2.2 各顶点间的最短路径:
Floyd算法(带权图,无权图)
Floyd算法(Floyd-Warshall算法)
堆排序算法
for*3算法:
使用动态规划思想,将问题的求解分为多个阶段
对于n个顶点的图G,求任意一对顶点Vi->Vj之间的最短路径可分为如下几个阶段:#初始:不允许在其他顶点中转,最短路径是?
0:若允许在V0中转,最短路径是?
1:若允许在V0、V1中转,最短路径是?
2:若允许在V0、V1、V2中转,最短路径是?
…
n-1:若允许在V0、V1、V2…Vn-1中转,最短路径是?
辅助矩阵:
A(i):目前来看,各顶点间的最短路径长度
path(i):两个顶点之间的中转点
即: 若A(k-1)[i][j] > A(k-1)[i][k] + A(k-1)[k][j]
则A(k)[i][j] = A(k-1)[i][k] + A(k-1)[k][j],path(k)[i][j] = k
否则:A(k)与path(k)保持原值。
初始化:A(-1):不经过任何点中专,即为图的邻接矩阵;path(-1):两个顶尖之间的中转点,全为-1;
A | V0 | V1 | V2 | path | V0 | V1 | V2 | |
---|---|---|---|---|---|---|---|---|
V0 | 0 | 6 | 13 | V0 | -1 | -1 | -1 | |
V1 | 10 | 0 | 4 | V1 | -1 | -1 | -1 | |
V2 | 5 | ∞ | 0 | V2 | -1 | -1 | -1 |
经过三轮后,角标i表示第i轮变化的位置
A | V0 | V1 | V2 | path | V0 | V1 | V2 | |
---|---|---|---|---|---|---|---|---|
V0 | 0 | 6 | 102 | V0 | -1 | -1 | 1 | |
V1 | 93 | 0 | 4 | V1 | 2 | -1 | -1 | |
V2 | 5 | 111 | 0 | V2 | -1 | 0 | -1 |
//......准备工作,根据图的信息初始化矩阵A和 path (如上图)
for (int k=0; k<n; k++){ //考虑以vk作为中转点
for (int i=0; i<n; i++){ //遍历整个矩阵,i为行号,j为列号
for (int j=0; j<n; j++){
if (A[i][j]>A[i][k]+A[k][j]){ //以 vk为中转点的路径更短
A[i][j]=A[i][k]+A[k][j]; //更新最短路径长度
path[i][j]=k; //中转点
}}}}
void find(int x, int y) //输出路径
{
if (path[x][y] == -1) { //如果x,y之间无节点,则结束查找
return;
}
else { //如果x,y之间具有节点t,则查找t, y
int t = path[x][y];
find(t, y);
cout << "->" << t;
}
return;
}
void wayoutput(MGraph &G, int p)
{
for (int i = 0; i < G.vex; i++)
{
cout << "从 v"<< p <<" 到 v" << i << "的最短路径长度为:" << A[p][i] << endl;
cout << "路径为: "<<p;
find(p, i);
cout << "->" << i << endl;
}
}
时间复杂度O(v3)
注意:主要考手算,路径中存在负权值,这个算法也适合,但是若图中含有带负权值的回路,则不适合。
总结 | BFS算法 | Dijkstra算法 | Floyd算法 |
---|---|---|---|
无权 | ✔ | ✔ | ✔ |
带权 | ✘ | ✔ | ✔ |
带负权 | ✘ | ✘ | ✔ |
带负权回路 | ✘ | ✘ | ✘ |
时间复杂度 | O(|V|2)或O(|V|+|E|) | O(|V|2) | O(|V|3) |
通常用于 | 求无权图的单元最短路径 | 求带权图的单元最短路径 | 求带权图中各顶点间的最短路径 |
3.3 有向无环图
有向无环图∶若一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph)
3.3.1 描述表达式
求用有向无环图描述表达式最少需要多少节点,步骤
i.把各个操作数不重复的排成一排
ii.标出各个运算符的生效顺序(先后顺序有点出入无所谓)
iii.按顺序加入运算符,注意“分层”
iv.从底向上逐层检董同层为运算符是否可以合并
例:(a * b) * (a * b) * (a * b) * c
注意:每个操作数最多出现一次
3.3.2 拓扑排序
AOV网(Activity on vertex Network,用顶点表示活动的网)∶
用DAG图〈有向无环图)表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行
拓扑排序:找到做事的先后顺序
定义1:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
①每个顶点出现且只出现一次。
②若顶点4在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。
定义2:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。
拓扑排序的实现(必须是有向无环图):
i.从AOV网中选择一个没有前驱的顶点并输出。
ii.从网中删除该顶点和所有以它为起点的有向边。
iii.重复i和ii直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
#define MaxvertexNum 100 //图中顶点数目的最大值
typeder struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点的位置
struct Arcode *nextarc; //指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct vNode{ //顶点表结点
vertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxvertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum, arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
int indefree[MaxvertexNum] //当前顶点的入度
int print[MaxvertexNum] //记录拓扑排序序列
bool Topologicalsort ( Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0 ; i<G.vexnum;i++)
if (indegree[i]==0 )
Push (S,i); //将所有入度为0的顶点进栈
int count=0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(s) ){ //栈不空,则存在入度为0的顶点
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for( p=c.vertices [ i].firstarc;p;p=P->nextarc ) {
//将所有i指向的顶点的入度减1,并且将入度减为o的顶点压入栈S
v=p->adjvex;
indegree[v] = indegree[v] - 1;
if(indegree[v] == 0)
Push (S,v); //入度为0,则入栈
}//rof
}//elihw
if (count<G.vexnum)
return false; //排序失败,有向图中有回路
else
return true; //拓扑排序成功
类似的可以实现逆拓扑排序,此时使用邻接矩阵或逆邻接表储存图比较方便。
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(v=0; v<G.vexnum; ++v)
visited[v]=FALSE; //初始化已访问标记数据
for(v=0; v<G.vexnum; ++v) //本代码中是从v=0开始遍历
if(!visited[v])
DFS(G,v);
}
void DFS(Graph G,int v){ //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v ); w>=0; w=NextNeighor(G,v,w)){
if(!visited [w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}//fi
}//rof
print(v); //输出顶点
}
3.3.3 关键路径
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)
AOE网具有以下性质:
i.只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
ii.只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的;
iii.在AOE网中仅有一个入度为O的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
活动a的最早开始时间e(i):指该活动弧的起点所表示的事件的最早发生时间
活动a的最迟开始时间l(i):它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
活动a的时间余量iHN)-e(i):表示在不增加完成整个工程所需总时间的情况下,活动a可以拖延的时间。
若一个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0即l(i)=e(i)的活动a是关键活动由关键活动组成的路径就是关键路径.
步骤:
i.求所有事件的最早发生时间ve( ),当前活动前的活动最快结束的时间
按照拓扑排序序列,依次求各个顶点的ve( ):
ve(源点) = 0; ve(k) = Max{ve(j) + Weight(Vj, Vk)} Vj为Vk的任意前驱
ii.求所有事件的最迟发生时间vl( ),当前活动最晚开始的时间
按逆拓扑排序序列,依次求各个顶点的vl( ):
vl(汇点) = ve(汇点); vl(k) = Min{vl(j) - Weight(Vk, Vj)} Vj为Vk的任意后继
iii.求所有活动的最早发生时间e( ),活动弧的起点事件的最早开始时间
若边<Vk, Vj>表示活动ai,则有e(i)=ve(k)
iv.求所有活动的最迟发生时间I( ),活动弧的终点的最迟发生时间与该活动所需时间的差
若边<Vk, Vj>表示活动ai,则有l(i)=vl(j) - Weight(Vk, Vj)
v.求所有活动的时间余量d( ), d( ) = 0的活动为关键活动(边)
d(i) = l(i) - e(i), d(i) = 0 的活动对应的顶点构成一条路径
若关键活动耗时增加,则整个工程的工期将增长;缩短关键活动的时间,可以缩短整个工程的工期;
当缩短到一定程度时,关键活动可能会变成非关键活动;
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。