第五章:圖(圖的應用)
1.最小生成樹
生成樹:連通圖包含全部頂點的一個極小連通子圖
這裏需要注意的是是一個極小連通子圖
上面第一個是一個連通圖,右側兩個圖實它的生成樹,他們包含了全部頂點且是極小連通子圖,如果我們把圖2中A和D鏈接起來他就不是一個生成樹了,因爲他不是極小的,如果我們把圖3中的C和D中的邊去掉,他也不是生成樹了,因爲他不是連通的。
如上圖,我們把圖的邊加上權值他就叫做網,我們找出它的兩個上生成樹,其中圖2的所有邊的權值的和爲:2+2+5+6=15,圖3的所有邊的權值之和是:1+2+4+5=12,可以看出圖3的權值之和最小,則圖3就是最小生成樹。
最小生成樹:對於帶權的無向連通圖
G=(V,E),G的所有生成樹當中邊的權值之和最小的生成樹爲G的最小生成樹(MST)
注意是
帶權的無向連通圖
,因爲帶權了纔有最小的概念,只有無向連通圖纔有生成樹
1.1性質
不一定唯一:最小生成樹一定只有一種嗎?
如上圖中的圖2和圖3他們都是最小生成樹,所以最下生成樹不一定唯一。
這是是不一定,所有也有唯一的情況
,比如:
- 所有邊的權重皆不相同(沒有權重相同的邊最小生成樹必爲一,可以自行嘗試)
- 圖中n個頂點只有n-1條邊(這樣的無向圖【網】它的最小生成樹只有一個就是他自己,所以唯一)
性質
-
最小生成樹不一定唯一,即最小生成樹的樹形不一定唯一。當帶權無向連通圖G的各邊權值不等時或G只有結點數減1條邊時,MST唯一
-
最小生成樹的權值是唯一的,且是最小
-
最小生成樹的邊數爲頂點數減1
1.2算法
我們根據貪心算法設計瞭如下最小生成樹算法(貪心:每一步儘量做出最好的選擇)
//僞代碼
GENRIC_MST(G){
T=NULL;
while T //未形成一棵生成樹
do //找到一條最小代價邊(u,v)並且加如T後不會產生迴路
T=Tu(u,v);
}
兩種最小生成樹算法:Prim
、Kruskal
1.3Prim
初始化:向空的結果樹T=(Vt,Et)中添加圖G=(V,E)的任一頂點u0
,使Vt={u0}
,Et爲空集;
循環(直到Vt=V):從圖G中選擇滿足{(u,v)|u∈Vt,v∈V-Vt}
且具有最小權值的邊(u,v),並置Vt=VtU{v},Et=EtU{(u,v)}
{(u,v)|u∈Vt,v∈V-Vt}
相當於將已經添加到結果集中的結點和未添加進來的結點進行連接,這種方法就避免了生成的生成樹出現迴路且具有最小權值的邊:這個就是爲了我們的最終目的啦,生成樹的權值之和最小,即最小生成樹
Vt=VtU{v},Et=EtU{(u,v)}
將新添加的頂點添加到結果的頂點集中,邊添加到結果的邊集中
使用Prim
算法生成最小生成樹的過程:首先假設我們選擇A作爲初始頂點,然後我們從剩下的:B、C、E、D中選擇一個頂點組成一條邊,滿足這樣的邊有三個:AB、AC、AE,並且我們需要選擇該權值最小的那個,可以發現是AC邊
接着我們繼續找,此時可以選擇的頂點有三個B、E、D,可以組成的邊有:AB、AE、CB、CE、CD,我們任然選擇權值最小的那個CB邊
我們繼續找,此時可以選擇的頂點有D和E,可以組成的邊有:AE、CE、CD(注意這裏沒有AB,因爲AB邊會形成通路,當然我們可選擇的頂點也沒有B),任然選擇權值最小的那個AE
接着我們繼續找,此時可以選擇頂點只有D,可以組成的邊只有CD
此時最小生成樹構成成功,Prim算法
僞代碼,算法思想
void Prim(G,T){
T = 空集;//結果集
U = {w};//將w頂點加如結果的頂點集中
while((V-U)!=空集){
設(u,v)是使u∈(V-U),且權值最小的邊;
T=TuP{(u,v)};
U=Uu{V};
}
}
我需要兩個輔助數組將這個僞代碼轉成算法代碼:min_weight[n]
,adjvex[n]
min_weight[n]
它的數組大小是頂點的數量大小,他代表我們每次循環時,我們知道我們的邊一個頂點要從結果集中選擇另一個頂點要從剩下的頂點中選擇,它存放了到每一個頂點:我們已經生成的頂點到我們剩下的頂點的權值最小的那條邊的,數組下標是被選擇的頂點的下標,例如min_weight[2]
代表我們已經挑選的頂點到剩下的頂點之前權重最小的那一條邊,下標2則代表結果頂點集中的頂點到剩下的頂點中下標2的頂點。adjvex[n]
存放該頂點由那一個頂點引入結果集中的
我們任然使用上圖作爲例子,首先選A作爲初始頂點,然後我們要初始化兩個數組:
這裏有一個無窮大的原因是A到D是沒有邊的,在實際算法中令這個無窮大爲一個比較大的整數就可以了
接着我們同樣按照上面的過程找和A頂點組成的邊的權值最小的點C,接着我們此時要修改min_weight數組了
我們可以看到將3改成了2,因爲在此時結果頂點集中可以存在一條從C到B的邊它的權值爲2比之前的AB3更小,同樣修改∞爲5,因爲也存一條CE邊,它的權值爲5,因爲CE的權值爲6,所以4我們不需要修改,同時我們將1改成了0,因爲C頂點已經被取走了,這裏其中adjvex
數組的值也進行修改了,我們修改adjvex[2]=0(2是c頂點的下標)
,因爲A頂點的下標爲0,所以看不出來。
同樣我們繼續挑選權值最小的邊BC,同樣修改輔助數組此時我們已經將A、B、C三個頂點選走所數組的前三個值爲0,因爲B頂點是由C頂點挑選走的,所以修改adjvex=[1]=2(1代表B頂點下標,2代表C頂點下標)
,由於不存在權值更小的邊,所以不修改數組的值
我們繼續尋找頂點,找到AE,同樣我們修改此時修改min_weight=[4]=0
,同時修改adjvex[4]=0
,同樣我們繼續尋找找到CD邊,我們修改min_weight[3]=0
,修改adjvex[3]=2
最終ming_weight
數組中的值全部爲0,說明所有頂點都已經被選擇
代碼實現,時間複雜度O(V^2)
,該算法的時間複雜度和邊沒有關係,所以他比較適合稠密圖
void MST_Prim(Graph G){ //傳入圖G
int min_weight[G.vexnum];//數組大小爲頂點的數量
int adjvex[G.vexnum];//數組大小爲頂點的數量
for(int i=0; i<G.vexnum;i++){
//可以看到初始頂點是下標爲0的頂點,且可以看出我們的頂點這裏採用的是鄰接矩陣存儲的
min_weight[i]=G.Edge[0][i];//存放權值
adjvex[i]=0;//初始化爲0
}
int min_arc; //存放當前挑選的最小邊的權重
int Min_vex; //挑選這樣的一條邊的另一個端點的數組下標
for(int i=1;i<G.vexnum;i++){//循環剩餘頂點注意這裏i初始值爲1,因爲我們已經挑選一個了
min_arc=MAX;//將最小邊的權重置爲max,這裏是爲了求最小值的時候可以進行比較
for(int j=1;j<G.vexnum;j++){//挑選滿足條件的邊
//min_weight[j]!=0不等於0代表這個頂點還沒有被添加到結果集
//min_weight[j]<min_arc 判斷權值是否是最小的
if(min_weight[j]!=0&&min_weight[j]<min_arc){
min_arc=min_weight[j];//如果小於之前的權值,修改最小的權值
min_vex=j;//同時保存最小權值結點的下標
}
}
min_weight[min_vex]=0;//表示已經該頂點已經被選擇出來了
for(int j=0;j<G.vexnum;j++){//循環修改對應數組下標下輔助變量的值
//min_weight[j]!=0不等於0代表這個頂點還沒有被添加到結果集
//G.Edge[min_arc][j]<min_weight[j] 因爲結果集中加入了新頂點,所以最小的權值可能會發生改變
//所以我們需要修改輔助數組的權值
if(min_weight[j]!=0&&G.Edge[min_arc][j]<min_weight[j]){
min_weight[j]=G.Edge[min_arc][j];//如果加入的這條邊的權值比之前的權值小則替換之前的
adjvex[j]=min_arc;//同時修改adjvex數組,因爲新加入的點是通過該頂點達到的,所以修改其爲對應頂點的下標
}
}
}
}
1.4Kruskal
初始化:Vt=V,Et=空集,即是每個頂點構成一顆獨立的樹,T是一個僅含V個頂點的森林;
循環(直到T爲樹):按圖G的邊的權值遞增的順序依次從E-Et中選擇一條邊,若這條邊加入後不構成迴路,則將其加入Et,否則捨棄。
首先將這個n個頂點放到結果集,這n個頂點構成了n棵樹組成的森林,然後我們將圖G得邊得權值按照遞增的順序排序,接着從頭挑選權值最小的那條邊AC發現不構成迴路,則可以加入Et中,同樣我們繼續挑選BC這條邊不夠成迴路可以加入,接着是AB這條邊,但是它回構成迴路,不加入…最後形成最小生成樹
Kruskal
主要使用了堆排序對邊的權重進行排序,然後使用並查集(數組)這個數據挑選邊。代碼實現
代碼實現,時間複雜度:O(E*logE)
,適用與稀疏圖
typedef struct Edge{
int a,b;//改變兩個端點的下標
int weight;//權重
};
void MST_Kruskal(Graph G,Edge *edges,int *parent){//圖G、屬於該圖的邊的集合、並查集輔助變量
heap_sort(edges);//堆排序
Initial(parent);//初始化並查集
for(int i=0;i<G.arcnum;i++){//循環次數爲邊的數目
int a_root=Find(parent,edges[i].a);//求第一個端點所在並查集的根結點的下標
int b_root=Find(parent,edges[i].b);//求第而個端點所在並查集的根結點的下標
if(a_root!=b_root){//如果不相同
Union(parent,a_root,b_root);//併入結果集
}
}
}
2.最短路徑
我們之前學過無權圖單源最短路徑問題
,那是無權圖,我們找最短路徑只需找出邊最少的就行,但是這裏我們討論的是帶權的最短路徑,此時就不能之看邊了。
最短路徑:兩個頂點之前帶權路徑長度最短的路徑爲最短路徑
在帶權圖中,把一個頂點v到另一個頂點u所經歷的權值之和稱爲,路徑的帶權路徑長度
我們有兩種算法:Dijkstra
、Floyd
2.1Dijkstra(迪傑斯特拉)
Dijkstra(迪傑斯特拉)
:計算帶權圖單源最短路徑。我們需要以下幾個輔助數組:
s[]
標記已經計算完成的頂點:數組中的值全部初始化爲0(未計算完成),源點下標的值初始化爲1(計算完成)dist[]
記錄從源點v0到其他各頂點當前的
最短路徑長度:數組中的值初始化爲源點到各個頂點邊的權值,即dist[i]=arcs[0][i]
(我們這裏源點是0,所以從第0行取值初始化,這樣看源點的變化)path[]
記錄從最短路徑中頂點的前驅頂點
,即path[i]
爲v到vi最短路徑上vi的前驅頂點(最後我們不斷的求前驅繫結點組成的就是一條最短路徑):數組中值的初始化,若源點v0到該頂點vi有一條有向邊(無向邊),則令path[i]=0
,否則path[i]=-1
實現過程:
- 初始化數組,並集合S初始化未爲{0}(令源點爲v0)
- 從頂點集合V-S中選出Vj,滿足
dist[j]=Min{dist[i],vi∈V-S}
,Vj就是當前求得的最短路徑的終點,並令S=S∪{j} - 修改此時V0出發到集合V-S上任一頂點Vk的最短路徑的長度:若
dist[j]+arcs[j][k]<dist[k]
則令dist[k]=dist[j]+arcs[j][k];path[k]=j;
- 重複 2、3操作n-1次,直到S中包含全部頂點
舉個栗子:我們描述出下圖的Dijstra的算法執行過程
首先我們設置源點爲0,接着我們初始化三個輔助數組,其中dist[]存儲的是0到每一個頂點的權值因爲03不存在邊,所以存儲∞,s[0]=1表示0這個頂點已經計算完成,path[]初始化代表如果存在頂點0到另一個頂點有一條有向邊,則設置值爲頂點0的下標爲0,否則爲-1,初始化完成,開始執行具體的執行過程
第一輪:首先我們挑選dist[]中最小的值,我們知道是3,它對應的頂點爲2,所以我們挑選2這個頂點,挑選完成後我們需要修改dist[]數組,挑選2完成之後會帶來新的有向邊,我知道04的權值是8,但是加入2之後存在024這樣的一條有向邊它的權值是7,所以我們需要修改dist[4]=7,同時由於2頂點計算過了,所以我們修改s[2]=1,同時由於此時我們是通過2頂點到達的4頂點所以修改paht[4]=2
第二輪:依舊挑選最小的那個值5(0和3值對應的頂點已經計算過了),它是1頂點對應的值,加入1頂點後帶來了12和13邊,對應的我們要修改dist[]的權值,這裏我們只需修改從0到3的權值即可,因爲從0-1-2的權值爲7,而從0-2的權值爲3,7>3不需要修改。同時修改s[1]=1,然後修改paht[3]=1,因爲3頂點我們是通過1頂點抵達,權值爲下標爲1
第三輪:我們挑選到3這個頂點(0、3和5值對應的頂點已經計算過了),3這個頂點加入後會帶來32這條邊,所以但是2頂點已經計算完成了,所以不需要修改dist[]數組,然後我們修改s[3]=1,同樣path[]數組不需要修改
第四輪:只剩下值爲7這個頂點4未被挑選了,4頂點加入後爲未帶來新的邊,所以不修改dist[]數組,修改s[4]=1
最後遍歷完成,最後我們得到s[]數組值都爲1,表示所有頂點計算完成,同時dist[]數組代表頂點0到其他頂點的最短路徑長度,比如5代表了0-1的最短路徑長度。我們還知道path[]數組保存了最短路徑經歷的序列,那麼我們是怎麼通過path[]數組計算路徑序列的呢?比如我們計算0->1路徑的序列,它的長度爲5,序列:首先1對應的值爲0,說明它的前驅結點的下標爲0,paht[0]=-1,所以結束,序列爲:0->1;計算0->2路徑的序列,他的長度爲3,2對應的值爲0,他的前驅結點爲下標爲0,paht[0]=-1,所以結束,序列爲:0->2;計算0->3路徑的序列,他的長度爲6,3對應的值爲1,則說明它的前驅結點的下標爲1,path[1]=0,path[0]=-1,所以結束,序列爲:0->1->3;…
代碼實現,時間複雜第O(V^2)
注意:Dijkstra算法並不適用於含有負權值邊的圖
3.2Floyd(弗洛伊德算法)
Floyd(弗洛伊德算法):計算各個頂點之間
的最短路徑
也就是求任意兩個點之間的最短路徑。這個問題這也被稱爲“多源最短路徑”問題。
如上圖,我們使用Floyd算法,進行計算各個頂點之間的最最短路徑,首先進行初始化,兩個頂點之間有路徑的值爲權值,沒有路徑的值爲∞。
接着我們加入第一個頂點0頂點,我們隨意找一個一條邊比如A^(-1)[2][1]
,它的值爲∞,表示從2頂點到1頂點沒有邊,然後加入0頂點後,我我們知道從2頂點到0頂點也沒有邊值爲∞,從0頂點到1頂點有邊值爲1,∞+1還是∞,所以它的值A^(-1)[2][1]
的值不進行更改,按照這樣的方法將整個矩陣遍歷一遍得到下面的矩陣:
接着我們繼續加入頂點,加入頂點1,我們仍然按照上面的方法進行計算,得到A^(1)矩陣:
任然按照上面的方法得到A(2)和A(3)矩陣,其中A^(3)矩陣中的值就是各個頂點之間的最短路徑
代碼實現,時間複雜度:O(V^3)
整個算法的思想也可以參考整個博客 Floyd-傻子也能看懂的弗洛伊德算法(轉)
3.拓撲排序
如上圖,表示的就是我們只有學習了計算機基礎,才能學習程序語言,然後才能學習數據結構接着才能學習算法分析,我們如果按照發生順序對他們進行排序的話:計算機基礎-程序語言-數據結構-算法分析,得到的這個序列就是拓撲排序
序列。首先我們需要了解:
有向無環圖
:不存在環的有向圖,簡稱DAG圖。
AOV網
:若用一個.AG圖表示一個工程,其頂點表示活動,用有向邊<vi,vj>
表示活動 vi 先於活動 vj 進行的傳遞關係,則將這種DAG稱爲頂點表示活動網絡,記爲AOV網。(雖然它叫網,但是它的邊是沒有權值的)
拓撲排序
:對DAG所有頂點的一種排序,使若存在一條從頂點A到頂點B的路徑,在排序中B排在A的後面
算法思想
-
1)從DAG圖中選擇一個沒有前驅的頂點並輸出
-
2)從圖中刪除該頂點和所有以它爲起點的有向邊
-
3)重複1)、2)直到當前的DAG圖爲空或當前圖中不存在無前驅的頂點爲止。後一種情況說明圖中有環(則不是有向無環圖)。
我們知道每次我們刪除的是頂點入度爲0的頂點,所以我們用一個輔助數組來標記當前頂點的入度,然後找出值爲0的下標,我們發現是頂點0,所以我們刪除頂點0,輸出,並刪除0頂點對應的所有的出邊
然後我們要修改數組,,接着我們繼續找值爲0的下標,我們發現是1頂點,接着我們刪除1頂點,輸出,並刪除1頂點對應的所有的出邊
刪除之後我們依舊要修改數組,,繼續找值爲0的下標,我們發現是2頂點,然後我們刪除2頂點,輸出,並刪除2頂點對應的所有的出邊
同樣刪除之後修改數組,,最後還剩3頂點,同樣刪除,輸出。這樣我們就得到這個有向無環圖的拓撲排序序列:{0,1,2,3}。我們下面看一個有環的例子
同樣首先我們初始化一個數組,然後找到值爲0的頂點0,接着刪除頂點0,並輸出,並刪除頂點0對應的出邊
然後修改數組:,然後我們發現沒有頂點的入度爲0,即爲算法描述中第三個步驟當前圖中不存在無前驅的頂點,此時圖爲有環圖。
即:算法結束時沒有訪問所有頂點,則存在以剩下頂點組成的環。
然後我們繼續看一個例子:
首先我們初始化一個數組:,然後找出值爲0的頂點,找到了0頂點,然後刪除0頂點,輸出,並刪除0頂點對應的出邊
接着修改數組:,此時我們發現有兩個頂點的入度都爲0,此時選擇那個一個頂點先開始都可以,那麼到最後我們發現此有向無環圖有兩種拓撲排序的序列:{0,1,2,3} 或 {0,2,1,3}
即:拓撲排序的結果不一定唯一
代碼實現,時間複雜度:O(V+E)
拓撲排序的特點:
- 若鄰接矩陣爲三角矩陣,則存在拓撲排序;反之不一定成立
例如:,上三角矩陣,它如果是某一個有向圖的鄰接矩陣的話,則對應在改有向圖中一定滿足如下條件:Vi->Vj
中的i<j
,那麼在該有向圖中一定不存在環狀結構,那麼一定存在拓撲排序的排序序列,下三角矩陣同理。
反之不一定成立因爲比如:0->2,2->1,我們也可以得到一個拓撲排序序列,但是這個圖對應的鄰接矩陣不是一個三角矩陣。
4.關鍵路徑
上面我們講解了拓撲排序的AVO網
這裏我們講另外一種網AOE網
,使用每一個有向邊表示活動,權值常常代表一個活動所需要的金錢或者時間開銷等等,頂點表示的是事件
AOE網
:在有向帶權圖中,以頂點表示事件,以有向邊表示活動,以邊上權值表示完成該活動的開銷(如完成活動所需要的時間),則稱這種有向圖爲用邊表示活動的網絡,簡稱AOE網
上面講的AOV網是沒有權重的
AOE網
有以下特點
:
- 它始終
只有一個
頂點是完全沒有入邊的,我們稱爲源點,如上圖的V1
- 它始終
只有一個
頂點是完全沒有出邊的,我們稱爲匯點,如上圖的V6
頂點與入邊的關係:如上圖V4
頂點和入邊a3
、a5
,邊代表活動,點代表事件,只有a3
和a5
的活動都結束才能開始V4
頂點的事件(上圖V2
和V3
事件同時開始)
頂點與出邊的關係:如上圖V4
頂點和出邊a7
,表示只有V4
頂點的事件完成後才能進行a7
活動
關鍵路徑
:從原點到匯點最大路徑長度的路徑(權重和最大的路徑)稱爲關鍵路徑,關鍵路徑上的活動爲 關鍵活動
關鍵路徑爲什麼關鍵?我們知道例如一個工程,關鍵路徑上的關鍵活動直接決定的工程的完成事件,我們知道了關鍵路徑,我們就可以進行優化關鍵路徑,提升工程的完成效率等等
4.1關鍵路徑的計算
1、事件Vk
的最早發生時間Ve(k)
我們可以看一下圖中的頂點V4
,從V1
到頂點V4
(我們默認V1
耗時爲0),有兩條路徑:V1->V2->V4
;V1->V3->V4
;路徑1耗時3+2=5,路徑2耗時2+4=6;那麼事件V4
最早發生時間是?這時可能有人會說5小時,其實答案是6,因爲事件V4
的發生必須滿足a3
和a5
同時完成。
Ve(源點)=0; Ve(k)=Max{Ve(j)+Weight(Vj,Vk)}
Weight(Vj,Vk)
:頂點Vj
到當前頂入邊的權值
上圖各個頂點的最早發生時間
2、事件Vk
的最遲發生時間Vl(k)
解釋:我們在不推遲整個工程的完成時間的情況下,每個事件它最遲可以什麼事件開始
計算方式其實和最早發生事件是相反的,我們從匯點向源點。
Vl(匯點)=Ve(匯點);//令匯點的最遲發生時間等於匯點的最早發生時間,如果我們匯點的最遲發生時間大於匯點的最早發生時間那麼我們就等於推遲了工程的結束時間,所以只能相等
Vl(j)=Min{Vl(k)-Weight(Vj,Vk)}
我們計算最早發生時間使用了入邊,這裏我們使用出邊向源點進行逆推。
例如:Vl(2)
的最遲發生時間,我們可以使用Ve(4)-a3=4
和Ve(5)-a4=3
進行比較找出最小值
上圖各個頂點的最遲發發生時間
3、活動ai
的最早開始時間e(i)
我們知道每一個事件所需事件爲0,那麼每一個活動的最早開始時間就是整個活動的弧的弧頭這個事件的最早開始時間即
若存在<Vk,Vj>
表示活動ai
,則e(i)=Ve(k)
我們只需找到每個事件的最早開始事件,然後對應每個活動即可找到每個活動的最早開始時間
4、活動ai
的最遲開始時間l(j)
若存在<Vk,Vj>
表示活動ai
,則l(i)=Vl(j)-Weight(Vk,Vj)
我們只需找到每個事件的最遲開始時間,然後依次計算每個活動的最遲開始時間:例如a1
的最遲開始時間等於Vl2-a1的權重=4-3=1
5、活動ai
的差額d(i)=l(i)-e(i)
最遲發生時間減去最早發生時間等與活動的差額,我們先計算出每個活動的最遲發生時間和最早發生時間然後做個減法
我們通過活動的差額可以找到關鍵活動,其中值爲零的就是關鍵活動,值爲零說明此活動的最遲發生時間和最早發生時間相同,對於這些活動我們不能早開始也不能晚開始,都會影響整個工程的時間,這些活動當然就是工程的關鍵活動,我們利用這些關鍵活動可以組成關鍵路徑:V1->V3->V4->V6
,我們如果想優化整個工程就要優化整個工程的關鍵路徑上的關鍵活動。
注意:縮短關鍵活動時間可以加快整個工程,但縮短到一定大小時關鍵路徑會發生改變
例子:
在該AOE網
中有以下關鍵活動路徑:{a2,a4,a3,a7};{a2,a4,a5,a8};{a2,a6,a8}
由此可以看出不是所有的AOE網
只有一條關鍵路徑,其實AOE網
可能有多條關鍵路徑。那麼就出現了一個問題,如果我們想要縮減整個工程的時間的話,如果只縮減一條關鍵路徑上的關鍵活動就可以嗎?答案是可能可以,如果該關鍵活動包含在所有的關鍵活動中。就如上圖,如果我們縮減a2
即可達到縮減整個工程的目的。但是如果我們縮減a5
,不能改變整個工程的時間。
即:當網中關鍵路徑不唯一時,只有加快的關鍵活動或關鍵活動組合包括在所有的關鍵路徑上才能縮短工期。
7.理木客
數據結構相關知識,公衆號理木客同步更新中,歡迎關注