數學使咱們可以發現概念和聯繫這些概念的規律,這些概念和規律給了咱們理解天然現象的鑰匙。node
——愛因斯坦ios
本文代碼基於C++實現,閱讀本文,須要有如下知識算法
教熟練使用C++ STL庫中的vector,map,pair等;數組
對於遞歸和簡單搜索算法(dfs,bfs)有粗淺的理解;數據結構
稍微的離散數學或者是線性代數知識(多是我瞎掰的,沒有也罷 😂 ) 架構
本文針對算法或數據結構初學者(好比我)寫下,本人不才,若有錯誤請輕噴 😄 。 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算法的模板能夠求出如下結果
圖論的問題和每個節點的信息息息相關,而如何使用圖論模型,關鍵在於如何定義「節點」。
關於這個問題,我很喜歡《算法圖解》裏的講解方式——將「狀態」轉化爲「信息」儲存到「節點」裏,每一個狀態是一個「節點」,狀態變化的過程就是「邊」。這即是鏈接實際問題和圖論算法的橋樑,理解了這個思想,不少模型創建的困惑就能迎刃而解了,圖論的其餘問題基本都能經過這個思想來建模。
但願個人拋磚引玉能引發更多的思考! 😄 (蒟蒻鞠躬)。