Loading

数据结构笔记六:图

图的定义

图G顶点集V边集E组成,记为\(G=\{V,E\}\),其中\(V(G)\)表示图G中顶点的有限非空集;\(E(G)\)

表示图G中顶点之间的关系(边)集合。若\(V=\{v_1,v_2,...,v_n\}\),则用\(|V|\)表示图G中顶点的个数,

也称图G的阶\(E=\{(u,v)|v\in V,v\in V\}\),\(|E|\)表示图G中边的条数。

注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集

image-20210823202014930

无向图,无向图

E无向边(简称)的有限集合时,则图G为无向图。边是顶点的无序对,记为\((v,w)==(w,v)\)

image-20210823202534612

E有向边(简称)的有限集合时,则图G为有向图。边是顶点的有序对,记为\((v,w)\),v为弧头,w称为弧尾。

image-20210823202647361

简单图,多重图

简单图——①不存在重复边 ②不存在顶点到自身的边

image-20210823202755598

多重图——图G中某两个结点之间的边数多余一条,又允许顶点通过同一条边和自己关联,则G为多重图。

image-20210823202920358

顶点的度,入度,出度

对于无向图顶点v的度是指依附于该顶点的边的条数,记为\(TD(V)\)

在具有n个顶点,e条边的无向图中,\(\sum_{i=1}^nTD(v_i)=2e\),即无向图的全部顶点的度的和等于边数的2倍

对于有向图:

  • 入度是以顶点v为终点的有向边的数目,记为\(ID(V)\);
  • 出度是以顶点v为起点的有向边的数目,记为\(OD(V)\)

顶点v的度等于其入度和出度之和,即\(TD(v)=ID(v)+OD(v)\)

在具有n个顶点,e条边的有向图中,\(\sum_{i=1}^nID(v_i)=\sum_{i=1}^OD(v_i)=e\)

顶点-顶点关系的描述

  • 路径——顶点\(v_p\)到顶点\(v_q\)之间的一条路径是指顶点序列
  • 回路——第一个顶点和最后一个顶点相同的路径称为回路或环
  • 简单路径——在路径序列中,顶点不重复出现的路径称为简单路径
  • 简单回路——除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路
  • 路径长度——路径上边的数目
  • 点到点的距离——从顶点u出现到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷\((\infty)\)
  • 无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通
  • 有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的。

连通图,强连通图

若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图

常见考点:对于n个顶点的无向图G

  • 若G是连通图,则最少有\(n-1\)条边
  • 若G是非连通图,则最多有\(C_{n-1}^2\)条边

若图中任何一对顶点都是强连通的,则称此图为强连通图。

常见考点:对于n个顶点的无向图G

  • 若G是强连通图,则最少有\(n\)条边(形成回路)

研究图的局部——子图

设有两个图\(G=\{V,E\}\)\(G'=\{V',E'\}\),若\(V'\)\(V\)的子集,且\(E'\)\(E\)的子集,则称\(G'\)\(G\)的子图。

若有满足\(V(G')=V(G)\)的子图\(G'\),则称其为\(G\)生成子图

image-20210823211101058

连通分量

无向图极大连通子图称为连通分量

image-20210823211329437

有向图中极大强连通子图称为有向图的强连通分量

image-20210823211506763

生成树

连通图的生成树是包含图中全部顶点的极小连通子图。

若图中顶点数为n,则它的生成树含有\(n-1\)条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。

image-20210823211628623

生成森林

非连通图中,连通分量的生成树构成了非连通图的生成森林

边的权、带权图/网

边的权——在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值

带权图/网——边上带有权值的图称为带权图,也称网。

带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。

几种特殊形态的图

无向完全图——无向图中任意两个顶点之间都存在边

若无向图的顶点数\(|V|=n\),则\(|E|\in[0,C_n^2]=[0,n(n-1)/2]\)

image-20210823212432661

有向完全图——有向图中任意两个顶点之间都存储方向相反的两条弧。

若有向图的顶点数\(|V|=n\),则\(|E|\in[0,2C_n^2]=[0,n(n-1)]\)

image-20210823212538035

稀疏图——边数很少的图,反之为稠密图

没有绝对的界限,一般来说\(|E|<|V|log|V|\)时,可以视为稀疏图

——不存在回路,且连通的无向图

image-20210823212754610

常见考点:n个顶点的图,若\(|E|>n-1\),则一定会有回路

有向树——一个顶点的入度为0,其余顶点入度均为1的有向图,称为有向树。

图的存储

邻接矩阵法

image-20210823214233164

#define MaxVertexNum 100			//顶点数目的最大值
typedef struct
{
    char Vex[MaxVertexNum];			//顶点表
    int Edge[MaxVertexNum][MaxVertexNum];		//邻接矩阵,边表
    int vecnum,arcnum;				//图的当前顶点数和边数/弧数
}MGraph;

第i个结点的=第i行(列)的非零元素个数(无向图)

第i个结点的出度=第i行的非零元素个数

第i个结点的入度=第i列的非零元素个数

第i个结点的度=第i行,列的非零元素个数之和

//邻接矩阵带权图(网)
#define MaxVertexNum 100			//顶点数目的最大值
#define INFINITY 最大的int值		 //定义“无穷”
typedef struct
{
    char Vex[MaxVertexNum];			//顶点表
    int Edge[MaxVertexNum][MaxVertexNum];		//邻接矩阵,边表
    int vecnum,arcnum;				//图的当前顶点数和边数/弧数
}MGraph;

性能分析

空间复杂度:\(O(|V|^2)\)——只和顶点数相关,和实际的边数无关

适合用于存储稠密图,无向图的邻接矩阵时对称矩阵,可以压缩存储(只存储上/下三角区)

性质

设图G的邻接矩阵为A(矩阵元素为0/1),则\(A^n\)的元素\(A^n[i][j]\)等于由顶点i到顶点j的长度为n的路径的数目。

邻接表法(顺序+链式存储)

image-20210823221305151

#define MaxVertexNum 100			//顶点数目的最大值
//“边/弧”
typedef struct ArcNode{
    int adjvex;						//边/弧指向哪个结点
    struct ArcNode* next;			//指向下一条弧的指针
    //InfoType info;				//边权值
}
//顶点
typedef struct VNodes{
    VertexType data;		//顶点信息
    ArcNode* first;			//第一条边/弧
}VNode,AdjList[MaxVertexNum];

//用邻接表存储的图
typedef struct{
    AdjList vertices;
    int vexnum,arcnum;
}ALGraph;

无向图:边结点的数量是\(2|E|\),整体空间复杂度\(O(|V|+2|E|)\)

有向图:边结点的数量是\(|E|\),整体空间复杂度\(O(|V|+|E|)\)

总结

十字链表法

image-20210824143153421

性能分析:

空间复杂度:\(O(|V|+|E|)\)

指定顶点的出边——绿色线;指定顶点的入边——橙色线

十字链表只用于存储有向图

邻接多重表法

image-20210824143640340

空间复杂度:\(O(|V|+|E|)\)

删除边,删除节点等操作很方便

邻接多重表只用于存储无向图

总结

image-20210824143925292

图的基本操作

Adjacent(G,x,y):判断图G是否存在边\(<x,y>\)\((x,y)\)

image-20210824145031896

image-20210824145038538

Neighbors(G,x):列出图G中与结点x邻接的边

image-20210824145140759

image-20210824145151543

InsertVertex(G.x):在图G中插入顶点x

image-20210824145246903

DeleteVertex(G,x):从图G中删除顶点x

image-20210824145453042

image-20210824145505781

AddEdge(G,x,y):若无向边\((x,y)\)或有向边\(<x,y>\)不存在,则向图G中添加该边。

image-20210824145607554

image-20210824145618478

FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号,若x没有邻接点或图中不存在x,则返回\(-1\)

image-20210824145720961

image-20210824145735430

NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1

image-20210824145953809

Get_edge_value(G,x,y):获取图G中边\((x,y)\)\(<x,y>\)对应的权值

Set_edge_value(G,x,y,v):设置图G中边\((x,y)\)\(<x,y>\)对应的权值为v

image-20210824150114786

图的遍历

广度优先遍历(BFS)

  1. 找到与一个顶点相邻的所有顶点

    image-20210824150601821

  2. 标记那些顶点被访问过

  3. 需要一个辅助队列

bool visited[MAX_VERTEX_NUM];			//访问标记数组
//广度优先遍历
void BFS(Graph G,int v)					//从顶点v除发
{
    visit(v);
    visited[v]=TRUE;					//对v做已访问标记
    Enqueue(Q,v);						//顶点v入队列Q
    while(!isEmpty(Q))
    {
        DeQueue(Q,v);					//顶点v出队列
        for(int w=FristNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
        {
            //检测v所有邻接点
            if(!visited[w])					//w为w的尚未访问的邻接顶点
            {
                visit(w);					
                visited[w]=TRUE;
                EnQueue(Q,w);
            }
        }
    }
}

image-20210824151332776

同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一

同一个图邻接表表示方式不唯一,因此广度优先遍历序列不唯一

//对图G进行广度优先遍历
void BFSTraverse(Graph G)
{
    for(int i=0;i<G.vexnum;++i)
        visited[i]=FALSE;
    InitQueue(Q);
    for(int i=0;i<G.vexnum;++i)
        if(!visited[i])					//对每个连通分量调用一次BFS
            BFS(G,i);
}

对于无向图,调用BFS函数的次数=连通分量数

复杂度分析

image-20210824152015772

空间复杂度:最坏情况,辅助队列大小为\(O(|V|)\)

image-20210824152147463

邻接矩阵存储的图:

访问\(|V|\)个顶点需要\(O(|V|)\)时间,查找每个顶点的邻接点都需要\(O(|V|)\)的时间,而总共有\(|V|\)顶点时间复杂度=\(O(|V|^2)\)

邻接表存储的图:

访问\(|V|\)个顶点需要\(O(|V|)\)时间,查找每个顶点的邻接点都需要\(O(||)E\)的时间,而总共有\(|V|\)顶点时间复杂度=\(O(|V|+|E|)\)+

广度优先生成树

image-20210824152637069

广度优先生成森林

image-20210824152704876

深度优先遍历(DFS)

bool visited[MAX_VERTEX_NUM];			//访问标记数组
//深度优先遍历
void DFS(Graph G,int v)					//从顶点v除发
{
    visit(v);
    visited[v]=TRUE;					//对v做已访问标记
    for(int w=FristNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
    {
        if(!visited[w])					//w为u的尚未访问的邻接顶点
            DFS(G,w);
    }
}
//对图G进行深度优先遍历
void DFSTraverse(Graph G)
{
    for(int i=0;i<G.vexnum;++i)
        visited[i]=FALSE;
    for(int i=0;i<G.vexnum;++i)
        if(!visited[i])				
            DFS(G,i);
}

复杂度分析

空间复杂度:来自函数调用栈,最坏情况,递归深度为\(O|V|\)

image-20210824153703285

image-20210824153720221

最好情况:\(O(1)\)

时间复杂度=访问各结点所需时间+探索各条边的所需时间

image-20210824153832978

深度优先生成树

image-20210824154317513

image-20210824154325555

深度优先生成森林

image-20210824154358401

image-20210824154404987

图的遍历与图的连通性

image-20210824154443209

无向图进行BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数

对于连通图,只需调用一次BFS/DFS

image-20210824154556012

有向图进行BFS/DFS遍历,调用BFS/DFS函数的次数要具体问题具体分析

若其实顶点到其他各顶点都有路径,则只需调用1次BFS/DFS函数

image-20210824154722658

对于强连通图,从任一结点出发都只需调用1次BFS/DFS函数

最小生成树

对于一个带权连通无向图\(G=(V,E)\),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimun-Spanning-Tree,MST)

image-20210824205800437

image-20210824210018729

Prim算法(普里姆)

从某一个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

image-20210824210214776

时间复杂度:\(O(|V|^2)\)

适合用于边稠密图。

实现思想

image-20210824211443868

Kruskal算法(克鲁斯卡尔)

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通

image-20210824210941338

时间复杂度:\(O(|E|log_2|E|)\)

适合用于边稀疏图。

实现思想

image-20210824211712331

最短路径问题

image-20210824211859948

BFS(无权图)

对BFS的小修改,在visit一个顶点时,修改其最短路径长度d[]并在path[]记录前驱结点

//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G,int u)					//从顶点v除发
{
    //d[i]表示从u到i结点的最短路径
    for(int i=0;i<G.vexnum;++i)
    {
        d[i]=无穷大;			//初始化路径长度
        path[i]=-1;			   //最短路径从哪个顶点过来
    }
    d[u]=0;
    visited[v]=TRUE;					//对v做已访问标记
    Enqueue(Q,u);						//顶点v入队列Q
    while(!isEmpty(Q))
    {
        DeQueue(Q,u);					//顶点v出队列
        for(int w=FristNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
        {
            //检测v所有邻接点
            if(!visited[w])					//w为w的尚未访问的邻接顶点
            {
                d[w]=d[u]+1;				//路径长度加1
                path[w]=u;			 			
                visited[w]=TRUE;
                EnQueue(Q,w);
            }
        }
    }
}

Dijkstra算法(迪杰斯特拉)

image-20210824213744621

初始:若从\(V_0\)开始,令final[0]=true;dist[0]=0;path[0]=-1;其余顶点final[k]=false;disk[k]=arcs[0][k];path[k]=(arcs[0][k]==无穷大)?-1:0

n-1轮处理:循环遍历所有顶点,找到还没确定最短路径,且dist最小的顶点\(V_i\),令final[i]=true。并检查所有邻接自\(V_i\)的顶点,对于邻接自\(V_i\)的顶点\(V_j\),若final[j]==falsedist[i]+arc[i][j]<dist[j],则令dist[j]=dist[i]+arc[i][j];path[j]=i(arcs[i][j]表示\(Vi\)\(V_j\)d的弧的权值)

用于负权值带权图

image-20210824214920741

结论:Dijkstra算法不适用于有负权值的带权图。

Floyd算法

image-20210824215241782

image-20210825101417630

image-20210825101356146

\(A^{(k-1)}[i][j]>A^{(k-1)}[i][k]+A^{(k-1)}[k][j]\)

\(A^{(k)}[i][j]=A^{(k-1)}[i][k]+A^{(k-1)}[k][j]\)\(path^{(k)}[i][j]=k\)

否则,\(A^{(k)}\)\(path^{(k)}\)都保持原值

//准备工作,根据图的信息初始化矩阵A和path(如上图)
for(int k=0;k<n;k++)//考虑以vk作为中转点
    for(int i=0;i<n;i++)		//遍历整个矩阵,i为行号,j为列号
        if(A[i][j]>A[i][k]+A[k][j])		//以vk为中转点的路径更短
        {
            A[i][j]=A[i][k]+A[k][j];	//更新最短路径长度
            path[i][j]=k;			    //中转点
        }

不能解决的问题

image-20210825102033693

Floyd算法不能解决带有“负权回路”的图,这种图有可能没有最短路径

总结

image-20210825102008787

有向无环图

有向无环图:若一个有向图中不存在环,则称为有向无环图。简称DAG图(Directed Acyclic Graph)

image-20210825102242685

解题方法:

  1. 把各个操作数不重复排成一排
  2. 标出各个运算符的生效顺序(先后顺序有点出入无所谓)
  3. 按顺序加入运算符,注意“分层”
  4. 从低向上逐层检查同层的运算符是否可以合体

image-20210825103201207

拓扑排序

AOV网(Activity On Vertex Network,用顶点表示活动的网);

用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边\(<V_i,V_j>\)表示活动\(V_i\)必须先于活动\(V_j\)进行

image-20210825103431558

拓扑排序:

在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:

  • 每个顶点出现且只出现一次
  • 若顶点A在序列中排在顶点B的签名,则在图中不存在从顶点B到顶点A的路径

或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若在存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。

拓扑排序实现:

  1. 从AOV网中选择一个没有前驱(入度为0)的顶点并输出
  2. 从网中删除该顶点和所有以它为起点的有向边
  3. 重复①和②直到当前的AOV网为空,或当前网中不存在无前驱的顶点为止

image-20210825104406339

bool TopologicalSort(Grapg G)
{
    InitStack(S);				//初始化栈,存储入度为0的顶点
    for(int i=0;i<G.vexnum;i++)
        if(indegree[i]==0)
            Push(S,i);					//将所有入度为0的顶点进栈
    int count=0;				//计数,记录当前已经输出的顶点数
    while(!IsEmpty(S))			//栈不空,则存在入度为0的顶点
    {
        Pop(S,i);				//栈顶元素出栈
        print[count++]=i;		//输出顶点I
        for(p=G.vertices[i].firstarc;p;p=p->nextarc)
        {
            //将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈
            v=p->adjvex;
            if(!(--indegree[v]))
                Push(S,v);
        }
    }
    if(count<G.vexnum)
        return false;				//排序失败,有向图中有回路
    else
        return true;
}

时间复杂度:\(O(|V|+|E|)\),若采用邻接矩阵,则需\(O(|V|^2)\)

逆拓扑排序

对一个AOV网。如果采用下列步骤进行排序,则称之为逆拓扑排序

  1. 从AOV网中选择一个没有后继(出度为0)的顶点并输出
  2. 从网中删除该顶点和所有以它为终点的有向边
  3. 重复①和②直到当前的AOV网为空

逆拓扑排序的实现(DFS算法)

bool visited[MAX_VERTEX_NUM];			//访问标记数组
//对图G进行深度优先遍历
void DFSTraverse(Graph G)
{
    for(int i=0;i<G.vexnum;++i)
        visited[i]=FALSE;
    for(int i=0;i<G.vexnum;++i)
        if(!visited[i])				
            DFS(G,i);
}
//深度优先遍历
void DFS(Graph G,int v)					//从顶点v除发
{
    visit(v);
    visited[v]=TRUE;					//对v做已访问标记
    for(int w=FristNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
    {
        if(!visited[w])					//w为u的尚未访问的邻接顶点
            DFS(G,w);
    }
    
    print(v);							//输出顶点
}

关键路径

在带权有向图中,以顶点表示时间,以有向边表示活动,以边上的权值表示完成该活动的开销(入完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Acitvity On Edge NetWork)

image-20210825115236864

AOE网具有以下两个性质:

  1. 只有在某顶点所代表的事件发生后,从该顶点出发的各有边所代表的活动才能开始
  2. 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生,另外,有些活动是可以并行进行的。

在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始

仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束

从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。

完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。

事件\(v_k\)的最早发生时间\(Ve(k)\)——决定了所有从\(V_k\)开始的活动能够开工的最早时间

活动\(a_e\)的最早发生时间\(e(i)\)——指该活动弧的起点所表示的事件的最早发生时间

事件\(v_k\)的最迟发生时间\(Vl(k)\)——它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。

活动\(a_e\)的最迟发生时间\(l(i)\)——它是指该活动弧的终点所表示的事件的最迟发生时间于该活动所需时间之差。

活动\(a_i\)时间余量\(d(i)=l(i)-e(i)\),表示在不增加完成整个工程所需总时间的情况下,活动\(a_i\)可以拖延的时间

若一个活动的时间余量为零,则说明该活动必须要如期完成,\(d(i)=0\)\(l(i)=e(i)\)的活动\(a_i\)是关键活动

关键活动组成的路径就是关键路径

求关键路径的步骤:

  1. 求所有事件的最早发生事件\(ve()\)

    拓扑排序序列,一次求各个顶点的\(ve(k)\)

    \(ve(源点)=0\)\(ve(k)=Max\{ve(j)+Weight(v_j,v_k)\},v_j为v_k的任意前驱\)

    image-20210825121640448

  2. 求所有事件的最迟发生时间\(Vl()\)

    逆拓扑排序序列,一次求各个顶点的\(vl(k)\)

    \(vl(汇点)=ve(汇点)\)\(v(lk)=Min\{vl(j)+Weight(v_k,v_j)\},v_j为v_k的任意后继\)

    image-20210825121830522

  3. 求所有活动的最早发生时间\(e()\)

    若边\(<v_k,v_j>\)表示活动\(a_i\),则有\(e(i)=ve(k)\)

    image-20210825121942581

  4. 求所有活动的最迟发生时间\(l()\)

    若边\(<v_k,v_j>\)表示活动\(a_i\),则有\(l(i)=vl(j)-Weight(v_k,v_j)\)

    image-20210825122152636

  5. 求所有活动的时间余量\(d()\)

    \(d(i)=l(i)-e(i)\)

    image-20210825122242795

关键活动,关键路径的特性

若关键活动耗时增加,则整个工程的工期将增长

缩短关键活动的时间,可以缩短整个工程的工期

当缩短到一定程度时,关键活动可能会变成非关键活动

可能有多条关键路径,只提高一条关键路径上的关键活动并不能缩短整个工程的工期,只有加快那些包括在所有关键路径的关键活动才能缩短工期。

posted @ 2021-08-26 17:15  Ligo丶  阅读(363)  评论(0编辑  收藏  举报