1. 程式人生 > >關於鏈式前向星及其簡單運用

關於鏈式前向星及其簡單運用

首先來一段網上關於前向星的描述:

前向星是一種特殊的邊集陣列,我們把邊集陣列中的每一條邊按照起點從小到大排序,如果起點相同就按照終點從小到大排序,

並記錄下以某個點為起點的所有邊在陣列中的起始位置和儲存長度,那麼前向星就構造好了.

用len[i]來記錄所有以i為起點的邊在陣列中的儲存長度.

用head[i]記錄以i為邊集在陣列中的第一個儲存位置.

再來一段另外的一個介紹

圖的儲存一般有兩種:鄰接矩陣、鄰接表(鄰接表包括一種東西叫前向星)。

若圖是稀疏圖,邊很少,開二維陣列a[][]很浪費;

若點很多(如10000個點)a[10000][10000]又會爆.只能 前向星做.

前向星的效率不是很高,優化後為鏈式前向星,直接介紹鏈式前向星。

來一張圖片用來介紹一下前向星:

我們輸入邊的順序為:

 

1 2

2 3

3 4

1 3

4 1

1 5

4 5

 

那麼排完序後就得到:

 

編號:     1      2      3      4      5      6      7

起點u:    1      1      1      2      3      4      4

終點v:    2      3      5      3      4      1      5

但是利用前向星會有排序操作,如果用快排時間至少為O(nlog(n))

如果用鏈式前向星,就可以避免排序.

那麼我們先來建立一個結構體

struct NODE{
	int w; // 權值
	int to; // 該條邊的末尾的點
	int next; //next表示與第i條邊同起點的下一條邊的儲存位置,其實就是下一個邊的起點的下標
}edge[MAXN];

 再來看一段比較重要的程式碼,具體看註釋 

void Add(int u, int v, int w) {  //起點u, 終點v, 權值w 
	//cnt為邊的計數,從1開始計 
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	edge[cnt].to = v;
	head[u] = cnt++;    //第一條邊為當前邊 ,其實head在這裡面可以理解為連結串列中的頭指標的類似的作用
} 

下面來看一段網上摘抄的程式碼來簡單理解鏈式前向星,結構體設定的有些不一樣,不過不影響閱讀;

 

​
#include<bits/stdc++.h>
using namespace std;
#define MAXN 100501
struct NODE{
	int w;
	int e;
	int next; //next[i]表示與第i條邊同起點的上一條邊的儲存位置
}edge[MAXN];
int cnt;
int head[MAXN]; 
void add(int u,int v,int w){
	edge[cnt].w=w;
	edge[cnt].e=v;    //edge[i]表示第i條邊的終點 
	edge[cnt].next=head[u]; //head[i]表示以i為起點的最後一條邊的儲存位置 
	head[u]=cnt++;
}
int main(){
	memset(head,0,sizeof(head));
	cnt=1;
	int n;
	cin>>n;
	int a,b,c;
	while(n--){
		cin>>a>>b>>c;
		add(a,b,c);
	}
	int start;
	cin>>start;
	for(int i=head[start];i!=0;i=edge[i].next)
	   cout<<start<<"->"<<edge[i].e<<" "<<edge[i].w<<endl;
	return 0;
}

​

結果如圖:

我們來對該圖的輸入輸出做一個分析

注意cnt的初值我們初始化為1                  cnt
edge[1].next = head[1] = 0, head[1] = 1,  2 ;
edge[2].next = head[2] = 0, head[2] = 2,  3 ; 
edge[3].next = head[3] = 0, head[3] = 3,  4 ; 
edge[4].next = head[1] = 1, head[1] = 4,  5 ; 
edge[5].next = head[4] = 0, head[4] = 5,  6 ; 
edge[6].next = head[1] = 4, head[1] = 6,  7 ;
我們可以看出cnt的值一直在變,其實可以理解為陣列的下標,然後head值得就是指向該邊的上一條邊的下標,這個還得靠自己結合程式碼悟,我也一時無法解釋清楚

在這裡我們可以更加深入理解head陣列的作用,說實話我也是看了這個才明白他的真正的作用,關於他的遍歷是倒著來的,一直到初始值,初始值根據需要設定為-1或者0

接下來我們來看一個題目,運用到了並查集與鏈式前向星,根據這個題目我們來更熟悉一下他的運用:

 

題目描述

很久以前,在一個遙遠的星系,一個黑暗的帝國靠著它的超級武器統治著整個星系。

某一天,憑著一個偶然的機遇,一支反抗軍摧毀了帝國的超級武器,並攻下了星系中幾乎所有的星球。這些星球通過特殊的以太隧道互相直接或間接地連線。

但好景不長,很快帝國又重新造出了他的超級武器。憑藉這超級武器的力量,帝國開始有計劃地摧毀反抗軍佔領的星球。由於星球的不斷被摧毀,兩個星球之間的通訊通道也開始不可靠起來。

現在,反抗軍首領交給你一個任務:給出原來兩個星球之間的以太隧道連通情況以及帝國打擊的星球順序,以儘量快的速度求出每一次打擊之後反抗軍佔據的星球的連通塊的個數。(如果兩個星球可以通過現存的以太通道直接或間接地連通,則這兩個星球在同一個連通塊中)。

輸入輸出格式

輸入格式:

 

輸入檔案第一行包含兩個整數,NN (1 < = N < = 2M1<=N<=2M) 和 MM (1 < = M < = 200,0001<=M<=200,000),分別表示星球的數目和以太隧道的數目。星球用 00 ~ N-1N−1 的整數編號。

接下來的 MM 行,每行包括兩個整數 XX, YY,其中( 0 < = X <> Y0<=X<>Y 表示星球 xx 和星球 yy 之間有 “以太” 隧道,可以直接通訊。

接下來的一行為一個整數 kk ,表示將遭受攻擊的星球的數目。

接下來的 kk 行,每行有一個整數,按照順序列出了帝國軍的攻擊目標。這 kk 個數互不相同,且都在 00 到 n-1n−1 的範圍內。

 

輸出格式:

 

第一行是開始時星球的連通塊個數。接下來的 KK 行,每行一個整數,表示經過該次打擊後現存星球的連通塊個數。

 

輸入輸出樣例

輸入樣例#1: 複製

8 13
0 1
1 6
6 5
5 0
0 6
1 2
2 3
3 4
4 5
7 1
7 2
7 6
3 6
5
1
6
3
5
7

輸出樣例#1: 複製

1
1
1
2
3
3

附送一段題解程式碼:

// luogu-judger-enable-o2
#include<iostream>
#define IOS ios::sync_with_stdio(false)
using namespace std;
const int maxn = 4e5 + 4;
int father[maxn], head[maxn], store[maxn], ans[maxn];
int cnt = 0;
bool vis[maxn];
struct Node{
    int from;
    int to;
    int next;
}edge[maxn];
//和一般的前向星還是有一些小小的差別
void insert(int u, int v) {
    edge[cnt].from = u;
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

int findFather(int x) {
    while (x != father[x]) x = father[x];
    return x;
}

int Union(int a, int b) {
    int faU = findFather(a);
    int faV = findFather(b);
    if (faU != faV) {
        father[faU] = faV;
        return 1;
    }
    return 0;
}
//和一般的思路不一樣,我們假設所有的星球都是毀滅的,然後每次修復好一個星球,並且將結果儲存到數組裡,最後輸出即可,這樣可以有個更好的思路
int main() {
    IOS;//可省略
    int n, m, a, b, k, x;
    cin >> n >> m;
    fill(head, head + maxn, -1);
    for (int i = 0; i < n; i++) {
        father[i] = i;
    }
    for (int i = 0; i < m; i++) {
        cin >> a >> b;
        insert(a, b);//無向圖
        insert(b, a);
    }
    cin >> k;
    int total = n - k;//假設全部都是毀滅狀態
    for (int i = 0; i < k; i++){
        cin >> x;
        store[i] = x;
        vis[x] = true;
    }
    for (int i = 0; i < 2 * m; i++) {//2*m的原因是因為有2*m條邊,因為是無向邊,所以乘以2
        if (vis[edge[i].from] == false && vis[edge[i].to] == false) {
            if(Union(edge[i].from, edge[i].to))
                total--;
        }
    }
    ans[k] = total;
    for (int i = k - 1; i >= 0; i--) {
        int u = store[i];
        total++;
        vis[u] = false;
        for (int j = head[u]; j != -1; j = edge[j].next) {
            if (vis[edge[j].to] == false && Union(edge[j].from, edge[j].to)) {
                total--;
            }
        }
        ans[i] = total;
    }
    for (int i = 0; i <= k; i++) {
        cout << ans[i] << endl;
    }
    return 0;
}

關於鏈式前向星還可以看看其他部落格,不過很多題目都運用到了這個技巧,例如spfa使用鏈式前向星之類的,之後再刷題目的過程中會慢慢熟悉的