提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
目录
前言
提示:以下是本篇文章正文内容,下面案例可供参考
一、图的术语
网络:加权有向图和加权无向图,统称为网络。简单路径:一条路径上除了第一个顶点和最后一个顶点可能相同之外,其余各顶点都不相同。简单回路或 简单环:简单路径上第一个顶点和最后一个顶点相同。(无向图)连通图:在一个无向图G中,如果任意两个顶点对之间都是连通的,称G是连通图。极大连通子图:将该子图外的任意一个顶点增加进子图都会造成子图不连通,且 该子图包含了其中顶点间所有的边,该子图称极大连通子图。(无向图中的概念)连通分量:无向图的极大连通子图称连通分量。(有向图)强连通图:在一个有向图G中,如果任意两个顶点对之间都是连通的,称G是强连通图。n个节点的强连通图,边的数目最大为n(n-1),最小数目为n(有向图必须成环才能相互连通)。(有向图)弱连通图:如果将有向图的有向边换成无向边得到的图连通,则此有向图是弱连通图(有向图)强连通分量:有向图的极大连通子图,称强连通分量。(无向图无回路)树:无回路的连通图生成树:连通图的 极小连通子图,该子图包含连通图的所有 n个顶点,但只含它的 n-1条边。如果去掉一条边,这个子图将不连通;如果增加一条边,必存在回路。不唯一性:一个连通图的生成树并不保证唯一。
➢ 线性结构:每个元素只有一个直接前驱和一个直接后继。➢ 树形结构:每个数据元素有一个直接前驱,但可以有多个直接后继。➢ 图形结构:数据元素之间的关系是任意的。每个数据元素可以和任意多个数 据元素相关,有任意多个直接前驱和直接后继。在无向图中,甚至是互为前驱后继。
二、图的存储
➢ 在一维数组中存储顶点信息,在二维矩阵中存储边的信息。➢ 如果非加权图中,存在一条自顶点vi到vj 的有向边或无向边,那么在二维矩阵(如a)中,a[i][j] = 1,否则 a[i][j] = 0。➢ 按照简单图的定义,主对角线上元素a[i][i] = 0,即顶点到自身没有边相连。➢ 无向图的邻接矩阵是以主对角线为轴对称的。加权邻接矩阵➢ 如果加权图中,存在一条自顶点vi到vj 的有向边或无向边,那么在二维矩阵(如a)中,a[i][j] = 权值,否则 a[i][j] = ∞。➢ 按照简单图的定义,主对角线上元素a[i][i] = 0或者∞,即顶点到自身没有边相连。优点:易于判断边,添加边,删除边,容易得到节点的出度和入度缺点:存储空间大
➢ 如果图是稠密图(边数非常多):对有向图,采用邻接矩阵是合适的。对无向图,因关于主对角线对称,可只存储其上三角矩阵或下三角矩阵。➢ 如果图是稀疏图(边数很少),且非零元素的分布没有规律:✓ 通常的做法是只存储其中的非零元素和非零元素所在的位置,每个非零元素a[i][j]用一个三元组来表示:(i, j, a[i][j]).✓ 将此三元组按照一定的次序排列,如先按照行序再按照列序排列。✓ 三元组可以放在顺序表或者链表中
➢ 顶点依然用一个一维数组来存储,而边的存储是将由同一个顶点出发的所有边组成一条单链表。➢ 存储顶点的一维数组称 顶点表,存储边信息的单链表称 边表。一个图由顶点表和边表共同表示。➢ 顶点表不仅保存各个顶点的信息,还保存由该顶点射出的边形成的单链表中首结点的地址(首指针),这种方法称 邻接表表示法
➢ 仅存储有边的信息,不存储无边信息,在图比较稀疏的情况下,空间的利用率大大提高。➢ 无向图,同一条边存储了两次。➢ 计算某个顶点v的出度(有向图)或者度(无向图),只需遍历该顶点v指向的边表,即利于计算出度O(n)。➢ 计算某个顶点v的入度(有向图),需要遍历所有顶点v指向的边表,即不利于计算入度。( 逆邻接表:有向图的逆邻接表中,顶点表保存该顶点的射入边形成的单链表的首结点地址,有利于计算顶点的入度)时间复杂度: O(|V|+|E|) 或 O(|E|) (如果|E|>|V|)优化:顶点表也用一个单链表表示。此时,dest用顶点结点地址而不用下标。
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
//单链表头插法
//eg.插入边ab,bc,ad e(0)=b,e(1)=c,e(2)=d
//a->d->b 头结点索引h(a)=2 ne(2)=0索引为2的边终点的下一个节点索引序号
//b->c h(b)=c的索引值1
//c
//d
int h[N], e[N], ne[N], idx;
//h 邻接表存储树,有n个节点,所以需要n个队列头节点
//e 存储元素 记录边的终点
//ne 存储列表的next值
//idx 单链表指针 从00开始 索引功能
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
//索引为idx的边终点为b,下一条边是a起点链表的原头结点索引,a起点链表的新头结点变为该边,索引++
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);//默认头结点索引为-1
其他存储方法
1.邻接多重表:只适用于无向图的链式存储结构
2.十字链表:有向图的链式存储结构
3.无向图的邻接矩阵可用一维数组存储。因为无向图对称,所以可以只存储上三角的信息。
三、图的遍历
1.深度优先遍历
dfs:从u出发,访问与u邻接的每个点并依此递归,若未访问过则访问 邻接表(O(n+m))
//邻接表
void dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
//邻接矩阵
void dfs(int u)
{
st[u]=true;
for(int i=0;i<v_num;i++){
if(G[u][i]&&!st[u]) dfs(i);
}
}
拓展dfs:(时间戳time)
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
dfn[u]=time;//第一次被发现的时间
time=time+1;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) {
pre[j]=u;
dfs(j);
}
}
fin[u]=time;//记录遍历结束时间
time=time+1;
}
//递推打印路径
void print(int i)
{
if(pre[i]==-1)//提前把pre置为-1
{
cout<<e[i];
return;
}
print(pre[i]);
cout<<"->"<<e[i];
}
应用:
查找路径:利用拓展的pre.如果j=target 则找到,根据pre数组,倒推路径。可从终点走到起点,得到正向路径。
走迷宫:邻接矩阵,走法用嵌套数组adj表示,可用k方便代号。
2.广度优先遍历
bfs:未被发现 发现(入队) 结束(所有邻接节点都访问完,出队)每个结点入队一次、出队一次• 每个结点被查询(第8行代码)的次数等于其入度(?)• 时间复杂度:O(n+m)• 空间复杂度:O(n)遍历不唯一,非递归
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);//入队的是索引
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
1. 图的深度优先遍历和广度优先遍历既适用于有向图,也适用于无向图。2. 遍历中,已访问过的邻接点将不再被访问,故遍历结果只能是树形结构。比较:1. 深度优先遍历的特点是“一条路跑到黑” ,如果面临的问题是能找到一个解就可以,深度优先遍历一般是首选。其搜索深度一般比广度优先遍历要搜索的宽度小很多。2. 广度优先遍历的特点是层层扩散。如果面临的问题是要找到一个距离出发点最近的解,那么广度优先遍历是最好的选择。3. 广度优先遍历需要程序员自己写个队列,代码比较长。而且这个队列要能同时存储一整层顶点,如果是一棵满二叉树,每层顶点的个数是呈指数级增长的,所以耗费的空间会比较大。
三、图的连通性
无向图的连通性
for(int v=0;v<v_num;i++) st[v]=0;
cnt=0;
for(int v=0;v<v.num;v++){
if(!st[v]){
cnt++;//连通分量数目++
bfs(v);
}
}
if(cnt==1) ret=true;//一个分量,联通
else
{ret=false;cout<<cnt;}
应用:六度空间理论
有向图的连通性
四、图的应用
欧拉路径:如果图中的一条路径经过了图中每条边一次且仅一次,这条路径称欧拉路径。欧拉回路:如果一条欧拉路径的起点和终点相同,这条路径是一个回路,称欧拉回路。(欧回)欧拉图: 具有欧拉回路的图称欧拉图(简称 E图),半欧拉图:具有欧拉路径但不具有欧拉回路的图称半欧拉图。一笔画: 从图中一个顶点出发进行深度优先搜索,一直往前走,没有任何回溯,观察是否有一条路径能走遍图中所有的边且每条边都只走了一次,这就是一笔画问题
欧拉定理:
➢ 一个无向连通图中,如果度为奇数的顶点超过了2个,则欧拉路径是不存在的。➢ 一个无向连通图中,如果除了两个顶点的度是奇数而其他顶点的度都是偶数,则从一个度为奇数的顶点出发一定能找到一条经过每条边一次且仅一次的路径回到另外一个度为奇数的顶点。➢ 一个无向连通图中,如果顶点的度都是偶数,则从任意一个顶点出发都能找到经过每条边一次且仅一次并回到原来的顶点的路径(回路)。
求解欧拉回路:(不断拼接小回路)
1. 任选一个顶点 v,从该顶点出发开始深度优先搜索,搜索路径上都是由未访问过的边构成,搜索中访问这些边(加标记!),直到回到顶点 v 且 v 没有尚未被访问的边( 思考:如果还有边,怎么操作?),此时便得到了一个回路(非简单回路!(回路中可能包含回路,有其他重复点)),此回路为当前结果回路。2. 在搜索路径上另外找一个尚有未访问边的顶点,继续如上操作,找到另外一个回路,将该回路拼接在当前结果回路上,形成一个大的、新的当前结果回路。3. 如果在当前结果回路中,还有中间某结点有尚未访问的边,回到2;如果没有任何中间顶点尚余未访问的边,访问结束,当前结果回路即欧拉回路
Acwing1184
int &i
直接修改hu,等价于删边,相当于当前弧优化
~i == -i -1。 这意味着~是将对应内存单元的内容按位求反的意思。 意思是,i减到-1时跳出循环
void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
void dfs(int u){
for(int &i = h[u]; ~i; ){
if(used[i]){ //如果这条边用过了
i = ne[i]; //删除这条边
continue;
}
used[i] = true; //标记这条边已使用
if(ver == 1) used[i^1] = true; //如果是无向图,那么这条边的反向边也要标记使用过了
int t;
if(ver == 1){
t = i/2 + 1;
if(i&1) t = -t; //(0,1) (2,3) (4,5) 奇数编号是返回的边
}else t = i+1;
int j = e[i];
i = ne[i];
dfs(j);
ans[cnt++] = t;
}
}
int main()
{
scanf("%d%d%d",&ver,&n,&m);
memset(h,-1,sizeof h);
for(int i = 0; i<m; i++){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
if(ver == 1) add(b,a); //无向边
din[b]++, dout[a]++;
}
if(ver == 1){
for(int i = 1; i<=n; i++){
if(din[i]+dout[i] &1){
//无向图含欧拉回路的充要条件是每个点的度都为偶数
puts("NO");
return 0;
}
}
}else{
for(int i = 1; i<=n; i++){
if(din[i] != dout[i]){
//有向图含欧拉回路的充要条件是每个点的入度等于出度
puts("NO");
return 0;
}
}
}
for(int i = 1; i<=n; i++){
if(~h[i]) {
dfs(i);
break;
}
}
if(cnt < m){
puts("NO");
return 0;
}
puts("YES");
for(int i = cnt-1; i>=0; --i){
cout<<ans[i]<<" ";
}
return 0;
}
总结
第十六周习题
1.用邻接表法存储图,占用的存储空间数只与图中结点个数有关,而与边数无关❌
邻接表存每个顶点的邻接结点,跟点和边都有关系
习题
1.采用邻接表存储的图的深度优先遍历算法类似于二叉树的___A____。
A.先序遍历 B.中序遍历 C.后序遍历 D.按层遍历
2. 采用邻接表存储的图的广度优先遍历算法类似于二叉树的___D____。
A.先序遍历 B.中序遍历 C.后序遍历 D.按层遍历
3.稀疏矩阵一般的压缩存储方法有两种,即 三元组表 和 十字链表
4.有e条边的无向图,在邻接表中有e个结点。❌ 2*e
5.一个连通图的生成树是一个极小连通子图。
6.判断一个无向图是一棵树的条件是有n个顶点,n-1条边的无向连通图。