【PAT】4. 圖
【PAT】4. 圖
圖的儲存
鄰接矩陣:設圖G(V,E)的頂點編號為0,1,…,N-1,則如果G[i][j]為0,說明頂點i和頂點j之前不存在邊。鄰接矩陣只適用於頂點數目不太大(一般不超過1000)的題目
鄰接表:如果把同一個頂點的所有出邊放在一個列表中,那麼N個頂點就會有N個列表(沒有出邊,則對應空表),這N個列表被稱為圖G的鄰接表。用vector來實現鄰接表。
//vector<int> Adj[N]; //鄰接表只存放每條邊的終點編號而不存放邊權。
struct Node{
int v; //邊的終點編號
int w; //邊權
Node(int _v, int _w) : v(_v), w(_w) {} //建構函式
};
vector<Node> Adj[N];
Adj[1].push_back(Node(3,4)); //建構函式可以不定義臨時變數來實現加邊操作
圖的遍歷
DFS遍歷圖的基本思路就是將經過的頂點設定為已訪問,在下次遞迴碰到這個結點時就不再去處理,直到整個圖的頂點都被標記為已訪問。如果已知給定的圖是一個連通圖,則只需要一次DFS就能完成遍歷。
DFS(u){ //訪問頂點u
vis[u] = true; //設定u已被訪問
for(從u出發能到達的所有頂點v){ //列舉從u出發可以到達的所有頂點v
if(vis[v] == false){ //如果v未被訪問
DFS(v); //遞迴訪問v
}
}
}
DFSTrave(G){ //遍歷圖G
for(G的所有頂點u){ //對圖G的所有頂點u
if(vis[u] == false){ //如果u未被訪問
DFS(u); //訪問u所在的連通塊
}
}
}
BFS遍歷圖的基本思想是建立一個佇列,並把從該頂點出發可以到達的未曾加入過佇列(而不是訪問)的頂點全部加入佇列,直到佇列為空
bool inq[maxn] = {false}
BFS(u){ // 遍歷u所在的連通塊
queue q; //定義佇列q
將u入隊;
inq[u] = true; //設定u已被加入過佇列
while(q非空){ //只要佇列非空
取出q的隊首元素u進行訪問;
for(從u出發可達的所有頂點v){ //列舉從u能直接到達的頂點v
if(inq[v] == false){ //如果v未曾加入過佇列
將v入隊;
inq[v] = true; //設定v已被加入過佇列
}
}
}
}
BFSTrave(G){ //遍歷圖G
for(G的所有頂點u){ //列舉G的所有頂點
if(inq[u] == false){ //如果u未曾加入過佇列
BFS(u); //遍歷u所在的連通塊
}
}
}
最短路徑
對任意給出的圖G(V,E)和起點S、終點T,如何求從S到T的最短路徑
Dijkstra
解決單源最短路問題,即給定圖G(V,E)和起點s,求從起點s到達其它頂點的最短距離
設集合S存放已被訪問的結點(即已經確定該結點的最短距離),然後執行n次下面的步驟(n為結點個數)
- 每次從集合V-S(即未確定最短距離)中選擇與起點s的最短距離最小的一個頂點(記為u),訪問並加入集合S(設定其距離已經被確定)
- 之後,令u為中介點,優化起點s與所有從u能到達的頂點v之間的最短距離
集合S的實現、起點s到達頂點 V i V_i Vi(0<=i<=n-1)的最短距離的實現
- 集合S可以用一個bool型陣列vis[]來實現,vis[i] = true表示頂點 V i V_i Vi已經被訪問
- 令int型陣列d[]表示起點s到達頂點 V i V_i Vi的最短距離,初始時除了起點s的d[s]賦為0,其餘頂點都賦為一個很大的數(0x3fffffff)。
//G為圖,一般設定為全域性變數;陣列d為源點到達各點的最短路徑長度,s為起點
Dijkstra(G,d[],s){
初始化;
for(迴圈n次){
u = 使d[u]最小的還未被訪問的頂點的標號;
設u已被訪問;
for(從u出發能到達的所有頂點v){
if(v未被訪問&&以u為中介點使s到頂點v的最短距離d[v]更優){
優化d[v];
}
}
}
}
- 臨界矩陣的時間複雜度 O ( V 2 ) O(V^2) O(V2),鄰接表的時間複雜度 O ( V 2 + E ) O(V^2+E) O(V2+E)。注意到尋找最小d[u]的過程可以不必達到O(V)的複雜度,而可以使用堆優化來降低複雜度。使用STL中的優先佇列使得鄰接表實現的演算法時間複雜度降為 O ( V L o g V + E ) O(VLogV+E) O(VLogV+E)
- Dijkstra演算法只能應對所有邊權都是非負數的情況,如果出現負數最好使用SPFA演算法
- 如果題目給出的是無向邊,只需要把無向邊當成兩條指向相反的有向邊即可,在輸入的時候進行處理。
求解從起點 V 1 V_1 V1到達 V i V_i Vi最短路徑:設定陣列pre[],令pre[v]表示從起點到頂點v的最短路徑上v的前一個結點,當條件成立時將u賦給pre[v]
if(v未被訪問&&以u為中介點使s到頂點v的最短距離d[v]更優){
優化d[v];
令v的前驅為u;
}
然後用遞迴不斷利用pre[]的資訊尋找前驅,直至到達起點後從遞迴深處開始輸出。
如果碰到有兩條即兩條以上可以達到最短距離的路徑,題目會給出一個第二標尺(第一標尺是距離),要求在所有最短路徑中選擇第二標尺最優的一條路徑
- 給每條邊再增加一個邊權(比如說花費),然後要求再最短路徑有多條時要求路徑上的花費之和最小
- 給每個點增加一個點權(例如每個城市能蒐集到的物資),然後在最短路徑有多條時要求路徑上的點權之和最大
- 直接問有多少條最短路徑
對這三種出題方法,都只需要增加一個陣列來存放新增的邊權或點券或最短路徑條數,然後在演算法中修改**優化d[v]**的那個步驟即可
- 新增邊權。以新增的邊權代表花費為例,用cost[u][v]表示u->的花費(由題目輸入),並增加一個陣列c[],令從起點s到達頂點u的最少花費為c[u],初始化時只有c[s] = 0、其餘c[u]均為INF。這樣在
d[u] + G[u][v] < d[v]
(即可以使s到v的最短距離d[v]更優)時更新d[v]和c[v],當d[u] + G[u][v] == d[v]
(即最短距離相同)且c[u]+cost[u][v]<c[v]
(即可以使s到v的最少花費更優)時更新c[v],c[v]=c[u]+cost[u][v]
。 - 新增點權。以新增的點權代表城市中能收集到的物資為例,用weight[u]表示城市u中的物資數目(由題目輸入),並增加一個陣列w[],令從起點s到達頂點u可以收集到的最大物資為w[u],初始化時w[s]為weight[s],其餘均為0。更新方法同上
- 求最短路徑條數。只需要增加一個陣列num[],令從起點s到達頂點u的最短路徑條數為num[u],初始化時只有num[s]為1、其餘num[n]均為0。更新方法與上面稍有不同,在
d[u] + G[u][v] < d[v]
(即可以使s到v的最短距離d[v]更優)時更新d[v],並讓num[v]繼承numu,而當d[u] + G[u][v] == d[v]
(即最短距離相同)時將num[u]加到num[v]上((num[v]+=num[u]))。
for (int v = 0; v < n; v++){ //遍歷所有頂點v
//如果v未被訪問 && u能到達v
if(vis[v] == false && G[u][v] != INF){
if(d[u] + G[u][v] < d[v]){ //以u為中介點可以使d[v]更優
d[v] = d[u] + G[u][v];
更新第二標尺;
}else if(d[u] + G[u][v] == d[v] && 第二標尺可以更優){
更新第二標尺;
}
}
}
DFS+Dijkstra
如果出現了一些邏輯更為複雜的計算邊權或點權的方式,此時按上面的方式只使用Dijkstra演算法就不一定能計算出正確結果(原因時不一定滿足最優子結構),或者即使能算出,邏輯也極其複雜,容易寫錯。可以用一種更通用、又模板化的方式解決此類問題——Dijkstra + DFS。
先在Dijkstra演算法中記錄下所有最短路徑(只考慮距離),然後從這些最短路徑中選出一條第二標尺最優的路徑(因為在給定一一條路徑的情況下,針對這條路徑的資訊都可以通過邊權和點權很容易計算出來!)
- 使用Dijkstra演算法記錄所有最短路徑
- 定義變長陣列pre,存放結點v的所有能產生最短路徑的前驅結點
vector<int> pre[maxv]
(注:對需要查詢某個頂點u是否在頂點v前驅中的題目,可以把pre陣列設定為set<int>陣列,此時使用pre[v].count(u)來查詢比較方便),通過vector型別的pre陣列,就可以使用DFS來獲取所有最短路徑。 - 在更新d[v]的過程中更新pre陣列
- 如果
d[u] + G[u][v] < d[v]
,說明以u為中介點可以使d[v]更優,此時需要令v的前驅結點為u。即清空原先的pre[v],再新增u。 - 如果
d[u] + G[u][v] == d[v]
,說明以u為中介點可以找到一條相同距離的路徑,因此v的前驅結點在原來的基礎上新增上u結點(不必先清空pre[v])。
- 如果
- 定義變長陣列pre,存放結點v的所有能產生最短路徑的前驅結點
- 遍歷所有最短路徑,找出一條使第二標尺最優的路徑
- pre陣列會產生一條遞迴樹,遍歷這條樹,每次到達葉子結點,就會產生一條完整的最短路徑,因此每得到一條完整路徑,就可以對這條路徑計算其第二標尺的值,令其與當前第二標尺的最優值進行比較。如果更優則更新最優值,並用這條路徑覆蓋當前的最優路徑。
- DFS遞迴函式
- 全域性變數的第二標尺最優值
optValue
,記錄最優路徑的陣列path
,臨時記錄DFS遍歷到葉子結點時的路徑tempPath
- 遞迴邊界:當前訪問的結點時葉子結點(路徑的起點st)。此時對tempPath中存放的路徑求出第二標尺的值value,並與optValue比較,如果更優,則更新optValue並把tempPath覆蓋path。
- 遞迴式(在遞迴過程中生成tempPath):在訪問當前結點v時將v加到tempPath的最後面,然後遍歷pre[v]中的所有結點並進行遞迴,等pre[v]的所有結點遍歷完畢後再把tempPath最後面的v彈出。注意葉子結點沒有辦法通過上面寫法直接加入tempPath,需要在訪問到葉子結點時臨時加入(在遞迴邊界中加入)。
- 存放在tempPath中的路徑結點時逆序的,訪問結點需要倒著進行。
- 全域性變數的第二標尺最優值
- 如果需要同時計算最短路徑(指距離最短)的條數,既可以在Dijkstra程式碼中新增num陣列,也可以開一個全域性變數來記錄最短路徑條數,當DFS到達葉子結點時令該全域性變數加1
- 注意頂點的下標範圍需要根據題意來考慮是n~1還是1~n,或是在某些有n+1個結點的題目裡是0~n
Bellman-Ford演算法和SPFA演算法
如果出現了負權邊,Dijkstra就會失效,而Bellman-Ford演算法可解決單源最短路徑問題,但也能處理有負邊權的情況。因為如果圖中有負環,且從源點可以到達,那麼就會影響最短路徑的求解。
Bellman-Ford演算法設定一個陣列d,用來存放從源點到各個頂點的最短距離。同時Bellman-Ford演算法返回一個bool值:如果其存在從源點可達的負環,那麼函式將返回false;否則,函式將返回true,此時陣列d中存放的值就是從源點到達各頂點的最短距離。
- 對圖中的邊進行V-1輪操作,每輪都遍歷圖中所有的邊:對每條邊
u->v
,如果以u為中介點可以使d[v]更小,即d[u] + length[u->u] < d[v]
成立時,用d[u] + length[u->u]更新d[v](鬆弛操作)。時間複雜度是O(VE)
,n(V)是頂點個數,E是邊數。 - 再對所有邊進行一輪操作,判斷是否有某條邊u->v仍然滿足
d[u] + length[u->u] < d[v]
,如果有,說明圖中有從源點可達的負環,返回false;否則,說明陣列d中的所有值都已經達到最優,返回true。for(i = 0; i < n - 1; i++){ //執行n-1輪操作,其中n為頂點數 for(each edge u->v){ //每輪操作都遍歷所有邊 if(d[u] + length[u->v] < d[v]){ //以u為中介點可以使d[v]更小 d[v] = d[u] + length[u->v]; //鬆弛操作 } } } for(each edge u->v){ //對每條邊進行判斷 if(d[u] + length[u->v] < d[v]){ //如果仍可以被鬆弛 return false; //說明圖中有從源點可達的負環 } } return true; //陣列d中的所有值都已經達到最優
- Bellman-Ford演算法需要遍歷所有邊,使用鄰接表會比較方便;如果使用鄰接矩陣,時間複雜度會上升到 O ( V 3 ) O(V^3) O(V3)
- 如果在某一輪操作時,發現所有邊搜沒有被鬆弛,說明陣列d中的所有值都已經達到最優,不需要再繼續,提前退出即可。
- 最短路徑的求解方法、有多重標尺時的做法均與Dijkstra演算法中介紹的相同。唯一要注意的是統計最短路徑條數的做法:BF演算法期間會多次訪問曾經訪問過的頂點,因此需要設定記錄前驅的陣列
set<int> pre[maxn]
,且當遇到一條和已有最短路徑長度相同的路徑時,必須重新計算最短路徑長度。int num[maxv]; //num[]記錄最短路徑條數 set<int> pre[maxv]; ... if(d[u] + dis < d[v]){ //以u為中介點時能令d[u]變小 ... num[v] = num[u]; //和Dijkstra一樣覆蓋 pre[v].clear(); pre[v].insert(u); }else if(d[u] + dis == d[v]){ //找到一條相同長度的路徑 ... pre[v].insert(u); num[v] = 0; //重新統計num[v] for(set<int>::iterator it = pre[v].begin(); it != pre[v].end(); it++){ num[v] += num[*it]; } }
注意到,BF演算法的每輪操作都需要操作所有邊,這其中大量無意義的操作嚴重影響了演算法的效能,注意到,只有當某個頂點u的d[u]值改變時,從它出發的邊的臨界點v的d[v]值才有可能被改變。由此可以優化:建立一個佇列,每次將隊首頂點u取出,然後對從u出發的所有邊u->v進行鬆弛操作,也就是判斷d[u] + length[u->u] < d[v]
是否成立,如果成立,則覆蓋,於是d[v]獲得更優的值,此時如果v不在佇列中,就把v加入佇列。這樣操作直到佇列為空(說明圖中沒有從源點可達的負環),或是某個頂點的入隊次數超過V-1(說明圖中存在從源點可達的負環)
queue<int> q;
源點s入隊;
while(佇列非空){
取出隊首元素u;
for(u的所有鄰接邊u->v){
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
if(v不在當前佇列){
v入隊;
if(v入隊次數大於n-1){
說明有可達負環,return;
}
}
}
}
}
優化後的演算法稱為SPFA(Shortest Path Faster Algorithm),期望時間複雜度是 O ( k E ) O(kE) O(kE),E是圖的邊數,k是一個常數,很多情況下k不超過2,此演算法異常高效,並且經常性地優於堆優化的Dijkstra演算法。但如果圖中有從源點可達的負環,傳統SFPA的時間複雜度就會退化成O(VE)。
- 使用SFPA可以判斷是否存在從源點可達的負環,但如果負環從源點不可達,則需要新增一個輔助頂點C,並新增一條從源點到達C的有向邊以及V-1條從C到達除源點外各頂點的有向邊才能判斷負環是否存在。
- SPFA十分靈活,其內部的寫法可以根據具體場景的不同進行調整,示例程式碼中的佇列可以替換成優先佇列以加快速度;或者替換成雙端佇列,使用SLF優化和LLL優化
- 示例程式碼是SPFA的BFS版本,如果將佇列替換成棧,則可以實現DFS版本的SPFA,對判環有奇效。
Floyd演算法(弗洛伊德演算法)
用來解決全源最短路問題,即對給定的圖G(V,E),求任意兩點u,v之間的最短路徑長度,時間複雜度是 O ( n 3 ) O(n^3) O(n3),頂點數n限制在200內,因此適合用鄰接矩陣來實現Floyd演算法。
演算法基於這樣一個事實:如果存在頂點k,使得以k作為中介點時頂點i和頂點j的當前最短距離縮短,則使用頂點k作為頂點i和頂點j的中介點,即當dis[i][k]+dis[k][j]<dis[i][j]
時,令dis[i][j]=dis[i][k]+dis[k][j]
(其中dis[i][j]表示從頂點i到頂點j的最短距離)。
列舉頂點k∈[1,n]
以頂點k作為中介點,列舉所有頂點對i和j(i∈[1,n], j∈[1,n])
如果dis[i][k] + dis[k][j] < dis[i][j]成立
賦值dis[i][j] = dis[i][k] + dis[k][j]
需要注意的是,不能把最外層的k迴圈放到內層(即產生i->j->k的三種迴圈),會導致最後結果出錯,因為如果當較後訪問的dis[u][v]有了優化之後,前面訪問的dis[i][j]會因為已經被訪問而無法獲得進一步優化(這裡i、j先於u、v進行訪問)
最小生成樹
最小生成樹(Minimum Spanning Tree, MST)是在一個給定的無向圖G(V,E)中求一棵樹,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權之和最小。
- 最小生成樹是樹,其邊數等於頂點數減1,且樹內一定不會有環
- 對給定的圖G(V,E),其最小生成樹可以不唯一,但其邊權之和一定是唯一的
prim演算法
Dijkstra演算法和prim演算法實際上是相同的思路,只不過是陣列d[]的含義不同罷了。
prim演算法的基本思想是對圖G(V,E)設定集合S來存放已被訪問的頂點,然後執行n此下面的兩個步驟(n為頂點個數)
- 每次從集合V-S中選擇與集合S最近的一個頂點(記為u),訪問u並將其加入集合S,同時把這條離集合S最近的邊加入最小生成樹中
- 令頂點u作為集合S與集合V-S連線的介面,優化從u能到達的未訪問頂點v與集合S的最短距離
和Dijkstra演算法一樣,時間複雜度為 O ( V 2 ) O(V^2) O(V2),其實鄰接表實現的prim演算法可以通過堆優化使時間複雜度降為O(VlogV+E)。另外 O ( V 2 ) O(V^2) O(V2)的時間複雜度也說明,儘量在圖的頂點數目少而邊數較多的情況下(即稠密圖)使用prim演算法。
kruskal演算法
採用了“邊貪心”的策略,基本思想為:在初始狀態時隱去圖中的所有邊,這樣圖中每個頂點都自成一個連通塊。之後執行下面的步驟:
-
對所有邊按邊權從小到大進行排序
-
按邊權從小到大測試所有邊,如果當前測試邊所連線的兩個頂點不在同一個連通塊中,則把這條測試邊加入當前最小生成樹中;否則,將邊捨棄。
-
執行步驟2,直到最小生成樹中的邊數等於總頂點數減1或是測試完所有邊時結束。而當結束時如果最小生成樹的邊數小於總頂點數減1,說明該圖不連通。
struct edge{ int u, v;//邊的兩個端點編號 int cost;//邊權 }E[maxe];//最多有maxe條邊 bool cmp(edge a, edge b){//讓陣列E按邊權從大到小排序 return a.cost < b.cost; } int kruskal(){ 令最小生成樹的邊權之和為ans、最小生成樹的當前邊數Num_Edge; 將所有邊按邊權從小到大排序; for(從小到大列舉所有邊){ if(當前測試邊的兩個端點在不同的連通塊中){ 將該測試邊加入最小生成樹中; ans += 測試邊的邊權; 最小生成樹的當前邊數Num_Edge加1; 當邊數Num_Edge等於頂點數減1時結束迴圈; } } return ans; }
有兩個問題
- 如何判斷測試邊的兩個端點是否在不同的連通塊中
- 如何將測試邊加入最小生成樹中
把每個連通塊當作一個集合,問題轉換為判斷兩個端點是否在同一個集合中——並查集,通過並查集的查詢操作來解決第一個問題;把測試邊的兩個端點所在集合合併,就能達到將邊加入最小生成樹的效果。
時間複雜度是 O ( E l o g E ) O(ElogE) O(ElogE),其中E為圖的邊數。kruskal適合頂點數較多,邊數較少的情況(稀疏圖)。
拓撲排序
拓撲排序時將有向無環圖G的所有頂點排成一個線性序列,使得對圖G中的任意兩個頂點u、v,如果存在邊u->v,那麼在序列中u一定在v前面。這個序列又叫拓撲序列。
原理:如果某一門課沒有先導課程或是所有先導課程都已經學習完畢,那麼這門課就可以學習了。如果有多門這樣的課,那它們的學習順序任意。對應到圖中的求解方法:
- 定義一個佇列q,並把所有入度為0的結點加入佇列。
- 取隊首結點,輸出。然後刪除所有從它出發的邊,並令這些邊到達的頂點的入度減1,如果某個頂點的入度減為0,則將其加入佇列
- 反覆進行步驟2操作,直到佇列為空。如果佇列為空時入過隊的結點數目恰好為N,說明拓撲排序成功,圖G為有向無環圖;否則,拓撲排序失敗,圖G中有環。
用鄰接表實現拓撲排序,額外建立一個陣列inDegree[maxv]來記錄結點的入度,並在程式一開始讀入圖時記錄好每個結點的入度。
拓撲排序很重要的應用是判斷一個給定的圖是否是有向無環圖。如果返回true,說明是有向無環圖。
注意,如果要求有多個入度為0的頂點,選擇編號最小的頂點,那麼把queue改成priority_queue,並保持隊首元素(堆頂元素)是優先佇列中最小的元素即可(用set也可以)。
關鍵路徑
- 頂點活動(Activity On Vertex, AOV)網是指用頂點表示活動,而用邊集表示活動間優先關係的有向圖。
- 邊活動(Activity On Edge, AOE)網是指用帶權的邊集表示活動,而用頂點表示事件的有向圖,其中邊權表示完成活動需要的時間
求解有向無環圖(DAG)最長路徑(關鍵路徑)的方法:先求點,再夾邊。
- 由於關鍵活動是那些不允許拖延的活動,因此這些活動的最早開始時間必須等於最遲開始時間。因此可以設定陣列e和l,其中
e[r]
和l[r]
分別表示活動 a r a_r ar的最早開始時間和最遲開始時間。於是,求出這兩個陣列後,就可以通過判斷e[r]==l[r]是否成立來確定活動r是否是關鍵活動。 - 求解陣列e和l:事件(頂點)
V
i
V_i
Vi經過活動(邊)
a
r
a_r
ar之後到達事件
V
j
V_j
Vj,事件的最早發生時間可以理解成舊活動的最早結束時間,事件的最遲發生時間可以理解成新活動的最遲開始事件。設定陣列ve和vl,其中
ve[i]
和vl[i]
分別表示事件i的最早發生時間和最遲發生時間,然後就可以將求解e[r]
和l[r]
轉換成求解這兩個新的陣列。- 對活動(邊)
a
r
a_r
ar來說,只要在事件(頂點)
V
i
V_i
Vi最早發生時馬上開始,就可以使得活動
a
r
a_r
ar的開始時間最早,因此
e[r]=ve[i]
。 - 如果
l[r]
是活動 a r a_r ar的最遲發生時間,那麼l[r]+length[r]
就是事件 V j V_j Vj的最遲發生時間(length[r]表示活動 a r a_r ar的邊權)。因此l[r] = vl[j] - length[r]
。
- 對活動(邊)
a
r
a_r
ar來說,只要在事件(頂點)
V
i
V_i
Vi最早發生時馬上開始,就可以使得活動
a
r
a_r
ar的開始時間最早,因此
步驟總結:
- 按拓撲排序和逆拓排序分別計算各頂點(時間)的最早發生時間和最遲發生時間
- 用上面的結果計算各邊(活動)的最早發生時間和最遲發生時間
e[i->j]=l[1->j]
的活動即為關鍵活動
相關文章
- [數字影像學筆記] 4.直方圖變換2筆記直方圖
- 4.運算子
- 4.陣列陣列
- PAT A1025 PAT Ranking(sort部分排名)
- PAT-B 1085 PAT單位排行【模擬】
- 4.部署kuberntes node
- 4.流程控制
- 4. 決策樹
- 4. laravel 路由(1)Laravel路由
- 4.安裝MySQLMySql
- PAT A1101
- PAT A1041
- PAT A1054
- PAT 乙級
- 多少個PAT
- 4.結構型模式模式
- 4.資料型別資料型別
- 4. PHP 函式 strrchr ()PHP函式
- 4. 使用webpack打包TSWeb
- 4.編寫規則
- 4. render, redirect, HttpResponse, reverseHTTP
- PAT乙級1023
- 【Java】若依(ruoyi)——4.部署Java
- 實驗4.浮動路由路由
- Welcome to YARP - 4.限流 (Rate Limiting)MIT
- 4.《python自省指南》學習Python
- LeetCode 4. Median of Two Sorted ArraysLeetCode
- PAT甲級1032 Sharing
- pat-554. 磚牆
- 【PAT】1006 Sign In and Sign Out
- PAT Advanced 1004 Counting Leaves
- PAT-A1119 題解
- PAT-B 1046 划拳
- 【PAT_1062】To Buy or Not to Buy
- PAT-A Java實現Java
- 4. Spring對IoC的實現Spring
- 4. CSS 背景和滑鼠屬性CSS
- 4. union-find演算法演算法