Graph


Graph

定义&概念

相关概念:

  • 有向图无向图简单图完全图有向完全图(弧数为前者两倍),稀疏图、稠密图;
  • 顶点不重复->简单路径/回路/环;
  • (无向图中)连通图:任意两个顶点都是连通的;连通分量:无向图中的极大连通子图;
  • (有向图中)强连通图:a->b,b->a均存在路径;强连通分量:有向图中的极大连通子图;
  • 生成树:一个极小连通子图;在非连通图中,连通分量的生成树构成了非连通图的生成森林

存储结构

数组表示法(邻接矩阵)

对于无向图的邻接矩阵,其一定是一个对称矩阵,而对于有向图而言并不对称;

#define INFINITY INT_MAX  // 最大值
#define MAX_VERTEX_NUM 20 // 最大顶点个数

typedef int VRType;     // 顶点关系类型,无权图1,0表相邻,带权图表权值
typedef int *InfoType;  // 弧相关信息指针
typedef int VertexType; // 顶点向量

typedef enum
{
  DG,
  DN,
  UDG,
  UDN
} GraphKind; //{有向图,有向网,无向图,无向网}

typedef struct ArcCell
{
  VRType adj;
  InfoType *info;
} ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct
{
  VertexType vexs[MAX_VERTEX_NUM];
  AdjMatrix arcs;     // 邻接矩阵
  int vexnum, arcnum; // 顶点数及弧数
  GraphKind kind;     // 图的种类标志
} MGraph;

邻接矩阵表示法的空间复杂度为O(n^2),适合稠密图进行存储表示;

邻接表

链式存储结构,在邻接表中,对图中每个顶点建立一个单链表,单链表中每个节点表示依附于该顶点的各边。
每个节点由三个域组成,邻接点域指示与该弧指向的顶点位置;链域指示下一条边或弧的节点;数据域存储边或弧的相关信息,如权值等;在每个链表上附设一个表头节点,包含指向第一条弧的链域以及顶点信息;

// 邻接表
#define MAX_VERTEX_NUM 20
typedef struct ArcNode
{ // 弧节点
  int adjvex;              // 该弧指向的顶点位置
  struct ArcNode *nextarc; // 指向下一条弧的指针
  InfoType *info;          // 该弧相关信息的指针
} ArcNode;

typedef struct VNode
{ 
  VertexType data;   // 顶点信息
  ArcNode *firstarc; // 指向第一条依附于该顶点的弧的指针
} VNode, AdjList[MAX_VERTEX_NUM];

typedef struct
{
  AdjList vertices;
  int vexnum, arcnum; // 图的当前顶点数和弧数
  int kind;           // 图的种类标志
} ALGraph;

邻接表的表示方式便于实现对于出度的求解,然而对于入度,则必须遍历整个邻接表。为此可以建立一个有向图的逆邻接表,加速求解给定顶点的入度;

十字链表

十字链表可视作将有向图的邻接表与逆邻接表结合起来,
弧节点中有五个域,尾域tailvex和头域headvex分别指示弧尾和弧头这两个顶点在图中的位置;链域hlink指向弧头相同的下一条弧;链域tlink指向弧尾相同的下一条弧;info域指示相关信息;
顶点节点有三个域,data域存储节点相关信息;firstin及firstout两个链域分别指向以该顶点为弧头或弧尾的第一个弧节点;

// 十字链表
#define MAX_VERTEX_NUM 20
typedef struct ArcBox
{
  int tailvex, headvex;         // 该弧的尾和头顶点的位置
  struct ArcBox *hlink, *tlink; // 分别为弧头相同和弧尾相同的弧的链域
  InfoType *info;               // 该弧相关信息
} ArcBox;

typedef struct VexNode
{
  VertexType data;
  ArcBox *firstin, *firstout; // 分别指向该顶点的第一条入弧和出弧
} VexNode;

typedef struct
{
  VexNode xlist[MAX_VERTEX_NUM]; // 表头向量
  int vexnum, arcnum;            // 有向图的当前顶点数和弧数
} OLGraph;

邻接多重表

未完待续……

图的遍历

深度优先搜索(DFS)

类似于树的先根遍历,假设初始状态是图中所有顶点未曾被访问,DFS从某一顶点v出发,访问该节点,依次从v的未被访问的邻接点出发深度优先遍历图,指示图中所有和v有路径相通的顶点都被访问到;若此时还有顶点未被访问,则另选一个未被访问的顶点做起点,继续DFS;

typedef int Status;
int visited[MAX_VERTEX_NUM]; // 访问标记数组
Status (*VisitFunc)(int v);  // 函数变量

//该处不一定是MGraph
void DFSTraverse(MGraph G, Status (*Visit)(int v))
{                    // 对图G作深度优先遍历
  VisitFunc = Visit; // 使用全局变量VisitFunc, 使DFS不必设函数指针参数
  for (int v = 0; v < G.vexnum; v++)
    visited[v] = 0; // 初始化
  for (int v = 0; v < G.vexnum; v++)
    if (!visited[v]) // 对未访问节点调用DFS
      DFS(G, v);
}

void DFS(MGraph G, int v)
{ // 从第v个顶点出发递归地深度优先遍历图G
  visited[v] = 1;
  VisitFunc(v);
  for (int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
    if (!visited[w])
      DFS(G, w); // 对尚未访问的邻接顶点w递归调用DFS
}

广度优先搜索

类似于树的按层次遍历,假设从图中某顶点v出发,访问v后依次访问v的各个未曾访问的邻接点,然后从这些邻接点出发依次访问他们的邻接点,而且要求“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至所有已访问顶点的邻接点均被访问。同样,若存在未被访问的节点,选取并重复上述过程;

void BFSTraverse(MGraph G, Status (*Visit)(int v))
{ // 按广度优先非递归遍历图G,使用辅助队列Q和访问标志数组visited
  for (int v = 0; v < G.vexnum; v++)
    visited[v] = 0; // 初始化
  SqQueue *Q = (SqQueue *)malloc(sizeof(SqQueue));
  InitQueue(Q); //
  for (int v = 0; v < G.vexnum; v++)
  {
    if (!visited[v])
    {
      visited[v] = 1;
      Visit(v);
      EnQueue(Q, v); // 入队,便于之后先层次遍历
      while (!QueueEmpty(Q))
      { // 若队列不空
        int *u = (int *)malloc(sizeof(int));
        DeQueue(Q, u); // 出队,进行层次遍历
        for (int w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
          if (!visited[w])
          {
            visited[w] = 1;
            Visit(w);
            EnQueue(Q, w); // 将初次访问节点再次入队
          }
      }
    }
  }
}

图的连通性问题

无向图的连通分量和生成树

由DFS搜索得到的为深度优先生成树(森林),由BFS搜索得到的为广度优先生成树(森林)
假设以孩子兄弟链表作为生成森林的存储结构,如下算法生成非连通图的深度优先生成森林:

……

有向图的强连通分量

未完待续……

最小生成树

构造连通网的最小代价生成树(简称为最小生成树)问题;
多数算法借助最小生成树的一种简称为MST的性质:假设连通网N=(V,{E}),U是顶点集V的一个非空子集,若(u,v)是一条具有最小权值的边,其中顶点u属于U,顶点v属于V-U,则必存在一颗包含边(u,v)的最小生成树;
如下介绍普里姆算法(Prim)和克鲁斯卡尔算法(Kruskal):

  • Prim

从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复找最短的边并添加;

// 记录从顶点集U->U-V的代价最小的边的辅助数组定义:
struct
{
  VertexType adjvex;
  VRType lowcost;
} closedge[MAX_VERTEX_NUM];

void MiniSpanTree_PRIM(MGraph G, VertexType u)
{ // 用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边
  int k = LocateVex(G, u);
  for (int j = 0; j < G.vexnum; j++)
  { // 辅助数组初始化,此时顶点集中元素只有u
    if (j != k)
    {
      closedge[j].adjvex = u;
      closedge[j].lowcost = G.arcs[k][j].adj;
    }
  }
  closedge[k].lowcost = 0; // 初始顶点,U={u}
  for (int i = 1; i < G.vexnum; i++)
  {
    k = minimum(closedge);
    //minimum函数实现在U-V顶点集中找到距U顶点集最近的顶点
    //即closedge中在U-V顶点集中lowcost最小的顶点
    printf(closedge[k].adjvex, G.vexs[k]);    //输出生成树的边
    closedge[k].lowcost = 0;    //将该点加入U顶点集中,即此时代价为0
    for (int j = 0; j < G.vexnum; j++)
    { //由于k并入U中,重新规划辅助数组,判断代价是否有更小的变化
      if (G.arcs[k][j].adj < closedge[j].lowcost)
      {
        closedge[j].adjvex = G.vexs[k];
        closedge[j].lowcost = G.arcs[k][j].adj;
      }
    }
  }
}
  • Kruskal

假设初始状态为n个顶点而无边的存在,在该图中每个顶点自成一个连通分量;在E中选择代价最小的边,若该边依附的顶点在T的不同连通分量上,则可以将该边加入T中,否则另取下一条代价最小的边;

未完待续……

有向无环图及其应用

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网;一个无环的有向图称作**有向无环图(DAG)**;

拓扑排序

由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序

如何进行拓扑排序:

  • 从有向图中选一个没有前驱的顶点并输出;
  • 从图中删除该顶点和所有以它为尾的弧;
  • 重复以上过程,直至全部顶点均输出,或者当前图中不存在无前驱的顶点为止;

采取邻接表作为有向图的存储结构,且在头结点中增加一个存放顶点入度的数组indegree;同时为避免重复检测入度为零的顶点,另设一栈暂存所有入度为零的顶点;

Status TopologicalSort(ALGraph G)
{
  int indegree[MAX_VERTEX_NUM];
  FindInDegree(G, indegree); // 对各顶点求入度
  SqStack *S = (SqStack *)malloc(sizeof(SqStack));
  InitStack(S); // 建零入度顶点栈
  for (int i = 0; i < G.vexnum; i++)
  { // 入度为零者进栈
    if (!indegree[i])
      Push(S, i);
  }
  int count = 0;
  while (!StackEmpty(S))
  {
    int *i = (int *)malloc(sizeof(int));
    Pop(S, i);
    printf(i, G.vertices[*i].data);
    count++; // 输出i号顶点并计数
    for (ArcNode *p = G.vertices[*i].firstarc; p; p = p->nextarc)
    {
      int k = p->adjvex;
      if (!(--indegree[k]))
        Push(S, k); // 对于删除节点后入度减为零的节点进行入栈操作
    }
  }
  if (count < G.vexnum)
    return ERROR; // 此时有向图有回路
  else
    return OK;
}

由于输出每个顶点的同时还要删除以它为起点的边,因此拓扑排序的时间复杂度为O(n+e);
考虑有向图中无环时,也可以利用深度优先遍历实现拓扑排序,由于从某点出发进行深度优先搜索遍历时,最先退出DFS函数的顶点即出度为零的顶点,也即是拓扑有序序列中最后一个顶点,因此按照退出DFS函数的先后记录下来的顶点序列即为逆向的拓扑有序序列;

关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销,称之用边表示活动的网络,简称AOE网

再次表明AOE网与AOV网的异同,两者都是有向无环图,不同之处在于边和顶点表示的含义是不同的,AOE网中的边有权值,而AOV网中的边无权值,仅表示顶点之间的前后关系;

AOE网有如下性质:

  • 在某顶点代表事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
  • 在进入某顶点的各有向边所代表的的活动都已结束后,该顶点所代表的的事件才能发生;

相关概念:顶点(源点),结束顶点(汇点);路径长度指各个活动所持续的时间之和;关键路径指从源点到汇点具有最大长度的路径,同时在关键路径上的活动称之为关键活动
未完待续……

最短路径

两顶点之间经过的边上权值之和最小的路径,且称路径上的第一个顶点为源点,最后一个顶点为终点;

迪杰斯特拉算法(Dijkstra)

弗洛伊德算法(Floyd)

未完待续……


文章作者: HOUCQ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 HOUCQ !
评论
  目录