圖的應用--最小生成樹、最短路徑、拓撲排序、關鍵路徑

1 最小生成樹(minimum spanning tree)

(1)基本概念
生成樹的概念:
一個有 n 個結點的連通圖的生成樹是原圖的極小連通子圖,且包含原圖中的所有 n 個結點,并且有保持圖連通的最少的邊。
最小生成樹的概念:
帶權連通圖中代價最小的生成樹。

構造最小生成樹的算法有許多,基本原則是:
盡可能選取權值最小的邊,但不能構成回路;
選取n-1條邊構成最小生成樹。

(2)Prim算法
假設 G=(V,E)為一網圖,其中 V 為網圖中所有頂點的集合,E 為網圖中所有帶權邊的集合。設置兩個新的集合 U 和 T,其中集合 U 用于存放 G 的最小生成樹中的頂點, 集合 T 存放 G 的最小生成樹中的邊。令集合 U 的初值為 U={v_1}(假設構造最小生成樹時, 從頂點 v_1 出發(fā)),集合 T 的初值為 T={}。

①算法思想
從所有u∈U,v∈V-U的頂點中,選取具有最小權值的邊(u,v),將頂點v加入到集合U中,將邊(u,v)加入到集合T中,如此不斷重復,直到U=V時,最小生成樹構建完畢,這時集合T中包含了最小生成樹的所有邊。
Prim算法的時間復雜度為O(n^2),與網中的邊數(shù)無關,因此適用于求邊稠密的網的最小生成樹。

Prim算法

下圖演示了從出發(fā),一步一步把結點k拉到U中。
Prim算法 adjvex 和 lowcost 的變化

②算法實現(xiàn)
使用鄰接矩陣(二維數(shù)組)表示圖,兩個頂點之間不存在邊的權值為機器內允許的最大值。
為便于算法實現(xiàn),設置一個一維數(shù)組closedge[n],用來保存(V-U)集合中各頂點到U中頂點具有權值最小的邊。
數(shù)組元素的定義是:

#define MAX_EDGE 30
struct {
    int adjvex;//邊所依附于U中權值最小的頂點
    int lowcost;//該邊的權值
} closedge[MAX_EDGE];

closedge[j].adjvex = k,表明邊(v_j,v_k)是(V-U)中頂點v_j到U中權值最小的邊,頂點v_k是該邊所依附的U中的頂點。
closedge[j].lowcost存放該邊的權值。

③算法步驟
1)從V中挑出一個頂點v_k加到U中,初始化closedge數(shù)組,記錄(V-U)中每一個頂點到v_k的權值。
從closedge中選擇一條權值不為0最小的邊(v_j,v_k),然后做:
closedege[j].lowcost = 0; //將v_j加入到U中
2)根據(jù)新加入的v_j更新closedge中每個元素:遍歷剩余的(V-U)所有頂點v_i到頂點v_j的權值是否小于到頂點v_k的權值,如果小于則更新數(shù)組
closedge[i].adjvex = j;
closedge[i].lowcost = cost(i,j);
3)重復(1)-(2) ,就得到最小生成樹。

在 Prim算法中,圖采用鄰接矩陣存儲,所構造的最小生成樹用一維數(shù)組存儲其n-1條邊,每條邊的存儲結構描述:

typedef struct MSTEdge {
    int vex1, vex2;//邊所依附的圖中兩個頂點
    int weight;//邊的權值
} MSTEdge;

算法實現(xiàn):
貪心算法思想: 局部最優(yōu)+調整=全局最優(yōu)

#include <stdio.h>
#include <stdlib.h>

#define MAX_VEX 30
#define INFINTY 65535//最大值

typedef enum {
    DG, AG, WDG, WAG  //{有向圖,無向圖,帶權有向圖,帶權無向圖}
} GraphKind;

typedef struct {
    GraphKind kind; /* 圖的種類標志 */
    int verxtexNum, arcNum; /* 圖的當前頂點數(shù)和弧數(shù) */
    char vexs[MAX_VEX]; //頂點集合
    int edges[MAX_VEX][MAX_VEX];//鄰接矩陣
} MGraph;/* 圖的結構定義 */

typedef struct MSTEdge {
    int vex1, vex2;//邊所依附的圖中兩個頂點
    int weight;//邊的權值
} MSTEdge;//邊的定義

#define MAX_EDGE 30
struct {
    int adjvex;//到U中權值最小的邊所依附U的頂點
    int lowcost;//該邊的權值
} closedge[MAX_EDGE];//數(shù)組下標代表V-U的頂點的編號

MSTEdge *Prim_MST(MGraph *G, int u) {
    //從第u個頂點開始構造G的最小生成樹
    MSTEdge *TE;//存放最小生成樹n-1條邊的數(shù)組指針
    int min,k;
    for (int i = 0; i < G->verxtexNum; i++) {//初始化數(shù)組closedge
        closedge[i].adjvex = u;//都依附于頂點u
        closedge[i].lowcost = G->edges[i][u];//到頂點u的權值
    }
    closedge[u].lowcost = 0;//把頂點u拉到集合U中
    TE = (MSTEdge *) malloc((G->verxtexNum - 1) * sizeof(MSTEdge));
    for (int i = 0; i < G->verxtexNum - 1; i++) {//循環(huán)n-1次,拉頂點到集合U中
        min = INFINTY;
        for (int j = 0; j < G->verxtexNum; j++) {//遍歷closedge數(shù)組,找到最小權值的邊及頂點
            if (closedge[j].lowcost != 0 && closedge[j].lowcost < min) {
                min = closedge[j].lowcost;
                k = j;
            }
        }
        TE[i].vex1 = closedge[k].adjvex;
        TE[i].vex2 = k;
        TE[i].weight = closedge[k].lowcost;
        closedge[k].lowcost = 0;//把頂點k拉到U中
        for (int i = 0; i < G->verxtexNum; i++) { //修改closedge中各個元素的值
            if (G->edges[i][k] < closedge[i].lowcost) {
                closedge[i].lowcost = G->edges[i][k];
                closedge[i].adjvex = k;
            }
        }
    }
    return TE;
}

(3)Kruskal算法
①算法思想
設G=(V,E)是具有n個頂點的連通網,T=(U,TE)是其最小生成樹。
初值U=V,TE={}。
對G中的邊按權值大小從小到大依次選取。
選取權值最小的邊(v_i,v_j),若邊(v_i,v_j)加入到TE后形成回路,則舍棄改邊,否則將該邊并入到TE中。
重復上面步驟,直到TE中包含n-1條邊。

Kruskal算法

②算法實現(xiàn)
Kruskal算法實現(xiàn)的關鍵是:當一條邊加入到TE的集合后,如何判斷是否構成回路?
簡單的解決方法是:定義一個一維數(shù)組Vset[n],存放圖T中每個頂點所在的連通分量的編號。
初值:Vset[i] = i,表示每個頂點各自組成一個連通分量,連通分量的編號簡單地使用頂點在圖中的位置(編號)。
當往T中增加一條邊(v_i,v_j)時,先檢查Vset[i]和Vset[j]的值:
若Vset[i] = Vset[j]:表明v_iv_j處在同一個連通分量中,加入此邊會形成回路;
若Vset[i] ≠ Vset[j],則加入此邊不會形成回路,將此邊加入到生成樹的邊集中。
加入一條新邊后,將兩個不同的連通分量合并:將一個連通分量的編號換成另一個連通分量的編號。

MSTEdge *Kruskal_MST(MGraph *G){
    MSTEdge *TE;
    int *Vset = (int *) malloc(G->verxtexNum * sizeof(int));//Vset數(shù)組
    for (int i = 0; i < G->verxtexNum; i++) {
        Vset[i] = i;//初始化數(shù)組Vset[n]
    }
    MSTEdge * edgelist = getEdgeList(G);
    sort(edgelist);//對表權值按從小到大排序
    int j = 0, k = 0, s1, s2;
    while (j < G->arcNum && k < G->verxtexNum - 1) {
        s1 = Vset[edgelist[j].vex1];
        s2 = Vset[edgelist[j].vex2];
        if (s1 != s2) {//若邊的兩個頂點的連通分量編號不同,邊加入到TE中
            TE[k].vex1 = edgelist[j].vex1;
            TE[k].vex2 = edgelist[j].vex2;
            TE[k].weight = edgelist[j].weight;
            k++;
            for (int i = 0; i < G->verxtexNum; i++) {
                if(Vset[i] == s2) {
                    Vset[i] = s1;//把連通分量改為較小的那一個
                }
            }
        }
        j++;
    }
    free(Vset);
    return TE;
}

時間復雜度為O(eloge + n^2)。

2 最短路徑

(1)單源點最短路徑 Dijkstra算法

對于給定的有向圖G=(V,E)及單個源點v_s,求v_s到G的其即余各頂點的最短路徑。
Dijkstra提出了一種按路徑長度遞增次序產生最短路徑的算法, Dijkstra算法。

基本思想:
從圖中的給定源點到其它各個頂點之間客觀上應存在一條最短路徑,從這組最短路徑中,按其長度的遞增次序,依次求出到不同頂點的最短路徑和路徑長度。
即按長度遞增的次序生成各頂點的最短路徑,即先求出長度最小的一條最短路徑,然后求出長度第二小的最短路徑,以此類推,直到求出長度最長的最短路徑。

算法思想說明:
設給定源點V_s,S為已求得最短路徑的終點集,開始時令S={V_s}。當求得第一條最短路徑(V_s,V_i)后,S為{V_s,V_i}。根據(jù)以下結論可求下一條最短路徑。
設下一條最短路徑終點為V_j,則V_j只有以下某一種情況:
1.源點到終點有直接的弧<V_s,V_j>;
2.從 V_s出發(fā)到V_j的這條最短路徑所經過的所有中間頂點必定在S中。即只有這條最短路徑的最后一條弧才是S內某個頂點鏈接到S外的頂點V_j
若定義一個數(shù)組dist[n],其每個dist[i]分量保存從V_s出發(fā)中間只經過集合S中的頂點而到達V_i的所有路徑中長度最小的路徑長度值,則下一條最短路徑的終點V_j必定是不在S中且值最小的頂點,即:
dist[i] = min{dist[k] | V_k ∈ V-S}

算法設計:
如何存放最短路徑長度:
用一維數(shù)組dist[j]存儲,源點V_s默認,dist[j]表示V_jV_s的路徑長度。
如何存放最短路徑:
從源點到其它頂點的最短路徑有n-1條,一條最短路徑用一個一維數(shù)組表示,如從頂點0到5的最短路徑為為0、2、3、5,表示為path[5]={0,2,3,5},所有n-1條最短路徑可以用二維數(shù)組path[][]存儲。

算法步驟
①令S={V_s},用帶權的鄰接矩陣表示有向圖,對圖中每個頂點V_i按以下原則置初值:
dist[i] = 0 , i=s
dist[i] = W_{si} , i≠s且<V_s,V_i>有路徑
dist[i] = , i≠s且<V_s,V_i>沒有路徑
②選擇一個頂點V_j,使得:
dist[j] = min{dist[k] } V_k ∈ V-S}
V_j就是求得的下一條最短路徑終點,將V_j并入到S中。
③對V-S中的每個頂點V_k,修改dist[k],方法是:
若dist[j] + W_{jk} < dist[k],則修改為:dist[k] = dist[j] + W_{jk}
④ 重復②,③,直到S=V為止。

算法實現(xiàn):
用帶權的鄰接矩陣表示有向圖,對Prim算法略加改動就成了Dijkstra算法,將Prim算法中求每個頂點V_k的lowcost值用dist[k]代替即可。
設數(shù)組path[n]保存從V_s到其它頂點的最短路徑。若path[i]=k,表示從V_sV_i的最短路徑中V_i的前一個頂點為V_k
設數(shù)組final[n],標識一個頂點是否已加入S中。

#include <stdio.h>
#include <stdlib.h>

#define MAX_VEX 30
#define INFINITY 25535
typedef enum {
    DG, AG, WDG, WAG  //{有向圖,無向圖,帶權有向圖,帶權無向圖}
} GraphKind;
typedef struct {
    GraphKind kind; /* 圖的種類標志 */
    int verxtexNum, arcNum; /* 圖的當前頂點數(shù)和弧數(shù) */
    char vexs[MAX_VEX]; //頂點集合
    int edges[MAX_VEX][MAX_VEX];//鄰接矩陣
} MGraph;/* 圖的結構定義 */

typedef enum {
    TRUE, FALSE
} boolean;

boolean final[MAX_VEX];
int path[MAX_VEX], dist[MAX_VEX];

void dijkstraPath(MGraph *G, int v) {  //從圖G中的頂點v出發(fā)到其余各頂點的最短路徑
    int m,min;
    for (int i = 0; i < G->verxtexNum; i++) {//各數(shù)組的初始化
        path[i] = v;
        final[i] = FALSE;
        dist[i] = G->edges[v][i];
    }
    //設置S={v}
    dist[v] = 0;
    final[v] = TRUE;
    for (int i = 0; i < G->verxtexNum - 1; i++) {//其余n-1個頂點
        while(final[m] == TRUE) {//找不在S中的頂點
            m++;
        }
        min = INFINITY;
        for (int i = 0; i < G->verxtexNum; i++) {//求出當前最小的dist[i]值
            if (final[i] == FALSE && dist[m] < min) {
                min = dist[m];
                m = i;
            }
        }
        final[m] = TRUE;
        for (int i = 0; i < G->verxtexNum; i++) {//修改dist和path數(shù)組的值
            if (final[i] == FALSE && dist[m] + G->edges[m][i] < dist[i]) {
                dist[i] = dist[m] + G->edges[m][i];
                path[i] = m;
            }
        }
    }
}

從 Vo 到其余各頂點的最短路徑, 以及運算過程中dist數(shù)組的變化狀況,如下所示:


時間復雜度O(n^2)

(2)每一對頂點間的最短路徑 Floyd算法

用Dijkstra算法重復對每一個頂點求最短路徑,時間復雜度O(n^3)。Floyd提出了另一種算法,形式步驟更為簡單,時間復雜度仍是O(n^3)

算法思想:
設頂點集S(初值為空),用數(shù)組A的每個元素A[i][j]保存從V_i只經過S中的頂點到達V_j的最短路徑長度,其思想是:
①初始時令S={},A[i][j]的賦初值方式是
A[i][j] = 0 , i = j時
A[i][j] = W_{ij} , i ≠ j且V_i,V_j有路徑
A[i][j] = , i ≠ j時且V_i,V_j沒有路徑
②將圖中一個頂點且V_k加入到S中,修正A[i][j]的值,因為從V_i只經過S中的頂點V_k到達V_j的路徑長度可能比原來不經過V_k的路徑更短,修改方法是:
A[i][j] = min{A[i][j],(A[i][k] + A[k][j])}
③重復②,直到G的所有頂點都加入到S中為止。

算法實現(xiàn):
※定義二維數(shù)組Path[n][n],元素Path[i][j]保存從V_iV_j的最短路徑所經過的頂點。
※若Path[i][j] = k,從V_iV_j經過V_k,最短路徑序列是(V_i,\cdots,V_k,\cdots,V_j),則路徑子序列:(V_i,\cdots,V_k)(V_k,\cdots,V_j)一定是從V_iV_k和從V_kV_j的最短路徑。從而可以根據(jù)Path[i][k]和Path[k][j]的值再找到該路徑上所經過的其它頂點,依此類推。
※初始化為Path[i][j] = -1,表示從V_iV_j不經過任何S中的頂點。當某個頂點V_k加入到S中后使A[i][j]變小時,令Path[i][j] = k。

下面是 Floyd 算法的一個例子,給出算法過程
初始A中為權值,Path中全為-1,S為{}
先把V_0拉到S中,觀察其余結點有沒有經過V_0使路徑更短。從V_2V_1經過V_0路徑為7,更改A[2][1] = 7,Path[2][1] = 0。
以此類推。

Floyd算法過程

V0到V1 :路徑是{ 0, 1 } ,長度是2 ;
V0到V2 :路徑是{ 0, 1, 2 } ,長度是6 ;
V1到V0 :路徑是{ 1, 2, 0 } ,長度是9 ;
V1到V2 :路徑是{ 1, 2 } ,路徑長度是4 ;
V2到V0 :路徑是{ 2, 0 } ,路徑長度是5 ;
V2 到 V1 :路徑是{ 2, 0,1},路徑長度是 7 ;

#include <stdio.h>
#include <stdlib.h>

#define MAX_VEX 30
typedef struct {
    int verxtexNum, arcNum; /* 圖的當前頂點數(shù)和弧數(shù) */
    char vexs[MAX_VEX]; //頂點集合
    int edges[MAX_VEX][MAX_VEX];//鄰接矩陣
} MGraph;/* 圖的結構定義 */

int A[MAX_VEX][MAX_VEX];
int Path[MAX_VEX][MAX_VEX];
void floydPath (MGraph *G) {
    for (int i = 0; i < G->verxtexNum; i++) {//各數(shù)組的初始化
        for (int j = 0; j < G->verxtexNum; j++) {
            A[i][j] = G->edges[i][j];
            Path[i][j] = -1;
        }
    }
    for (int i = 0; i < G->verxtexNum; i++) {//如果i到k經過j距離更短的話,更改A和Path
        for (int j = 0; j < G->verxtexNum; j++) {
            for (int k = 0; k < G->verxtexNum; k++) {
                if (A[i][j] + A[j][k] < A[i][k]) {
                    A[i][k] = A[i][j] + A[j][k];
                    Path[i][k] = j;
                }
            }
        }
    }
}

3 拓撲排序

一個工程都可分為若干個稱為活動的子工程,各個子工程受到一定的條件約束:
某個子工程必須開始于另一個子工程完成之后;
整個工程有一個起點和一個重點。
對工程的活動加以抽象:圖中頂點表示活動,有向邊表示活動之間的優(yōu)先關系,這樣的有向圖稱為AOV網(Activity On Vertex Network)。
在AOV網中,若有有向邊<i,j>,則i是j的直接前驅,j是i的直接后繼。
AOV網不能有環(huán)。
檢查方法:對有向圖的頂點進行拓撲排序,若所有頂點都在其拓撲有序序列中,則無環(huán)。

有向圖的拓撲排序:
構造AOV網中頂點的一個拓撲線性序列(v_1,v_2,\cdots,v_n),使得該線性序列不僅保持原來有向圖中頂點之間的優(yōu)先關系,而且對原圖中沒有優(yōu)先關系的頂點之間也建立一種人為的優(yōu)先關系。

算法思想:
①在AOV網中選擇一個沒有前驅的頂點且輸出;
②在AOV網中刪除該頂點以及從該頂點出發(fā)的所有有向邊;
③重復①、②,直到圖中全部頂點都已輸出(圖中無環(huán))或圖中不存在無前驅的頂點(圖中必有環(huán))。


拓撲排序過程

算法實現(xiàn)說明
采用正鄰接鏈表作為AOV網的存儲結構;
設立堆棧,用來暫存入度為0的頂點;
刪除頂點以它為尾的弧,弧頭頂點的入度減1。

算法實現(xiàn)

#include <stdio.h>
#include <stdlib.h>

#define MAX_VEX 30
typedef enum {
    DG, AG, WDG, WAG
} GraphKind;

typedef struct LinkNode {
    int adjvex;//鄰接點在頭結點數(shù)組中的位置(下標)
    int weight;//權值
    struct LinkNode *nextarc;//指向下一個表結點
} LinkNode;/*表結點類型定義 */

typedef struct VexNode {
    char data; // 頂點信息
    int indegree; //入度
    LinkNode *firstarc; // 指向第一個表結點
} VexNode;/* 頂點結點類型定義 */

typedef struct {
    GraphKind kind;/*圖的種類標志 */
    int vexnum;//頂點數(shù)
    VexNode AdjList[MAX_VEX];//鄰接表
} ALGraph;/* 圖的結構定義 */

void countIndegree(ALGraph *G) {
    LinkNode *p;
    for (int i = 0; i < G->vexnum; i++) {//頂點入度初始化
        G->AdjList[i].indegree = 0;
    }
    for (int i = 0; i < G->vexnum; i++) {
        p = G->AdjList[i].firstarc;
        while (p != NULL) {//頂點入度統(tǒng)計
            G->AdjList[p->adjvex].indegree++;
            p = p->nextarc;
        }
    }
}

int topologicSort(ALGraph *G, int topol[]) {
    //頂點的拓撲序列保存在一維數(shù)組topol中
    int stack[MAX_VEX];
    int top = -1, count = 0, boolean, no,vexNo;
    LinkNode *p;
    countIndegree(G);
    for (int i = 0; i < G->vexnum; i++) {
        if (G->AdjList[i].indegree == 0) {
            stack[++top] = G->AdjList[i].data;
        }
    }
    do {
        if (top == -1) {
            boolean = 0;
        } else {
            no = stack[top--];//棧頂元素出棧
            topol[count++] = no;//記錄頂點序列
            p = G->AdjList[no].firstarc;
            while (p != NULL) {//刪除以頂點為尾的弧
                vexNo = p->adjvex;
                G->AdjList[vexNo].indegree--;
                if (G->AdjList[vexNo].indegree == 0) {
                    stack[++top] = vexNo;
                }
                p = p->nextarc;
            }
        }
    } while (boolean == 0);
    if(count < G->vexnum) {
        return -1;
    } else {
        return 1;
    }
}

算法分析:
設 AOV 網有 n 個頂點,e 條邊,則算法的主要執(zhí)行是:
統(tǒng)計各頂點的入度:時間復雜度是 O(n+e) ;
入度為 0 的頂點入棧:時間復雜度是 O(n) ;
拓撲排序過程:頂點入棧和出棧操作執(zhí)行 n 次,入度減 1 的操作共執(zhí)行 e 次,時間復雜度是 O(n+e) ;
因此,整個算法的時間復雜度是 O(n+e) 。

4 關鍵路徑

AOE(Activity On Edge)是邊表示活動的有向無環(huán)圖。頂點表示事件(Event),每個事件表示在其前的所有活動已經完成,其后的活動可以開始;弧表示活動,弧上的權值表示相應活動所需的時間或費用。
由于 AOE 網中的某些活動能夠同時進行,故完成整個工程所必須花費的時間應該為源點到終點的最大路徑長度。
具有最大路徑長度的路徑稱為關鍵路徑。關鍵路徑上的活動稱為關鍵活動。關鍵路徑長度是整個工程所需的最短工期。

利用AOE 網進行工程管理時要需解決的主要問題是:
①計算完成整個工程的最短路徑。
②確定關鍵路徑,以找出哪些活動是影響工程進度的關鍵。

v_0是起點,從v_0v_i的最長路徑長度稱為事件v_i的最早發(fā)生時間,即是以v_i為尾的所有活動的最早發(fā)生時間。若活動a_i是弧<j, k>,持續(xù)時間是 dut(<j, k>)
e(i):表示活動a_i的最早開始時間;
l(i):在不影響進度的前提下,表示活動a_i的開始時間;
則l(i) - e(i)表示活動a_i的時間余量,若l(i) - e(i) = 0,表示活動a_i是關鍵活動。
ve(i):表示事件v_i的最早發(fā)生時間,即從起點到頂點v_i的最長路徑長度;
vl(i):表示事件v_i的最晚發(fā)生時間。
則有以下關系:

最早發(fā)生時間 ve(j)的計算:
源點事件的最早發(fā)生時間設為 0;除源點外,只有進入頂點 vj 的所有弧所代表的活動全部結束后,事件 vj 才能發(fā)生。即只有 vj 的所有前驅事件 vi 的最早發(fā)生時間 ve(i)計算出來后,才能計算 ve(j) 。
方法是:對所有事件進行拓撲排序,然后依次按拓撲順序計算每個事件的最早發(fā)生時間。

最晚發(fā)生時間 vl(j)的計算:
只有 vj 的所有后繼事件 vk 的最晚發(fā)生時間 vl(k)計算出來后,才能計算 vl(j) 。
方法是:按拓撲排序的逆順序,依次計算每個事件的最晚發(fā)生時間。


求 AOE 中關鍵路徑和關鍵活動
算法思想
1 利用拓撲排序求出 AOE 網的一個拓撲序列;
2 從拓撲排序的序列的第一個頂點(源點)開始,按拓撲順序依次計算每個事件的最早發(fā)生時間 ve(i) ;
3 從拓撲排序的序列的最后一個頂點(匯點)開始,按逆拓撲順序依次計算每個事件的最晚發(fā)生時間 vl(i) ;

AOE網

拓 撲 排 序 的 序 列 :( v 0 , v 1 , v 2 , v 3 , v 4 , v 5 , v 6 , v 7 , v 8 )
計算各個事件的 ve(i)和 vl(i)值:



其次計算e和l


根據(jù)關鍵路徑的定義,知該 AOE 網的關鍵路徑是: (v0, v2, v4, v7 , v8) 和(v0, v2,v5 , v7,v8) 。
關鍵路徑活動是:<v0, v2>,<v2, v4>,<v2, v5>,<v4, v7>,<v5, v7>,<v5, v8> 。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容