1、基本概念
- 完全无向图:图中任意两个不同的顶点间都有一条无向边,这样的无向图称为完全无向图,完全无向图含有 e=n(n-1)/2 条边
- 完全有向图:图中任意两个不同的顶点间都有两条方向相反的弧,这样的有向图称为完全有向图,完全有向图含有 e=n(n-1) 条弧
- 稀疏图和稠密图:若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图
- 关联:边(v,w) 和顶点v 和w 相关联
- 度:无向图:和顶点v 关联的边的数目定义为顶点的度
- 度:有向图:对有向图来说,又分为出度和入度。顶点的出度: 以顶点v为弧尾的弧的数目;顶点的入度: 以顶点v为弧头的弧的数目。顶点的度(TD)=出度(OD)+入度(ID);弧头是箭头的方向,弧尾是无箭头的方向。
- 对无向图:连通图:每个顶点之间都能通过不同的路径互相到达,则称图G是连通图,否则称为非连通图。如能找到一条含全部顶点的路径则说明是连通图。
- 有向图:都有以vi为起点, vj 为终点以及以vj为起点,vi为终点的有向路径,称图G是强连通图,否则称为非强连通图。若G是非强连通图,则极大的强连通子图称为G的强连通分量。
-
图的深度优先搜索算法类似于二叉树的先序遍历
2、生成树和生成森林
(1)图的生成树(包含全部的顶点和n-1条边)
一个连通图(无向图)的生成树是一个极小连通子图,它含有图中全部n个顶点和只有足以构成一棵树的n-1条边,称为图的生成树
关于无向图的生成树的几个结论:
◆ 一棵有n个顶点的生成树有且仅有n-1条边;
◆ 如果一个图有n个顶点和小于n-1条边,则是非连通图;
◆ 如果多于n-1条边,则一定有环;
◆ 有n-1条边的图不一定是生成树。
(2)生成森林
对非连通图,则称各个连通分量的生成树的集合为此非连通图的生成森林。
3、邻接矩阵和邻接表
一个图中,有n条边,e个结点,那么:邻接矩阵的空间复杂度为O(n^2^),与边的个数无关。邻接表的空间复杂度为O(n+e),与图中的结点个数和边的个数都有关。
4、度数与边数的关系
在任何图中,所有顶点的度数之和=边数的2倍,因此,所有顶点的度数之和一定是偶数。
度数:
(1)无向图:和顶点v 关联的边的数目定义为顶点的度
(2)有向图:分为出度和入度
- 顶点的出度: 以顶点v为弧尾的弧的数目;
- 顶点的入度: 以顶点v为弧头的弧的数目。
- 顶点的度(TD)=出度(OD)+入度(ID)
- 所有顶点的入度和=所有顶点的出度和
5、
(一)图的存储结构:邻接表和邻接矩阵类
1、邻接矩阵(用数组表示)
(1)无向矩阵(一定对称)
- 无向图的邻接矩阵必为对称阵,网(弧或边带权的图分别称作有向网或无向网)的邻接矩阵A[i][j]=wij或∞。
- 每一行是对应顶点的度数
- 哪里有1,说明两者之间有一条边
- 由邻接矩阵转为无向矩阵的时候,只看行就可以,哪里有1,就在两者之间连上一条边。注意:无向,所以A与E,E与A之间均有一条边,但是,只需要画一条即可。
(2)有向矩阵(可能对称也可能不对称)
- 有向矩阵的邻接矩阵为非对称矩阵,箭头从行指向列
- 每一行是对应顶点的出度
- 每一列是对应顶点的入度
- 由邻接矩阵转为有向矩阵的时候,只需要看行就可以,如果有从A到B,那么就画一条从A指向B的箭头,往下依次同理
(3)两者的共同点
易于求顶点度(区分有/无向图)、求邻接点,易判断两点间是否有弧或边相连,但不利于稀疏图的存储,因弧不存在时也要存储相应信息,且要预先分配足够大空间。一般,对称矩阵看成无向,非对称矩阵看成有向。
2、邻接表
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode
{
Vertex AdjV; /* 邻接点下标 */
PtrToAdjVNode Next; /* 指向下一个邻接点的指针 */
};
/* 顶点表头结点的定义 */
typedef struct Vnode
{
PtrToAdjVNode FirstEdge; /* 边表头指针 */
} AdjList[MaxVertexNum]; /* AdjList是邻接表类型 */
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode
{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
AdjList G; /* 邻接表 */
};
typedef PtrToGNode LGraph; /* 以邻接表方式存储的图类型 */
不管怎样表示邻接表,都需要有顶点,有边数,邻接表头数组,邻接表链表,不管是怎么写,其实都是在创建这四者。
(1)无向邻接表
- 如果链表上放的是数字的话,那么顶点从0开始,数字即代表该位置处的顶点
- 如果链表上放的是字母的话,就不需要做进一步的转换
- 后面所指的就是与其相关联的顶点,在自己创建邻接表的时候是只要相关联的,放到后面就行,没有任何顺序可言,但是一旦创建,就有一定的关联了,在进行遍历的时候,谁在前面,谁还没有遍历,则先遍历谁
(2)有向邻接表
- 如果链表上放的是数字的话,那么顶点从0开始,数字即代表该位置处的顶点
- 如果链表上放的是字母的话,就不需要做进一步的转换
- 后面所指的就是与其相关联的顶点,在自己创建邻接表的时候是只要相关联的,放到后面就行,没有任何顺序可言,但是一旦创建,就有一定的关联了,在进行遍历的时候,谁在前面,谁还没有遍历,则先遍历谁(1在4前面,0在1前面)
(3)两者的共同点
在有向图的邻接表中不易找到指向该顶点的弧
3、重要知识点
(1)用相邻矩阵法存储图,占用的存储空间数只与图中结点个数有关,而与边数无关,用邻接表法存储图,占用的存储空间数只与图中结点个数和边数都有关
(2)有向图的邻接矩阵可以是对称的,也可以是不对称的,但是无向图的邻接矩阵一定是对称的
(3)设N个顶点E条边的图用邻接表存储,则求每个顶点入度的时间复杂度为O(N+E),设N个顶点E条边的图用邻接矩阵存储,则求每个顶点入度的时间复杂度为O(N方)
(4)对于一个具有N个顶点的无向图,若采用邻接矩阵表示,则该矩阵的大小是N方
(二)连通分量和强连通分量
(1)对于无向图来说分为连通图和连通分量
- 如能找到一条含全部顶点的路径则说明是连通图,也就是说,若无向图G中任意两个顶点之间都有路径相通,则称此图为连通图,否则是非连通图。
- 如果是非连通图,则极大的连通子图称为G的连通分量。
(2)对于有向图来说分为强连通图和强连通分量
- 如能vi ,vj 属于V,都有以vi为起点, vj 为终点以及以vj为起点,vi为终点的有向路径,称图G是强连通图(判断是否为连通图一般看不连接的两个点)//沿着某一个方向能找到一条含全部顶点的回路//则称图G是强连通图,否则称为非强连通图。
- 如果是非强连通图,则其各个极大强连通子图称作它的强连通分量。
- 假设已经判断出一个集合为强连通分量,那么再判断剩下的是否为强连通分量的时候,应该将已经判断出来的点和与其相关联的边删除掉。
- 如果某一个点被孤立起来或者和剩下的没法构成强连通分量(即使有边,仍无法构成强连通分量),也就是没有可以和它组成强连通图的点了,那么它自己就是一个强连通分量,因为,自身可以到达自身。
(三)深度优先搜索和广度优先搜索(能一次搜索完的就是连通图,否则就有连通分量)
1、深度优先遍历(图的深度优先遍历类似于二叉树的先序遍历)
(1)邻接矩阵
-
访问图中的某一个顶点v
-
从v的未被访问的邻接点出发到点v1,访问v1的邻接点,如果v1有未被访问的邻接点,那么就继续访问这个未被访问的邻接点,否则回退到上一个点,一直回退到某一个含有未被访问的邻接的点为止,然后继续进行遍历,如果按照这个方法遍历完之后
-
若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
(2)邻接表
在邻接表中,如果已经给出了一个邻接表,那么在遍历的时候,就有一定的顺序,上图的例子,从1出发,到底是先走到2还是3还是5呢?邻接表谁接在1的后面就先走谁。在邻接表中,谁在前面,谁还没有遍历,则先遍历谁。
2、广度优先遍历(图的广度优先遍历类似于二叉树的层序遍历)
(1)邻接矩阵
- 先从一个点出发,将其所有的邻接点放到一个对列中
- 当放完之后
- 从队头的点开始,找是否有邻接点,如果有的话,再从队尾接上
- 如果没有就队列头部元素出队,然后找新的对列元素时候有邻接点,如果有的话,再从队尾接上
- 之后的过程一样
- 但按照这样的方式找完之后,如果图中还有没有被访问的点的话,就从未访问的点开始进行同样的过程
(2)邻接表
在邻接表中,如果已经给出了一个邻接表,那么在遍历的时候,就有一定的顺序,在邻接表中,谁在前面,谁还没有遍历,则先遍历谁,先将谁放到对列中。
(四)重要结论
1、若无向图G =(V,E)中含N个顶点,要保证图G在任何情况下都是连通的,则需要的边数最少是(N-1)(N-2)/2+1
因为,若保证无向图在任何情况下都是连通的,即任意变动图G中的边,图G始终保持连通,首先需要G的任意N-1个结点构成完全连通子图G1,需要(N-1)(N-2)/2条边,然后在添加一条边使第N结点与G1连起来,共需(N-1)(N-2)/2+1条边
2、如果G是一个有X条边的非连通无向图,那么该图顶点个数最少为多少?
顶点数最少,所以也就是,如果X条边使某一个图构成完全子图,那么再多一个顶点,这个图就是一个非连通无向图了。所以N*(N-1)/2=X,所以顶点数为N+1。
3、设无向图的顶点个数为N,则该图最多有N*(N-1)/2条边
因为,无向图拥有的最多边数的时候是完全图,为N*(N-1)/2条边
4、在一个无向图中,所有顶点的度数之和等于所有边数的2倍,在一个有向图中,所有顶点的入度与出度之和等于所有边之和的2倍,即所有顶点的度数之和是边数的两倍,度数一定是偶数
5、在任一有向图中,所有顶点的入度之和与所有顶点的出度之和相等
6、图可以没有边,但是顶点数一定不能为0
7、对于一个具有N个顶点的无向图,要连通所有顶点至少需要N-1条边,需要最少边数的情况是一条直线
8、图的广度优先遍历类似于二叉树的层序遍历,图的深度优先遍历类似于二叉树的先序遍历
6、
7、
8、
9、
- 连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。
- 强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。
- 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
10、
11、
12、
(一)图的最小生成树Prim算法和Kruskal算法
- 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
1、Prim算法(添加顶点):取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 w。在添加的顶点 w 和已经在生成树上的顶点v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 w 之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有 n 个顶点为止。
(1)图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
(2)在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
(3)重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
- 先列出V1到各个点的权值,根据列出的值,就可以确定到其中一个顶点的最小值,确定之后就不能再改变了,比如V1到V2
- 然后,确定点所对应的行,对除去确定的值之外的值进行跟新
- 然后再取,再跟新
取确定的最小的一点——除确定的值之外,若小于确定的点,则换——取、换................
2、Kruskal算法(添加边):将权值从小到大排序,然后依次选取最小的边,如果构成圈则不取,如果没构成圈则取
紫色的是因为构成环而无法选取的边
(1)把图中的所有边按代价从小到大排序;即使是同时为最小的数,如果符合条件,就加上,不能因为是同一个数就再加一次
(2)把图中的n个顶点看成独立的n棵树组成的森林;
(3) 按权值从小到大选择边,所选的边连接之后不能构成环,如果构成环,则舍弃这条边继续看下一条边
(4)重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
3、做题详知:https://blog.csdn.net/fighting123678/article/details/84144998
(二)最短路径Dijkstra算法和Floyd算法
有权图的最短路径求法
Dijkstra算法:https://blog.csdn.net/qq_35644234/article/details/60870719?tdsourcetag=s_pcqq_aiomsg(与Prim过程相似,但是不同,可以按照Prim的思想,全部都求出,然后再做题)
假设想求从V1到各个点的最短路径
从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
- 先列出V1到各个点的权值,根据列出的值,就可以确定到其中一个顶点的最短路径,确定之后就不能再改变了,比如V1到V2
- 然后看,确定这个点对应的列(比如V2)的这个点与邻接点之间的权值,比如V2与V3之间连通,V2与V4之间连通,就可以求出V1—V3,V1—V4,如果比原来位置上的数小的话,就替换成这个小的数,当所有与V2相邻的数都完成这一步之后
- 求出此时的一个最小值,这个值就确定了,不能再修改,然后再像第二步一样进行代换,再确定最小值
- .........................................
- 直到除去V1点外其余的点都确定为止
取确定的最小的一点——借这点所在的列更新此行的值——除确定的值之外,若小于确定的点,则换——取、更新、换................
Floyd算法:https://blog.csdn.net/jeffleo/article/details/53349825 (自认为最好理解的一个博客)
https://blog.csdn.net/qq_35644234/article/details/60875818(从i到j有好多的方法,可以有好多个中间点,在矩阵中,从i到k,从k到j,不断的一个个的试,最终最小的即为所求)
无权图最短路径求法
无权图的最短路径也就是要求两点之间最少几跳可达,那么我们可以这样,用广度遍历,从起点开始一层层遍历,如果第一次遍历到终点,那么肯定是最短路径。
2、做题详知:https://blog.csdn.net/fighting123678/article/details/84146402
最短路径和最小生生成树代码总结https://blog.csdn.net/fighting123678/article/details/84145908
(三)拓扑排序与关键路径
1、拓扑排序步骤
https://blog.csdn.net/qq_35644234/article/details/60578189
- 从图中没有前驱的顶点中择一并输出(栈/队列)
- 从图中“删除”此顶点及所有从其出发的弧(也就是相关的弧)
- 重复上述两步,至图空(得一全序),或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
2、关键路径步骤
https://blog.csdn.net/key_mql/article/details/52237595
ve(j)是从某一个确定的顶点到其余顶点可行路径中的最大值
vl(j)中的j是ve(j)中最大值所对应的顶点,vl(j)是最大值-从别的顶点到此顶点的最大值(也就是最小值)
e(i)是弧尾的ve(i)
l(i)是弧头的vl(i)-权值
如果e(i)==l(i),那么这个点就是关键路径上的一个点
13、
14、
所以说,在邻接表中也应当注意顺序
15、
16、
17、
18、
19、
ca=cb+ba;ba=bc+ca;所以3和4正确
19、邻接矩阵存储图的深度优先遍历 (20 分)
试实现邻接矩阵存储图的深度优先遍历。
函数接口定义:
void DFS( MGraph Graph, Vertex V, void (*Visit)(Vertex) );
其中MGraph
是邻接矩阵存储的图,定义如下:
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
WeightType G[MaxVertexNum][MaxVertexNum]; /* 邻接矩阵 */
};
typedef PtrToGNode MGraph; /* 以邻接矩阵存储的图类型 */
函数DFS
应从第V
个顶点出发递归地深度优先遍历图Graph
,遍历时用裁判定义的函数Visit
访问每个顶点。当访问邻接点时,要求按序号递增的顺序。题目保证V
是图中的合法顶点。
裁判测试程序样例:
#include <stdio.h>
typedef enum {false, true} bool;
#define MaxVertexNum 10 /* 最大顶点数设为10 */
#define INFINITY 65535 /* ∞设为双字节无符号整数的最大值65535*/
typedef int Vertex; /* 用顶点下标表示顶点,为整型 */
typedef int WeightType; /* 边的权值设为整型 */
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
WeightType G[MaxVertexNum][MaxVertexNum]; /* 邻接矩阵 */
};
typedef PtrToGNode MGraph; /* 以邻接矩阵存储的图类型 */
bool Visited[MaxVertexNum]; /* 顶点的访问标记 */
MGraph CreateGraph(); /* 创建图并且将Visited初始化为false;裁判实现,细节不表 */
void Visit( Vertex V )
{
printf(" %d", V);
}
void DFS( MGraph Graph, Vertex V, void (*Visit)(Vertex) );
int main()
{
MGraph G;
Vertex V;
G = CreateGraph();
scanf("%d", &V);
printf("DFS from %d:", V);
DFS(G, V, Visit);
return 0;
}
/* 你的代码将被嵌在这里 */
输入样例:给定图如下
5
输出样例:
DFS from 5: 5 1 3 0 2 4 6
答案:
void DFS( MGraph Graph, Vertex V, void (*Visit)(Vertex) )
{
if(!Visited[V])
{
Visit(V);
Visited[V]=true;
}
int i;
for(i=0;i<Graph->Nv;i++)
{
if(Graph->G[V][i]==1&&!Visited[i])
{
DFS(Graph,i,Visit);
}
}
}
20、邻接表存储图的广度优先遍历 (20 分)
试实现邻接表存储图的广度优先遍历。
函数接口定义:
void BFS ( LGraph Graph, Vertex S, void (*Visit)(Vertex) );
其中LGraph
是邻接表存储的图,定义如下:
/* 邻接点的定义 */
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode{
Vertex AdjV; /* 邻接点下标 */
PtrToAdjVNode Next; /* 指向下一个邻接点的指针 */
};
/* 顶点表头结点的定义 */
typedef struct Vnode{
PtrToAdjVNode FirstEdge; /* 边表头指针 */
} AdjList[MaxVertexNum]; /* AdjList是邻接表类型 */
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
AdjList G; /* 邻接表 */
};
typedef PtrToGNode LGraph; /* 以邻接表方式存储的图类型 */
函数BFS
应从第S
个顶点出发对邻接表存储的图Graph
进行广度优先搜索,遍历时用裁判定义的函数Visit
访问每个顶点。当访问邻接点时,要求按邻接表顺序访问。题目保证S
是图中的合法顶点。
裁判测试程序样例:
#include <stdio.h>
typedef enum {false, true} bool;
#define MaxVertexNum 10 /* 最大顶点数设为10 */
typedef int Vertex; /* 用顶点下标表示顶点,为整型 */
/* 邻接点的定义 */
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode{
Vertex AdjV; /* 邻接点下标 */
PtrToAdjVNode Next; /* 指向下一个邻接点的指针 */
};
/* 顶点表头结点的定义 */
typedef struct Vnode{
PtrToAdjVNode FirstEdge; /* 边表头指针 */
} AdjList[MaxVertexNum]; /* AdjList是邻接表类型 */
/* 图结点的定义 */
typedef struct GNode *PtrToGNode;
struct GNode{
int Nv; /* 顶点数 */
int Ne; /* 边数 */
AdjList G; /* 邻接表 */
};
typedef PtrToGNode LGraph; /* 以邻接表方式存储的图类型 */
bool Visited[MaxVertexNum]; /* 顶点的访问标记 */
LGraph CreateGraph(); /* 创建图并且将Visited初始化为false;裁判实现,细节不表 */
void Visit( Vertex V )
{
printf(" %d", V);
}
void BFS ( LGraph Graph, Vertex S, void (*Visit)(Vertex) );
int main()
{
LGraph G;
Vertex S;
G = CreateGraph();
scanf("%d", &S);
printf("BFS from %d:", S);
BFS(G, S, Visit);
return 0;
}
/* 你的代码将被嵌在这里 */
输入样例:给定图如下
2
输出样例:
BFS from 2: 2 0 3 5 4 1 6
答案:
void BFS ( LGraph Graph, Vertex S, void (*Visit)(Vertex) )
{
int q[1000];
int tail=0,head=0;
if(!Visited[S])
{
Visit(S);
Visited[S]=true;
q[tail++]=S;
}
while(tail!=head)
{
PtrToAdjVNode p=Graph->G[q[head++]].FirstEdge;
while(p)
{
Vertex pos=p->AdjV;
if(!Visited[pos])
{
Visit(pos);
Visited[pos]=true;
q[tail++]=pos;
}
p=p->Next;
}
}
}