文章目录
- 图的认识
- 图的概念
- 无向图
- 有向图
- 简单图
- 完全图
- 子图
- 连通、连通图、连通分量
- 边的权和网
- 加权图
- 邻接和关联
- 路径
- 简单路径、简单回路
- 环
- 顶点的度、入度和出度
- 割点(关节点)
- 桥(割边)
- 距离
- 有向树
- 图的表示
- 邻接列表
- 邻接矩阵
- 图的遍历
- 深度优先遍历
- 广度优先遍历
- 生成树
图的认识
图(Graph)是由顶点和连接顶点的边构成的离散结构。一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。
注意:线性表可以是空表,树可以是空树,图不可以是空图,图可以没有边,但是至少要有一个顶点。
图是最灵活的数据结构之一,很多问题都可以使用图模型进行建模求解。
树可以说只是图的特例,但树比图复杂很多
注意:顶点有时也称为节点或者交点,边有时也称为链接。
图有各种形状和大小。边可以有权重(weight),即每一条边会被分配一个正数或者负数值。边可以是有方向的。有方向的边意味着是单方面的关系。从顶点 X 到 顶点 Y 的边是将 X 联向 Y,不是将 Y 联向 X。
树和链表,都可以被当成是树,是一种更简单的形式。他们都有顶点(节点)和边(连接)。
图的概念
图是由顶点集合以及顶点间的关系集合组成的一种数据结构。
由顶点 V 集和边 E 集构成,因此图可以表示成 Graph = (V,E)
V是顶点的有穷非空集合;E是顶点之间关系的有穷集合,也叫边集合。
图按照边或弧的多少分稀疏图和稠密图
无向图
无向图:顶点对<x,y>是无序的。
无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边,用无序偶对(Vi,Vj)来表示
无向图顶点的边数叫做度。
无向图中,顶点的度就是与顶点相关联的边的数目,没有入度和出度。
如果图中任意两个顶点时间的边都是无向边,则称该图为无向图:
上图是无向图,所以连接顶点A与D的边,可以表示为无序对(A,D),也可以写成(D,A)
对于如上无向图来说,G=(V,{E}) 其中顶点集合V={A,B,C,D};边集合E={(A,B),(B,C),(C,D),(D,A),(A,C)}
完全无向图:若有n个顶点的无向图有n(n-1)/2 条边, 则此图为完全无向图。
有向图
如果任意两个顶点之间都存在边叫完全图,有向的叫有向图。
有向图:具有方向性,顶点 (u,v)之间的关系和顶点 (v,u)之间的关系不同,后者或许不存在。
有向图:顶点对<x,y>是有序的;
有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也称为弧。
用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。
连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A,D>表示弧。注意不能写成<D,A>。
对于如上有向图来说,G=(V,{E})其中顶点集合V={A,B,C,D};弧集合E={<A,D>,<B,A>,<C,A>,<B,C>}
图中顶点之间有邻接点。有向图顶点分为入度和出度。
简单图
若无重复的边或顶点到自身的边则叫简单图。
1)不存在重复边
2)不存在顶点到自身的边
所以上面的有向图和无向图都是简单图。与简单图相对的是多重图,即:两个结点直接边数多于一条,又允许顶点通过同一条边与自己关联。
完全图
无向图中任意两点之间都存在边,称为无向完全图;
有向图中任意两点之间都存在方向向反的两条弧,称为有向完全图
完全有向图:有n个顶点的有向图有n(n-1)条边, 则此图为完全有向图。
子图
若有两个图G=(V,E),G1=(V1,E2),若V1是V的子集且E2是E的子集,称G1是G的子图。
连通、连通图、连通分量
1.连通
在无向图中,两顶点有路径存在,就称为连通的。
2.连通图
若图中任意两顶点都连通,同此图为连通图。
如果对于任意两个顶点都是连通的,则称该图是连通图。
对于有向图,如果图中每一对顶点Vi和Vj是双向连通的,则该图是强连通图。
连通:无向图中每一对不同的顶点之间都有路径。如果这个条件在有向图里也成立,那么是强连通的。如果从顶点v到顶点v’有路径或从顶点v’到顶点v有路径,则称顶点v和顶点v’是连通的。
有向图的连通分支:将有向图的方向忽略后,任何两个顶点之间总是存在路径,则该有向图是弱连通的。有向图的子图是强连通的,且不包含在更大的连通子图中,则可以称为图的强连通分支。
上图, a 、e没有到 { b , c , d } 中的顶点的路径,所以各自是独立的连通分支。因此,上图有三个强连通分支,用集合写出来就是: { { a } , { e } , { b , c , d } }
3.连通分量
无向图中的极大连通子图称为连通分量。
双连通图:不含任何割点的图。
边的权和网
图上的边和弧上带权则称为网
图中每条边上标有某种含义的数值,该数值称为该边的权值。这种图称为带树图,也称作网。
加权图
加权图:与加权图对应的就是无权图,又叫等权图。如果一张图不含权重信息,我们认为边与边之间没有差别。
邻接和关联
邻接(adjacency):邻接是两个顶点之间的一种关系。如果图包含 (u,v),则称顶点v 与顶点u邻接。在无向图中,这也意味着顶点 u 与顶点 v 邻接。
关联(incidence):关联是边和顶点之间的关系。在有向图中,边 (u,v)从顶点 u开始关联到 v,或者相反,从v关联到u。在有向图中,边不一定是对称的,有去无回是完全有可能的。
路径
两顶点之间的路径指顶点之间经过的顶点序列,经过路径上边的数目称为路径长度。若有n个顶点,且边数大于n-1,此图一定有环。
路径(path):依次遍历顶点序列之间的边所形成的轨迹。
树中根节点到任意节点的路径是唯一的,但是图中顶点与顶点之间的路径却不是唯一的。
路径的长度是路径上的边或弧的数目。
无权图的路径长是路径包含的边数。
有权图的路径长要乘以每条边的权。
简单路径、简单回路
简单路径:没有重复顶点的路径称为简单路径。
除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
环
环:包含相同的顶点两次或者两次以上。上图序列 <1,2,4,3,1>,1出现了两次,当然还有其它的环,比如<1,4,3,1>。
无环图:没有环的图,其中,有向无环图有特殊的名称(DAG(Directed Acyline Graph)
顶点的度、入度和出度
顶点的度为以该顶点为一个端点的边的数目。
对于无向图,顶点的边数为度,度数之和是顶点边数的两倍。
对于有向图,入度是以顶点为终点,出度相反。有向图的全部顶点入度之和等于出度之和且等于边数。顶点的度等于入度与出度之和。
割点(关节点)
割点:如果移除某个顶点将使图或者分支失去连通性,则称该顶点为割点。某些特定的顶点对于保持图或连通分支的连通性有特殊的重要意义。
割点的重要性不言而喻。如果你想要破坏互联网,你就应该找到它的关节点。同样,要防范敌人的攻击,首要保护的也应该是关节点
桥(割边)
桥(割边):和割点类似,删除一条边,就产生比原图更多的连通分支的子图,这条边就称为割边或者桥。
距离
若两顶点存在路径,其中最短路径长度为距离。
有向树
有一个顶点的入度为0,其余顶点的入度均为1的有向图称作有向树。
图的表示
邻接列表
邻接列表:在邻接列表实现中,每一个顶点会存储一个从它这里开始的边的列表。比如,如果顶点A 有一条边到B、C和D,那么A的列表中会有3条边,邻接列表只描述了指向外部的边。A 有一条边到B,但是B没有边到A,所以 A没有出现在B的邻接列表中。
优点:
(1) 便于增加和删除结点。
(2) 便于统计边的数目,按顶点表顺序扫描所有边表可得到边的数目,时间复杂度为O(n+e)。
(3)空间效率高。对于一个具有n个顶点e条边的图G,若图G是无向图,则在邻接表表示中有n个顶点表结点和2n个边表结点。若G是有向图,则在它的邻接表表示或逆邻接表表示中均有n个顶点表结点和e个边表结点。因此,邻接表的空间复杂度为O(n+e)。
缺点:
(1) 不便于判断顶点之间是否有边,要判断vi 和vj之间是否有边,就需扫描第i个边表,最换情况下要耗费O(n)时间。
(2) 不便于计算有向图各个顶点的度。
邻接矩阵
图最常见的表示形式为邻接链表和邻接矩阵
特点:
⑴0表示这两个顶点之间没有边,1表示有边
⑵顶点的度是行内数组之和
⑶求顶点的邻接点,遍历行内元素即可
邻接矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图来说,M (j ,i) = M (i, j),所以其邻接矩阵是一个对称矩阵;对于有向图来说,则未必是一个对称矩阵。邻接矩阵的对角线元素都为0。下图是一个无向图和对应的邻接矩阵:
需要注意的是,当边上有权值的时候,称之为网图,则邻接矩阵中的元素不再仅是0和1了,邻接矩阵M中的元素定义为:
邻接矩阵:在邻接矩阵实现中,由行和列都表示顶点,由两个顶点所决定的矩阵对应元素表示这里两个顶点是否相连、如果相连这个值表示的是相连边的权重。例如,如果从顶点A到顶点B有一条权重为 5.6 的边,那么矩阵中第A行第B列的位置的元素值应该是5.6:
优点:
(1) 便于判断两个顶点之间是否有 边,即根据A[i][j] = 0或1来判断。
(2) 便于计算各顶点的度。对于无向图,邻接矩阵的第i行元素之和就是顶点i的度。对于有向图,第i行元素之和就是顶点i的出度,第i列元素之和就是顶点i的入度。
缺点:
(1) 不便于增加删除顶点。
(2)空间复杂度高。如果是有向图,n个顶点需要n*n个单元存储边。如果无向图,其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角元素,这样需要n(n-1)/2个单元。无论哪种存储方式,邻接矩阵表示法的空间复杂度均为0(n*n)
区别:
邻接列表在表示稀疏图时非常紧凑而成为了通常的选择,但稀疏图表示时使用邻接矩阵,会浪费很多内存空间,遍历的时候也会增加开销。
如果图是稠密图,可以选择更加方便的邻接矩阵。
还有,顶点之间有多种关系的时候,也不适合使用矩阵。因为表示的时候,矩阵中的每一个元素都会被当作一个表
图的遍历
深度优先遍历
在深度优先搜索中,保存候补节点是栈,栈的性质就是先进后出,即最先进入该栈的候补节点就最后进行搜索。
深度优先遍历,也有称为深度优先搜索(Depth_First_Search),简称DFS。其实,就像是一棵树的前序遍历。
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
#include<iostream>
using namespace std;
int n,m,x,y,a[105][105],book[105][105]= {};
int minn = 999999;
void dfs(int starx,int stary,int step) {int nx,ny;if(starx==x&&stary==y) {if(minn > step) {minn = step;}return ;}int next[4][2]= {0,1,0,-1,1,0,-1,0};for(int i=0; i<4; i++) {nx = starx+next[i][0]; ny = stary+next[i][1];if(nx>n||nx<1||ny>m||ny<1)continue;if(a[nx][ny]==0&&book[nx][ny]==0) {book[nx][ny]=1;dfs(nx,ny,step+1);book[nx][ny]=0;}}return ;
}
int main() {cin>>n>>m>>x>>y;for(int i=1; i<=n; i++) {for(int j=1; j<=m; j++) {cin>>a[i][j];}}book[1][1]=1;dfs(1,1,0);cout<<"最小值为=";cout<<minn<<endl;return 0;
}
/*
5 4 4 3
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
*/
邻接矩阵表达方式:
#define MAXVEX 100 //最大顶点数
typedef int Boolean; //Boolean 是布尔类型,其值是TRUE 或FALSE
Boolean visited[MAXVEX]; //访问标志数组
#define TRUE 1
#define FALSE 0//邻接矩阵的深度优先递归算法
void DFS(Graph g, int i)
{int j;visited[i] = TRUE;printf("%c ", g.vexs[i]); //打印顶点,也可以其他操作for(j = 0; j < g.numVertexes; j++){if(g.arc[i][j] == 1 && !visited[j]){DFS(g, j); //对为访问的邻接顶点递归调用}}
}//邻接矩阵的深度遍历操作
void DFSTraverse(Graph g)
{int i;for(i = 0; i < g.numVertexes; i++){visited[i] = FALSE; //初始化所有顶点状态都是未访问过状态}for(i = 0; i < g.numVertexes; i++){if(!visited[i]) //对未访问的顶点调用DFS,若是连通图,只会执行一次{DFS(g,i);}}
}
邻接表存储结构:
//邻接表的深度递归算法
void DFS(GraphList g, int i)
{EdgeNode *p;visited[i] = TRUE;printf("%c ", g->adjList[i].data); //打印顶点,也可以其他操作p = g->adjList[i].firstedge;while(p){if(!visited[p->adjvex]){DFS(g, p->adjvex); //对访问的邻接顶点递归调用}p = p->next;}
}//邻接表的深度遍历操作
void DFSTraverse(GraphList g)
{int i;for(i = 0; i < g.numVertexes; i++){visited[i] = FALSE;}for(i = 0; i < g.numVertexes; i++){if(!visited[i]){DFS(g, i);}}
}
对比两个不同的存储结构的深度优先遍历算法,对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找某个顶点的邻接点需要访问矩阵中的所有元素,因为需要O(n2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
广度优先遍历
广度优先搜索和深度优先搜索一样,都是对图进行搜索的算法,都是从起点开始顺着边搜索,此时并不知道图的整体结构,直到找到指定节点(即终点)。在此过程中每走到一个节点,就会判断一次它是否为终点。
广度优先搜索会根据离起点的距离,按照从近到远的顺序对各节点进行搜索。而深度优先搜索会沿着一条路径不断往下搜索直到不能再继续为止,然后再折返,开始搜索下一条路径。
在广度优先搜索中,有一个保存候补节点的队列,队列的性质就是先进先出,即先进入该队列的候补节点就先进行搜索。
广度优先搜索:将点存储到队列中
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS。
深度遍历类似树的前序遍历,广度优先遍历类似于树的层序遍历。
邻接矩阵的广度遍历算法:
void BFSTraverse(Graph g)
{int i, j;Queue q;for(i = 0; i < g.numVertexes; i++){visited[i] = FALSE;}InitQueue(&q);for(i = 0; i < g.numVertexes; i++)//对每个顶点做循环{if(!visited[i]) //若是未访问过{visited[i] = TRUE;printf("%c ", g.vexs[i]); //打印结点,也可以其他操作EnQueue(&q, i); //将此结点入队列while(!QueueEmpty(q)) //将队中元素出队列,赋值给{int m;DeQueue(&q, &m); for(j = 0; j < g.numVertexes; j++){//判断其他顶点若与当前顶点存在边且未访问过if(g.arc[m][j] == 1 && !visited[j]){visited[j] = TRUE;printf("%c ", g.vexs[j]);EnQueue(&q, j);}}}}}
}
邻接表的广度优先遍历:
void BFSTraverse(GraphList g)
{int i;EdgeNode *p;Queue q;for(i = 0; i < g.numVertexes; i++){visited[i] = FALSE;}InitQueue(&q);for(i = 0; i < g.numVertexes; i++){if(!visited[i]){visited[i] = TRUE;printf("%c ", g.adjList[i].data); //打印顶点,也可以其他操作EnQueue(&q, i);while(!QueueEmpty(q)){int m;DeQueue(&q, &m);p = g.adjList[m].firstedge; 找到当前顶点边表链表头指针while(p){if(!visited[p->adjvex]){visited[p->adjvex] = TRUE;printf("%c ", g.adjList[p->adjvex].data);EnQueue(&q, p->adjvex);}p = p->next;}}}}
}
对比图的深度优先遍历与广度优先遍历算法,会发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问顺序不同。可见两者在全图遍历上是没有优劣之分的,只是不同的情况选择不同的算法。
生成树
生成树的性质
一个有n个顶点的连通图的生成树有且仅有n-1条边
一个连通图的生成树并不唯一