数学使咱们可以发现概念和联系这些概念的规律,这些概念和规律给了咱们理解天然现象的钥匙。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算法的模板能够求出如下结果
图论的问题和每个节点的信息息息相关,而如何使用图论模型,关键在于如何定义“节点”。
关于这个问题,我很喜欢《算法图解》里的讲解方式——将“状态”转化为“信息”储存到“节点”里,每一个状态是一个“节点”,状态变化的过程就是“边”。这即是链接实际问题和图论算法的桥梁,理解了这个思想,不少模型创建的困惑就能迎刃而解了,图论的其余问题基本都能经过这个思想来建模。
但愿个人抛砖引玉能引发更多的思考! 😄 (蒟蒻鞠躬)。