acm之圖論基礎


1、圖的定義

 是一個頂點集合V和一個頂點間關系的集合E組成,記G=(V,E) 
V:頂點的有限非空集合。 
E:頂點間關系的有限集合(邊集)。 
存在一個結點v,可能含有多個前驅節點和后繼結點。 

 

1頂點(vertex)

上圖中黑色的帶數字的點就是頂點,表示某個事物或對象。由於圖的術語沒有標准化,因此,稱頂點為點、節點、結點、端點等都是可以的。叫什么無所謂,理解是什么才是關鍵。

2邊(edge)

ACM圖的存儲(轉載自劍紫青天,但是他github掛了

對於ACM圖論方面的題目總是免不了首先要建圖存圖,使用合適的存圖方式不但是AC的必要條件,解題事半功倍。

以下主要分析三種常見的存圖方式的優缺點以及代碼實現

  • 鄰接矩陣
  • 鄰接表
  • 鏈式前向星

鄰接矩陣

鄰接矩陣是三種存圖方式中最簡單也最為暴力的一種存圖方式了。

存圖思想

使用一個矩陣來描述一個圖,對於矩陣的第i行第j列的值,表示編號為i的頂點到編號為j的頂點的權值。

代碼實現

對於鄰接矩陣來說,它的代碼實現都十分簡單,二維數組就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <string.h>

// 最大頂點數
const int V = 1000;

// 鄰接矩陣的定義
// mat[i][j] 表示 頂點`i`到頂點`j`的權值

int mat[V][V];


// 鄰接矩陣的初始化操作
// 假設權值為零表示沒有該邊

memset(mat, 0, sizeof(mat))


// 增加邊
// 新增頂點`i`到頂點`j`的邊,權值為`w`

mat[i][j] = w;


// 刪除邊
// 刪除頂點`i`到頂點`j`的邊

mat[i][j] = 0;


// 查詢邊
// 查詢頂點`i`到頂點`j`的邊權

mat[i][j];

優點

使用鄰接矩陣來進行建圖存圖有以下優點

  • 簡單易學

    這個肯定不用多說,哪怕是沒學過線性代數的童鞋也很容易理解這樣的存圖方式。

  • 代碼易寫,簡單好操作

    上面的代碼實現已經展示了要定義這個數據結構以及實現初始化,增加刪除邊等操作有多么的簡單。

  • 對已確定的邊進行操作,效率高

    確定邊(已知兩頂點編號)要進行增加或刪除邊(或者說更改邊權)以及查詢邊權等操作,時間復雜度為$O(1)$。

  • 易處理重邊

    你可以隨時覆蓋掉重邊,可以自己實現存儲最新的邊,權值最大的邊或權值最小的邊等。
    當然,如果你非要使用鄰接矩陣存圖還要存重邊也不是不可以。

缺點

鄰接矩陣存圖雖然簡單優雅,但是它的一些缺點卻幾乎是致命的。

  • 過高的空間復雜度

    對於頂點數V,鄰接矩陣存圖的空間復雜度高達$O(V^2)$頂點數上了一萬可以不用考慮這種存圖方式了。
    對於稀疏圖來說,鄰接矩陣存圖內存浪費太嚴重,這也是鄰接矩陣存圖在ACM題目中十分罕見的根本原因。

  • 對於不確定邊的查詢效率一般

    比如,我找個編號為1出發的第一條邊我還要一條條邊判斷是否存在(權值是否為0)。

鄰接表

鄰接表在三種常用的存圖方式中屬於較為中庸和普遍的存圖方式了,缺點不致命,優點不明顯。

存圖思想

鄰接矩陣對於每個頂點使用定長的數組來存儲以該點出發的邊的情況。第i個數組的第j個值存儲的是從頂點i到頂點j的邊的權值。
鄰接表則是對於每個頂點使用不定長的鏈表來存儲以該點出發的邊的情況。因此對於第i個鏈表的第j個值實際上存儲的是從編號為i的頂點出發的第j條邊的情況。

一般來說,如果有邊權的話,鄰接表的鏈表存儲的是一個結構體,這個結構體存儲該邊的終點以及邊權。

下面給個鄰接表與鄰接矩陣存圖的示例比較。

links&matrix.png

代碼實現

在ACM題目中,動態的數據結構一般是不被推薦的,因為動態開辟內存比較消耗時間,且寫起來復雜容易出錯。
大部分情況我們使用C++STL里的vector作為鏈表來實現圖的鄰接表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <vector>

using namespace std;

// 最大頂點數
const int V = 100000;


// vector實現的鄰接表的定義
// 不考慮邊權,存儲類型為int型

vector<int> e[V];


// 鄰接表的初始化操作
// 將起點為`i`的邊鏈表全部清空

e[i].clear();


// 增加邊
// 新增頂點`i`到頂點`j`的邊

e[i].push_back(j);


// 查詢邊

e[i][0]; // 查詢以`i`為起點的第一條邊`i->e[i][0]`

for (int j=0; j<(int)e[i].size(); ++j) {
if (e[i][j] == k) { // 查詢邊`i->k`
// do something.
}
}

優點

  • 較為簡單易學

    相比鄰接矩陣,無非是數組轉鏈表加上存儲值的意義不同而已,不需要轉太大的彎。

  • 代碼易寫,不復雜

    代碼實現已經演示過了,較簡單,不容易寫錯。

  • 內存利用率較高

    對於頂點數V與邊數E空間復雜度為$O(V+E)$。能較好處理稀疏圖的存儲

  • 對不確定邊的操作方便效率也不錯

    比如,要遍歷從某點出發的所有邊,不會像鄰接矩陣一樣可能會遍歷到不存在的邊。

缺點

  • 重邊不好處理

    判重比較麻煩,還要遍歷已有的邊,不能直接判斷。
    一般情況下使用鄰接表存圖是會存儲重邊的,不會做重邊的判斷。
    所以如果要解決重邊的影響一般不在存邊的情況下做文章。

  • 對確定邊的操作效率不高

    比如對於給定i->j的邊要進行查詢或修改等操作只有通過遍歷這種方式找到了。

鏈式前向星

鏈式前向星是前向星的升級版,因為它可以完美代替前向星,所以就跳過前向星的學習,直接學習鏈式前向星。

存圖思想

這種存圖方式的數據結構主要是邊集數組,顧名思義,圖的邊是用數組來存儲的
當然想要完美表示圖結構,光有一個邊集數組還不夠,還要有一個數組存儲指向每一個點的第一條邊的“指針”。
而每一條邊都需要存儲接下來一條邊的“指針”,這樣就能夠像類似鄰接表一樣方便遍歷每一個點的所有邊了。

代碼實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <string.h>

// 最大頂點數
const int V = 100000;
// 最大邊數
const int E = 100000;

// 邊結構體的定義
struct Edge {
int to; // 表示這條邊的另外一個頂點
int next; // 指向下一條邊的數組下標,值為-1表示沒有下一條邊
};


// head[i] 表示頂點`i`的第一條邊的數組下標,-1表示頂點`i`沒有邊
int head[V];
Edge edge[E];


// 鏈式前向星初始化,只需要初始化頂點數組就可以了
memset(head, -1, sizeof(head));


// 增加邊的方式
// 新增邊 a -> b,該邊的數組下標為`id`
inline void AddEdge(int a, int b, int id)
{
edge[id].to = b;
edge[id].next = head[a]; // 新增的邊要成為頂點`a`的第一條邊,而不是最后一條邊
head[a] = id;
return;
}

// 遍歷從`a`點出去的所有邊
for (int i=head[a]; i!=-1; i=e[i].next) {
// e[i] 就是你當前遍歷的邊 a -> e[i].to
}

優點

  • 內存利用率高

    相比vector實現的鄰接表而言,可以准確開辟最多邊數的內存,不像vector實現的鄰接表有爆內存的風險。

  • 對不確定邊的操作方便效率也不錯

    這點和鄰接表一樣,不會遍歷到不存在的邊。

缺點

  • 難於理解,代碼較復雜

    這種存圖方式相對於鄰接表來說比較難理解,代碼雖然不是很復雜但是不熟練的話寫起來也不是方便。

  • 重邊不好處理

    這點與鄰接表一樣,只有通過遍歷判重。

  • 對確定邊的操作效率不高

    也與鄰接表一樣,不能通過兩點馬上確定邊,只能遍歷查找。

總結

對於鄰接矩陣存圖來說,由於內存消耗的局限性,它的適用范圍比較狹窄,幾乎只能在簡單圖論題目中見到。

鄰接表存圖最為常見的一種,絕大部分采用C++STL中的vector實現,一般情況下大部分圖論題目都能使用該存圖方式。

但是鏈式前向星其實是一種較好替代鄰接表來存圖的數據結構,在鄰接表存圖不能使用時可以使用,幾乎可以用於全部圖論題目。

上圖中頂點之間藍色的線條就是邊,表示事物與事物之間的關系。需要注意的是邊表示的是頂點之間的邏輯關系,粗細長短都無所謂的。包括上面的頂點也一樣,表示邏輯事物或對象,畫的時候大小形狀都無所謂。

最短路是什么呢,就是兩個頂點間最短的距離

floyd算法(3重循環的思行代碼

    for(int k=1; k<=n; k++)
        for(int i=1; i<=n; i++)
            for(int j=1; j<=n; j++)
                M[i][j]=min(M[i][j],M[i][k]+M[k][j]);

n條邊m條路

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int INF=0x3f3f3f3f;
int main()
{
    int n,m,M[100][100];
    scanf("%d%d",&n,&m);
    memset(M,INF,sizeof M);
    for(int i=1; i<=n; i++)
        M[i][i]=0;
    for(int i=1,u,v,w; i<=m; i++)
        scanf("%d%d%d",&u,&v,&w),M[u][v]=w;
    for(int k=1; k<=n; k++)
        for(int i=1; i<=n; i++)
            for(int j=1; j<=n; j++)
                M[i][j]=min(M[i][j],M[i][k]+M[k][j]);
    for(int i=1; i<=n; i++)
    {
        for(int j=1; j<=n; j++)
            printf("%d ",M[i][j]);
        printf("\n");
    }
    return 0;
}

 

 題目推薦 Stockbroker Grapevine

 

附錄:矩陣相乘

矩陣A乘以B(15 分)

給定兩個矩陣A和B,要求你計算它們的乘積矩陣AB。需要注意的是,只有規模匹配的矩陣才可以相乘。即若A有Ra​​行、Ca​​列,B有Rb​​行、Cb​​列,則只有Ca​​與Rb​​相等時,兩個矩陣才能相乘。

輸入格式:

輸入先后給出兩個矩陣A和B。對於每個矩陣,首先在一行中給出其行數R和列數C,隨后R行,每行給出C個整數,以1個空格分隔,且行首尾沒有多余的空格。輸入保證兩個矩陣的R和C都是正數,並且所有整數的絕對值不超過100。

輸出格式:

若輸入的兩個矩陣的規模是匹配的,則按照輸入的格式輸出乘積矩陣AB,否則輸出Error: Ca != Rb,其中CaA的列數,RbB的行數。

輸入樣例1:

2 3
1 2 3
4 5 6
3 4
7 8 9 0
-1 -2 -3 -4
5 6 7 8

輸出樣例1:

2 4
20 22 24 16
53 58 63 28

輸入樣例2:

3 2
38 26
43 -5
0 17
3 2
-11 57
99 68
81 72

輸出樣例2:

Error: 2 != 3
作者: 陳越
單位: 浙江大學
時間限制: 400ms
內存限制: 64MB
代碼長度限制: 16KB

線代概念題
復制代碼
#include<bits/stdc++.h>
using namespace std;
int A[105][105],B[105][105],C[105][105];
int main()
{
    int a,b;
    cin>>a>>b;
    for(int i=0; i<a; i++)
        for(int j=0; j<b; j++)
            cin>>A[i][j];
    int c,d;
    cin>>c>>d;
    for(int i=0; i<c; i++)
        for(int j=0; j<d; j++)
            cin>>B[i][j];
    if(b!=c)
        cout<<"Error: "<<b<<" != "<<c;
    else
    {
        cout<<a<<" "<<d<<"\n";
        for(int i=0; i<a; i++)
            for(int j=0; j<d; j++)
                for(int k=0; k<b; k++)
                    C[i][j]+=A[i][k]*B[k][j];
        for(int i=0; i<a; i++)
        {
            cout<<C[i][0];
            for(int j=1; j<d; j++)
                cout<<" "<<C[i][j];
            cout<<"\n";
        }
    }
}
復制代碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM