23. 图

一、什么是图

  图 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) | u ∈ V, v ∈ V\}\),用 \(|E|\) 表示图 G 中 边的条数

  若 E 是 无向边(简称 )的有限集合时,则图 G 为 无向图。边是顶点的无序对,记为 (v, w) 或 (w, v),因为 (v, w) = (w, v),其中 v、w 是顶点。可以说顶点 w 和顶点 v 互为邻接点。边 (v, w) 依附于顶点 w 和 v,或者说边 (v, w) 和顶点 v、w 相关联。

  若 E 是 有向边(也称 )的有限集合时,则图 G 为 有向图。弧是顶点的有序对,即为 <v, w>(<v, w> ≠ <w, v>),其中 v、w 是顶点,v 称为 弧尾,w 称 弧头,<v, w> 称为从顶点 v 到顶点 w 的弧,也称 v 邻接到 w,或 w 邻接自 v。

  对于 无向图 而言,顶点的度 是指依附于该顶点的边的条数,记为 TD(v)。在具有 n 个顶点、e 条边的无向图中,\(\sum_{i=1}^{n}{TD(V_{i})} = 2e\),即无向图的全部顶点的度的和等于边数的 2 倍。

  对于 有向图 而言,入度 是指以顶点 v 为终点的有向边的数目,记为 ID(v)。出度 是指顶点 v 为起点的有向边的数目,记为 OD(v)。顶点的度 等于其 入度与出度之和,即 TD(v)=ID(v)+OD(v)。在具有 n 个顶点、e 条边的有向图中,\(\sum_{i=1}^{n}{ID(V_{i})} = \sum_{i=1}^{n}{OD(V_{i})} = e\)

  • 简单图:不存在重复边,即不存在顶点到自身的边。
  • 多重图:图 G 中某两个结点之间的边数多余一条,又允许顶点通过同一条边和自己关联。
  • 路径:顶点 \(v_{p}\) 到顶点 \(v_{q}\) 之间的一条路径是指顶点序列。
  • 回路:第一个顶点和最后一个顶点相同的路径称为回路或环。
  • 简单路径:在路径序列中,顶点不重复出现的路径称为简单路径。
  • 简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复的回路称为简单回路。
  • 路径长度:路径上边的数目。
  • 点到点的距离:从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离。若从 u 到 v 根本不存在,则记该距离为无穷。
  • 连通:在 无向图 中,若从顶点 v 到顶点 w 有路径存在,则 v 和 w 是连通的。
  • 强连通:在 有向图 中,若从顶点 v 到顶点 w 和从顶点 w 到顶点 v 之间都有路径存在,则 v 和 w 是强连通的。
  • 连通图:若图 G 中任意两个顶点都是连通的,则称图 G 为 连通图,否则称为 非连通图
  • 强连通图:若图中任何一个顶点都是强连通的,则称此图为 强连通图
  • 子图:设有两个图 G=(V, E) 和 G'=(V', E')。若 V' 是 V 的子集,且 E' 是 E 的子集,则称 G' 是 G 的 子图
  • 生成子图:若满足 V(G') = V(G) 的子图,则称其为 G 的 生成子图
  • 连通分量无向图 中的 极大连通子图 称为 连通分量
  • 强连通分量:有向图 中的 极大连通子图 称为有向图的 强连通分量
  • 生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图。
  • 生成森林:在连通图中,连通分量的生成树构成了非连通图的生成森林。
  • 边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
  • 带权图:边上带有权值的图称为 带权图,也称
  • 带权路径长度:当图是 带权图 时,一条 路径上所有边的权值之和,称为该路径的 带权路径长度
  • 无向完全图:无向图中任意两个顶点之间都存在边。
  • 有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧。
  • 稀疏图:边数很少的图称为 稀疏图,反之称为 稠密图
  • :不存在回路,且连通的无向图。
  • 有向树:一个顶点的入读为 0,其余顶点的入读均为 1 的有向图,称为 有向树

图

ADT Graph
{
Data:
    图G∈Graph, G(V,E)是由一个非空的有限顶点集合V和一个有限边集合E组成
Operation:
    Graph Create(void);                                 // 创建一个空的图
    Graph InsertVertex(Graph G, Vertex V);              // 向图G中插入顶点V
    Graph InsertEdge(Graph G, Edge E);                  // 向图G中插入边E
    void DFS(Graph G, Vertex V);                        // 从顶点V出发深度优先遍历图G
    void BFS(Graph G, Vertex V);                        // 从顶点V出发广度优先遍历图G
    void ShortestPath(Graph G, Vertex V, int Dist[]);   // 计算图G中顶点V出发到其他任一顶点的最短路径
    void MST(Graph G);                                  // 计算图G的最小生成树
} ADT Graph;

图不可以空,即顶点集(V)一定是非空集。

对于 n 个顶点无向图 G,若 G 是连通图,则最少有 n-1 条边。

极大连通子图必须连通,且包含尽可能多的顶点和边。

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

若完全无向图的顶点数 \(|V| = n\),则 \(|E| ∈ [0, C_{n}^{2}] = [0, n(n-1)/2]\)

若有向完全图的顶点数 \(|V| = n\),则 \(|E| ∈ [0, 2C_{n}^{2}] = [0, n(n-1)]\)

稀疏图和稠密图之间没有绝对的界限,一般来说 \(|E| < |V|log|V|\) 时,可以将 G 视为稀疏图。

n 个顶点的树,必有 n-1 条边,即 n 个顶点的图,若 \(|E| > n - 1\),则一定有回路。

二、图的表示

2.1、邻接矩阵表示法

  我们可以使用一个二维数组来表示图。对于每条边 (u, v),我们置 A[u][v]=1,否则数组的元素就是 0。如果边有一个权,那么我们可以置 A[u][v] 等于该权,而使用一个很大或者很小的权作为标记表示不存在的边。

邻接矩阵表示法

#define MAX_VERTEX_NUM      5                                                   // 顶点数最大值

typedef char VertexType;
typedef int EdgeType;

// 图
typedef struct AdjMatrixGraph
{
    VertexType Vertex[MAX_VERTEX_NUM];                                          // 顶点表,存放顶点数据
    EdgeType Matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];                            // 邻接矩阵,边表,存放权重
    int VertexNum;                                                              // 顶点数
    int EdgeNum;                                                                // 边数
} AdjMatrixGraph, * MGraph;

// 边
typedef struct EdgeNode
{
    int V1, V2;                                                                 // 有向边<V1, V2>
    EdgeType Weight;                                                            // 权重
} EdgeNode, * Edge;

  对于无向图而言,第 i 个结点的度等于第 i 行(或第 i 列)的非零元素个数。对于有向图而言,第 i 个结点的出度等于第i 行的非零元素个数,入度等于第 i 列非零元素个数。第 i 个结点的度等于第 i 行、第 i 列非零元素个数之和。

  对于无向图的存储,我们可以使用三角矩阵的方式压缩数组,这样可以省一般的空间。用一个长度为 \(\frac{N(N+1)}{2}\) 的一维数组 A 存储,则 \(G_{ig}\) 在 A 中对应的下标为:

\[(i * (i + 1) / 2 + j) \]

2.2、邻接表表示法

  若图是稀疏图的话,推荐使用邻接表表示法。对每一个顶点,我们使用一个表存放所有的链接的顶点。G[N] 为指针数组,对应矩阵每行一个链表。

邻接表表示法

#define MAX_VERTEX_NUM      5                                                   // 顶点数最大值

typedef char VertexType;
typedef int EdgeType;

// 邻接结点
typedef struct AdjacencyNode
{
    int AdjVertex;                                                              // 邻接点下标
    EdgeType Weight;                                                            // 边权值
    struct AdjacencyNode * Next;                                                // 指向下一条边的指针
} AdjacencyNode, * AdjNode;

// 顶点
typedef struct VertexNode
{
    VertexType Data;                                                            // 顶点数据
    AdjNode FirstEdge;                                                          // 指向第一条边的指针
} VertexNode, AdjList[MAX_VERTEX_NUM];

// 图
typedef struct AdjListGraph
{
    AdjList List;                                                               // 邻接表
    int VertexNum;                                                              // 顶点数
    int EdgeNum;                                                                // 边数
} AdjListGraph, * LGraph;

// 边
typedef struct EdgeNode
{
    int V1, V2;                                                                 // 有向边<V1, V2>
    EdgeType Weight;                                                            // 权重
} EdgeNode, * Edge;

对于无向图而言,邻接表需要 N 个头指针,2E 个结点(每个结点至少 2 个域)。对于有向图,邻接表需要 N 个头指针,E 个结点(每个结点至少 2 个域)。

对于有向图而言,只能计算某个结点的出度,我们还需要构建一个逆邻接表(存指向自己的边)来方便计算入度。

三、图的建立

3.1、邻接矩阵存储

/**
 * @brief 创建有VertexNum个顶点没有边的图
 * 
 * @param VertexNum 顶点个数
 * @return MGraph 指向图的指针
 */
MGraph CreateGraph(int VertexNum)
{
    MGraph G = (MGraph)malloc(sizeof(AdjMatrixGraph));

    G->VertexNum = VertexNum;
    G->EdgeNum = 0;
    for (int v = 0; v < VertexNum; v++)
    {
        for (int w = 0; w < VertexNum; w++)
        {
            G->Matrix[v][w] = 0;
        }
    }

    return G;
}
/**
 * @brief 向图中插入边
 * 
 * @param G 图
 * @param E 边
 */
void InsertEdge(MGraph G, Edge E)
{
    // 插入有向边<V1, V2>
    G->Matrix[E->V1][E->V2] = E->Weight; 
    // 对于无向图,还要插入边<V2, V1>
    G->Matrix[E->V2][E->V1] = E->Weight;
}
/**
 * @brief 图的建立
 * 
 * @return MGraph 返回指向图的指针
 */
MGraph BuildGraph(void)
{
    MGraph G = NULL;
    Edge E = NULL;

    printf("请输入定点数:\n");
    scanf("%d", &G->VertexNum);

    if (G->VertexNum != 0)
    {
        E = (Edge) malloc(sizeof(EdgeNode));
        while (E != NULL)
        {
            printf("请输入边<V1, V2>的顶点序号和权重:\n");
            scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
            InsertEdge(G, E);
        }
    }

    // 若顶点有数据,读入数据
    printf("请输入顶点数据:\n");
    for (int v = 0; v < G->VertexNum; v++)
    {
        scanf(" %c", &G->Vertex[v]);
    }
  
    return G;
}
/**
 * @brief 图的建立
 * 
 * @return MGraph 返回指向图的指针
 */
MGraph BuildGraph(void)
{
    MGraph G = NULL;
    Edge E = NULL;
    int VertexNum = 0;

    printf("请输入顶点数:\n");
    scanf("%d", &VertexNum);

    G = CreateGraph(VertexNum);

    printf("请输入边数:\n");
    scanf("%d", &G->EdgeNum);

    if (G->EdgeNum != 0)
    {
        E = (Edge) malloc(sizeof(EdgeNode));
        printf("请输入边<V1, V2>的顶点序号和权重:\n");

        for (int e = 0; e < G->EdgeNum; e++)
        {
            scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
            InsertEdge(G, E);
        }
    }

    // 若顶点有数据,读入数据
    printf("请输入顶点数据:\n");
    for (int v = 0; v < G->VertexNum; v++)
    {
        scanf(" %c", &G->Vertex[v]);
    }
  
    return G;
}

3.2、邻接表存储

/**
 * @brief 创建有VertexNum个顶点没有边的图
 * 
 * @param VertexNum 顶点个数
 * @return LGraph 指向图的指针
 */
LGraph CreateGraph(int VertexNum)
{
    LGraph G = (LGraph)malloc(sizeof(AdjListGraph));

    G->VertexNum = VertexNum;
    G->EdgeNum = 0;
  
    for (int v = 0; v < VertexNum; v++)
    {
        G->List[v].FirstEdge = NULL;
    }

    return G;
}
/**
 * @brief 向图中插入边
 * 
 * @param G 图
 * @param E 边
 */
void InsertEdge(LGraph G, Edge E)
{
    // 插入有向边<V1, V2>
    AdjNode NewNode = (AdjNode)malloc(sizeof(AdjacencyNode));
    NewNode->AdjVertex = E->V2;
    NewNode->Weight = E->Weight;
    // 将V2插入到V1的表头
    NewNode->Next = G->List[E->V1].FirstEdge;
    G->List[E->V1].FirstEdge = NewNode;

    // 对于无向图,还要插入边<V2, V1>
    NewNode = (AdjNode)malloc(sizeof(AdjacencyNode));
    NewNode->AdjVertex = E->V1;
    NewNode->Weight = E->Weight;
    // 将V1插入到V2的表头
    NewNode->Next = G->List[E->V2].FirstEdge;
    G->List[E->V2].FirstEdge = NewNode;
}
/**
 * @brief 图的建立
 * 
 * @return MGraph 返回指向图的指针
 */
LGraph BuildGraph(void)
{
    LGraph G = NULL;
    Edge E = NULL;
    int VertexNum = 0;

    printf("请输入顶点数:\n");
    scanf("%d", &VertexNum);

    G = CreateGraph(VertexNum);

    printf("请输入边数:\n");
    scanf("%d", &G->EdgeNum);

    if (G->EdgeNum != 0)
    {
        E = (Edge) malloc(sizeof(EdgeNode));
        printf("请输入边<V1, V2>的顶点序号和权重:\n");

        for (int e = 0; e < G->EdgeNum; e++)
        {
            scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
            InsertEdge(G, E);
        }
    }

    // 若顶点有数据,读入数据
    printf("请输入顶点数据:\n");
    for (int v = 0; v < G->VertexNum; v++)
    {
        scanf(" %c", &G->List[v].Data);
    }
  
    return G;
}

四、图的遍历

4.1、深度优先搜索

  深度优先搜索(Depth First Search,简称 DFS),它的具体步骤如下:

  • 在访问图中某一个起始顶点 v 后,由 v 出发,访问它的任意邻接顶点 w1。
  • 在从 w1 出发,访问与 w1 邻接但还未访问的顶点 w2。
  • 然后再从 w2 出发,进行类似的访问。
  • 如此进行下去,直到到达所有的邻接顶点都被访问过的顶点 u 为止。
  • 接着,返回上一步,退到前一次刚访问的顶点,看是否还有其它没有被访问的邻接顶点。
  • 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问。
  • 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。

深度优先遍历

/**
 * @brief 深度优先遍历
 * 
 * @param G 图
 */
void DFS_Traverse(Graph G)
{
    int visited[G->VertexNum];

    for (int i = 0; i < G->VertexNum; i++)
    {
        visited[i] = 0;
    }  

    for (int v = 0; v < G->VertexNum; v++)
    {
        if (visited[v] == 0)
        {
            DFS(G, visited, v);
        }
    }
}
/**
 * @brief 深度优先核心实现
 * 
 * @param G 图
 * @param visited 访问数据
 * @param v 第一个访问的邻接点
 */
void DFS(Graph G, int visited[], int v)
{
    Visit(G, v);                                                                // 访问顶点v
    visited[v] = 1;                                                             // 标记顶点v为已访问

    int w = GetFirstNeighbor(G, v);                                             // 获取结点v的第一个邻接结点
    while (w != -1)                                                             // 如果存在邻接结点
    {
        if (visited[w] == 0)                                                    // 如果邻接结点没有被访问过
        {
            DFS(G, visited, w);                                                 // 递归访问下一个邻接结点
        }
        // 如果邻接结点已经被访问过,获取下一个邻接结点
        w = GetNextNeighbor(G, v, w);
    }
}

  不同的存储实现方式下,Visit()、GetFirstNeighbor() 和 GetNextNeighbor() 方法的实现有所差别。如果用邻接矩阵的存放方式,它的实现方式如下:

/**
 * @brief 访问结点
 * 
 * @param G 图
 * @param v 结点
 */
void Visit(MGraph G, int v)
{
    printf("%c ", G->Vertex[v]);
}
/**
 * @brief 获取第一个邻接结点
 * 
 * @param G 图
 * @param V 获取谁的邻接结点
 * @return int 如果有返回邻接结点的下标,否则返回-1
 */
int GetFirstNeighbor(MGraph G, int V)
{
    for (int i = 0; i < G->VertexNum; i++)
    {
        if (G->Matrix[V][i] > 0)
        {
            return i;
        }
    }
  
    return -1;
}
/**
 * @brief 根据前一个邻接结点获取下一个邻接结点
 * 
 * @param G 图
 * @param V 获取谁的邻接结点
 * @param W 前一个邻接结点
 * @return int 如果有返回邻接结点的下标,否则返回-1
 */
int GetNextNeighbor(MGraph G, int V, int W)
{
    for (int i = W + 1; i < G->VertexNum; i++)
    {
        if (G->Matrix[V][i] > 0)
        {
            return i;
        }
    }

    return -1;
}

  如果,我们用邻接表的方式存储,它的实现如下:

/**
 * @brief 访问结点
 * 
 * @param G 图
 * @param v 结点
 */
void Visit(LGraph G, int v)
{
    printf("%c ", G->List[v].Data);
}
/**
 * @brief 获取第一个邻接结点
 * 
 * @param G 图
 * @param V 获取谁的邻接结点
 * @return int 如果有返回邻接结点的下标,否则返回-1
 */
int GetFirstNeighbor(LGraph G, int V)
{
    if (G->List[V].FirstEdge == NULL)
    {
        return -1;
    }

    return G->List[V].FirstEdge->AdjVertex;;
}
/**
 * @brief 根据前一个邻接结点获取下一个邻接结点
 * 
 * @param G 图
 * @param V 获取谁的邻接结点
 * @param W 前一个邻接结点
 * @return int 如果有返回邻接结点的下标,否则返回-1
 */
int GetNextNeighbor(LGraph G, int V, int W)
{
    AdjNode adjNode = G->List[V].FirstEdge;

    while (adjNode != NULL && adjNode->AdjVertex != W)
    {
        adjNode = adjNode->Next;
    }

    if (adjNode == NULL || adjNode->Next == NULL)
    {
        return -1;
    }
  
    return adjNode->Next->AdjVertex;
}

若有 N 个顶点、E 条边,用邻接矩阵存储图,时间复杂度为 \(O(N^{2})\),用邻接表存储图,时间复杂度为:\(O(O+E)\)

4.2、广度优先搜素

  广度优先搜索(Breadth First Search,简称 BFS),它的具体步骤如下:从图中的某一结点出发,首先一次访问该结点的所有邻接顶点,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点。重复此过程,直至所有顶点均被访问为止。

广度优先遍历

/**
 * @brief 广度优先遍历
 * 
 * @param G 图
 */
void BFS_Traverse(Graph G)
{
    int visited[G->VertexNum];

    for (int i = 0; i < G->VertexNum; i++)
    {
        visited[i] = 0;
    }  

    for (int v = 0; v < G->VertexNum; v++)
    {
        if (visited[v] == 0)
        {
            BFS(G, visited, v);
        }
    }
}
/**
 * @brief 广度优先遍历递归函数
 * 
 * @param G 图
 * @param visited 访问数据
 * @param v 第一个访问的邻接点
 */
void BFS(Graph G, int visited[], int v)
{
    PQueue Q = CreateQueue();

    int u = -1, w = -1;
    Visit(G, v);                                                                // 访问顶点v
    visited[v] = 1;                                                             // 标记顶点v为已访问
    Enqueue(Q, v);                                                              // 将v入队

    while (Q->Front != NULL)
    {
        u = Dequeue(Q);                                                         // 取出队头结点下标
        w = GetFirstNeighbor(G, u);                                             // 得到u的第一个邻接点下标w
        while (w != -1)
        {
            if (visited[w] == 0)                                                // 如果没有访问过
            {
                Visit(G, w);                                                    // 访问顶点w
                visited[w] = 1;                                                 // 标记顶点w为已访问
                Enqueue(Q, w);                                                  // 将w入队
            }
            else
            {
                w = GetNextNeighbor(G, u, w);                                   // 获取u的下一个邻接点
            }
        }
    }
}

若有 N 个顶点、E 条边,用邻接矩阵存储图,时间复杂度为 \(O(N^{2})\),用邻接表存储图,时间复杂度为:\(O(O+E)\)

posted @ 2023-07-29 19:52  星光樱梦  阅读(12)  评论(0编辑  收藏  举报