【数据结构与算法】data structures & algorithms 第五章:图的数据结构

数据结构与算法系列文章目录

【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用



一、什么是图

  • 表示多对多的关系

  • 包含

    • 一组顶点:通常用 V ( V e r t e x ) V(Vertex) V(Vertex)表示顶点集合
    • 一组边:通常用 E ( E d g e ) E(Edge) E(Edge)表示边的集合
      • 边是顶点对:无向边 ( v , w ) ∈ E (v, w)\in E (v,w)E,其中 v , w ∈ V v, w\in V v,wV
      • 有向边 < v , w > <v, w> <v,w>表示从 v v v指向 w w w的边(单行线)
      • 不考虑重边和自回路
  • 抽象数据类型定义

    • 类型名称:图(Graph)
    • 数据对象集: G ( V , E ) G(V, E) G(V,E)由一个非空的有限顶点集合 V V V和一个有限边集合 E E E组成
    • 操作集:对于任意图 G ∈ G r a p h G\in Graph GGraph,以及 v ∈ V , e ∈ E v\in V, e\in E vV,eE
      • Graph create():建立并返回空图
      • Graph insertVertex(Graph G, Vertex v):将v插入G
      • Graph insertEdge(Graph G, Edge e):将e插入G
      • 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的最小生成树
  • 常见术语

    • 无向图:两结点之间没有方向,只有一条路径
    • 有向图:两结点之间存在有方向限制的路径
    • 网络:上述图的边是没有权重的,如果为每条边附上权重,则称为网络

二、怎么在程序中表示一个图

1、邻接矩阵

  • G [ N ] [ N ] G[N][N] G[N][N]——N个顶点从0到N-1编号
  • G [ i ] [ j ] = { 1 若 < v i , v j > 是 G 中 的 边 0 否 则 G[i][j] = \begin{cases} 1 \hspace{1cm} 若<v_i, v_j>是G中的边 \\ 0 \hspace{1cm}否则\end{cases} G[i][j]={1<vi,vj>G0

在这里插入图片描述

无向图的邻接矩阵是关于对角线对称的,因此我们应该想办法省下一半的空间来存储无向图

  • 无向图的存储,可以用一个长度为 N ( N + 1 ) / 2 N(N+1)/2 N(N+1)/2的一维数组A存储 { G 00 , G 10 , G 11 , ⋯   , G n − 1 , n − 1 } \{ G_{00}, G_{10}, G_{11}, \cdots, G_{n-1,n-1} \} {G00,G10,G11,,Gn1,n1},则 G i j G_{ij} Gij在A中对应的下标为: ( i ∗ ( i + 1 ) / 2 + j ) (i*(i+1)/2+j) (i(i+1)/2+j)

  • 有向图的度,计算某结点的出度,则计算对应结点的行中的非零个数;计算某结点的入度,则计算对应结点的列中的非零个数;

  • 对于网络,只要把 G [ i ] [ j ] G[i][j] G[i][j]的值定义为边 < v i , v j > <v_i, v_j> <vi,vj>的权重即可

  • 邻接矩阵的优点:

    • 直观、简单、好理解
    • 方便检查任意一对顶点间是否存在边
    • 方便检查任意一对的所有“邻接点”(有边直接相连的顶点)
    • 方便计算任一顶点的”度“(从该点发出的边数为”出度“,指向该点的边数为”入度“)
      • 无向图:对应行(或列)非零元素的个数
      • 有向图:对应行非零元素的个数是”出度“;对应列非零元素个数是”入度“
  • 邻接矩阵的缺点:

    • 浪费空间——存稀释图(点很多二边很少)有大量无效元素
      • 对稠密图(特别是完全图)还是很合算的
    • 浪费时间——统计稀释图中一共有多少条边

2、邻接表

  • G [ N ] G[N] G[N]为指针数组,对应矩阵每行一个链表,只存非零元素;一定要够稀疏才合算
  • 对于网络,结构中要增加权重的域

在这里插入图片描述

  • 邻接表的优缺点
    • 方便找任一顶点的所有”邻接点“
    • 节约稀疏图的空间
      • 需要N个头指针 + 2E个结点(每个结点至少2个域)
    • 方便计算任一顶点的”度“
      • 无向图:可以
      • 有向图:只能计算”出度“;要计算”入度“,需要构造”逆邻接表“(存指向自己的i边)
    • 不方便检查任意一对顶点之间是否存在边

三、图的遍历

1、深度优先搜索

  • 深度优先搜索(Depth First Search, DFS)
//类似树的 先序 遍历
void DFS(Vertex V)
{
    visited[V] = true;
    for (V 的每个邻接点 W)
    {
        if (!visited[W])
        {
            DFS(W);
        }
    }
}
  • DFS先搜索当前顶点是否可以往下遍历,

    • 若可以则往下遍历,
    • 若不可以则往回退一个结点,并搜索当前结点是否有其它没有遍历过的结点,
      • 若有则往下遍历,
      • 若没有则往回退一个结点,
    • 如此循环,直到退到初始结点为止
  • 若有N个结点,E条边,时间复杂度

    • 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
    • 用邻接矩阵存储图,为 O ( N 2 ) O(N^2) O(N2)

2、广度优先搜索

  • 广度优先搜索(Breadth First Search, BFS)
void BFS(Vertex V)
{
    visited[V] = true;
    //将当前结点进行入队操作
    enQueue(V, Q);
    while (!isEmpty(Q))
    {
        //出队操作
        V = deQueue(Q);
        //检索当前结点的所有子结点,
        //将未入队的子结点进行入队操作
        for (V 的每个邻接点 W)
        {
            if (!visited[W])
            {
                visited[W] = true;
                enQueue(W, Q);
            }
        }
    }
}
  • BFS会遍历当前结点的所有子结点,将没有在队列中的子结点进行入队操作,

    • 完成操作后,队列进行出队操作则得到一个结点,
    • 此时重复上述所有操作,直到当前结点没有可以入队的子结点为止
  • 若有N个结点、E条边,时间复杂度

    • 用邻接表存储图,为 O ( N + E ) O(N+E) O(N+E)
    • 用邻接矩阵存储图,为 O ( N 2 ) O(N^2) O(N2)

3、图的连通术语与处理

  • 连通:如果从 V 到 W 存在一条(无向)路径,则称为 V 和 W 是连通的

  • 路径:V 到 W 的路径是一系列顶点 { V , V 1 , V 2 , ⋯   , V n , W } \{V, V_1, V_2, \cdots, V_n, W\} {V,V1,V2,,Vn,W}的集合,其中任一对相邻的顶点之间都有图的边。
    路径的长度是路径中的边数(如果带有权重,则是所有边的权重和)。
    如果 V 到 W 之间的所有顶点都不同,则称为简单路径

  • 回路:起点等于终点的路径

  • 连通图:图中任意两顶点均连通

  • 连通分量:无向图的极大连通子图

    • 极大顶点数:再加1个顶点就不连通
    • 极大边数:包含子图中所有顶点相连的所有边
  • 强连通:有向图中顶点 V 和 W 之间存在双向路径,则称 V 和 W 是强连通

  • 强连通图:有向图中任意两顶点均强连通

  • 强连通分量:有向图的极大强连通子图

  • 存在单独图中有不连通的结点的处理方法

void listCompnents(Graph G)
{
    //防止漏掉某个不连通的结点
    //整个函数将会遍历所有状态不是true的结点
    for (each V in G)
    {
        if (!visited[v])
        {
            DFS(V);  //or BFS(V)
        }
    }
}
void DFS(Vertex V)
{
    visited[V] = true;
    for (each W in V)
    {
        if (!visited[W])
        {
            DFS(W);
        }
    }
}

四、应用案例:拯救007

  • 007被放在孤岛位置,要通过跳跃每个黑点,抵达岸上;
  • 故依次建立一个笛卡尔坐标系,中心点即孤岛位置

在这里插入图片描述

  • 整个问题本质是图的遍历问题

  • 下面是一般的图的遍历程序

void listComponents(Graph G)
{
    for (each V in G)
    {
        if (!visited[V])
        {
            DFS(V);
        }
    }
}
  • 在这个问题上,需要做出一定的修改
void save007(Graph G)
{
    //这里设置循环体,为了避免忽略有不相互连通的子图
    for (each V in G)
    {
        //要考虑子结点是否被访问过
        //且要判断007能否跳过去
        if (!visited[V] && firstJump(V))
        {
            //通过DFS算法得到最终答案
            answer = DFS(V);
            if (answer == "YES")
            {
                break;
            }
        }
    }
    if (answer == "YES")
    {
        cout << "YES" << endl;
    }
    else
    {
        cout << "NO" << endl;
    }
}
  • 下面是传统的DFS算法
void DFS (Vertex V)
{
    visited[V] = true;
    for (each W in V)
    {
        if (!visited[W])
        {
            DFS(W);
        }
    }
}
  • 在这里,我们需要做出一定的修改
int DFS (Vertex V)
{
    //表示当前结点已被访问
    visited[V] = true;
    //判断是否已经上岸
    if (isSafe(V))
    {
        answer = "YES";
    }
    else
    {
        //往下遍历V的子结点
        for (each W in V)
        {
            //判断该子节点是否已被访问
            //且判断是否能跳过去
            if (!visited[W] && jump(V, W))
            {
                //递归调用DFS函数
                answer = DFS(W);
                if (answer == "YES")
                {
                    break;
                }
            }
        }
    }
    return answer;
}

五、应用案例:六度空间

  • 你和任何一个陌生人之间所间隔的人不会超过六个人
  • 给定社交网络图,请对每个结点计算符合“六度空间”理论的结点占结点总数的百分比

在这里插入图片描述

  • 算法思路

    • 对每个结点,进行广度优先搜索
    • 搜索过程中累计访问的结点数
    • 需要记录层数,仅计算六层以内的结点数
  • 主函数

void SDS (Graph G)
{
    //遍历起点的所有子结点,避免忽略不连通的子图
    for (each V in G)
    {
        count = BFS(V);
        cout << (count / N) << endl;
    }
}
  • 对传统的BFS算法做出修改
int BFS (Vertex V)
{
    visited[V] = true;
    count = 1;	//六层内的结点数
    level = 0;	//层数
    last = V;	//本层的最后一个结点
    enQueue(V, Q);
    
    while (!isEmpty(Q))
    {
        V = deQueue(Q);
        for (each W in V)
        {
            if (!visited[W])
            {
                visited[W] = true;
                enQueue(W, Q);
                count++;
                tail = W;	//下一层的最后一个结点
            }
        }
        
        //判断结点是否为本层的最后一个结点
        if (V == last)
        {
            level++;	//更新层数
            last = tail;	//往下层移动
        }
        
        //仅计算六层内的结点数
        if (level == 6)
        {
            break;
        }
    }
    //返回输入结点V的结果
    return count;
}

在这里插入图片描述

六、如何建立图

1、用邻接矩阵表示图

  • 邻接矩阵表示
    G [ i ] [ j ] = { 1 若 < v i , v j > 是 G 中 的 边 0 否 则 G[i][j] = \begin{cases} 1 & 若<v_i,v_j>是G中的边 \\ 0 &否则 \end{cases} G[i][j]={10<vi,vj>G

  • 以邻接矩阵表示图

struct GNode
{
    int Nv; //顶点数
    int Ne; //边数
    weightType G[maxVertexNum][maxVertexNum];
    dataType data[maxVertexNum]; //存顶点的数据
};

typedef GNode* MGraph //以邻接矩阵存储的图类型
  • MGraph初始化
typedef int Vertex; //用顶点下标表示顶点,区别于整型,方便确认
MGraph createGraph (int vertexNum)
{
    MGraph Graph = new GNode;
    
    Graph->Nv = vertexNum;
    Graph->Ne = 0;
    
    for (Vertex V = 0; V < Graph->Nv; V++)
    {
        for (Vertex W = 0; W < Graph->Nv; W++)
        {
            Graph->G[V][W] = 0; //或者是无穷大
        }
    }
    
    return Graph;
}
  • 向MGraph中插入边
struct ENode
{
    Vertex V1, V1; //有向边<V1, V2>
    weightType Weight; //权重
};
typedef ENode* Edge;

void insertEdge (MGraph Graph, Edge E)
{
    //插入边<V1, V2>
    Graph->G[E->V1][E->V2] = E->Weight;
    
    //若是无向图,还要插入边<V2, V1>
    Graph->G[E->V2][E->V1] = E->Weight;
}
  • 完整地建立一个MGraph
MGraph bulidMGraph ()
{
    int Nv, Ne;
    
    cout << "enter vertex number: " << endl;
    cin >> Nv;
    MGraph Graph = createGraph (Nv);
    
    cout << "enter edge number: " << endl;
    cin >> Ne;
    Graph->Ne = Ne;
    
    if (Graph->Ne != 0)
    {
        Edge E = new ENode;
        for (int i = 0; i < Graph->Ne; i++)
        {
            cout << "perpectly enter vertex1, vertex2 and weight: " << endl;
            cin >> E->V1;
            cin >> E->V2;
            cin >> E->Weight;
            insertEdge(Graph, E);
        }
    }
    
    //如果顶点有数据的话,读入数据
    for (Vertex V = 0; V < Graph->Nv; V++)
    {
        cin >> Graph->data[V];
    }
    
    return Graph;
}

2、用邻接表表示图

  • 邻接表: G [ N ] G[N] G[N]为指针数组,对应矩阵每行一个链表,只存非零元素

在这里插入图片描述

  • 用邻接表表示图
struct GNode
{
    int Nv; //顶点数
    int Ne; //边数
    adjList G; //邻接表
};

typedef GNode* LGraph;
//定义链表中的结点
struct AdjVNode
{
    Vertex AdjV; //邻接点下标
    weightType Weight; //边权重
    AdjVNode* Next;
};

typedef AdjVNode* PtrToAdjVNode;
struct VNode
{
    PtrToDjVNode firstEdge;
    dataType data; //存顶点的数据
};

typedef VNode adjList[maxVertexNum];
  • LGraph初始化
//初始化一个有vertexNum个顶点但没有边的图
typedef int Vertex;
LGraph createGraph (int vertexNum)
{
    LGraph Graph = new GNode;
    Graph->Nv = vertexNum;
    Graph->Ne = 0;
    
    for (Vertex V = 0; V < Graph->Nv; V++)
    {
        Graph->G[V].firstEdge = NULL;
    }
    
    return Graph;
}
  • 向LGraph中插入边
struct ENode
{
    Vertex V1, V1; //有向边<V1, V2>
    weightType Weight; //权重
};
typedef ENode* Edge;

void insertEdge (LGraph Graph, Edge E)
{
    //插入<V1, V2>
    //为V2建立新的邻接点
    PtrToAdjVNode newNode = new AdjVNode;
    newNode->AdjV = E->V2;
    newNode->Weight = E->Weight;
    //将V2插入V1的表头
    newNode->Next = Graph->G[E->V1].firstEdge;
    Graph->G[E->V1].firstEdge = newNode;
    
    //若是无向图,还要插入边<V2, V1>
    //为V1建立新的邻接点
    PtrToAdjVNode newNode = new AdjVNode;
    newNode->AdjV = E->V1;
    newNode->Weight = E->Weight;
    //将V1插入V2的表头
    newNode->Next = Graph->G[E->V2].firstEdge;
    Graph->G[E->V2].firstEdge = newNode;
}
  • 完整地建立LGraph
LGraph bulidLGraph ()
{
    int Nv, Ne;
    
    cout << "enter vertex number: " << endl;
    cin >> Nv;
    LGraph Graph = createGraph (Nv);
    
    cout << "enter edge number: " << endl;
    cin >> Ne;
    Graph->Ne = Ne;
    
    if (Graph->Ne != 0)
    {
        Edge E = new ENode;
        for (int i = 0; i < Graph->Ne; i++)
        {
            cout << "perspectly enter vertex1, vertex2 and weight: " << endl;
            cin >> E->V1;
            cin >> E->V2;
            cin >> E->Weight;
            insertEdge (Graph, E);
        }
    }
    
    //如果顶点有数据的话,读入数据
    for (Vertex V = 0; V < Graph->Nv; V++)
    {
        cin >> Graph->G[V].data;
    }
    
    return Graph;
}

七、习题讲解

1、树的二次遍历(tree traversals again)

  • 通过非递归中序遍历,得到先序、中序和后序的数组

  • Push的顺序为先序遍历

  • Pop的顺序为中序遍历

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 后序数组的获取

在这里插入图片描述

void solve (int PreL, int inL, int postL, int n)
{
    if (n == 0)
    {
        return;
    }
    if (n == 1)
    {
        post[postL] = pre[PreL];
        return;
    }
    
    root = pre[PreL];
    post[postL + n - 1] = root;
    for (int i = 0; i < n; i++)
    {
        if (in[inL + i] == root)
        {
            break;
        }
    }
    L = i; 
    R = n - L -1;
    solve (PreL + 1, inL, postL, L);
    solve (PreL + L + 1, inL + L + 1, postL + L, R);
}

2、完全二叉搜索树(Complete binary search tree)

  • 二叉搜索树 + 完全二叉树

在这里插入图片描述

  • 树的表示法,用数组更好

    • 建立完全二叉树,不会浪费空间
    • 层序遍历,相当于直接顺序输出
  • 核心算法
    A L e f t ALeft ALeft为排序后的输入序列 A A A左边第一个结点的下角标
    A R i g h t ARight ARight为右边最后一个结点的下角标
    T R o o t TRoot TRoot为结果树对应根结点的下角标

void solve (int ALeft, int ARight, int TRoot)
{
    //初始调用为 solve (0, N-1, 0)
    n = ARight - ALeft + 1;
    if (n == 0)
    {
        return;
    }
    L = getLeftLength(n); //计算n个结点的树其左子树的结点数
    T[TRoot] = A[ALeft + L];
    leftTRoot = TRoot * 2 + 1;
    rightTRoot = leftTRoot + 1;
    solve (ALeft, ALeft + L - 1, leftTRoot);
    solve (ALeft + L + 1, ARight, rightTRoot);
}
  • 计算左子树的结点数
    • 基本公式
      Ⅰ、在perfect binary tree中,每层的结点数为 n h = 2 h − 1 n_h = 2^{h-1} nh=2h1
      Ⅱ、在perfect binary tree中,全部结点数为 2 H − 1 2^H-1 2H1
    • 计算步骤
      Ⅰ、通过总结点数 N N N,得到完全二叉树中满二叉树的层数,即 H = ⌊ log ⁡ 2 N + 1 ⌋ H=\lfloor \log_2^{N+1} \rfloor H=log2N+1
      Ⅱ、有了 H H H,通过 2 H − 1 + X = N 2^H-1+X=N 2H1+X=N得到最后一层的结点数 X X X
      Ⅲ、 X X X需要满足条件,即 X = m i n { X , 2 H − 1 } X=min\{X, 2^{H-1}\} X=min{X,2H1}
      Ⅳ、最后得到左子树的总结点数为, L = 2 H − 1 − 1 + X L=2^{H-1}-1+X L=2H11+X

3、最短路径问题

  • 最短路径问题的抽象
    在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的那一条路径

    • 这条路径就是两点之间的最短路径(Shortest Path)
    • 第一个顶点为源点(Source)
    • 最后一个顶点为终点(Destination)
  • 问题分类

    • 单源最短路径问题:
      从某固定源点出发,求其到所有其它顶点的最短路径
      • (有向)无权图
      • (有向)有权图
    • 多源最短路径问题:
      求任意两顶点之间的最短路径

3.1、无权图的单源最短路径算法

  • 按照递增(非递减)的顺序找出到各个顶点的最短路
  • 这里一般采用BFS-广度优先搜索

在这里插入图片描述

d i s t [ W ] = S 到 W 的 最 短 距 离 dist[W] = S到W的最短距离 dist[W]=SW

d i s t [ S ] = 0 dist[S] = 0 dist[S]=0

p a t h [ W ] = S 到 W 的 路 上 经 过 的 某 顶 点 path[W] = S到W的路上经过的某顶点 path[W]=SW

初始化dist数组的值为-1,path数组的值为0

void unWeighted (Vertex S)
{
    enQueue(S, Q);
    while (!isEmpty(Q))
    {
        V = deQUeue(Q);
        for (V 的每个邻接点W)
        {
            if (dist[W] == -1)
            {
                dist[W] = dist[V] + 1;
                path[W] = V;
                enQueue(W, Q);
            }
        }
    }
}

时间复杂度: T = O ( ∣ V ∣ + ∣ E ∣ ) T = O(|V| + |E|) T=O(V+E)

举例,假设图有5个结点,寻找最短路径

下标12345
dist-1-1-1-1-1
path00000

3.2、有权图的单源最短路径算法

  • 按照递增的顺序找出到各个顶点的最短路径
  • 这里一般采用Dijkstra算法

在这里插入图片描述

这里要指出Dijkstra算法中,不能出现负值圈,不然会出现循环路线

  • Digkstra算法 狄克斯特拉算法
    • S = { 源 点 s + 已 经 确 定 了 最 短 路 径 的 顶 点 v i } S = \{源点s + 已经确定了最短路径的顶点v_i\} S={s+vi}
    • 对任一未收录的顶点 V V V,定义 d i s t [ v ] dist[v] dist[v] s s s v v v的最短路径长度,但该路径仅经过S中的顶点。即路径 { s → ( v i ∈ S ) → v } \{s\rightarrow (v_i\in S) \rightarrow v\} {s(viS)v}的最小长度
    • 若路径是按照**递增(非递减)**的顺序生成的,则
      • 真正的最短必须只经过 S S S中的顶点
      • 每次从未收录的顶点中选一个 d i s t dist dist最小的收录(贪心)
      • 增加一个 v v v进去 S S S,可能影响另外一个 w w w d i s t dist dist
        • d i s t [ w ] = m i n { d i s t [ w ] , d i s t [ v ] + < v , w > 的 权 值 } dist[w] = min\{dist[w], dist[v] + <v, w>的权值\} dist[w]=min{dist[w],dist[v]+<v,w>}

这里 d i s t dist dist做初始化时,不能用-1,要用∞

d i s t [ v ] dist[v] dist[v]指的是顶点 v v v到源点的最短距离

void Dijkstra (Vertex s)
{
    while (1)
    {
        v = 未收录顶点中dist最小者;
        if (这样的v不存在)
        {
            break;
        }
        collected[v] = true;
        for (v的每个邻接点w)
        {
            if (collected[w] == false)
            {
                if (dist[v] + E < dist[w])
                {
                    //E是v到w的边权值
                    dist[w] = dist[v] + E;
                    path[w] = v;
                }
            }
        }
    }
}
  • 时间复杂度
    • 方法1:直接扫描所有未收录顶点,这里花费的时间复杂度为 O ( ∣ V ∣ ) O(|V|) O(V)
      时间复杂度为 T = O ( ∣ V ∣ 2 + ∣ E ∣ ) T = O(|V|^2 + |E|) T=O(V2+E)
      对于稠密图(边数多的图)效果好
    • 方法2:将 d i s t dist dist存在最小堆中,这里花费为 O ( log ⁡ ∣ V ∣ ) O(\log|V|) O(logV)
      更新 d i s t [ w ] dist[w] dist[w]的值,花费为 O ( log ⁡ ∣ V ∣ ) O(\log|V|) O(logV)
      时间复杂度为 T = O ( ∣ V ∣ log ⁡ ∣ V ∣ + ∣ E ∣ log ⁡ ∣ V ∣ ) = O ( ∣ E ∣ log ⁡ ∣ V ∣ ) T = O(|V|\log|V| + |E|\log|V|) = O(|E|\log|V|) T=O(VlogV+ElogV)=O(ElogV)
      对于稀疏图(边数少的图)效果好

3.3、多源最短路径算法

  • 方法1:直接将单源最短路径算法调用 ∣ V ∣ |V| V
    T = O ( ∣ V ∣ 3 + ∣ E ∣ × ∣ V ∣ ) T = O(|V|^3 + |E|\times |V|) T=O(V3+E×V)
    对于稀疏图效果好

  • 方法2:Floyd算法
    T = O ( ∣ V ∣ 3 ) T = O(|V|^3) T=O(V3)

  • Floyd算法

    • D k [ i ] [ j ] = D^k[i][j] = Dk[i][j]= 路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i{lk}j}的最小长度
    • D 0 , D 1 , ⋯   , D ∣ V ∣ − 1 [ i ] [ j ] D^0, D^1, \cdots , D^{|V| - 1}[i][j] D0,D1,,DV1[i][j]即给出了 i i i j j j 的真正最短距离
    • D k − 1 D^{k - 1} Dk1已经完成,递推到 D k D^k Dk时:
      • 或者 $k \notin $ 最短路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i{lk}j},则 D k = D k − 1 D^k = D^{k - 1} Dk=Dk1
      • 或者 $k \in $ 最短路径 { i → { l ≤ k } → j } \{ i \rightarrow \{ l \leq k \} \rightarrow j \} {i{lk}j},则该路径必定由两段最短路径组成 D k [ i ] [ j ] = D k − 1 [ i ] [ k ] + D k − 1 [ k ] [ j ] D^k[i][j] = D^{k - 1}[i][k] + D^{k - 1}[k][j] Dk[i][j]=Dk1[i][k]+Dk1[k][j]
void floyd ()
{
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < N; ++j)
        {
            D[i][j] = G[i][j];
            path[i][j] = -1;
        }
    }
    
    for (int k = 0; k < N; ++k)
    {
        for (int i = 0; i < N; ++i)
        {
            for (int j = 0; j < N; ++j)
            {
                if (D[i][k] + D[k][j] < D[i][j])
                {
                    D[i][j] = D[i][k] + D[k][j];
                    path[i][j] = k;
                }
            }
        }
    }
}

时间复杂度为 T = O ( ∣ V ∣ 3 ) T = O(|V|^3) T=O(V3)

4、解决最短路径问题的相关算法

4.1、Dijkstra算法(迪杰斯特拉算法)

  • 特点
    该算法使用了BFS解决赋权有向图无向图单源最短路径问题,并最终得到一个最短路径树;
    该算法常用于路由算法或作为其它图算法的一个子模块

  • 思路
    采用的是贪心策划

    • 初始化中,给定一个起点 A A A,以及含有所有点到起点的邻接距离的数组;若不是邻接点则初始化为无穷;
    • 在数组中找到最短距离的顶点 B B B,可以确定这个距离就是该点到起点的最短距离;(注意:Dijkstra算法不能出现负权值边)
    • 查看该点的邻接点 B B B(没有访问过的顶点),判断若以该点为中间点,起点到达其它点的距离是否会更短;若更短,则更新为更短的距离;
    • 完成上述操作后,将点 B B B标记为已访问
    • 重复上述操作,直到所有顶点都已被访问

4.2、Floyd算法(弗罗伊德算法)

  • 特点
    该算法是解决任意两点之间的最短路径的,可以正确处理有向图无向图或**负权(但不可存在负权回路)**的最短路径问题,同时也被用于计算有向图的传递闭包

  • 思路
    该算法计算图 G = ( V , E ) G = (V, E) G=(V,E)中各个顶点的最短路径时,需要引入两个矩阵
    矩阵 D D D的元素 a [ i ] [ j ] a[i][j] a[i][j]表示顶点 i i i到顶点 j j j的距离
    矩阵 P P P的元素 b [ i ] [ j ] b[i][j] b[i][j]表示顶点 i i i经过经过 b [ i ] [ j ] b[i][j] b[i][j]记录的顶点,然后到达顶点 j j j
    假设图 G G G中顶点个数为 N N N,则需要对两个矩阵进行 N N N次更新

    • 初始化中,矩阵 D D D记录两个顶点之间邻接距离,如果它们不是邻接点,则用∞表示;矩阵 P P P则在 b [ i ] [ j ] b[i][j] b[i][j]存储顶点 j j j
    • 对顶点 k k k进行计算,比较 a [ i ] [ k ] + a [ k ] [ j ] a[i][k] + a[k][j] a[i][k]+a[k][j] a [ i ] [ j ] a[i][j] a[i][j]的大小,如果前者较小,则更新 a [ i ] [ j ] a[i][j] a[i][j]的值;其次在矩阵 P P P的对应元素 b [ i ] [ j ] b[i][j] b[i][j]进行更新,存储顶点 k k k
    • 如此操作,直到所有的顶点均被访问

4.3、SPFA算法

  • 特点
    该算法与Dijkstra算法很像,也是求解单源最短路径问题;
    原理是对图进行 V − 1 V-1 V1次操作,得到所有可能的最短路径;
    比Dijsktra算法好的地方是边的权值可以是负数,实现简单;缺点是时间复杂度高,高达 O ( V E ) O(VE) O(VE)

  • 思路
    区别与Dijkstra算法的贪心策略,这里使用的是优先队列
    用数组dist记录每个结点的最短路径估计值,采取动态逼近法:设立一个先进先出的队列用来保存待优化的结点,每次优化则弹出首结点;

    • 初始化中,在dist中除源点为0,其余顶点的值为∞;并将源点压入队列;
    • 弹出优先队列中的首结点,对其邻接顶点进行松弛操作;
    • 若有邻接点被松弛了,且不再队列中,则将其假如队列;
    • 重复操作,若有邻接点被松弛,但已经在队列中,则不作操作;
    • 重复操作,若没有邻接点被松弛,且队列为空,则停止循环;
    • 此时数组dist已经存储了最短路径

5、最小生成树(Minimum Spanning Tree)

5.1、简介

  • 定义
    满足下面三点

      • 无回路
      • ∣ V ∣ |V| V个顶点一定有 ∣ V ∣ − 1 |V|-1 V1条边
    • 生成树
      • 包含图中所有顶点
      • ∣ V ∣ − 1 |V|-1 V1条边都在图里
    • 边的权值和最小
  • 注意

    • 最小生成树存在 表示 图是连通的
    • 向生成树中任加一条边都一定构成回路
  • 贪心算法

    • 什么是”贪“:每一步都要最好的
    • 什么是”好“:权值最小的边
    • 约束:
      • 只能用图里有的边
      • 只能正好用掉 ∣ V ∣ − 1 |V|-1 V1 条边
      • 不能有回路

5.2、Prim算法(用于稠密图合算)

  • 步骤
    • 随机选择一个顶点作为初始点;
    • 寻找初始点邻接权值最小的顶点,并组成生成树;
    • 寻找生成树中邻接权值最小的顶点,判断是否构成回路,并更新生成树;
    • 重复上一步,直到生成树的边数达到 ∣ V ∣ − 1 |V|-1 V1 或 图中没有边可访问;
void Prim()
{
    MST = {s}; //生成树的顶点集合
    while (1)
    {
        v = 未收录顶点中dist最小者;
        if (这样的v不存在) break;
        
        将v收录进MST: dist[v] = 0;
        for (v的每个邻接点w)
            if (dist[w] != 0)
            {
                if (E < dist[w])
                {
                    dist[w] = E;
                    parent[w] = v;
                }
            }
    }
    if (MST中收录的顶点不到|V|)
        Error ("生成树不存在");
}
  • 初始化中, d i s t [ v ]    =   E ( s , v ) dist[v] \ \ = \ E_{(s,v)} dist[v]  = E(s,v) 或正无穷, p a r e n t [ s ]    =   − 1 parent[s] \ \ = \ -1 parent[s]  = 1
  • 时间复杂度为 T   =   O ( ∣ v ∣ 2 ) T\ =\ O(|v|^2) T = O(v2); 稠密图合算

5.3、Kruskal算法(用于稀疏图合算)

  • 区别于Prim算法,该算法每次先找出未访问过的最小边;
  • 判断是否构成回路;
  • 重复上述步骤,直到满足条件
void Kruskal (Graph G)
{
    MST = {};
    while ( MST中不到 |V|-1 条边 && E中还有边)
    {
        从E中取一条权值最小的边 E(v,w);  //用最小堆存储所有边E(v,w)从E中删除;
        if (E(v,w)不在MST中构成回路)  //并查集E(v,w)加入MST;
        else
            彻底无视E(v,w);
    }
    if (MST中不到|v|-1条边)
        Error("生成树不存在");
}

时间复杂度为 T   =   O ( ∣ E ∣ log ⁡ ∣ E ∣ ) T\ = \ O(|E|\log|E|) T = O(ElogE)

八、拓扑排序

1、AOV网络(Activity On Vertex)

  • 简介

    • 顶点表示活动,有向边表示活动的先导关系
    • AOV网是有向无环图,即不应该带有回路,因为若带有回路,则会陷入死循环;
    • 所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列
    • AOV网络的拓扑序列不是唯一的;
    • 只有完成了所有前驱事件后才可以进行后续事件;
  • 例子

在这里插入图片描述

上图是AOV网络,下面列出事件发生的顺序

在这里插入图片描述

  • 不聪明的算法,选择直接遍历
void topSort()
{
    for (int cnt = 0; cnt < |v|; +=cnt)
    {
        v = 未输出的入度为0的顶;  //O(|v|)
        if (这样的v不存在)
        {
            Error("图中有回路");
            break;
        }
        
        输出v,或记录v的输出序号;
        for (v 的每个邻接点w)
        {
            inDegreee[w]--;
        }
    }
}

时间复杂度为 T    =    O ( ∣ v ∣ 2 ) T\ \ =\ \ O(|v|^2) T  =  O(v2)

  • 聪明的算法,随时将入度为0的顶点放到一个容器里
void topSort()
{
    for (图中每个顶点v)
    {
        if (inDegree[v] == 0)
            enQueue(Q, v);
        while (!isEmpty(Q))
        {
            v = deQueue(Q);
            输出v,或记录v的输出序号;
            cnt++;
            for (v 的每个邻接点w)
            {
                if (--inDegree[w] == 0)
                    enQueue(Q, w);
            }
        }
        
        if (cnt != |v|)
            Error("图中有回路");
    }
}

时间复杂度为 T    =    O ( ∣ v ∣   +   ∣ E ∣ ) T\ \ =\ \ O(|v|\ +\ |E|) T  =  O(v + E)

此算法可以用来检测有向图是否DAG(Directed Acyclic Graph,有向无环图)

2、AOE网络(Activity On Edge)

  • 简介

    • 一种带权有向图,弧上的权值表示活动持续的时间;
    • 关键路径问题(由绝对不允许延误的活动组成的路径),一般用于安排项目的工序;
  • 例子

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值