图
图的定义和基本术语
1、图的定义
“图” G可以表示为两个集合:G =(V, E)。每条边是一个顶点对(v, w)∈ E , 并且 v, w ∈ V;
通常:
|V| vertex表示顶点的数量(|V| ≥ 1);
|E| edge表示边的数量(|E| ≥ 0);
集合V = { B,C,D,F,H,L,W,X,Y,Z };
顶点集: |V| = 10;
集合E = { (Z,B),(Z,W),(B,W),(B,L),(B,D), (D,L),(W,X),(W,L),(L,H),(L,F),(X,H), (X,Y),(H,Y),(H,F),(H,C), (F,C),(Y,C) };
边集: |E|= 17;
2、图的基本术语
1、有向图
若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为,其 中v,w是顶点,v称为弧尾,w称为弧头,称为从顶点v到顶点w的弧,也称v邻接到w,或 w邻接自v;
G =(V ,E )
V = { 1 , 2 , 3 }
E = { < 1 , 2 > , < 2 , 1 > , < 2 , 3 > }
2、无向图
若E是无向边(简称边) 的有限集合时,则图G为无向图。 边是顶点的无序对,记为(v, w)或(w, v),因为(v, w) = (w, v),其中v、w是顶点。可以说顶点w和顶点v互为邻接点。 边(v, w) 依附于顶点w和v,或者说边(v, w)和顶点v、w相关联;
G =(V , E)
V = { 1 , 2 , 3 , 4 }
E = { ( 1 , 2 ) ,( 1 , 3 ) ,( 1 , 4 ) ,( 2 , 3 ) , ( 2 , 4 ) , ( 3 , 4 ) }
3、邻接点
如果(v, w)或 < v, w >是图中任意一条边,那么称v和w互为“邻接点(Adjacent Vertices)”;
4、简单图与多重图
一个图若满足:①不存在重复边;②不存在顶点到自身的边,则称图为简单图。若图中某两个结点 之间的边数多于一条,又允许顶点通过同一条边和自己关联,则为多重图。多重图的定义和简单图是 相对的;
5、无向完全图
无向图中任意两个顶点之间都存在边,有n(n-1)/2条边的称为无向完全图;
6、有向完全图
有向图中任意两个顶点之间都存在方向相反的两条弧,有n(n-1)条弧的称为有向完全图;
7、顶点的度(degree)、入度(in-degree)、出度(out-degree)
度(v) = 与顶点v相关的边数 ID(v) = 3; OD(v) = 1; TD(v) = 4 给定 n 个顶点和 e 条边的图G, 则有:
8、路经相关概念
路径:顶点Vp到顶点Vq之间的一条路径是指顶点序列;
回路:第一个顶点和最后一个顶点相同的路径称为回路或环;
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径
无环图:不存在任何回路的图;
有向无环图:不存在回路的有向图,也称DAG;
路径长度:路径上边的数目;
9、连通图(强连通图)
在无(有)向图G=( V, {E} )中,若对任何两个顶点 v、u 都存在从v 到 u 的路径,则称 G是连通图(强连通图);
10、无向图的顶点连通、连通图、连通分量:
如果无向图从一个顶点vi到另一个顶点vj (i≠j)有路径,则称顶点vi和vj是“连通的 (Connected)” 无向图中任意两顶点都是连通的,则称该图是“连通图(Connected Graph)” 无向图的极大连通子图称为“连通分量(Connected Component)” 连通分量的概念包含4个要点:子图、连通、极大顶点数、极大边数;
11、有向图的强连通图、强连通分量:
有向图中任意一对顶点vi和vj (i≠j)均既有从vi到vj的路径,也有从vj到vi的路径,则称该 有向图是“强连通图(Strongly Connected Graph)” 有向图的极大强连通子图称为“强连通分量(Strongly Connected Component)”。 强连通分量的概念也包含前面4个要点;
12、子图
设有两个图G=(V,{E})、G1=(V1,{E1}),若V1∈ V,E1 ∈ E ,则称 G1是G的子图。 例:(b)、© 是 (a) 的子图;
13、极小连通子图:
该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通。 极大连通子图(包含边最多的连通子图)即本身(包含所有边);
14、生成树:
包含无向图G 所有顶点的极小连通子图;
极小连通子图:必定包含且仅包含G的n-1条边;
生成树有可能不唯一;
生成树满足下面4个条件之一(完全等价):
① G有n-1条边,且没有环;
② G有n-1条边,且是连通的;
③ G中的每一对顶点有且只有一条路径相连;
④ G是连通的,但删除任何一条边就会使它不连通;
图的存储结构
1、图的类型定义
CreateGraph(&G,V,VR)
初始条件:V是图的顶点集,VR是图中弧的集合;
操作结果:按V和VR的定义构造图G;
DFSTraverse(G)
初始条件:图G存在;
操作结果:对图进行深度优先遍历;
BFSTraverse(G)
初始条件:图G存在;
操作结果:对图进行广度优先遍历;
2、图的存储结构
1、图没有真正意义上的顺序存储结构,但可以借助二维数组来表示图的元素之间的关系;
2、以顶点为核心,建立邻接点和弧的关系;
3、邻接矩阵
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系);
设图 A = (V, E) 有 n 个顶点,则图的邻接矩阵是一个二维数组 A.Edge[n][n], 定义为:
1、图:两个顶点和边或弧两部分组成,合在一起比较困难,可以分为两个结构来存储;
2、顶点:不区分大小,主次,用一个一维数组来存储,记录各个顶点的信息;
3、边或弧:顶点和顶点的关系,用邻接矩阵来存储,表示各个顶点之间的邻接关系;
无向图
有向图
创建无向图
//用两个数组分别存储顶点表和邻接矩阵
#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;
Status CreateUDN(AMGraph &G){ //采用邻接矩阵表示法,创建无向网G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i = 0; i<G.vexnum; ++i)
cin>>G.vexs[i]; //依次输入点的信息
for(i = 0; i<G.vexnum;++i) //初始化邻接矩阵,边的权值均置为极大值
for(j = 0; j<G.vexnum;++j)
G.arcs[i][j] = MaxInt;
for(k = 0; k<G.arcnum;++k){ //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边依附的顶点及权值
i = LocateVex(G, v1);
j = LocateVex(G, v2); //确定v1和v2在G中的位置
G.arcs[i][j] = w; //边( v1, v2 )的权值置为w
G.arcs[j][i] = G.arcs[i][j]; //置( v1, v2 )的对称边( v2, v1 )的权值为w
} //for
return OK;
} //CreateUDN
查询顶点是否在顶点表中,若在则返回顶点表中的下标;否则返回-1
int LocateVex(MGraph G,VertexType u) {
int i;
for(i=0;i<G.vexnum;++i)
if(u==G.vexs[i])
return i;
return -1;
}
4、邻接表
对每个顶点vi 建立一个单链表,把与vi有关联的边的信息链接起来,每个结点设为3个域;
每个单链表有一个头结点(设为2个域),存vi信息;
每个单链表的头结点另外用顺序存储结构存储;
出度 OD(Vi)=单链出边表中链接的结点数
入度 ID(Vi)=邻接点域为Vi的弧个数
度 TD(Vi) = OD( Vi ) + I D( Vi )
//创建无向网
#define MVNum 100 //最大顶点数
typedef struct ArcNode{ //边结点
int adjvex; //该边所指向的顶点的位置
struct ArcNode * nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
typedef struct VNode{
VerTexType data; //顶点信息
ArcNode * firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型
typedef struct{
AdjList vertices; //邻接表
int vexnum, arcnum; //图的当前顶点数和边数
}ALGraph;
Status CreateUDG(ALGraph &G){ //采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i = 0; i<G.vexnum; ++i){ //输入各点,构造表头结点表
cin>> G.vertices[i].data; //输入顶点值
G.vertices[i].firstarc=NULL; //初始化表头结点的指针域为NULL
} //for
for(k = 0; k<G.arcnum;++k){ //输入各边,构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
i = LocateVex(G, v1);
j = LocateVex(G, v2);
p1=new ArcNode; //生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc= G.vertices[i].firstarc;
G.vertices[i].firstarc=p1; //将新结点*p1插入顶点vi的边表头部
p2=new ArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex=i; //邻接点序号为i
p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2; //将新结点*p2插入顶点vj的边表头部
} //for
return OK;
} //CreateUDG
5、邻接表与邻接矩阵
1、联系:
邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数;
2、区别:
(1)对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一 (链接次序与顶点编号无关);
(2)邻接矩阵的空间复杂度为O(n2),而邻接表的空间复杂度为O(n+e);
3、用途:
邻接矩阵多用于稠密图;而邻接表多用于稀疏图;
6、十字链表—用于有向图
- 结点表中的结点的表示:data firstin firstout
1、data:顶点数据域;
2、firstin:入边单链表头指针;
3、firstout:出边单链表头指针;
- 边结点表中的结点的表示:tailvex headvex Tlink Hlink info
1、 info:边结点的数据域,保存边的权值等;
2、tailvex:本条边的出发结点的地址;
3、Hlink:终止结点相同的边中的下一条边的地址;
4、headvex:本条边的终止结点的地址;
5、Tlink:出发结点相同的边中的下一条边的地址;
7、邻接多重表—用于无向图
- 结点表中的结点的表示: data firstedge
1、Data:结点的数据域,保存结点的数据值;
2、firstedge: 结点的指针域,给出自该结点出发 的的第一条边的边结点的地址;
- 边结点表中的结点的表示:mark ivex ilink jvex jlink info
1、ivex: 本条边依附的一个结点的地址;
2、ilink: 依附于该结点(地址由ivex给出 )的边中的下一条边的地址;
3、info: 边结点的数据域,保存边的权值等;
4、mark:边结点的标志域,用于标识该条边是否被访问过;
5、 jvex: 本条边依附的另一个结点的地址;
6、jlink: 依附于该结点(地址由jvex给出)的边中的下 一条边的地址;
图的遍历
1、图的遍历定义
【问题】 采用什么策略,可以不遗漏地“走遍”图的每个顶点?
【迷宫探索】有一个地下通道迷宫,通道(假设它都是直的)所有交叉点(包括通道的端点) 上都有一盏灯和一个开关。请问如何从某个起点开始在迷宫中点亮所有的灯并回到起点?以 什么样的策略,走遍下图迷宫的所有通道的交叉点(顶点0-7)?
【问题】怎样避免重复访问?
解决思路:设置辅助数组 visited [n ],用来标记每个 被访问过的顶点。初始状态为0 i 被访问,改 visited [i]为1,防止被多次访问;
2、深度优先搜索
- 深度优先搜索(Depth First Search,简称DFS )
步骤:
① 从图中某个顶点v出发,访问v;
② 找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步 骤,直至刚访问过的顶点没有未被访问的邻接点为止;
③ 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点, 访问该顶点;
④ 重复步骤 ②和③,直至图中所有顶点都被访问过,搜索结束;
计算机如何实现DFS?
void DFS(AMGraph G, int v){ //图G为邻接矩阵类型
cout<<v; visited[v] = true; //访问第v个顶点
for(w = 0; w< G.vexnum; w++) //依次检查邻接矩阵v所在的行
if((G.arcs[v][w]!=0)&& (!visited[w]))
DFS(G, w); //w是v的邻接点,如果w未访问,则递归调用DFS
}
void DFS(ALGraph G, int v){ //图G为邻接表类型
cout<<v;
visited[v] = true; //访问第v个顶点
p= G.vertices[v].firstarc; //p指向v的边链表的第一个边结点
while(p!=NULL){ //边结点非空
w=p->adjvex; //表示w是v的邻接点
if(!visited[w]) DFS(G, w); //如果w未访问,则递归调用DFS
p=p->nextarc; //p指向下一个边结点
}
}
- 深度优先搜索效率分析
用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)。 用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n 个 头结点的时间,时间复杂度为O(n+e);
结论:稠密图适于在邻接矩阵上进行深度遍历;
稀疏图适于在邻接表上进行深度遍历;
3、广度优先搜索
- 广度优先搜索(Breadth First Search,简称BFS )
void Level_Traverse (BiTree T){ //二叉树的层次遍历
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q, T); //T进队
while(!QueueEmpty(Q)){ //队列非空
DeQueue(Q, u); //队头元素出队并置为u
cout<<u; //访问
if (p->lchild!=NULL) EnQueue(Q, T->lchild);
if (p->rchild!=NULL) EnQueue(Q, T->rchild);
} //while
}
- 广度优先搜索效率分析
如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整 一行( n 个元素),总的时间代价为O(n2);
用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加 上访问 n个头结点的时间,时间复杂度为O(n+e);
void BFS (Graph G, int v){ //按广度优先非递归遍历连通图G
cout<<v; visited[v] = true; //访问第v个顶点
InitQueue(Q); //辅助队列Q初始化,置空
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的尚未访问的邻接顶点
cout<<w; visited[w] = true;EnQueue(Q, w); //w进队
} //if
} //while
} //BFS
FirstAdjVex()函数代码实现的算法思路:
1、通过for循环遍历G的邻接表;
2、若邻接表某个单元存放值为1,则表明形参结点和i号结点之间有边,则返回此邻接点下标i;
3、若循环结束未找到相关邻接点,则返回-1;
int FirstAdjVex(Graph G, int v) { //返回v的第一个邻接表编号,没有则返回-1
for (int i = 0; i < G.vexnum; i++) {
if (G.arcs[v][i] == 1) //邻接表该处为1,表明形参结点和i号结点之间有边
return i;
}
return -1;
}
NextAdjVex()函数代码实现的算法思路:
1、通过for循环从w+1位置处开始遍历图的邻接表;
2、若找到某数组单元存放值为1,则返回此邻接点的下标i;
3、若循环结束,没找到相应邻接点,则返回-1;
int NextAdjVex(Graph& G, int v, int w) {
for (int i = w + 1; i < G.vexnum; i++) {
if (G.arcs[v][i] == 1)
return i;
}
return -1;
}
图的应用
1、最小生成树
极小连通子图:该子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通;
生成树:包含图G所有顶点的极小连通子图(n-1条边);
【典型用途】:欲在n个城市间建立通信网,则n个城市应铺n-1条线路;但因为每条线路都会有对 应的经济成本,而n个城市可能有n(n-1)/2 条线路,那么,如何选择n–1条线路,使总费用最少?
在网的多个生成树中,寻找一个各边权值之和最小的生成树。
- 贪心算法(Greedy Algorithm)
- 普里姆算法(Prim)—归并顶点
算法思想:
1、初始化 U={v}。v到其他顶点的所有边为侯选边;
2、重复以下步骤n-1次,使得其他n-1个顶点被加入到U中;
(1)从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
(2)考察当前V-U中的所有顶点 j,修改候选边;若(j,k)的权值小于原来和顶点 k关联的候选边,则用 (k,j) 取代后者作为候选边。
- 克鲁斯卡尔算法(Kruskal)—归并边
算法思想:
1、置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一 个顶点都构成一个连通分量);
2、将图G中的边按权值从小到大的顺序依次选取:
(1)若选取的边未使生成树T形成回路,则加入TE;
(2)否则舍弃,直到TE中包含(n-1)条边为止。
2、最短路径
【典型用途】:城市A到城市B有多条线路,但每条线路的交通费(或所需时间)不同,那么,如何 选择一条线路,使总费用(或总时间)最少?
在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径, 即最短路径。(注:最短路径与最小生成树不同,路径上不一定包含n个顶点);
两种常见的最短路径问题:
1、单源最短路径—用Dijkstra(迪杰斯特拉)算法;
2、所有顶点间的最短路径—用Floyd(弗洛伊德)算法;
- 迪杰斯特拉算法(Dijkstra)
主要特点:是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止;
- 弗洛伊德算法(Floyd )
弗洛伊德算法定义了两个二维矩阵:
1、二维数组 Path[i][j]:最短路径上顶点 Vj 的前驱结点的序号;
2、二维数组D[i][j]:记录顶点 Vi和Vj之间的最短路径长度。 初始时设置一个n阶方阵,令其对角线元素为0,若存在弧,< Vi,Vj >,则对应元素为权值;否则 为∞;逐步在原直接路径中增加中间顶点,若加入中间顶点后路径变短,则修改;否则维持原状,直 到所有顶点试探完毕。
3、拓扑结构
用有向图来描述一个工程或系统的进行过程。 一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成。
1、AOV网(Activity On Vertices)—用顶点表示活动的网络;
2、AOE网(Activity On Edges)—用边表示活动的网络;
在AOV网是顶点表示活动的网,它只描述了活动之间的约束关系,而AOE网是用有向边表示活动, 边上的权值表示活动持续的时间。 AOE网是建立在AOV网基础之上(活动之间约束关系没有矛盾),再来分析完成整个工程至少需要 多少时间,或者为缩短完成工程所需时间,应当加快那些活动等问题。
【典型用途】:某大学计算机专业部分必修的课程以及修学这些课程的先后顺序关系。
1、如果找得到任何一个入度为0的顶点v,则2,否则4;
2、输出顶点v,并从图中删除该顶点以及与其相连的所有边;
3、对改变后的图重复这一过程,转1;
4、如果已经输出全部顶点,则结束;否则该有向图不是DAG。
4、关键路径
【典型用途】“工程完成的最早时间是什么时候?”、“一个工程中哪些活动可以适当延迟,可以 延迟多长时间,而不影响整个工期?”——非关键活动等。
AOE网—带权的有向无环图;
顶点—事件或状态;
弧(有向边) —活动及发生的先后关系;
权—活动持续的时间;
起点—入度为0的顶点(只有一个);
终点—出度为0的顶点(只有一个);
AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,边上的权值表示 活动的持续时间,称这样的有向图叫做边表示活动的网,简称AOE网。AOE网中没有入边的顶点称 为始点(或源点),没有出边的顶点称为终点(或汇点);
AOE网的性质:
1、只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
2、只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。
AOE网应用举例
请问汽车厂造一辆汽车,最短需要多少时间? 其中生产轮子:0.5天,发动机3天,底盘2天,外壳2天,其他零件2天, 所有零件集中到一起,0.5天,组装成车并完成测试:2天。
关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。
关键活动:关键路径上的活动称为关键活动。
关键路径可能不只一条,重要的是找到关键活动;
要找出关键路径,必须找出关键活动, 即不按期完成就会影响整个工程完成的活动。
求关键路径的算法步骤:
1、从源点出发,令ve(源点)= 0,按拓扑有序求其余顶点的最早发生时间ve()。
2、从汇点出发,令vl(汇点)= ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl();
3、根据各顶点的ve()值求所有弧的最早开始时间e();
4、根据各顶点的vl()值求所有弧的最迟开始时间l();
5、求AOE网中所有活动的差额d(),找出所有d() = 0的活动构成关键路径。
链图片转存中…(img-KxsVxHWw-1728399612847)]
AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,边上的权值表示 活动的持续时间,称这样的有向图叫做边表示活动的网,简称AOE网。AOE网中没有入边的顶点称 为始点(或源点),没有出边的顶点称为终点(或汇点);
AOE网的性质:
1、只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始;
2、只有在进入某顶点的各活动都结束,该顶点所代表的事件才能发生。
AOE网应用举例
请问汽车厂造一辆汽车,最短需要多少时间? 其中生产轮子:0.5天,发动机3天,底盘2天,外壳2天,其他零件2天, 所有零件集中到一起,0.5天,组装成车并完成测试:2天。
[外链图片转存中…(img-dWU9nHer-1728399612848)]
关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。
关键活动:关键路径上的活动称为关键活动。
关键路径可能不只一条,重要的是找到关键活动;
要找出关键路径,必须找出关键活动, 即不按期完成就会影响整个工程完成的活动。
求关键路径的算法步骤:
1、从源点出发,令ve(源点)= 0,按拓扑有序求其余顶点的最早发生时间ve()。
2、从汇点出发,令vl(汇点)= ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl();
3、根据各顶点的ve()值求所有弧的最早开始时间e();
4、根据各顶点的vl()值求所有弧的最迟开始时间l();
5、求AOE网中所有活动的差额d(),找出所有d() = 0的活动构成关键路径。