小話數據結構-圖 (聚焦與於實現的理解)

數學使咱們可以發現概念和聯繫這些概念的規律,這些概念和規律給了咱們理解天然現象的鑰匙。node

——愛因斯坦ios

前言

本文代碼基於C++實現,閱讀本文,須要有如下知識算法

  1. 教熟練使用C++ STL庫中的vector,map,pair等;數組

  2. 對於遞歸和簡單搜索算法(dfs,bfs)有粗淺的理解;數據結構

  3. 稍微的離散數學或者是線性代數知識(多是我瞎掰的,沒有也罷 😂架構

本文針對算法或數據結構初學者(好比我)寫下,本人不才,若有錯誤請輕噴 😄app

瓶頸

在學習「數據結構」這門課以前,「圖論」這個略顯高級的詞彙看起來還與我那麼的遙遠,在通過了「離散數學」的學習以後,我慢慢認識到其實數據結構就是離散數學模型的代碼實現,而且在不斷的學習中,我開始能以我本身的思惟去理解圖論的知識。函數

數據結構的教科書上,每一個知識點都會系統的從「定義——術語——存儲結構——相關操做」娓娓道來,然而,各類各樣的障礙阻礙着咱們認知的過程。對於離散數學不熟悉的,一時間沒法抽象出模型,教材上冗長的代碼實現,給讀者一種晦澀難懂的感受。這時候咱們就要思考離散數學的本質——將具體問題抽象爲通常問題,在由算法解決。所以,咱們在一開始沒必要過於在乎方法,而是應該聚焦與實現,像大學物理作實驗同樣,多操做幾遍,天然就熟能生巧,甚至開發出新的理解。學習

你得從用戶的體驗出發,推倒用什麼技術。 ——喬布斯在1997年迴歸蘋果發佈會上回答提問spa

案例-引入

什麼是圖?簡單說就是點與點之間的網狀關係。

好比如下有6個城市之間的鐵路線。

 

 

 

地圖就是一個很經典的圖

那麼,咱們是如何表達他們的關係的呢

咱們使用鄰接表or鄰接矩陣

爲何是鄰接表/矩陣

鄰接表和鄰接矩陣是圖的兩種經常使用存儲表示方式,用於記錄圖中任意兩個頂點之間的連通關係,包括權值。

(在圖論相關的案例中,咱們會特別頻繁的用到這兩種表示方式)

咱們不談什麼二元組三元組的,想一想啊,圖重要的頂多就仨玩意:節點,邊,權值,而使用鄰接表和鄰接矩陣已經能夠清晰簡潔的表達這些關係了。

咱們先看看鄰接表和鄰接矩陣怎麼建;

鄰接表

emm,用圖表示就是上面那張圖啊,代碼實現的話,咱們用stl的vector更方便一點

#include <iostream>
#include <vector>struct edge{//邊信息,邊有啥屬性往裏丟就行
    int to;//該邊能夠去往的下一個節點編號,有明確指向,是單向邊。
    int value;//存邊的權值,若是沒有能夠直接忽略
    edge(int t,int v) :to(t), value(v){};//構造函數
};
​
vector<edge> node[n];//用vector實現鄰接表的存儲
//ps:邊信息只有一個能夠直接存int,兩個能夠用stl的pair,三個及以上就確定要用struct了
//vector<int> node[n],vector<pair<int, int> >都是可行的,本身清楚意思就行

 

這裏是什麼意思呢,就是我有n個vector數組,每個數組表示一個點的信息,vector裏存的是邊(edge),每個點到點的通路意味着有一個對應的edge信息

那麼,如何把信息存入鄰接表呢,咱們假設用一下流程輸入(大概就是平時作題的時候題目要求的輸出啦)

先假設每條邊的權值都是同樣的,就設爲「1」吧

//第一行爲兩個整數n,e,分別表示節點數,邊數,隨後n行,輸入節點信息,在隨後e行,接受邊信息

6 8

武漢 岳陽 南昌 長沙 株洲 湘潭

武漢 岳陽

岳陽 南昌

武漢 長沙

南昌 長沙

岳陽 長沙

長沙 株洲

長沙 湘潭

湘潭 株洲

#EOF

那麼,在C語言裏面咱們這麼處理輸入

map<string,int> tab;
map<int,string> tab0;
//創建散列表(哈希表),使每一個城市的編號和名字能夠相互鏈接
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++){
    string name;
    cin>>name;
    tab0.insert({i,name});
    tab.insert({name,i});
    //給每個城市編號
}
while(m--){
    edge tmp;
    string a,b;
    node[map[a]].push_back(edge(map[b],1));
    //構造函數中map[a]表示名爲a的城市對應的編號,1表示權值,存入vector數組g中
    node[map[b]].push_back(edge(map[a],1));
    //由於是雙向邊因此正向反向都存一遍
}

 

這樣的話,咱們的鄰接表就徹底存儲好了

對於任意一個點,咱們要遍歷其相鄰點,只須要用一下代碼

//輸入城市名字 輸出其相鄰全部城市的名字
string name;
cin>>name;
for(auto it=node[tab[name]].begin();it!=g[tab[name]].begin();it++){//遍歷name節點的全部邊
    cout<<tab0[it->to]<<endl;
}
 

假如咱們輸入

岳陽

那麼就會返回

武漢

南昌

長沙

整個鄰接表的存儲和訪問過程就是以上的樣子了

鄰接矩陣

這個就很好理解了,就是一個n*n的二維數組模擬矩陣,表達的是點與點之間的關係,咱們沿用上一個例子裏的輸入,咱們創建出來的矩陣大概是這樣

 

 

這個鄰接矩陣只是斷定有無直接相連的,咱們用一個6*6的二維數組能夠很輕鬆的建出來,沒有自旋,未聯通和自我比較設置爲0(false),已聯通即設置爲1(true)。

案例-常見問題

(PS:本人才疏學淺,只介紹部分案例的大體思惟路線,細節歡迎各位深刻思考)

咱們接着上一個案例看,對於不少狀況,鄰接矩陣像上面這樣,就算建出來了,可是咱們如今用的,是一個實實在在的生活中的例子,誰都直到武漢和南昌之間一定能夠經過鐵路線到達,只是會通過別的站臺。

這個時候就引出了一個問題,按照鄰接表來看,武漢和南昌其實是經過其餘的節點鏈接起來了的,只是沒有直接鏈接

然而此時,從「武漢「到」南昌」實際上有多條線路

武漢->岳陽->南昌

武漢->長沙->南昌

武漢->長沙->岳陽->南昌

武漢->岳陽->長沙->南昌

武漢->長沙->湘潭->株洲->南昌

………………

那咱們給其付的權值究竟是2,3,4仍是多少呢?

這就能夠引入到一個常見的圖論問題——「最短路」

最短路

(PS:最短路問題在算法競賽和數學建模競賽中都是很是常見的)

故名思意,當咱們想要直接去往某個目的地時,必定是講究時間效率的,咱們不肯意走太長,更不肯意繞圈子,用規範的話說就是:「找最短路,而且避免系統資源浪費」,那咱們就要先走一遍全部路徑,看看哪一個路徑可行(比如天天高鐵第一班車是「探路車」)。

假設咱們要從武漢出發,去南昌:

咱們從武漢開始遍歷武漢接下來能夠到達每個城市

 

 

如此以來,逐個分析每一個爲直接相連的點,咱們能夠獲得整個圖的帶權值的的鄰接矩陣表示,其中有一個要點,即點不能重複訪問,而BFS按照層次遍歷鄰接表的模式很是契合這個目的。咱們沿用上方的輸入和鄰接表的存儲形式,如下給出大體的僞代碼:

#include<queue>//stl的queue容器
void bfs(int st){
    queue<edge> qu;//建立隊列
    qu.push(起始狀態入隊);
    while(!qu.empty()){//當隊列非空
        if(當前狀態x方向可走)
            qu.push(當前狀態->x);//該狀態入隊
        if(當前狀態向y方向可走)
            qu.push(當前狀態->y);//該狀態入隊
        …………………
        處理(隊頂)qu.top();
        相應操做;
        qu.pop();//隊首彈出隊
    }//一次循環結束,執行下一次循環
}

如此以來,咱們就獲得了整個圖,每一個節點的詳細信息,能夠根據需求進行更細節的操做。

這即是最短路的基本思想,固然,實際狀況會更加複雜,好比邊的權值各有不一樣,是有向邊,出現負權值等狀況,也會有相應的算法(迪特斯科拉,貝爾曼-福德,弗洛伊德,SPFA,A_Star等算法),同時,在圖的遍歷時,經過鄰接矩陣咱們也能夠瞥見連通圖的不少性質,優美的現象能吸引人的思考,數學之美就在於這些奇妙之處。

歐拉路

咱們有目的性出行,確定也有旅遊出行,確定有人喜歡欣賞沿途的風景,我舉個例子,加入有一個岳陽人,他很喜歡看火車沿路的風景,他把以上6個城市做爲了本身旅行可規劃的目的地,他想在各個路線中穿梭,路線越長越好,可是他不喜歡看重複的風景,他想規劃一個走過的路不重複,並且最長的路線。走過的路不重複,就是所謂的歐拉路

由於咱們着重考慮路徑,因此使用以前斷定有無直接鏈接的鄰接矩陣就能夠了,咱們使用DFS來遍歷全部邊並找出最長的一條。

int g[N][N];//鄰接矩陣2維數組
//默認鄰接矩陣信息已經存入了該二維數組中
bool st[N][N];//標記某一條邊是否被訪問過
int ans;//存儲答案
void dfs(int start, int res) {
    for (int i = 0; i < n; i++) {
        if (g[start][i] == 1 || st[start][i] || st[i][start]) 
        //該邊能夠經過而且是第一次經過
            continue;
        st[u][i] = st[i][u] = true;//標記
        dfs(i, res + 1);//下一步
        st[u][i] = st[i][u] = false;//回溯
    }
    ans = max(ans, res);//保證獲得最大的結果
}

 

歐拉(回)路問題其實就是經典的「一筆畫問題」,應爲咱們每一步的斷定和操做都是固定的,經過dfs的「自相性」咱們每每能簡潔而優美的解決這一系列問題。

 

爲了繼續思考圖論的模型,咱們接下來不使用代碼討論另外兩種模型,這兩種模型的代碼模板很方便理解,瞭解了基本思路和模型,就很方便應用了。

 

並查集

咱們都知道每一個省有不少地放,如今隨意給你兩個市區的名稱,想要你判斷如下他們是否屬於同一個省份。

咱們將每一個城市存入其數據結構,能夠得出如下的狀況

 

 

並查集的關鍵在於處理父子節點的關係,這樣的數據架構能夠處理大量的「集合合併」操做

最小生成樹

再來一個例子,假設咱們要再部分城市之間架設最新最快的交通軌道,爲了使成本最低,如今要你選出一個方案,使架設的軌道線路最低:(如今咱們給邊附上權值)

 

 

這樣即是一個基礎的無向圖最小生成樹問題,咱們根據咱們已經創建的關係,採用並查集的數據結構,採用Kruskal或者Prim算法的模板能夠求出如下結果

更多……

圖論的問題和每個節點的信息息息相關,而如何使用圖論模型,關鍵在於如何定義「節點」

關於這個問題,我很喜歡《算法圖解》裏的講解方式——將「狀態」轉化爲「信息」儲存到「節點」裏,每一個狀態是一個「節點」,狀態變化的過程就是「邊」。這即是鏈接實際問題和圖論算法的橋樑,理解了這個思想,不少模型創建的困惑就能迎刃而解了,圖論的其餘問題基本都能經過這個思想來建模。

但願個人拋磚引玉能引發更多的思考! 😄 (蒟蒻鞠躬)

相關文章
相關標籤/搜索