图论及其应用——图
Posted 黑大帅之家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图论及其应用——图相关的知识,希望对你有一定的参考价值。
我们探索某个领域的知识,无不怀揣的核弹级的好奇心与求知欲,那么,今天,我们就将开始对图论的探索。
观察一副《机械迷城》 的一处谜题。
不得不承认,《机械迷城》这款解密游戏难度远胜于《纪念碑谷》, 其中一个困难点就在于——《纪念碑谷》的目标是很明确的,但是《机械迷城》往往需要自己凭感觉设立目标。而这里的关卡的目标,就是堵住第三个出水口。
为了解决这个谜题,如果不去考虑用暴力枚举的方法去试探(其实很多情况下都是用到这种情况)一开始,我们似乎会从模拟电路的角度来看待这个水管图,但是会发现它太过复杂,简单的电路图似乎很难以表达整个线路的构造,这里,我们会联想到一些能够表征点与点之间的关系的数学模型——那就是图论所擅长的领域。
“联系”或者“关系”,在自然界中就像任意两个物体之间的万有引力一样常见,而图论就是致力于将一个集合的个元素的相互关系给找出。
在图论中,用点指代“事物”,用边指代“事物间某种联系”,而在边上可以添加一种叫做“权值”的信息用以更加详细的表征这种联系,这就是图。
而基于图最基本的定义,会衍生出一些特殊的图譬如补图、二分图、完全图,这里暂且不去深究其定义。
那么我们有了强大的数学工具在手,再来解决上面这个谜题,就显得很小儿科了。我们从水的入口开始,顺着管道,分叉口作为图中的点,而水的流向以及是否有阀门则可以在点与点之间 边上体现。虽然画出其抽象图可能会比较麻烦,但是不难想象,如果从游戏开发人员的角度去看这个谜题,显然更加需要图论的应用。
通过上面的介绍,相信读者已经对图论有了很初步的了解了。那么我们开始结合具体的问题更加深入的探究图论知识的奥秘。
关于图的拓扑排序的问题。(Problem source:pku1128)
这里先非常粗略的给出图的拓扑排序的定义:即一个图的所有点排成一个序列,v1v2v3v4……,任取两点vi、vj,如果两者之间存在一条通路,那么一定是从vi -> vj , 那么此时我们说这是一个图的拓扑排序后的序列。
而拓扑排序在实现上其实也非常简单,我们遍历当前的图,找到一个没有入度(没有边进来)的点,作为拓扑序列的第一个点,一次类推,直到最后序列包含原图的所有点。
基于这种构造方法我们能够很好的理解——无向图、带环图是没有拓扑排序的。
题目大意:有五个已知的矩阵如上图所示,将它们堆叠在一起将形成一个新的矩阵。现在的问题是,给你一个堆叠后形成的矩阵,让你给出所有可以成立的堆叠顺序。
数理分析: 针对这个问题,选好思维出发的角度非常重要。根据题设的要求,我们能够保证每种字母框的每个边都会露出一个字母,那么凭借这个信息,我们就可以找到构成这个框的每个点在图中的位置。基于此,我们再去寻找在这个框上是否出现了其他字母,一旦出现,说明这个字母就在当前字母的上面。这样遍历下来,我们就可以将一个矩阵图,转化记载了各个字母相对位置的图——也就是我们所熟悉的点与点之间连接着线(有向)的那种抽象的图,做到了这一步,我们将构造出来的图和题意进行比较——出现的先后顺序,其实就表征了图中的有向性,而这其实也是拓扑排序所体现的,所以我们自然而然的要开始进行拓扑排序了。
这里题目的要求是给出所有的拓扑排序方案,结合上面构造某种拓扑排序的简单方法,再加上遍历图的一种方法——深搜,我们便可以找到所有的拓扑排序。
编程实现:图论相对来数在数理思维上并不是那么困难,而在编程实现上则比较困难。这里的困难点其实就体现在,同一个图的转化导致的不同信息呈现——这里就是一个具象的矩阵图转化成表征各个字母出现的前后关系的抽象图。有了这一步关键的过渡后,只需构造深度优先搜索把所有的情况遍历出来即可。
参考代码如下。(暂时还没AC)
#include <stdio.h> #include <string.h> #define maxn 35 #define maxm 35 #define kind 5 char ori[maxn][maxn], ans[kind + 1]; int m, n, in[kind], total; bool map[kind][kind]; struct Node { int x, y; } lt[maxm], rb[maxm]; //这里采用记录一个边框左上角的点和右下角的点的数据用以后面扫描边框上的其他字母. //因此初始化的时候对应着,左上角的点应该尽量往右下角初始化,右下角的点往左上角初始化。 void GetMap() { int i, j, t, k, x, y; memset(map , 0 , sizeof(map)); memset(in , -1 , sizeof(in)); memset(lt , 0x3f, sizeof(lt)); memset(rb , -1 , sizeof(rb)); for(i = total = 0; i < n; i++) for(j = 0; j < m; j++) { if(ori[i][j] == ‘.‘) continue; t = ori[i][j] - ‘A‘; if(in[t] == -1) { in[t] = 0; total++; } if(i < lt[t].x) lt[t].x = i; if(i > rb[t].x) rb[t].x = i; if(lt[t].y > j) lt[t].y = j; if(rb[t].y < j) rb[t].y = j; } for(i = 0; i < kind; i++) { for(x = lt[i].x; x <= rb[i].x; ++x) for(y = lt[i].y; y <= rb[i].y; ++y){ if(x > lt[i].x && y > lt[i].y && x < rb[i].x && y < rb[i].y) continue; t = ori[x][y] - ‘A‘; if(t != i && !map[i][t]){ map[i][t] = true; in[t]++; } } } } void DFS(int id) //fantastic! { if(id == total){ ans[id] = ‘\0‘; puts(ans); return; } for(int i = 0; i < kind; ++i) { if(in[i] == 0) { ans[id] = ‘A‘ + i; in[i] = -1; for(int j = 0; j < kind; ++j) if(map[i][j]) in[j]--; DFS(id + 1); for(int j = 0; j < maxm; ++j) if(map[i][j]) in[j]++; } } } int main() { int i; while(scanf("%d%d", &n, &m) == 2){ for(i = 0; i < n; ++i) scanf("%s", ori[i]); GetMap(); DFS(0); } return 0; }
让我们再来看一道关于图的拓扑排序的问题。(Prbolem source : hdu1285)
数理分析:我们在读题后,是否能将一些具体问题中的量更加抽象化的看待,使我们能否联想到拓扑排序的关键(运用很多数学知识都要依赖抽象化的能力),这里每个队伍就是图中的点,而胜负关系就是有向图中的箭头。如果抽象到了这一步,我们就会很自然的联想到拓扑排序了。
编程实现:值得注意的是,这里的后台给出的数据应该都是可解的。而至于多解的情况,这里不用像上面那题要利用dfs遍历出所有情况,这里只需给出可行解中队伍序号小的在前的第一种情况。这里在选点排序的时候只需从点集中按序号从小到大的搜索然后选点即可,这样构造出来的第一个一定是符合要求的。
参考代码如下:
#include<stdio.h> #include<string.h> const int maxn = 505; int n , m , G[maxn][maxn] , q[maxn] , Indegree[maxn]; using namespace std; void toposort() { int i , j , k; i = 0; while(i < n) { for(j = 1;j <= n;j++) { if(Indegree[j] == 0) { Indegree[j]--; q[i++] = j; for(k = 1;k <= n;k++) if(G[j][k]) Indegree[k]--; break; } } } } int main() { int x , y , i; while(scanf("%d %d" , &n , &m) != EOF) { memset(Indegree , 0 , sizeof(Indegree)); memset(G , 0 , sizeof(G)); memset(q , 0 , sizeof(q)); for(i = 0;i < m;i++) { scanf("%d %d" , &x , &y); if(G[x][y] == 0) //一步对处理输入数据的小小的优化,少了会引起超时。 { G[x][y] = 1; Indegree[y]++; } } toposort(); for(i = 0;i < n;i++) { if(i == n - 1) printf("%d\n" , q[i]); else printf("%d ",q[i]); } } }
我们再来看一道简单的拓扑排序的问题(Problem source :hdu2094)
这道题目是非常明显的拓扑排序的应用,而且十分简单,可以说是一个判断性的问题,那么这就大大简化的程序的复杂性。
这里的冠军,其实就是我们将实际的信息转化成图在转化拓扑排序后的图之后入度为0(没有人战胜它)的点,而在这里,我们在完成了上述操作后,只需再讨论有多少个这样入度为0的点,如果只有1个,那么显然就能够产生冠军。
参考代码如下。
#include<stdio.h> #include<string.h> const int maxn = 1005; using namespace std; char name[maxn][15]; int index , indegree[maxn]; int str_find(char * s) { int i; for(i = 0;i < index;i++) { if(strcmp(name[i] , s) == 0) break; } if(i == index) { strcpy(name[index++] , s); } return i; } int main() { int n; char s1[15] , s2[15]; int index1 , index2; while(scanf("%d",&n) != EOF && n) { index = 0 , memset(indegree , 0 ,sizeof(indegree)); for(int i = 0;i < n;i++) { scanf("%s %s" , s1 , s2); index1 = str_find(s1) , index2 = str_find(s2); indegree[index2]++; } int top_1 = 0; for(int i = 0;i < index;i++) { if(indegree[i] == 0) top_1++; if(top_1 > 1) break; } if(top_1 == 1) printf("Yes\n"); else printf("No\n"); } }
让我们再来看一道关于拓扑排序的简单问题(Problem source:hdu4324)
题目大意:假设这里有n个人,每个人有着自己的序号[1,n],这里分别给出n条信息,来告诉你第i个人喜欢第j个人。然后让你判断,这其中是否存早诸如a喜欢b,b喜欢c,c喜欢a的“三角恋情”。
数理分析:题目中给出的“三角恋”,其实对应着我们生成的graph中的环图,虽然按理讲环图是无法进行拓扑排序的,但是这里拓扑排序仅仅是用来进行判断,也就是说,我们对它进行拓扑排序,只要无法进行下去了,我们可以判断其已经有了环图。
然而不仅仅是需要环图,准确地讲这道题是让我们判断是否有三元的环图。我们这里假设v1v2v3v4v5……vi形成了环图,那么会存在v1->v2,v2->v3的关系,我们现在开始分别讨论v1和v3的关系。
①如果v1->v3,那么此时去掉v2,形成了i-1元环。
②如果v3->v1,那么此时已经形成了三元环。
而根据题设,v1和v3只存在上述两种关系。而针对①情况,采取相同的分析思路不断的缩减,最终一定会得到一个三元环。
编程实现:基于以上的数理分析,我们只需要判断我们生成的graph图是否带有环图即可,而这在编程上显然没有什么难度。
参考代码如下:
#include<stdio.h> #include<string.h> const int maxn = 2005; using namespace std; char graph[maxn][maxn]; int indegree[maxn]; int main() { int t ,tt, n; tt = 0; scanf("%d",&t); while(t--) { memset(indegree , 0 , sizeof(indegree)); scanf("%d" , &n); for(int i = 0;i < n;i++) { scanf("%s",graph[i]); for(int j = 0;j < n;j++) { if(graph[i][j] == ‘1‘) indegree[j]++; } } bool flag = false; for(int i = 0;i < n;i++) { int j; for(j = 0;j < n;j++) //没有入度为0的点,拓扑排序无法进行,说明出现了环图 if(indegree[j] == 0) break; if(j == n) { flag = true ; break; } else { indegree[j]--; //有入度为0的点,删除并遍历所有点更改对应的入度值,模拟拓扑排序的过程 for(int k = 0;k < n;k++) if(graph[j][k] == ‘1‘) indegree[k]--; } } if(flag) printf("Case #%d: Yes\n",++tt); else printf("Case #%d: No\n" ,++tt); } }
我们再来看一道有关拓扑排序的判断性问题。(Problem source:hdu3342)
题目大意:这里给出n个人之见m条关系,这里如果路人A是B的师傅,也是B的徒弟,则判定为非法关系,这里让你判断给出的关系是否合法。
数理分析:根据题意,我们生成graph图后,要判断两个点之间是否是双向箭头。
我们不知道给出的graph图会被分割成多少小图(即小图之见彼此独立,不存在任何通路),那我们就分析某个小图(此图中不存在与其他点不存在通路的点)。
题设说明,如果A是B的师傅,B是C的师傅,那么可以认定A是C的师傅,那么此时如果存在关系——C是A的师傅,那么就在graph图中生成了环,此时是存在违法关系的。而判断有无环生成,正是拓扑排序所能做的。
上面的分析是将环和拓扑排序联系了起来,而环的生成最少需要3个元素。于是我们考虑2个点之间的联系,发现事实上也是和拓扑排序对应的。
编程实现上:按照标准的拓扑排序的写法,需要设置三层循环,第一层循环表示扫描入度为0的点需要进行n次,第二层循环是找到入度为0的点,第三层循环在找到入度为0的点后,删除此点,对剩余点的入度值进行处理。拓扑排序后,依次删掉入度点为0的次数如果少于graph图含有的所有点的个数,则证明拓扑排序没有完成,出现了环或者A与B互为师徒,即非法关系。
参考代码如下。
#include<stdio.h> #include<string.h> using namespace std; const int maxn = 105; int G[maxn][maxn] , indegree[maxn]; int n , m; int cnt; void toposort() { int i , j ,k; for(i = 0;i < n;i++) { for(j = 0;j < n;j++) { if(indegree[j] == 0) { indegree[j]--; cnt++; for(k = 0;k < n;k++) if(G[j][k]) indegree[k]--; break; } } } } int main() { int x , y; while(scanf("%d%d",&n,&m) && (m || n)) { memset(G,0,sizeof(G)); memset(indegree , 0 , sizeof(indegree)); cnt = 0; for(int i = 0;i < m;i++) { scanf("%d%d",&x,&y); if(G[x][y] == 0) { G[x][y] = 1; indegree[y]++; } } toposort(); if(cnt == n) printf("YES\n"); else printf("NO\n"); } }
我们研究图所呈现出来的事物间的联系,一个很重要的方面即时图中的两点是否连通,而这个连通又可以分为直接连通和间接连通。而对于图的连通性的一个工具,叫做并查集。
这里其实不必引入过多的概念性的语言,我们可以这样简单的理解,所谓并查集,就是将一个完整的图中划分成一些小图,而这些小图需要具备的特征就是其含有的任意元素都是连通的,而小图和小图之见显然就是不连通的,这就是我们所说的——不相交集合,也就是并查集。
我们通过一个简单的题目再来认识一下所谓并查集的概念。(Problem source:hdu1232)
数理分析,这里其实就是让我们来找出在一个大图中,有多少个互不连通的小图,假设这里有x个小图,也就是所谓的连通分量,然后将小图看成整体,所需要建的道路显然是x-1。
编程实现:虽然城市与城市之间有道路,体现在图上面无所谓方向,但是这里为了编程实现,我们就生成有向图,这样就会方便我们找根节点然后判断城市是否在一个连通分量当中。
我们假设每次输入的城市a、b代表a->b,这里我们在储存方式上用F[a] = b来表示。那么我们开始假设有n(城市的个数)个连通分量,每次输入的道路所连接的两条城市的信息,递归寻找他们的根节点是否相同,如果相同 ,对应的F[a],F[b]存入他们所在连通分量的根节点(这里借用树中的概念),如果不相同,那么就需要建立新的连通分量。
最后遍历F[],有多少个-1,就说明有多少个连通分量。
参考代码如下。
#include<stdio.h> const int maxn = 1005; using namespace std; int F[maxn]; int find(int t) { if(F[t] == -1) return t; else return F[t] = find(F[t]); } void bing(int a , int b) { int t1 = find(a); int t2 = find(b); if(t1 != t2) F[t1] = t2; } int main() { int m , n; int a , b; while(scanf("%d",&n) , n) { for(int i = 1;i <= n;i++) F[i] = -1; scanf("%d",&m); for(int i = 1;i <= m;i++) { scanf("%d%d",&a,&b); bing(a,b); } int ans = 0; for(int i = 1;i <= n ;i++) if(F[i] == -1) ans++; printf("%d\n",ans - 1); } }
让我们再来看一道简单的并查集应用的题目。(Problem source : hdu 1213)
题目大意:给你一个数n表示有n个朋友,然后给出m条信息,分别表征了n个朋友中两两的相互关系,只有互相认识的朋友才能做一桌,问你需要几个桌子。
数理分析:我们从抽象化的graph图来看问题,显然是要求一个图里的连通分量嘛。所以这里使用最基本的并查集就可以解决问题了。
参考代码同上。
让我们再看一道有关并查集应用的问题。(Problem source:hdu1272)
数理分析:从这个题目描述中,需要两个节点有且只有一条路。基于存在性,我们可以想象到,这个graph图只能有一个连通分量。否则的话无法做到任意两个点之间有通路。基于唯一性,我们要判断只有一个连通分量的图是否有环(包括自环)。
编程实现:基于并查集原有的模板,我们只需在寻找父节点的时候加一步判断,即我们当前输入的道路连接的两个节点,他们的父节点之间存在通路,那么就将形成环,此时便不满足要求了。
参考代码如下。
#include<iostream> using namespace std; int const MAX = 100005; int F[MAX],flag,sign[MAX]; int Find(int t) { if(F[t] == t) return t; else return F[t] = Find(F[t]); } void bing(int x,int y) { int t1 =Find(x); int t2 =Find(y); if(t1!=t2) F[t1]=t2; else flag = 0; //父节点相同,成环 } void Init(int a,int b) { for(int i=1;i<MAX;i++) { F[i]=i; sign[i]=0; } sign[a] = sign[b]=1; flag=1; bing(a,b); } int main() { int i,a,b; while(cin>>a>>b) { if(a==-1&&b==-1) break; if(a==0&&b==0) { cout<<"Yes"<<endl; continue; } Init(a , b); while(cin>>a>>b) { if(a==0&&b==0) break; bing(a,b); sign[a]=sign[b]=1; } int cnt=0; for(i=1;i<MAX;i++) { if(sign[i]&&F[i]==i) //判断连通分量的个数 cnt++; if(cnt>1) {flag=0;break;} } if(flag) cout<<"Yes"<<endl; else cout<<"No"<<endl; } return 0; }
让我们再看一道基于并查集的变式问题。(Problem source : hdu 1856)
数理分析:这里其实就是给出一张graph图,让你找到含元素最多的连通分量的元素个数。
编程实现:基于已有的关于并查集的模板上,我们需要另开一个数组num[],来记录各个连通分量含有的元素个数。
这里非常值得注意的一点是,我们通过并查集实现对Graph图连通分量的记录,是在假定Graph图是有向的情况下的,所以这里在修改代码完成各个连通分量含有元素的时候,也需要注意图的有向性。这里如果方向弄反,访问一次父节点是不会出错,但如果访问第二次父节点,就会出现错误。
参考代码如下。
#include<stdio.h> using namespace std; const int maxn = 10000000+ 5; int F[maxn] , num[maxn]; int Find(int t) { if(F[t] == t) return t; else return F[t] = Find(F[t]); } void bing(int a , int b) { int t1 = Find(a); int t2 = Find(b); if(t1 != t2) { F[t1] = t2; num[t2] += num[t1]; //图是有向的,这里不能写成num[t1] += num[t2]。 } } void init() { for(int i = 1;i < maxn;i++) { num[i] = 1 , F[i] = i; } } int main() { int n; int i; int x , y; while(scanf("%d",&n) != EOF) { init(); if(n == 0) { printf("1\n"); continue; } int Max1 = 0; for(i = 0;i < n;i++) { scanf("%d%d",&x,&y); if(x > Max1) Max1 = x; if(y > Max1) Max1 = y; bing(x,y); } int Max2 = 0; for(i = 1;i <= Max1;i++) { if(Max2 < num[i]) Max2 = num[i]; } printf("%d\n",Max2); } }
让我们再来看一道关于并查集的简单应用(Problem source:poj 2236)
题目大意:给出数字n,表示有n台编号[1,n]的电脑,再给出两台电脑之间最大的通信距离d。随后输入n个电脑的平面坐标。随后开始进行两种操作,一种操作‘O’表示修电脑(之前给出的n台电脑是坏的),另外一种操作‘S’是输入两个编号,此时你就需要判断这两台电脑之间能否进行通信。
数理分析:这里我们注意到题目叙述的最后一句话,如果A、B同时可以和C进行通信,那么A、B之间就可以忽略距离限制也可以进行通信,所以我们这里考虑将可以互相通信的电脑放入一个集合,那么在进行‘S’操作的时候,我们只要判断这两个电脑是否在一个集合中即可了。
这也就联系到了我们所熟悉的并查集。
编程实现:基于最基础的并查集模板,我们考虑怎样恰到好处的运用以及微小的改动。在进行‘S‘操作的时候,显然是基于已经构建好的Graph图,所以在进行‘O‘操作的时候,我们应该开始用并查集的方法构建Graph图。这里我们通过一个数组来记录每个电脑的好坏情况,然后每次进行‘O‘操作的时候,我们都遍历一遍,将该操作下修好的电脑和剩余的好的电脑依次构造并查集。
参考代码如下。
#include<iostream> #include<cstdio> #include<cstring> #include<string> #include<algorithm> using namespace std; const int maxn = 1005; int d; struct point { int pre; int x , y; }p[maxn]; int Find(int t) { if(p[t].pre == t) return t; else return p[t].pre = Find(p[t].pre); } void bing(point p1,point p2) { int t1 = Find(p1.pre); int t2 = Find(p2.pre); if(t1 != t2) if((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y) <= d*d) p[t2].pre = t1; } int main() { int num; char ope; int ok; int from, to; scanf("%d%d", &num, &d); for(int i = 1; i <= num; ++i) p[i].pre = i; bool use[maxn]; memset(use, false, sizeof(use)); for(int i = 1; i <= num; ++i) scanf("%d%d", &p[i].x, &p[i].y); while(scanf("\n%c", &ope) != EOF) { if(ope == ‘O‘) { scanf("%d", &ok); use[ok] = true; for(int i = 1; i <= num; ++i) if(use[i] && i != ok) bing(p[i], p[ok]); } else { scanf("%d%d", &from, &to); if(Find(from) == Find(to)) printf("SUCCESS\n"); else printf("FAIL\n"); } } return 0; }
下面我们开始讨论图的遍历性问题。
对于图的遍历算法,我们常用拿来分析的是欧拉图和哈密顿图,这里我们先介绍欧拉图。
针对著名的格尼斯堡蹊跷问题,欧拉回路其实就是研究一个图中,能否存在这样一个回路,使得这个回路能够不重复的访问这个图中的所有边。通俗点说,就是从一个点出发,不重复地遍历了图中所有的边和所有的顶点,这样的路径,就称为欧拉回路。
对于一个图欧拉回路存在性的分析,我们可以用到并查集做简单判断(随后会给出一个简单的证明),而如果想要具体路径,则需要借助dfs来实现。
我们简单的来分析一下如何来判断一个图中是否存在欧拉回路。基于欧拉回路的定义,它需要满足如下的条件。
①这个图应该是连通的,即不存在孤立的店,或者说只有一个父节点。
②对于一个图来说,我们先从一个点出发找到一条回路,如果这条回路没有包含图中所有的边,我们则需要重新寻找。但是我们可以看到的是最终的当前路径对应的图G1一定是最终欧拉回路对应的图G2的一个子图,因此我们需要在G1的基础上进行拓展。而在G1的基础上进行拓展(遍历剩余的边)并且又要满足欧拉回路的定义,我们看到只有一种方法,即在G1图所表示的路径<v1v2v3……vn>中,在vi节点处又形成了以vi为起始点的新回路,这就是先了在原有G1的基础上,且满足欧拉回路的限制条件,并遍历到了更多的边,这样反复下去,即可判断出欧拉回路存在与否。
基于上面的分析,我们容易看到,欧拉回路存在的充要条件是对于任意节点Vi,deta(Vi)是偶数。(Vi的度数是偶数),上面的分析是基于无向图,推广到有向图之后,就是任意节点的度数是0。
基于此,我们就可以通过并查集来简单的判断一个图是否存在欧拉回路了。
然我们结合一个问题来具体实现一下这个算法。(Problem source : 1878)
参考代码如下。
#include<stdio.h> #include<string.h> const int maxn = 1010; using namespace std; int degree[maxn] , father[maxn]; int Find(int x) { if(x == father[x]) return x; else return father[x] = Find(father[x]); } int main() { int N,M,x,y,root; while(scanf("%d",&N) ,N) { int flag
以上是关于图论及其应用——图的主要内容,如果未能解决你的问题,请参考以下文章