华科大考研计算机系834大纲之数据结构(六)

本节大纲内容

  • 1 图的定义及术语

  • 图的定义

图G由两个集合V和E组成,记为G=(V,E),其中V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合,这些顶点偶对称为边。V(G)和E(G)通常分别表示图G的顶点集合和边集合,E(G)可以为空集。若E(G)为空,则图G中只有顶点没有边。

对于图G,若边集E(G)为有向边的集合,则称该图为有向图;若边集E(G)为无向边的集合,则称该图为无向图。

在有向图中,顶点对<x,y>是有序的,它称为从顶点x到顶点y的一条有向边。因此,<x,y>与<y,x>是不同的两条边。顶点对用一对尖括号括起来,x是有向边的始点,y是有向边的终点。<x,y>也称作一条弧,则x为弧尾,y为弧头(弧头就是箭头)。

在无向图中,顶点对(x,y)是无序的,它称为与顶点x和顶点y相关联的一条边。这条边没有特定的方向,(x,y)与(y,x)是同一条边。

有向图G1:

无向图G2:

  • 图的基本术语

用n表示图中顶点的数目,用e表示图中边的数目。
(1)子图
假设有两个图 G = ( V , E ) G=(V,E) G=(V,E) G 1 = ( V 1 , E 1 ) G_1=(V_1,E_1) G1=(V1,E1),如果 V 1 ⊆ V V_1\subseteq V V1V E 1 ⊆ E E_1\subseteq E E1E,则称 G 1 G_1 G1为G的子图。

(2)无向完全图和有向完全图
完全的含义:任意两个顶点之间都有一条线关联。
对于无向图,若具有 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2条边,则称为无向完全图。
一个顶点到另外n-1个顶点有n-1条边,n个就有n(n-1),去掉重复的,就变成 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2条边了。
对于有向图,若具有 n ( n − 1 ) n(n-1) n(n1)条弧,则称为有向完全图。

(3)稀疏图和稠密图
有很少条边或弧的图称为稀疏图,反之称为稠密图。但是多少边才算很少边呢?一般规定 e < n log ⁡ 2 n e< n\log_2 n e<nlog2n

(4)权和网
在实际应用中,每条边可以标上具有某种含义的数值,该数值称为该边上的权。这些权可以表示从一个顶点到另一个顶点的距离或消耗。这种带权的图通常称为网。

(5)邻接点
对于无向图G,如果图的边 ( V 1 , V 2 ) ⊆ E (V_1,V_2) \subseteq E (V1,V2)E,则称顶点 V 1 和 V 2 V_1和V_2 V1V2互为邻接点,即 V 1 , V 2 V_1,V_2 V1,V2相邻接。边 ( V 1 , V 2 ) (V_1,V_2) (V1,V2)依附于顶点 V 1 , V 2 V_1,V_2 V1,V2,或者说边 ( V 1 , V 2 ) (V_1,V_2) (V1,V2)与顶点 V 1 , V 2 V_1,V_2 V1,V2相关联。

(6)度,出度和入度
顶点 V V V的度是指与 V V V相关联的边的数目,记为 T D ( V ) TD(V) TD(V)。例如上图 G 2 G_2 G2的顶点 V 3 V_3 V3的度为3。

对于有向图,顶点 V V V的度分为入度和出度。
入度是指指向顶点 V V V的弧的数目,记作 I D ( V ) ID(V) ID(V);
出度是指从顶点 V V V出发的弧的数目,记作 O D ( V ) OD(V) OD(V)
顶点 V V V的度等于出度加入度,即 T D ( V ) = I D ( V ) + O D ( V ) TD(V)=ID(V)+OD(V) TD(V)=ID(V)+OD(V)

一般地,如果顶点 V i V_i Vi的度记为 T D ( V i ) TD(V_i) TD(Vi),那么一个有 n n n个顶点, e e e条边的图,满足如下关系。
e = 1 2 ∑ i = 1 n T D ( V i ) e= \frac{1}{2} \sum_{i=1}^n TD(V_i) e=21i=1nTD(Vi)

(7)路径和路径长度
在无向图G中,从顶点 V 1 V_1 V1到顶点 V 2 V_2 V2的路径是一个顶点序列(用括号括起来)。
在有向图G中,路径也是有向的,顶点序列用尖括号括起来。
路径长度就是一条路径上所经过的边或弧的数目。

(8)回路或环
第一个顶点和最后一个顶点相同的路径称为回路或环。

(9)简单路径、简单回路或简单环
简单的含义:就是顶点不重复出现。
序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点外,其余顶点不重复的回路,称为简单回路或简单环。

(10)连通、连通图和连通分量(无向图的特性)
在无向图G中,如果从顶点 V 1 V_1 V1到顶点 V 2 V_2 V2有路径,则称 V 1 V_1 V1 V 2 V_2 V2是连通的。如果对于任意两个顶点都是连通的,则称G为连通图。
连通分量就是无向图中的极大连通子图。这样理解,G是一个图,它有很多子图,在这些子图中,属于连通图的子图就是G的连通分量。

(11)强连通图和强连通分量(有向图的特性)
在有向图G中,如果对于每一对顶点,两个顶点之间可以相互到达,即都存在双向路径,则称G为强连通图。
强连通分量就是有向图的极大强连通子图。这样理解,G是一个图,它有很多子图,在这些子图中,属于强连通图的子图就是G的强连通分量。

(12)连通图的生成树
一个极小连通子图,什么是极小连通子图?
对于一个连通图G,它的极小连通子图为 G 1 G_1 G1
首先, G 1 G_1 G1含有图中全部顶点,但只有足以构成一棵树的 n − 1 n-1 n1条边,具有这样的条件的连通子图称为极小连通子图。而这样的极小连通子图称为连通图的生成树。

此时是生成树是连通的,但是它是极小连通,没有环。

一棵有n个顶点的生成树有且仅有n-1条边。如果少于n-1条边,则是非连通图;如果多于n-1条边,则一定有环。但是有n-1条边的不一定是生成树。

(13)有向树和生成森林
有一个顶点的入度为0,其余顶点的入度均为1,的有向图称为有向树。
一个有向图的生成森林是由若干棵有向树组成,含有图中的全部顶点,但只有足以构成若干树不相交的有向树的弧。

  • 2 图的物理存贮结构:邻接矩阵、邻接表、十字链表和邻接多重表

  • 邻接矩阵(数组表示法)

邻接矩阵是 表示顶点之间相邻关系的矩阵。设 G ( V , E ) G(V,E) G(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵。

注意:
如果是无向图,则它是对称矩阵,即 V i V_i Vi V j V_j Vj之间有边,那么第 i i i行第 j j j列为1,对称的第 j j j行第 i i i列也为1。
如果是有向图,则它的边是有方向的,所以只需记录一个方向的弧即可。

若G是网,则邻接矩阵可以定义为:

其中, w i , j w_{i,j} wi,j表示边上的权值; ♾ ♾ 表示计算机允许的、大于所有边上权值的数。
用邻接矩阵表示法表示图,除了一个用于存储邻接矩阵的二维数组外,还需一个一维数组来存储顶点信息。其形式说明如下:

#define MaxInt 32767
#define MVNum 100
typedef char VerTexType;	//假设顶点类型为字符型
typedef int ArcType;		//假设边的权值类型为整数型
typedef struct{
	VerTexType vexs[MVNum];	//顶点表
	ArcType arcs[MVNum][MVNum];	//邻接矩阵
	int vexnum,arcnum;			//图的当前点数和边数
}AMGraph;
  • 采用邻接矩阵表示法创建无向图

已知一个图的点和边,使用邻接矩阵表示法来创建此图的方法比较简单,下面以一个无向图为例来说明创建图的算法。

算法步骤:
1、输入顶点数和边数
2、依次输入点的信息存入顶点表中
3、初始化邻接矩阵,使每个权值初始化为极大值
4、构造邻接矩阵,依次输入每条边依附的顶点和其权值,确定两个顶点在图中的位置之后,使相应的边赋予相应的权值,同时使其对称变赋予同样的权值。

算法描述:

Status CreateUDN(AMGraph &G){
	cin>>G.vexnum;	//输入顶点数
	cin>>G.arcnum;	//输入边数
	for(i=0;i<G.vexnum;++i){
		cin>>G.vexs[i];	//输入点的信息
	}
	for(i=0;i<G.vexnum;++i)
		for(j=0;j<G.arcnum;++j)
			G.arcs[i][j]=MaxInt;	//初始化邻接矩阵,边的权值均置位最大值。
	for(k=0;k<arcnum;++k){
		cin>>V1;	
		cin>>V2;
		cin>>w;	//输入一条边依附的顶点及权值。
		i=LocateVex(G,V1);	//确定V1的位置,作为权值的行号
		k=LocateVex(G,V2);	//确定V2的位置,作为权值的列号
		G.arcs[i][j]=w;	//在邻接矩阵中的对应边写上权值
		G.arcs[j][i]=G.arcs[i][j];	//在无向图中,对称边也写上同样的权值
	}
	return ok;
}

邻接矩阵表示法的优缺点:
1、优点
(1)便于判断两个顶点之间是否有边
(2)便于计算各个顶点的度。对于无向图,邻接矩阵第 i i i行元素之和就是顶点 i i i的度;对于有向图,第 i i i行元素之和就是顶点 i i i的出度,第 i i i列元素之和就是顶点 i i i的入度。
2、缺点
(1)不便于增加和删除顶点。
(2)不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕。
(3)空间复杂度高。

  • 邻接表

邻接表是图的一种链式存储结构。在邻接表中,对图中每个顶点 v i v_i vi建立一个单链表,把与 v i v_i vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息,把这一结点看成链表的表头,其余结点存放有关边的信息,这样的邻接表由两部分组成:表头结点表和边表。
(1)表头结点表
它是以顺序结构的形式存储,以便可以随机访问任一顶点的边链表。表头结点包括数据域和链域。
数据域用来存放顶点名称和其他有关信息
链域用来指向链表中第一个结点

表头结点示意图:

(2)边表
边表中边结点包括邻接点域、数据域和链域。
邻接点域存储与该顶点邻接的点在图中的位置
数据域存储和边相关的信息,如权值
链域指示与该顶点邻接的下一条边的结点

边表结点示意图:

例1:

若无向图G有n个顶点和e条边,需n个表头结点和2e个表结点。
在无向图中,一条边依附两个顶点,而两个顶点需要各自填一次边的信息,即要填入两次边的信息。所以会有2e个表结点。

例2:

若有向图G有n个顶点和e条弧,则需n个表头结点和e个表结点。
有向图G的邻接表,顶点 v i v_i vi的出度为第i个单链表的长度。
求顶点 v i vi vi的入度需遍历全部单链表,统计结点值为i的结点数。

例3:

  • 逆邻接表

由以上例子可知,在无向图中,顶点 V i V_i Vi的度恰为第 i i i个链表中的结点数;而在有向图中,第 i i i个链表中的结点个数只是顶点 V i V_i Vi的出度,为求入度,必须遍历整个邻接表。
有时为了便于确定顶点的入度,可以建立一个有向图的逆邻接表,即对每个顶点 V i V_i Vi建立一个链接所有进入 V i V_i Vi的边的表。

  • 邻接表的定义

    #define MVNum 100	//最大顶点数
    typedef struct ArcNode{	//边结点
    	int adjvex;	//该边所指向的顶点的位置下标
    	struct ArcNode *nextarc; //指向下一条边的指针
    	OtherInfo info;	//和边相关的信息
    }ArcNode;
    
    typedef struct VNode{	//顶点信息
    	VerTexType data;	//顶点结点的数据
    	ArcNode *firstarc;	//指向第一条依附该顶点的边的指针
    }VNode,AdjList[MVNum];	//AdjList表示邻接表类型,存放顶点信息,是一个顺序表
    
    typedef struct{
    	AdjList vertices;	//邻接表类型,vertices是数组类型,对应于邻接表
    	int vexnum,arcnum;	//图的当前顶点数和边数
    }
    
  • 十字链表

十字链表是有向图的又一种链式存储结构。可以看成是将有向图的邻接表和逆邻接表结合起来得到的一种链表。在十字链表中,对应于有向图中每一条弧有一个结点,对应于,每个顶点也有一个结点。
结构图如下:

弧结点

顶点结点

  • 十字链表的画法

(1)先画出邻接表的结构
(2)再画逆邻接表的结构
(3)合成十字链表

例:
有向图:

(1)画出邻接表

(2)画出逆邻接表

(3)合成十字链表

  • 邻接多重表

虽然邻接表是无向图的一种很有效的存储结构,在邻接表中容易求得顶点和边的各种信息。但是,在邻接表中每一条边 ( V i , V j ) (V_i,V_j) (Vi,Vj)有两个结点,分别在第 i i i个和第 j j j个链表中,将会给某些图的操作带来不便。例如在某些图的应用问题中需要对边进行某种操作,如对已被搜索过的边做记号或删除一条边时,此时需要找到表示同一条边的两个结点。因此,在进行这一操作的无向图的问题中采用邻接多重表作存储结构更为适宜。

邻接多重表的结构和十字链表类似。在邻接多重表中,每一条边用一个结点表示。

边结构图:

顶点结构图:

例:

  • 3 图的遍历:深度优先搜索遍历与广度优先搜索遍历

  • 图的遍历

从某一顶点出发,按照某种规则对图中所有顶点访问且仅访问一次。图的遍历是求解图的连通性问题、拓扑排序和关键路径等算法的基础。

图中的任一顶点都可能和其余的顶点相邻接。所以在访问了某个顶点之后,可能沿着某条路径搜索之后,又回到了该顶点。

因此,设一个辅助数组visited[n],其初始值为0,一旦访问了顶点 V i V_i Vi,便置visited[i]为1。

根据搜索路径的方向,通常有两条遍历图的路径:深度优先搜索和广度优先搜索。他们对有向图和无向图都有适用。

  • 深度优先搜索(DFS)

深度优先搜索的遍历过程

深度优先搜索遍历类似于树的先序遍历,是树的先序遍历的推广。
对于一个连通图,深度优先搜索遍历过程如下:
(1)从图中某个顶点 V V V出发,访问V。
(2)找出刚访问过的顶点的一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步骤,直至访问过的顶点没有未被访问的邻接点为止。
(3)返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点。
(4)重复(2)和(3),直至图中所有顶点都被访问过,搜索结束。

例:

假设从A出发遍历:

解释:

从A出发沿B走到尽头得 ( A B D C ) (ABDC) (ABDC)
从C退回到前面的顶点,这个顶点还有未被访问的邻接点。退回到A,再次沿E走到尽头得 ( E G H F ) (EGHF) (EGHF)

总的序列为 ( A B D C ) + ( E G H F ) (ABDC)+(EGHF) (ABDC)+(EGHF)

从A出发的另一种序列:

假设从G出发:

由此可知遍历序列不唯一。

  • 广度优先搜索(BFS)

广度优先搜索类似于树的按层次遍历的过程。
(1)从图中某个顶点 v v v出发,访问 v v v
(2)依次访问 v v v各个未曾访问的邻接点
(3)分别从这些邻接点出发依次访问它们的邻接点,并使 “先被访问的顶点的邻接点” 先于 “后被访问的顶点的邻接点” 被访问。重复步骤(3),直至图中所有被访问的顶点的邻接点都被访问。

例:

从A出发遍历:

解释:

从A得到A的所有邻接点排序,得 A ( E F B ) A(EFB) A(EFB)
依次从A的邻接点开始,从E得出E的所有邻接点,得 ( G ) (G) (G)
从F开始,得 ( H ) (H) (H)
从B开始,得 ( D C ) (DC) (DC)
总的序列为: A ( E F B ) + ( G ) + ( H ) + ( D C ) A(EFB)+(G)+(H)+(DC) A(EFB)+(G)+(H)+(DC)

  • 4 图的连通性问题:DFS与BFS生成树、强连通分量的求解,最小生成树

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

例:

DFS生成树:

用深度优先搜索生成一棵树。

从A出发,序列为:ABDCFGEH

把它改为树的形状

换一种遍历序列即可得到另一棵DFS生成树:

BFS生成树

从A出发,得到序列为:AEFBGHIDC

把它改成树的形状

换一种序列即可得到另一棵BFS生成树:

另外还有DFS生成森林,BFS生成森林,它们都是按照搜索方法得出的。生成森林就是生成多棵树,原理不变。

  • 有向图的强连通分量

在有向图G中,从某个顶点v出发,顺着弧的方向进行深度优先搜索遍历,得到顶点集合 V 1 V_1 V1;再顶点v出发,逆着弧的方向进行深度优先搜索遍历,得到顶点集合 V 2 V_2 V2

得到一个强连通分量: G s = ( V s , V R s ) Gs=(Vs,VRs) Gs=Vs,VRs
其中: V s = V 1 ∩ V 2 Vs= V_1∩ V_2 Vs=V1V2 V R s VRs VRs V s Vs Vs中所有顶点在G中的弧。

例:

有向图:

从A出发,顺着弧的方向得到顶点集合:{A、B、D};逆的弧的方向得到:{A、B、C、D};交集为: {A、B、D};加上它们之间的所有弧得到强连通分量G1。

注意:把所有顺的弧走完,把所有逆的弧走完。

得出强连通分量:

  • 网的最小生成树

网是一种带权值的图(可以是无向图也可以是有向图)。
从上述的DFS和BFS生成树来看,网可以生成很多种树,但是我们在意的是最小生成树。什么是最小生成树呢?
网生成的很多树,但是树的各边权值之和可能不同。那么权值之和最小的就是最小生成树。

  • MST性质

构造生成树的算法有很多种,其中多数算法利用了最小生成树的MST性质:

假设 N = ( V , E ) N=(V,E) N=(V,E)是一个连通网, U U U是顶点集的一个非空子集。
( u , v ) (u,v) (u,v)是一条具有最小权值的边,其中 u ∈ U , v ∈ V − U u∈U,v∈V-U uUvVU,则必存在一棵包含 ( u , v ) (u,v) (u,v)的最小生成树。

MST说明:

假设网N的任何一棵最小生成树都不包含 ( u , v ) (u,v) (u,v)。设 T T T是连通网上的一棵最小生成树,当将边 ( u , v ) (u,v) (u,v)加入到T中时,由生成树的定义,T中必存在一条包含 ( u , v ) (u,v) (u,v)的回路。另一方面,由于T是生成树,则T上必存在另一条边 ( u ′ , v ′ ) (u',v') (u,v),其中 u ′ ∈ U , v ′ ∈ V − U u'∈U,v'∈V-U uUvVU,且 u 和 u ′ u和u' uu之间、 v 和 v ′ v和v' vv之间均有路径相同。删去边 ( u ′ , v ′ ) (u',v') (u,v),便可消除上述回路,同时得到另一棵生成树 T ′ T' T。因为 ( u , v ) (u,v) (u,v)的权值不高于 ( u ′ , v ′ ) (u',v') (u,v),则 T ′ T' T的权值亦不高于 T T T T ′ T' T是包含 ( u , v ) (u,v) (u,v)的一棵最小生成树。

  • 普利姆算法

构造过程

假设 N = ( V , E ) N=(V,E) N=(V,E)是连通图, T E TE TE N N N上最小生成树中边的集合。

(1) U = U= U= { u 0 u_0 u0 } ( u 0 ∈ V ) (u_0∈V) (u0V) T E = TE= TE={}。
(2)在所有 u ∈ U , v ∈ V − U u∈U,v∈V-U uUvVU的边 ( u , v ) ∈ E (u,v)∈E (u,v)E中找出一条权值最小的边 ( u 0 , v 0 ) (u_0,v_0) (u0,v0)并入集合 T E TE TE,同时 v 0 v_0 v0并入 U U U
(3)重复(2)直至 U = V U=V U=V为止。
此时 T E TE TE中必有 n − 1 n-1 n1条边,则 T = ( V , T E ) T=(V,TE) T=(V,TE) N N N的最小生成树。

普利姆算法逐步增加顶点,可称为“加点法”。

例:

第一步:在 U = U= U={ V 1 V_1 V1}中选取点 u u u,在 V − U = V-U= VU={ V 2 , V 3 , V 4 , V 5 , V 6 V_2,V_3,V_4,V_5,V_6 V2,V3,V4,V5,V6}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

第二步:在 U = U= U={ V 1 , V 3 V_1,V_3 V1,V3}中选取点 u u u,在 V − U = V-U= VU={ V 2 , V 4 , V 5 , V 6 V_2,V_4,V_5,V_6 V2,V4,V5,V6}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

第三步:在 U = U= U={ V 1 , V 3 , V 6 V_1,V_3,V_6 V1,V3,V6}中选取点 u u u,在 V − U = V-U= VU={ V 2 , V 4 , V 5 V_2,V_4,V_5 V2,V4,V5}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

第四步:在 U = U= U={ V 1 , V 3 , V 6 , V 4 V_1,V_3,V_6,V_4 V1,V3,V6,V4}中选取点 u u u,在 V − U = V-U= VU={ V 2 , V 5 V_2,V_5 V2,V5}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

第五步:在 U = U= U={ V 1 , V 3 , V 6 , V 4 , V 2 V_1,V_3,V_6,V_4,V_2 V1,V3,V6,V4,V2}中选取点 u u u,在 V − U = V-U= VU={ V 5 V_5 V5}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

第六步:在 U = U= U={ V 1 , V 3 , V 6 , V 4 , V 2 , V 5 V_1,V_3,V_6,V_4,V_2,V_5 V1,V3,V6,V4,V2,V5}中选取点 u u u,在 V − U = V-U= VU={}中选取点 v v v,得到一个权值最小的边 ( u , v ) (u,v) (u,v)

此时发现已没有顶点可选,算法结束。

  • 克鲁斯卡尔算法

克鲁斯卡尔算法的构造过程
假设连通网 N = ( V , E ) N=(V,E) N=(V,E),将N中的边按权值从小到大的顺序排列。
(1)初始状态为只有n个顶点而无边的非连通图 T = ( V , T=(V, T=(V,{} ) ) ),图中每个顶点自成一个连通分量。
(2)在E中选择权值最小的边,若该边依附的顶点落在 T T T中不同的连通分量上(即不形成回路),则将此边加入到 T T T中,否则舍去此边而选择下一条权值最小的边。
(3)重复(2),直至 T T T中所有顶点都在同一连通分量上为止。

克鲁斯卡尔算法逐步增加生成树的边,可称为“加边法”。

例:一个无向网

第一步:把所有边按权值大小排序,选取最小的边 e 0 e_0 e0,如果它连接两个不同的连通分量,则将此边加入到 T T T

第二步:在非T中的边的集合里,选取最小的边 e 0 e_0 e0,如果它连接两个不同的连通分量,则将此边加入到 T T T

第三步:在非T中的边的集合里,选取最小的边 e 0 e_0 e0,如果它连接两个不同的连通分量,则将此边加入到 T T T

第四步:在非T中的边的集合里,选取最小的边 e 0 e_0 e0,如果它连接两个不同的连通分量,则将此边加入到 T T T

第五步:在非T中的边的集合里,选取最小的边 e 0 e_0 e0,如果它连接两个不同的连通分量,则将此边加入到 T T T

此时权值为5的边有三条,那么,

第一种选法:发现权值为5的边连接在两个连通分量上,可取

第二种选法:发现权值为5的边没有连接在两个连通分量上,而形成了环,不可取

第三种选法:发现权值为5的边没有连接在两个连通分量上,而形成了环,不可取

因此用克鲁斯卡尔算法得到最终的生成树为:

  • 5 有向无环图(DAG)及应用: 拓扑排序、关键路径

一个无环的有向图称为有向无环图,简称DAG图。
有向无环图是描述一项工程或系统的进行过程的有效工具。通常把计划、施工过程、生产流程、程序流程等都当成一个工程。除了很小的工程外,一般的工程都可分为若干个称作活动的子工程,而这些子工程之间,通常受着一定条件的约束,如其中某些子工程的开始必须在另一些子工程完成之后。

用顶点表示活动,用弧表示表示活动间的优先关系的有向图称为顶点表示活动的网,简称AOV-网。在网中,若从顶点 v i v_i vi到顶点 v j v_j vj有一条有向路径,则 v i v_i vi v j v_j vj的前驱, v j v_j vj v i v_i vi的后继。若 < v i , v j > <v_i,v_j> <vi,vj>是网中的一条弧,则 v i v_i vi v j v_j vj的直接前驱, v j v_j vj v i v_i vi的直接后继。

在AOV-网中,不应该出现有向环,因为存在环意味着某项活动应以自己为先决条件,显然这是不对的。若设计出这样的流程图,工程便无法进行。而对程序的数据流程图来说,则表明存在一个死循环。因此,对给定的AOV-网应首先判断网中是否存在环。

检测的办法是对有向图的顶点进行拓扑排序,若网中所有顶点都在它的拓扑有序序列中,则该AOV-网中必不存在环。

所谓拓扑排序就是将AOV-网中所有顶点排成一个线性序列,该序列满足:若在AOV-网中由顶点 v i v_i vi v j v_j vj有一条路径,则在拓扑排序的线性序列中的顶点 v i v_i vi必定在顶点 v j v_j vj之前。

例如:一个AOV-网

它的拓扑排序为:

(1) C 1 , C 2 , C 3 , C 4 , C 5 , C 7 , C 9 , C 10 , C 11 , C 6 , C 12 , C 8 C_1,C_2,C_3,C_4,C_5,C_7,C_9,C_{10},C_{11},C_6,C_{12},C_8 C1,C2,C3,C4,C5,C7,C9,C10,C11,C6,C12,C8
(2) C 9 , C 10 , C 11 , C 6 , C 1 , C 12 , C 4 , C 2 , C 3 , C 5 , C 7 , C 8 C_9,C_{10},C_{11},C_6,C_1,C_{12},C_4,C_2,C_3,C_5,C_7,C_8 C9,C10,C11,C6,C1,C12,C4,C2,C3,C5,C7,C8

当然只要满足拓扑排序序列的要求即可,可以有多种排序结果。在此只写出两种。

  • 拓扑排序

拓扑排序的过程

(1)在有向图中选一个无前驱的顶点且输出它。
(2)从图中删除该顶点和所有以它为尾的弧。
(3)重复(1)和(2),直至不存在无前驱的顶点。
(4)若此时输出的顶点数小于有向图中的顶点数,则说明有向图中存在环,否则输出的顶点序列即为一个拓扑序列。

例:一个AOV-网

第一步:找出一个无前驱的顶点 V 6 V_6 V6,然后删除该顶点和所有以它为尾的弧。

第二步:再次找出一个无前驱的顶点 V 1 V_1 V1,然后删除该顶点和所有以它为尾的弧。

第三步:再次找出一个无前驱的顶点 V 4 V_4 V4,然后删除该顶点和所有以它为尾的弧。

第四步:再次找出一个无前驱的顶点 V 3 V_3 V3,然后删除该顶点和所有以它为尾的弧。

第五步:再次找出一个无前驱的顶点 V 2 V_2 V2,然后删除该顶点和所有以它为尾的弧。

第六步:再次找出一个无前驱的顶点 V 5 V_5 V5,然后删除该顶点和所有以它为尾的弧。
此时已经把所有顶点找完,即得出了一个拓扑排序序列: V 6 , V 1 , V 4 , V 3 , V 2 , V 5 V_6,V_1,V_4,V_3,V_2,V_5 V6,V1,V4,V3,V2,V5

  • 关键路径:

1、AOE-网
与AOV-网对应的是AOE-网,即以边表示活动的网。AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。通常,AOE-网可用来估算工程的完成时间。

例如:AOE-网

如上图所示为一个有11项活动的AOE-网。其中有9个事件 V 0 V_0 V0 V 8 V_8 V8,每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。

例如, V 0 V_0 V0表示整个工程的开始, V 8 V_8 V8表示整个工程的结束, V 4 V_4 V4表示 a 4 a_4 a4 a 5 a_5 a5已经完成, a 7 a_7 a7 a 8 a_8 a8已经开始了。

与每个活动相联系的数是执行该活动所需的时间,比如,活动 a 1 a_1 a1需要6天, a 2 a_2 a2需要四天等。

AOE-网在工程计划和经营管理中有广泛的应用,针对实际的应用问题,通常需要解决以下问题:
(1)估算完成整项工程至少需要多少时间;
(2)判断哪些活动是影响工程进度的关键。

工程进度的关键在于抓住关键活动。在一定的范围内,非关键活动的提前完成对于整个工程的进度没有直接的好处,它的稍许拖延也不会影响到整个工程的进度。工程的指挥者可以把非关键活动的人力和物力资源暂时调给关键活动,加速其进展速度,以使整个工程提前完工。

由于整个工程只有一个开始点和一个完成点,故在正常的情况下(无环),网中只有一个入度为零的点,称作源点,也只要一个出度为零的点,称作汇点。

在AOE-网中,一条路径各弧上的权值之和称为该路径的带权路径长度(简称路径长度)。

要估算整个工程完成的最短时间,就是找一条从源点到汇点的带权路径长度最长的路径,称为关键路径。

关键路径上的活动叫关键活动,这些活动是影响工程进度的关键,它们的提前或拖延将使整个工程提前和拖延。

例如上图, V 0 V_0 V0是源点, V 8 V_8 V8是汇点,关键路径有两条: ( V 0 , V 1 , V 4 , V 6 . V 8 ) (V_0,V_1,V_4,V_6.V_8) (V0,V1,V4,V6.V8) ( V 0 , V 1 , V 4 , V 7 , V 8 ) (V_0,V_1,V_4,V_7,V_8) (V0,V1,V4,V7,V8),长度均为18.

关键活动为: ( a 1 , a 4 , a 7 , a 10 ) (a_1,a_4,a_7,a_{10}) (a1,a4,a7,a10) ( a 1 , a 4 , a 8 , a 11 ) (a_1,a_4,a_8,a_{11}) (a1,a4,a8,a11)。比如关键活动 a 1 a_1 a1需要6天完成,如果 a 1 a_1 a1提前一天,整个工程也可以提前一天完成。

所以不论是估算工期,还是研究如何加快工程进度,主要问题就在于找到AOE-网的关键路径。那么,如何确定关键路径呢?

首先,定义四个描述量:

1、对于事件来说,事件有最早发生和最迟发生。
(1)事件 v i v_i vi的最早发生时间 v e ( i ) ve(i) ve(i)
进入事件 v i v_i vi的每一活动都结束, v i v_i vi才可发生,所以 v e ( i ) ve(i) ve(i)是从源点到 v i v_i vi的最长路径长度。
v e ( i ) ve(i) ve(i)的值,可根据拓扑排序从源点向汇点递推。通常将工程的开始顶点时间 v 0 v_0 v0的最早发生时间定义为0。

计算: 顶点 v v v的最早发生时间

规定源点: v e 0 = 0 ve_0=0 ve0=0

  • 找出指向 v v v的弧有几条,并找出每条弧的出发点。
  • 计算出这些出发点的最早发生时间。
  • 用这些出发点的最早发生时间,加上到达 v v v的活动持续时间,得出各个结果。
  • 得出结果最大的就是 v v v的最早发生时间。

(2)事件 v i v_i vi的最迟发生时间 v l ( i ) vl(i) vl(i)
事件 v i v_i vi的发生不得延误 v i v_i vi的每一后继事件的最迟发生时间。为了不拖延工期, v i v_i vi的最迟发生时间不得迟于其后继事件 v k v_k vk的最迟发生时间减去活动 < v j , v k > <v_j,v_k> <vj,vk>的持续时间。
求出 v e ( i ) ve(i) ve(i)后,可根据逆拓扑排序从汇点到源点递推,求出 v l ( i ) vl(i) vl(i)。规定 v l ( n − 1 ) = v e ( n − 1 ) vl(n-1)=ve(n-1) vl(n1)=ve(n1),即汇点的最早开始时间等于最晚发生时间。

计算: 顶点 v v v的最迟发生时间

规定汇点: v l n − 1 = v e n − 1 vl_{n-1}=ve_{n-1} vln1=ven1

  • 找出从 v v v出发的弧有几条,并找出弧指向的终点。
  • 计算出这些终点的最晚发生时间。
  • 用这些终点的最晚发生时间,减去逆到达 v v v的活动的持续时间,得出各结果。
  • 得出结果最小的就是 v v v的最迟发生时间。

2、对于活动来说,活动有最早开始和最晚开始。
(3)活动 a i = < v j , v k > a_i=<v_j,v_k> ai=<vj,vk>的最早开始时间。

只有时间 v j v_j vj发生了,活动 a i a_i ai才能开始,所以,活动 a i a_i ai的最早开始时间等于时间 v j v_j vj的最早发生时间。
即, e ( i ) = v e ( j ) e(i)=ve(j) e(i)=ve(j)

计算: 活动 a i a_i ai的最早开始时间

  • 找到这个活动的出发点。
  • 计算这个出发点的最早开始时间。
  • 出发点的最早开始时间就是活动 a i a_i ai的最早开始时间。

(4)活动 a i = < v j , v k > a_i=<v_j,v_k> ai=<vj,vk>的最晚开始时间。
活动 a i a_i ai的开始时间需保证不延误事件 v k v_k vk的最迟发生时间。所以活动 a i a_i ai的最晚开始时间 l ( i ) l(i) l(i)等于事件 v k v_k vk的最迟发生时间 v l ( k ) vl(k) vl(k),减去活动 a i a_i ai的持续时间。

计算: 活动 a i a_i ai的最晚开始时间。

  • 找到这个活动指向的终点。
  • 计算这个终点的最迟发生时间。
  • 终点的最迟发生时间减去活动 a i a_i ai的持续时间,得出结果便是 a i a_i ai的最晚开始时间。

到此,已经理解了四个描述量的含义及计算。

显然,对于关键活动而言,它不得拖延。意思就是,该活动的最早开始时间和最迟开始时间相等。
对于非关键活动而言,它可以拖延。意思就是,最迟开始时间和最早开始时间有一个差值,这个差值代表该工程的期限余量,在此范围内的适度延误不会影响整个工程的工期。

2、关键路径的求解过程
(1)对图中顶点进行排序,在排序过程中按拓扑序列求出每个事件的最早发生时间 v e ( i ) ve(i) ve(i)
(2)按逆拓扑序列求出每个事件的最迟发生时间 v l ( i ) vl(i) vl(i)
(3)求出每个活动 a i a_i ai的最早开始时间 e ( i ) e(i) e(i)
(4)求出每个活动 a i a_i ai的最晚开始时间 l ( i ) l(i) l(i)
(5)找出 e ( i ) = l ( i ) e(i)=l(i) e(i)=l(i)的活动,即为关键活动,由关键活动形成的由源点到汇点的每一条路径就是关键路径,关键路径有可能不止一条。

3、计算关键路径举例

例:AOE-网

其中一个拓扑序列为:012345678。按此序列进行下列计算。

(1)计算各顶点事件的最早发生时间

v e ( 0 ) ve(0) ve(0)=0
v e ( 1 ) ve(1) ve(1)=6
v e ( 2 ) ve(2) ve(2)=4
v e ( 3 ) ve(3) ve(3)=5
v e ( 4 ) ve(4) ve(4)=7
v e ( 5 ) ve(5) ve(5)=7
v e ( 6 ) ve(6) ve(6)=16
v e ( 7 ) ve(7) ve(7)=14
v e ( 8 ) ve(8) ve(8)=18

(2)计算各顶点事件最迟发生时间

v l ( 8 ) = v e ( 8 ) = 18 vl(8)=ve(8)=18 vl(8)=ve(8)=18
v l ( 7 ) = 14 vl(7)=14 vl(7)=14
v l ( 6 ) = 16 vl(6)=16 vl(6)=16
v l ( 5 ) = 10 vl(5)=10 vl(5)=10
v l ( 4 ) = 7 vl(4)=7 vl(4)=7
v l ( 3 ) = 8 vl(3)=8 vl(3)=8
v l ( 2 ) = 6 vl(2)=6 vl(2)=6
v l ( 1 ) = 6 vl(1)=6 vl(1)=6
v l ( 0 ) = 0 vl(0)=0 vl(0)=0

(3)计算各活动 a i a_i ai的最早开始时间

e ( a 1 ) = v e ( 0 ) = 0 e(a_1)=ve(0)=0 e(a1)=ve(0)=0
e ( a 2 ) = v e ( 0 ) = 0 e(a_2)=ve(0)=0 e(a2)=ve(0)=0
e ( a 3 ) = v e ( 0 ) = 0 e(a_3)=ve(0)=0 e(a3)=ve(0)=0
e ( a 4 ) = v e ( 1 ) = 6 e(a_4)=ve(1)=6 e(a4)=ve(1)=6
e ( a 5 ) = v e ( 2 ) = 4 e(a_5)=ve(2)=4 e(a5)=ve(2)=4
e ( a 6 ) = v e ( 3 ) = 5 e(a_6)=ve(3)=5 e(a6)=ve(3)=5
e ( a 7 ) = v e ( 4 ) = 7 e(a_7)=ve(4)=7 e(a7)=ve(4)=7
e ( a 8 ) = v e ( 4 ) = 7 e(a_8)=ve(4)=7 e(a8)=ve(4)=7
e ( a 9 ) = v e ( 5 ) = 7 e(a_9)=ve(5)=7 e(a9)=ve(5)=7
e ( a 10 ) = v e ( 6 ) = 16 e(a_{10})=ve(6)=16 e(a10)=ve(6)=16
e ( a 11 ) = v e ( 7 ) = 14 e(a_{11})=ve(7)=14 e(a11)=ve(7)=14

(4)计算各活动 a i a_i ai的最晚开始时间

l ( a 11 ) = 14 l(a_{11})=14 l(a11)=14
l ( a 10 ) = 16 l(a_{10})=16 l(a10)=16
l ( a 9 ) = 10 l(a_9)=10 l(a9)=10
l ( a 8 ) = 7 l(a_8)=7 l(a8)=7
l ( a 7 ) = 7 l(a_7)=7 l(a7)=7
l ( a 6 ) = 8 l(a_6)=8 l(a6)=8
l ( a 5 ) = 6 l(a_5)=6 l(a5)=6
l ( a 4 ) = 6 l(a_4)=6 l(a4)=6
l ( a 3 ) = 3 l(a_3)=3 l(a3)=3
l ( a 2 ) = 2 l(a_2)=2 l(a2)=2
l ( a 1 ) = 0 l(a_1)=0 l(a1)=0

(5)绘制成表

活动的开始时间表:

活动 a i a_i ai e ( i ) e(i) e(i) l ( i ) l(i) l(i) l ( i ) − e ( i ) l(i)-e(i) l(i)e(i)
a 1 a_1 a1000 ✅
a 2 a_2 a2022
a 3 a_3 a3033
a 4 a_4 a4660 ✅
a 5 a_5 a5462
a 6 a_6 a6583
a 7 a_7 a7770 ✅
a 8 a_8 a8770 ✅
a 9 a_9 a97103
a 10 a_{10} a1016160 ✅
a 11 a_{11} a1114140 ✅

由表可知,从源点到汇点有两条关键路径
(1) a i , a 4 , a 7 , a 10 a_i,a_4,a_7,a_{10} ai,a4,a7,a10
(2) a i , a 4 , a 8 , a 11 a_i,a_4,a_8,a_{11} ai,a4,a8,a11

关键路径图:

  • 6 最短路径:迪杰斯特拉算法、弗洛伊德算法

在带权有向网中,习惯上称路径上的第一个顶点为源点,最后一个顶点为终点。

主要讨论两种常见的最短路径问题:一种是求从源点到其余各顶点的最短路径,另一种是求每一对顶点之间的最短路径。

  • 迪杰斯拉算法

1、从源点到其余各顶点的最短路径。
给定带权有向图 G G G和源点 v 0 v_0 v0,求从 v 0 v_0 v0 G G G中其余各顶点的最短路径。迪杰斯拉出了一个按路径长度递增的次序产生最短路径的算法,称为迪杰斯拉算法。

2、迪杰斯拉算法的求解过程
对于网 N = ( V , E ) N=(V,E) N=(V,E),将 N N N中的顶点分成两组:
第一组 S S S:已求出的最短路径的终点集合(初始时只包含源点 v 0 v_0 v0)。
第二组 V − S V-S VS:尚未求出的最短路径的顶点集合(初始时为 V − V- V{ V 0 V_0 V0})。
算法将按各顶点与 v 0 v_0 v0间最短路径长度递增的次序,逐个将集合 V − S V-S VS中各顶点加入到集合S中去。在这个过程中,总保持从 v 0 v_0 v0到集合S中各顶点的路径长度始终不大于到集合 V − S V-S VS中各顶点的路径长度。

3、迪杰斯拉算法求解举例

例:

第一步:选取 v 0 v_0 v0为源点, S = S= S={ v 0 v_0 v0}, V − S = V-S= VS={ v 1 , v 2 , v 3 , v 4 , v 5 v_1,v_2,v_3,v_4,v_5 v1,v2,v3,v4,v5},从S中找出源点 v 0 v_0 v0到各点的路径。

v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 4 v_4 v4 v 5 v_5 v5
10( v 0 , v 2 v_0,v_2 v0,v2)30( v 0 , v 4 v_0,v_4 v0,v4)100 ( v 0 , v 5 ) (v_0,v_5) (v0,v5)

在各路径中,选出最短距离10,把 v 2 v_2 v2加入S。

第二步:从 S = S= S={ v 0 , v 2 v_0,v_2 v0,v2}, V − S = V-S= VS={ v 1 , v 3 , v 4 , v 5 v_1,v_3,v_4,v_5 v1,v3,v4,v5}。从 S S S中再次找到源点 v 0 v_0 v0 V − S V-S VS中各点的间接路径( v 0 v_0 v0可以通过 S S S中的点到达 V − S V-S VS中的点)。

v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 4 v_4 v4 v 5 v_5 v5
已结束60( v 0 , v 2 , v 3 v_0,v_2,v_3 v0,v2,v3)30( v 0 , v 4 v_0,v_4 v0,v4)100 ( v 0 , v 5 ) (v_0,v_5) (v0,v5)

在各路径中,选出最短距离30,把 v 4 v_4 v4加入S。

第三步:从 S = S= S={ v 0 , v 2 , v 4 v_0,v_2,v_4 v0,v2,v4}, V − S = V-S= VS={ v 1 , v 3 , v 5 v_1,v_3,v_5 v1,v3,v5}。从 S S S中再次找到源点 v 0 v_0 v0 V − S V-S VS中各点的间接路径( v 0 v_0 v0可以通过 S S S中的点到达 V − S V-S VS中的点)。

v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 4 v_4 v4 v 5 v_5 v5
已结束50( v 0 , v 4 , v 3 v_0,v_4,v_3 v0,v4,v3)已结束90 ( v 0 , v 4 , v 5 ) (v_0,v_4,v_5) (v0,v4,v5)

在各路径中,选出最短距离50,把 v 3 v_3 v3加入S。

第四步:从 S = S= S={ v 0 , v 2 , v 4 , v 3 v_0,v_2,v_4,v_3 v0,v2,v4,v3}, V − S = V-S= VS={ v 1 , v 5 v_1,v_5 v1,v5}。从 S S S中再次找到源点 v 0 v_0 v0 V − S V-S VS中各点的间接路径( v 0 v_0 v0可以通过 S S S中的点到达 V − S V-S VS中的点)。

v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 4 v_4 v4 v 5 v_5 v5
已结束已结束已结束60 ( v 0 , v 4 , v 3 , v 5 ) (v_0,v_4,v_3,v_5) (v0,v4,v3,v5)

在各路径中,选出最短距离90,把 v 5 v_5 v5加入S。

第五步:从 S = S= S={ v 0 , v 2 , v 4 , v 3 , v 5 v_0,v_2,v_4,v_3,v_5 v0,v2,v4,v3,v5}, V − S = V-S= VS={ v 1 v_1 v1}。从 S S S中再次找到源点 v 0 v_0 v0 V − S V-S VS中各点的间接路径( v 0 v_0 v0可以通过 S S S中的点到达 V − S V-S VS中的点)。

v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 4 v_4 v4 v 5 v_5 v5
已结束已结束已结束已结束

无穷大为不可达,即没有最短路径了,算法结束。

  • 弗洛伊德算法

1、每对顶点之间的最短路径

求解每一对顶点之间的最短路径有两种方法:其一分别以图中的每个顶点为源点共调用n次迪杰斯特拉算法;其二是用弗洛伊德算法。

递推产生一个n阶方阵序列 A ( − 1 ) , A ( − 0 ) , . . . , A ( k ) , . . . , A ( n − 1 ) A^{(-1)},A^{(-0)},...,A^{(k)},...,A^{(n-1)} A(1),A(0),...,A(k),...,A(n1),其中 A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j]表示从顶点 v i v_i vi v j v_j vj的路径长度, k k k表示绕行第 k k k个顶点的运算步骤。也就是逐步尝试在原路径中加入顶点k作为中间点。如果增加中间点后,得到的路径长度必原来的路径长度小,则以新路径代替原路径。

定义一个方阵序列: A ( − 1 ) , A ( − 0 ) , . . . , A ( k ) , . . . , A ( n − 1 ) A^{(-1)},A^{(-0)},...,A^{(k)},...,A^{(n-1)} A(1),A(0),...,A(k),...,A(n1),其中:
A ( − 1 ) [ i ] [ j ] = a r c s [ i ] [ j ] A^{(-1)}[i][j]=arcs[i][j] A(1)[i][j]=arcs[i][j]
A ( k ) [ i ] [ j ] = M i n ( A ( k − 1 ) [ i ] [ j ] , A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] A^{(k)}[i][j]=Min(A^{(k-1)}[i][j],A^{(k-1)}[i][k]+A^{(k-1)}[k][j] A(k)[i][j]=Min(A(k1)[i][j],A(k1)[i][k]+A(k1)[k][j]

2、弗洛伊德算法举例

例:

初始化:邻接矩阵

第一次迭代:在上一个矩阵的基础上进行修改,加入顶点1为中间顶点。
(1)在(1,1)的位置上画出两条虚线

(2)修改矩阵中每个位置的值
例如:(2,3)位置。
取出它的行号2,与虚线交点为(2,1),值为6
取出它的列号3,与虚线交点为(1,3),值为11
11+6=17>2,则(2,3)位置的值不变。
例如:(3,2)位置。
取出它的行号3,与虚线交点为(3,1),值为3
取出它的列号2,与虚线交点为(1,2),值为4
3+4=7<♾,则(2,3)位置的值变为7。
按照此法依次修改所有矩阵值,得出新的邻接矩阵。

第二次迭代:在上一个矩阵的基础上进行修改,加入顶点2为中间顶点。

(1)在(2,2)的位置上画出两条虚线

(2)修改矩阵中每个位置的值

例如:(1,3)位置。
取出它的行号1,与虚线交点为(1,2),值为4
取出它的列号3,与虚线交点为(2,3),值为2
2+4=6<11,则(1,3)位置的值变为6。

按照此法依次修改所有矩阵值,得出新的邻接矩阵。

第三次迭代:在上一个矩阵的基础上进行修改,加入顶点3为中间顶点。

(1)在(3,3)的位置上画出两条虚线

(2)修改矩阵中每个位置的值

例如:(2,1)位置。
取出它的行号2,与虚线交点为(2,3),值为2
取出它的列号1,与虚线交点为(3,1),值为3
2+3=5<6,则(2,1)位置的值变为5。

按照此法依次修改所有矩阵值,得出新的邻接矩阵。

到此已经把所有顶点都已迭代,算法结束。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值