鏈式前向星介紹以及原理

xcghvgshjdfghsd發表於2023-02-26

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陣列反轉再生成prelast陣列:

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題目。

5 參考

相關文章