1 鏈式前向星
1.1 簡介
鏈式前向星可用於儲存圖,本質上是一個靜態連結串列。
一般來說,儲存圖常見的兩種方式為:
- 鄰接矩陣
- 鄰接表
鄰接表的實現一般使用陣列實現,而鏈式前向星就是使用連結串列實現的鄰接表。
1.2 出處
出處可參考此處。
2 原理
鏈式前向星有兩個核心陣列:
pre
陣列:儲存的是邊的前向連結關係last
陣列:儲存的是某個點最後一次出現的邊的下標
感覺雲裡霧裡對吧,可以看看下面的詳細解釋。
2.1 pre
陣列
pre
陣列儲存的是一個鏈式的邊的前向關係,下標以及取值如下:
pre
陣列的下標:邊的下標pre
陣列的值:前向邊的下標,如果沒有前向邊,取值-1
這裡的前向邊是指,如果某個點,作為起始點,已經出現過邊x
,那麼,遍歷到以該點作為起始點的下一條邊y
時,邊y
的前向邊就是邊x
。
更新pre
陣列的時候,會遍歷每一條邊,更新該邊對應的前向邊。
比如,輸入的有向邊如下:
n=6 // 頂點數
[[0,1],[1,3],[3,5],[2,4],[2,3],[0,5],[0,3],[3,4]] // 邊
那麼:
- 對於第一條邊,下標為
0
,那麼會更新pre[0]
的值,邊為0->1
,而起始點點0
還沒有出現過前向邊,那麼pre[0]=-1
。這樣就建立了邊0->-1
的一個連結關係,也就是說,對於起始點點0
,它只有邊0
這一條邊 - 對於第二條邊,下標為
1
,那麼會更新pre[1]
的值,邊為1->3
,而起始點點1
還沒有出現過前向邊,那麼pre[1]=-1
。這樣就建立了邊1->-1
的一個連結關係,也就是說,對於起始點點1
,它只有邊1
這一條邊 - 對於第三條邊,下標為
2
,那麼會更新pre[2]
的值,邊為3->5
,而起始點點3
還沒有出現過前向邊,那麼pre[2]=-1
。這樣就建立了邊2->-1
的一個連結關係,也就是說,對於起始點點3
,它只有邊2
這一條邊 - 對於第四條邊,下標為
3
,那麼會更新pre[3]
的值,邊為2->4
,而起始點點2
還沒有出現過前向邊,那麼pre[3]=-1
。這樣就建立了邊3->-1
的一個連結關係,也就是說,對於起始點點2
,它只有邊3
這一條邊 - 對於第五條邊,下標為
4
,那麼會更新pre[4]
的值,邊為2->3
,而起始點點2
,已經出現過一條邊了,該邊的下標是3
,也就是前向邊為3
,那麼就會更新pre[4]
為前向邊的值,也就是pre[4]=3
。這樣,就建立了邊4->3->-1
的一個連結關係,也就是對於起始點點2
來說,目前有兩條邊,一條是邊4
,一條是邊3
- 對於第六條邊,下標為
5
,那麼會更新pre[5]
的值,邊為0->5
,而起始點點0
,已經出現過一條邊了,該邊的下標是邊0
,也就是前向邊為0
,那麼就會更新pre[5]
為前向邊的值,也就是pre[5]=0
。這樣,就建立了邊5->0->-1
的一個連結關係,也就是對於起始點點0
來說,目前有兩條邊,一條是邊5
,一條是邊0
- 對於第七條邊,下標為
6
,那麼會更新pre[6]
的值,邊為0->3
,而起始點點0
,已經出現過不止一條邊了,最後一次出現的邊為邊5
,也就是前向邊為5
,那麼就會更新pre[6]
為前向邊的值,也就是pre[6]=5
。這樣,就建立了邊6->5->0->-1
的一個連結關係,也就是對於起始點點0
來說,已經有三條邊了,一條是邊6
,一條是邊5
,一條是邊0
- 對於第八條邊,下標為
7
,那麼會更新pre[7]
的值,邊為3->4
,而起始點點3
,已經出現過一條邊了,該邊的下標是邊2
,也就是前向邊為2
,那麼就會更新pre[7]
為前向邊的值,也就是pre[7]=2
。這樣,就建立了邊7->2->-1
的一個連結關係,也就是對於起始點點3
來說,目前有兩條邊,一條是邊7
,一條是邊2
這樣,邊的連結關係就建立下來了:
點 邊的連結關係(邊的下標)
0 6->5->0->-1
1 1->-1
2 4->3->-1
3 7->2->-1
4 -1
5 -1
2.2 last
陣列
last
陣列儲存的是最後一次出現的前向邊的下標,下標以及取值如下:
last
陣列的下標:點last
陣列的值:最後一次出現的前向邊的下標
last
陣列會將所有值初始化為-1
,表示所有的點在沒有遍歷前都是沒有前向邊的。
使用上面的資料舉例:
n=6 // 頂點數
[[0,1],[1,3],[3,5],[2,4],[2,3],[0,5],[0,3],[3,4]] // 邊
last
陣列會與pre
陣列一起在遍歷邊的時候更新:
- 遍歷到第一條邊:下標為
0
,邊為0->1
,那麼會更新以0
為起始點的前向邊的值,也就是自己,last[0]=0
。然後,如果下一次遍歷到了以0
為起始點的邊,比如0->5
,那麼0->5
的前向邊就是邊0
,而邊0
就儲存在last[0]
中,下次需要的時候直接取last[0]
即可 - 遍歷到第二條邊:下標為
1
,邊為1->3
,那麼會更新以1
為起始點的最後一次出現的前向邊的值,也就是last[1]=1
- 遍歷到第三條邊:下標為
2
,邊為3->5
,那麼會更新以3
為起始點的最後一次出現的前向邊的值,也就是last[3]=2
- 遍歷到第四條邊:下標為
3
,邊為2->4
,那麼會更新以2
為起始點的最後一次出現的前向邊的值,也就是last[2]=3
- 遍歷到第五條邊:下標為
4
,邊為2->3
,那麼會更新以2
為起始點的最後一次出現的前向邊的值,也就是last[2]=4
- 遍歷到第六條邊:下標為
5
,邊為0->5
,那麼會更新以0
為起始點的最後一次出現的前向邊的值,也就是last[0]=5
- 遍歷到第七條邊:下標為
6
,邊為0->3
,那麼會更新以0
為起始點的最後一次出現的前向邊的值,也就是last[0]=6
- 遍歷到第八條邊:下標為
7
,邊為3->4
,那麼會更新以3
為起始點的最後一次出現的前向邊的值,也就是last[3]=7
在遍歷每條邊的時候,會先從last
陣列取值並賦給pre
去生成連結關係,然後更新last
陣列中對應起始點的值為當前的邊的下標。
3 程式碼
3.1 生成陣列
生成last
以及pre
陣列:
public class Solution {
private int[] pre;
private int[] last;
private void buildGraph(int n, int[][] edge) {
int edgeCount = edge.length;
pre = new int[edgeCount];
last = new int[n];
Arrays.fill(last, -1);
for (int i = 0; i < edgeCount; i++) {
int v0 = edge[i][0];
pre[i] = last[v0];
last[v0] = i;
}
}
}
pre
的範圍與邊數有關,而last
的範圍與點數有關。一開始需要初始化last
陣列為-1
,然後遍歷每一條邊:
- 遍歷邊時僅需要知道起始點即可,因為終點可以透過邊的下標獲取到,不需要儲存
- 遍歷時首先更新
pre
陣列為最後一次出現的前向邊的下標,也就是對應起始點的last
陣列的值 - 最後更新
last
陣列,對應起始點的值更新為當前邊的下標
3.2 遍歷
public class Solution {
private int[] pre;
private int[] last;
private void visit(int n, int[][] edge) {
for (int i = 0; i < n; i++) {
System.out.println("當前頂點:" + i);
for (int lastEdge = last[i]; lastEdge != -1; lastEdge = pre[lastEdge]) {
System.out.println(edge[lastEdge][0] + "->" + edge[lastEdge][1]);
}
}
}
}
遍歷從點開始,首先透過last
陣列取得最後一條出現的前向邊的下標,然後遍歷該邊,最後透過pre
陣列更新前向邊,也就是對連結關係進行遍歷。
3.3 完整測試程式碼
import java.util.Arrays;
public class Solution {
private int[] pre;
private int[] last;
private void buildGraph(int n, int[][] edge) {
int edgeCount = edge.length;
pre = new int[edgeCount];
last = new int[n];
Arrays.fill(last, -1);
for (int i = 0; i < edgeCount; i++) {
int v0 = edge[i][0];
pre[i] = last[v0];
last[v0] = i;
}
}
private void visit(int n, int[][] edge) {
for (int i = 0; i < n; i++) {
System.out.println("當前頂點:" + i);
for (int lastEdge = last[i]; lastEdge != -1; lastEdge = pre[lastEdge]) {
System.out.println(edge[lastEdge][0] + "->" + edge[lastEdge][1]);
}
}
}
public void build() {
int n = 6;
int[][] edge = {{0, 1}, {1, 3}, {3, 5}, {2, 4}, {2, 3}, {0, 5}, {0, 3}, {3, 4}};
buildGraph(n, edge);
visit(n, edge);
}
}
輸出:
當前頂點:0
0->3
0->5
0->1
當前頂點:1
1->3
當前頂點:2
2->3
2->4
當前頂點:3
3->4
3->5
當前頂點:4
當前頂點:5
可以看到輸出的順序與edge
陣列是相反的,比如edge
陣列中的以0
為起始點的邊的順序為0->1,0->5,0->3
,而輸出順序為0->3,0->5,0->1
,這是因為pre
的前向連結關係,生成pre
陣列的時候,採用的是類似連結串列中的“頭插法”生成。
如果想要和原來的順序保持一致,可以將edge
陣列反轉再生成pre
和last
陣列:
private void buildGraph(int n, int[][] edge) {
int edgeCount = edge.length;
int[][] reverseEdge = new int[edgeCount][2];
for (int i = 0; i < edgeCount; i++) {
reverseEdge[i] = edge[edgeCount - i - 1];
}
pre = new int[edgeCount];
last = new int[n];
Arrays.fill(last, -1);
for (int i = 0; i < edgeCount; i++) {
int v0 = reverseEdge[i][0];
pre[i] = last[v0];
last[v0] = i;
}
}
然後遍歷edge
陣列的時候也需要反轉:
private void visit(int n, int[][] edge) {
int edgeCount = edge.length;
int[][] reverseEdge = new int[edgeCount][2];
for (int i = 0; i < edgeCount; i++) {
reverseEdge[i] = edge[edgeCount - i - 1];
}
for (int i = 0; i < n; i++) {
System.out.println("當前頂點:" + i);
for (int lastEdge = last[i]; lastEdge != -1; lastEdge = pre[lastEdge]) {
System.out.println(reverseEdge[lastEdge][0] + "->" + reverseEdge[lastEdge][1]);
}
}
}
測試程式碼不變:
public void build() {
int n = 6;
int[][] edge = {{0, 1}, {1, 3}, {3, 5}, {2, 4}, {2, 3}, {0, 5}, {0, 3}, {3, 4}};
buildGraph(n, edge);
visit(n, edge);
}
輸出:
當前頂點:0
0->1
0->5
0->3
當前頂點:1
1->3
當前頂點:2
2->4
2->3
當前頂點:3
3->5
3->4
當前頂點:4
當前頂點:5
可以看到輸出順序和edge
對應的邊的順序一致了。
4 疑問
4.1 為什麼叫pre
陣列而不是next
陣列
筆者看到網上的文章很多都是如下三個陣列:
head[u]
陣列:表示以u
作為起點的第一條邊的編號next[cnt]
陣列:表示編號為cnt
的邊的下一條邊,這條邊與cnt
同一個起點to[cnt]
陣列:表示編號為cnt
的邊的終點
其中to[cnt]
陣列在本篇文章中沒有實現,因為已經有edge
陣列儲存了。
head[u]
陣列,相當於本篇文章中的last
陣列,而next[cnt]
陣列,相當於本篇文章中的pre
陣列。
那麼為什麼取不同的名字?
只是筆者認為,從自己的角度出發,這樣好比較理解。如果還是覺得難理解,可以到文末的參考連結處看一下其他文章。
4.2 這個東西到底有什麼用
鏈式前向星能做的題目,一般來說鄰接表也能做。鏈式前向星,不是用來幫你AC
題目來的,不是說某道題非得用它。它是用來幫助你在AC
的基礎上,進一步提高效率。鏈式前向星是一種最佳化手段,它只是幫助你最佳化,而不是學習了它,就能AC
題目。