一 图的基本概念
图(Graph)G由顶点集合V(G)和边集合E(G)构成
- 注意:线性表可以是空表,树可以是空树,但图不能是空图。就是说图不能一个顶点也没有,但边集可以为空,即V一定非空,E可以为空。
1 无向图
在图G中,如果代表边的顶点对是无序的,则称G为无向图。边记为(v,w),v,w互为邻接点。
2 有向图
如果表示边的顶点对是有序的,则称G为有向图。有向边(弧)记为<v,w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从v到w的弧。表示为:G1 = (V1,E1) V1 = {123}
3 顶点的度
无向图:顶点 i 连接的边数称为该顶点的度。(全部顶点的度的和=边数的2倍,因为每条边关联两个顶点)
有向图:顶点 i 的入度与出度之和为顶点的度。(全部顶点的入度和=全部顶点的出度和=边数)
- 入度:以顶点 i 为终点的入边的数目称为该顶点的入度。
- 出度:以顶点 i 为始点的出边的数目,称为该顶点的出度。
4 完全图
无向图:每两个顶点之间都存在着一条边,称为完全无向图,包含有n(n-1)/2条边。
有向图:每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有**n(n-1)**条边。
5 稠密图、稀疏图
当一个图接近完全图时,则称为稠密图。
相反,当一个图含有较少的边数(即当e << n(n-1))时,则称为稀疏图。
6 子图
设有两个图G=(V,E)和G’=(V’,E’),若V’是V的子集,即V’⊆V,且E’是E的子集,即E’⊆E,则称G’是G的子图。
在这里插入图片描述
7 路径和路径长度和回路
在一个图G=(V,E)中,从顶点i到顶点j的一条路径为顶点序列 i,i1,i2,…,im,j。
路径长度指一条路径上经过的边的数目。
第一个顶点和最后一个顶点相同的路径称为回路或环。
8 简单路径、简单回路
路径序列中,顶点不重复出现的路径称为简单路径。
除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
9 连通、连通图、连通分量
无向图中,若从顶点i到顶点j有路径,则称顶点i和j是连通的。
若图中任意两个顶点都连通,则称为连通图,否则称为非连通图。
无向图G中的极大连通子图称为G的连通分量。显然,任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。
10 强连通图、强连通分量
在有向图中,若从顶点i到顶点j有路径,则称从顶点i到j是连通的。
若图G中的任意两个顶点i和j都连通,即从顶点i到j和从顶点j到i都存在路径,则称图G是强连通图。
有向图G中的极大强连通子图称为G的强连通分量。显然,强连通图只有一个强连通分量,即本身。非强连通图有多个强连通分量。
- 注意:在无向图中讨论连通性,在无向图中讨论强连通性。
11 权和网
图中每一条边都可以附带有一个对应的数值,这种与边相关的数值称为权。边上带有权的图称为带权图,也称作网。
12 距离
如果从顶点u出发到顶点v的最短路径存在,则称此路径长度为u到v的距离,若u到v无路径,则距离记为∞
二 图的存储和基本操作
一图的存储
图主要有两种存储结构:邻接矩阵、邻接表。
邻接矩阵
邻接矩阵是表示顶点之间相邻关系的矩阵。设G=(V,E)是具有n (n>0)个顶点的图,顶点的编号依次为0~n-1。则图G的邻接矩阵A表示为n×n的二维数组。
-
A[i][j] = 1表示<i,j>∈E(G)或(i,j)∈E(G)。
-
带权图 A[i][j] = wij表示(i,j)∈E(G)或<i,j>∈E(G),0或∞表示(i,j)∉E(G)或<i,j>∈E(G)
const int N = 100; //顶点数目
typedef struct {
int no; //顶点编号
infoType info; //顶点其他信息
}VertexType;
typedef struct {
int edges[N][N]; //邻接矩阵
int n,e; //顶点数、边数
VertexType v[N]; //存放顶点信息
}MGraph;
- 适合稠密图
邻接表
对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
typedef struct ANode
{
int adjvex; //该边的终点编号
struct ANode *nextarc; // 下一条边的指针
//InfoType info; //该边的权值等信息
}ArcNode; //边结点类型
typedef struct Vnode
{
Vertex data; //顶点信息
ANode *firstarc; //指向第一条边的指针
}VNode; //头结点类型
typedef struct
{
VNode adjList[MAXV] ; //邻接表
int n,e; //图中顶点数n和边数e
}AdjGraph;
二 图的基本操作
对邻接矩阵操作很简单,这里介绍邻接表的基本操作
void CreateAdj(AdjGraph *&G) //创建图的邻接表
{
int i,j,k,w;
ArcNode *p;
G = (AdjGraph *)malloc(sizeof(AdjGraph));
cout << "请输入图的顶点数和弧的数目:";
cin >> G->n >> G->e;
for (i=0; i < G->n; i++){
//给邻接表中所有头结点的指针域置初值
G->adjList[i].firstarc = NULL;
G->adjList[i].data = i;
}
cout<<"请输入每条边的起始点和终止点的编号及边的权值,以空格隔开:\n";
for(k = 0; k < G->e; ++k){ //输入各边,构造邻接表
cin >> i >> j >> w; //输入一条边依附的两个顶点的编号
p=(ArcNode *)malloc(sizeof(ArcNode)); //生成一个新的边结点*p
p->adjvex=j; // 邻接点序号为j
p->weight=w; //头插入到边结点链表中
p->nextarc = G->adjList[i].firstarc;
G->adjList[i].firstarc=p;
}
}
void DispAdj(AdjGraph *G) //输出邻接表G
{ int i;
ArcNode *p;
for (i=0;i < G->n; i++)
{
p = G->adjList[i].firstarc;
printf("%3d: ",i);
while (p != NULL)
{
printf("%3d[%d]→",p->adjvex,p->weight);
p = p->nextarc;
}
printf("\n");
}
}
void DestroyAdj(AdjGraph *&G) //销毁邻接表
{
int i; ArcNode *pre,*p;
for (i=0;i < G->n;i++) //扫描所有的单链表
{
pre=G->adjList[i].firstarc;//p指向第i个单链表的首结点
if (pre != NULL)
{
p = pre->nextarc;
while (p != NULL) //释放第i个单链表的所有边结点
{
free(pre);
pre = p;
p = p->nextarc;
}
free(pre);
}
}
free(G); //释放头结点数组
}
三 图的遍历
从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历,图的遍历得到的顶点序列称为图遍历序列。
深度优先搜索(DFS)
设置一个visited[]全局数组,visited[i] = false表示顶点i没有访问; visited[i] = true表示顶点i记经访问过。
(1)从图中某个初始顶点v出发,首先访问初始顶点v。
(2)选择一个与顶点v相邻且没被访问过的顶点w,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
bool visited[MAX_V]; //记录顶点访问信息
void DFS(AdjGraph *G,int v){ //从顶点v出发,开始遍历
ArcNode *p;
int w;
visit(v); //访问结点v的数据域
visited[v] = true; //该节点已被访问
p = G->adjList[v].firstarc; //p指向顶点v的第一条边结点
while (p != NULL)
{
w = p->adjvex; //取出p中下一个节点的编号
if(visited[w] == 0) //如果w没被访问过,则访问
DFS(G,w); //若w顶点未访问,递归访问它
p = p->nextarc; //继续寻找下一个节点
}
}
void DFSTravel(AdjGraph * G){
for(int i = 0;i < G->n;i++)
if(visited[i]) DFS(G,i);
}
广度优先搜索(BFS)
1)访问初始点v,接着访问v的所有未被访问过的邻接点v0,v1, v2,…,Vn
(2)按照v0,v1,…,vn的次序,访问每一个顶点的所有未被访问过的邻接点。
(3)依次类推,直到图中所有和初始点v有路径相通的顶点都被访问过为止。
bool visited[MAX_V];
void BFS(AdjGraph * G,int v){
ArcNode *p;
int w,i;
visit(v); //访问结点v的数据域
visited[v] = true; //置节点已被访问
push(qu,v); //将节点v入队
while(!IsEmpty(qu)){ //队列不为空则循环
pop(qu.v); //取出队头元素
for(p = G->adjList[v].firstarc;p != NULL; p = p->nextarc){ //遍历v的所有邻接点
w = p->adjvex;
if(!visited[w]){ //如果节点w没有被访问过~没有入队
visit(w); //访问该节,并入队
visited[w] = true;
push(qu,w);
}
}
}
}
void BFSTravel(AdjGraph * G){
InitQueue(qu); //初始化队列qu
for(int i = 0;i < G->n;i++)
if(visited[i]) BFS(G,i);
}
四 生成树和最小生成树
生成树
一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边
- 在一课生成树上任意添加一条边,必定会构成一个环
由深度优先遍历得到的生成树称为深度优先生成树
由广度优先遍历得到的生成树称为广度优先生成树
- 注意:一个连通图的生成树不是唯一的
最小生成树
对于带权连通图G(每条边上的权均为大于零的实数),可能有多棵不同生成树。
每棵生成树的所有边的权值之和可能不同。
其中权值之和最小的生成树称为图的最小生成树。
求最小生成树算法有Prim、Kruskal两个算法。
Prim算法
此算法可以称为"加点法”,每次迭代选择权值最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
过程:
- 1、初始化集合u = {s},s到其他顶点的边作为后选边
- 2、重复以下步骤n-1次,使得其他n-1个顶点被加入到u中
- 2.1 从候选边中挑选出权值最小的边(u0,v0),该边在v = V-U(v是所有没有被加入到u的顶点的集合)中对应顶点为v0,将v0加入u中
- 2.2 枚举v0中所有边(v0,l),如果l在v中,且权值小于(u0,l),则利用(v0,l)更新(u0,l)
const int INF = 0x3f3f3f3f; //定义正无穷
bool st[N];
int prim(MGraph G){
memset(dist,0x3f,sizeof dist); //将所有候选边初始化为正无穷
dist[1] = 0; //将顶点1加入到集合u中
st[1] = true; //表示顶点1已被加入u中
int res = 0; //最小生成树权值之和
for(int i = 2;i <= n;i++) dist[i] = G[1][i]; //用顶点1去更新dist(即初始化1到其邻接点边的权值)
//循环n-1次
for(int i = 1;i < n;i++){
int minimum = INF; //用来记录候选边集合dist中权值最小的边
int t = -1; //t记录v中到u权值最小的顶点
//2.1 找到dist中权值最小的边
for(int k = 2;k <= n;k++){
if(!st[k] && dist[k] < temp){ //如果k不在u中,且k到u的距离小于minimum就将下标赋给t
minimum = dist[k]; //候选边中最小权值更新为dist[k]
t = k;
}
}
//如果t==-1,意味着在集合v找不到边连向集合u,该图不是连通图,没有最小生成树,返回INF
if(t == -1) return INF;
//否则,将顶点t加入u
st[t] = true;
res += minimum;
//2.2 利用t去更新dist
for(int k = 2;k <= n;k++) dist[k] = min(dist[k],G[t][k]);
}
return res;
}
Kruskal算法
此算法可称为"加边法",将图中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边构成环路,就可以选择它组成最小生成树。
过程:
- 1、置u的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
- 2、将图G中的边按权值从小到大的顺序依次选取之后
- 若选取的边未使生成树T形成回路,则加入TE
- 否则,将该边舍弃,一直到TE包含n-1条边(n个顶点)为止
struct Edge{
int a,b,w; //一条边有两个顶点,一个权值
}edges[M];
void kruskal(){
int res = 0; //最小生成树的权值之和
int cnt = 0; //当前边的数量
//将所有边升序排序
sort(edges,edges+m);
for(int i = 1;i <= n;i++) p[i] = i;
for(int i = 0; i < m;i++){
int a = edges[i].a;b = edges[i].b,w = edges[i].w;
//利用并查集查找顶点a与顶点b是否在同一个集合中
a = find(a),b = find(b);
//如果在同一个集合中,就不能选该边,否则就会构成环路
if(a != b){
p[a] = b; //将a所在的集合和b所在的集合进行合并
res += w;
cnt++; //边数+1
}
}
//如果边数不等于n-1的话,此图不适连通图,没有最小生成树
if(cnt < n - 1) res = INF;
return res;
}