图的基本算法及其应用省函授讲稿.docx
- 文档编号:7582559
- 上传时间:2023-01-25
- 格式:DOCX
- 页数:45
- 大小:176.92KB
图的基本算法及其应用省函授讲稿.docx
《图的基本算法及其应用省函授讲稿.docx》由会员分享,可在线阅读,更多相关《图的基本算法及其应用省函授讲稿.docx(45页珍藏版)》请在冰豆网上搜索。
图的基本算法及其应用省函授讲稿
图的基本算法及其应用
【学习与辅导方式】
自学为主,所有提到的例题和算法都要编写完整的程序,有疑问的地方可以在QQ群中提问。
内部资料,请勿外传。
【学习要点】
(1)掌握图论算法的基本概念和求解过程
(2)掌握图论算法的几种常见模型
(3)能够图论建模
(4)能够应用图论算法解决实际问题
(5)通过总结,掌握图论的几种算法与搜索之间的区别
第一节无向图的传递闭包问题
无向图的传递闭包主要用于判断图的连通性和图中满足条件的连通分支,具有很高的实用价值。
而且,借鉴无向图的传递闭包思想,可以计算图中每一对顶点之间的最短路径(实际上就是Floyed算法的思想)。
一、判断任两个顶点之间是否有路
例1、输入一张无向图,指出该图中哪些顶点对之间有路。
输入:
n(顶点数,1<=n<=20)
e(边数,1<=e<=210)
以下e行,每行为有边连接的一对顶点。
输出:
k行,每行两个数,为存在通路的顶点对序号i,j,输出时要求i [问题分析] 1、很容易想到,可以用宽度优先或深度优先遍历来解决。 因为从任意一个顶点出发,进行一次遍历,就可以求出此顶点和其它各个顶点的连通状况。 所以只要把每个顶点作为出发点都进行一次遍历,就能知道任意两个顶点之间是否有路存在。 一次遍历的时间复杂度为O(n),要穷举每个顶点,所以总的时间复杂度为O(n*n)。 [参考程序1] programex3_1; constmaxn=20; var link,longlink: array[1..maxn,1..maxn]ofboolean; visit: array[1..maxn]ofboolean; n,e,i,j,x,y: longint; proceduredfs(i: longint); varj: longint; begin forj: =1tondo if(notvisit[j])and(link[i,j])then begin visit[j]: =true; dfs(j); end; end; begin read(n,e); fillchar(link,sizeof(link),false); fori: =1toedo begin read(x,y); link[x,y]: =true; link[y,x]: =true; end; fillchar(longlink,sizeof(longlink),false); fori: =1tondo begin fillchar(visit,sizeof(visit),false); visit[i]: =true; dfs(i); forj: =1tondolonglink[i,j]: =visit[j]; end; fori: =1ton-1do forj: =i+1tondo iflonglink[i,j]then writeln(i,'',j); end. 输入: 34 13 34 42 21 输出: 12 13 23 2、设link,longlink: array[1..20,1..20]ofBoolean;分别存放无向图和它的传递闭包。 若longlink[i,j]=true,表示顶点对i,j之间有路;否则无路。 我们采用递推(迭代)的方法不断对longlink进行运算(产生longlink(0),longlink (1),……,longlink(n))。 在递推的过程中,路径长度的“+”运算和比较大小的运算用相应的逻辑运算符“and”和“or”代替。 对于i,j和k=1,……,n,如果图中顶点i至顶点j间存在通路且通路上所有顶点的序号均属于{1,2,……,k},则定义longlinkij(k)=true;否则值为false。 有: longlinkij(k)=longlinkij(k-1)or(longlinkik(k-1)andlonglinkkj(k-1)) 由于布尔型的存储量少于整数,且位逻辑运算的执行速度快于算术运算,所以空间和时间效率都很好。 传递闭包的计算过程如下: longlink的初值赋为link; fork: =1tondo fori: =1tondo forj: =1tondo longlink[i,j]=longlink[i,j]or(longlink[i,k]andlonglink[k,j]); 显然计算的时间复杂度为O(n*n*n)。 了解Floyed算法求最短路径问题的学生,一眼就应该看出这个程序段和思想与Floyed算法完全一致,程序如下: [参考程序2] programex3_2; constmaxn=20; var link,longlink: array[1..maxn,1..maxn]ofboolean; n,e,i,j,k,x,y: longint; begin read(n,e); fillchar(link,sizeof(link),false); fori: =1toedo begin read(x,y); link[x,y]: =true; link[y,x]: =true; end; longlink: =link; fork: =1tondo fori: =1tondo forj: =1tondo longlink[i,j]: =longlink[i,j]orlonglink[i,k]andlonglink[k,j]; fori: =1ton-1do forj: =i+1tondo iflonglink[i,j]then writeln(i,'',j); end. 输入: 34 13 34 42 21 输出: 12 13 23 二、寻找满足条件的连通分支 例2、输入一张顶点带权的无向图,分别计算含顶点数最多的一个连通分支和顶点的权之和最大的一个连通分支。 输入: n(顶点数,1<=n<=20) 以下n行,依次表示顶点1~顶点n上的权; e(边数,1<=e<=210) 以下e行,每行为有边连接的一对顶点。 输出: 两行,一行为含顶点数最多的一个连通分支,一行为顶点的权之和最大的一个连通分支,输出时按顶点编号从小到大输出。 [问题分析] 我们可以先通过例1的longlink计算出每个顶点所在的连通分支,然后在所有可能的连通分支中找出满足条件的解即可。 至于计算连通分支的顶点方案,只要分别从连通分支中任选一个代表顶点,由此出发,通过深度优先搜索即可得到顶点方案。 设: best,besti分别存放含顶点数最多的连通分支中的顶点数和代表顶点; max,maxk分别存放顶点的权之和最大的连通分支的顶点权之和和代表顶点; 计算best,besti,max,maxk的过程如下: 1、读入无向图的信息; 2、计算传递闭包longlink; 3、穷举每一个顶点 forI: =1tondo begin k: =0;s: =0 forj: =1tondo{计算顶点i所在连通分支中的顶点总数k和顶点的权之和s} iflonglink[i,j]thenbegin inc(k); inc(s,顶点j的权) end; ifk>bestthenbeginbest: =k;besti: =Iend; {若k为目前最大,则记入best,i作为代表顶点记入besti} ifs>maxthenbeginmax: =s;maxk: =Iend; {若s为目前最大,则记入max,i作为代表顶点记入maxk} ifk=nthenbreak;{若整个图是连通图,则退出} end; 4、dfs(besti);{从代表顶点besti出发,深度优先搜索含顶点数最多的连通分支} 5、dfs(maxk);{从代表顶点maxk出发,深度优先搜索顶点的权之和最大的连通分支} 显然,以上算法的时间复杂度为O(n*n)。 [参考程序] programex4; constmaxn=20; var w: array[1..maxn]oflongint; link,longlink: array[1..maxn,1..maxn]ofboolean; out: array[1..maxn]ofboolean; n,e,i,j,k,s,x,y,best,besti,max,maxk: longint; proceduredfs(k: longint); vari: longint; begin fori: =1tondo if(longlink[k,i])and(notout[i])then begin out[i]: =true; dfs(i); end; end; begin read(n); fori: =1tondoread(w[i]); read(e); fillchar(link,sizeof(link),false); fori: =1toedo begin read(x,y); link[x,y]: =true; link[x,y]: =true; end; longlink: =link; fork: =1tondo fori: =1tondo forj: =1tondo longlink[i,j]: =longlink[i,j]orlonglink[i,k]andlonglink[k,j]; best: =1;besti: =1; max: =w[1];maxk: =1; fori: =1tondo begin k: =0;s: =0; forj: =1tondo iflonglink[i,j]then begin inc(k); inc(s,w[i]) end; ifk>bestthenbeginbest: =k;besti: =i;end; ifs>maxthenbeginmax: =s;maxk: =i;end; ifk=nthenbreak; end; fillchar(out,sizeof(out),false); out[besti]: =true; dfs(besti); fori: =1tondo ifout[i]thenwrite(i,''); writeln; fillchar(out,sizeof(out),false); out[maxk]: =true; dfs(maxk); fori: =1tondo ifout[i]thenwrite(i,''); writeln; end. 输入: 5 3 4 5 8 10 5 12 13 25 34 45 输出: 125 345 第二节最小生成树算法 一、生成树的概念 若图是连通的无向图或强连通的有向图,则从其中任一个顶点出发调用一次bfs或dfs后便可以系统地访问图中所有顶点;若图是有根的有向图,则从根出发通过调用一次dfs或bfs亦可系统地访问所有顶点。 在这种情况下,图中所有顶点加上遍历过程中经过的边所构成的子图称为原图的生成树。 对于不连通的无向图和不是强连通的有向图,若有根或者从根外的任意顶点出发,调用一次bfs或dfs后不能系统地访问所有顶点,而只能得到以出发点为根的连通分支(或强连通分支)的生成树。 要访问其它顶点则还需要从没有访问过的顶点中找一个顶点作为起始点,再次调用bfs或dfs,这样得到的是生成森林。 由此可以看出,一个图的生成树是不唯一的,不同的搜索方法可以得到不同的生成树,即使是同一种搜索方法,出发点不同亦可导致不同的生成树。 如下图: 但不管如何,我们都可以证明: 具有n个顶点的带权连通图,其对应的生成树有n-1条边。 二、求图的最小生成树算法 严格来说,如果图G=(V,E)是一个连通的无向图,则把它的全部顶点V和一部分边E’构成一个子图G’,即G’=(V,E’),且边集E’能将图中所有顶点连通又不形成回路,则称子图G’是图G的一棵生成树。 对于加权连通图,生成树的权即为生成树中所有边上的权值总和,权值最小的生成树称为图的最小生成树。 求图的最小生成树具有很高的实际应用价值,比如下面的这个例题。 例3、城市公交网 [问题描述] 有一张城市地图,图中的顶点为城市,无向边代表两个城市间的连通关系,边上的权为在这两个城市之间修建高速公路的造价,研究后发现,这个地图有一个特点,即任一对城市都是连通的。 现在的问题是,要修建若干高速公路把所有城市联系起来,问如何设计可使得工程的总造价最少。 [输入] n(城市数,1<=n<=100) e(边数) 以下e行,每行3个数i,j,wij,表示在城市i,j之间修建高速公路的造价。 [输出] n-1行,每行为两个城市的序号,表明这两个城市间建一条高速公路。 [举例] 下面的图(A)表示一个5个城市的地图,图(B)、(C)是对图(A)分别进行深度优先遍历和广度优先遍历得到的一棵生成树,其权和分别为20和33,前者比后者好一些,但并不是最小生成树,最小生成树的权和为19。 [问题分析] 出发点: 具有n个顶点的带权连通图,其对应的生成树有n-1条边。 那么选哪n-1条边呢? 设图G的度为n,G=(V,E),我们介绍两种基于贪心的算法,Prim算法和Kruskal算法。 1、用Prim算法求最小生成树的思想如下: ①设置一个顶点的集合S和一个边的集合TE,S和TE的初始状态均为空集; ②选定图中的一个顶点K,从K开始生成最小生成树,将K加入到集合S; ③重复下列操作,直到选取了n-1条边: 选取一条权值最小的边(X,Y),其中X∈S,not(Y∈S); 将顶点Y加入集合S,边(X,Y)加入集合TE; ④得到最小生成树T=(S,TE) 上图是按照Prim算法,给出了例题中的图(A)最小生成树的生成过程(从顶点1开始)。 其中图(E)中的4条粗线将5个顶点连通成了一棵最小生成树。 Prim算法的正确性可以通过反证法证明。 因为操作是沿着边进行的,所以数据结构采用边集数组表示法,下面给出Prim算法构造图的最小生成树的具体算法框架。 ①从文件中读入图的邻接矩阵g; ②边集数组elist初始化; Fori: =1Ton-1Do Begin elist[i].fromv: =1;elist[i].endv: =i+1;elist[i].weight: =g[1,i+1]; End; ③求出最小生成树的n-1条边; Fork: =1Ton-1Do Begin min: =maxint;m: =k; Forj: =kTon-1Do{查找权值最小的一条边} Ifelist[j].weight =elist[j].weight;m: =j;End; Ifm<>kThenBegint: =elist[k];elist[k]: =elist[m];elist[m]: =t;End; {把权值最小的边调到第k个单元} j: =elist[k].endv;{j为新加入的顶点} Fori: =k+1Ton-1Do{修改未加入的边集} Begins: =elist[i].endv;w: =g[j,s]; Ifw ThenBeginelist[i].weight: =w;elist[i].fromv: =j;End; End; End; ④输出; 2、用Kruskal算法求最小生成树的思想如下: 设最小生成树为T=(V,TE),设置边的集合TE的初始状态为空集。 将图G中的边按权值从小到大排好序,然后从小的开始依次选取,若选取的边使生成树T不形成回路,则把它并入TE中,保留作为T的一条边;若选取的边使生成树形成回路,则将其舍弃;如此进行下去,直到TE中包含n-1条边为止。 最后的T即为最小生成树。 如何证明呢? 下图是按照Kruskal算法给出了例题中图(A)最小生成树的生成过程: Kruskal算法在实现过程中的关键和难点在于: 如何判断欲加入的一条边是否与生成树中已保留的边形成回路? 我们可以将顶点划分到不同的集合中,每个集合中的顶点表示一个无回路的连通分量,很明显算法开始时,把所有n个顶点划分到n个集合中,每个集合只有一个顶点,表明顶点之间互不相通。 当选取一条边时,若它的两个顶点分属于不同的集合,则表明此边连通了两个不同的连通分量,因每个连通分量无回路,所以连通后得到的连通分量仍不会产生回路,因此这条边应该保留,且把它们作为一个连通分量,即把它的两个顶点所在集合合并成一个集合。 如果选取的一条边的两个顶点属于同一个集合,则此边应该舍弃,因为同一个集合中的顶点是连通无回路的,若再加入一条边则必然产生回路。 下面给出利用Kruskal算法构造图的最小生成树的具体算法框架。 1将图的存储结构转换成边集数组表示的形式elist,并按照权值从小到大排好序; 2设数组C[1..n-1]用来存储最小生成树的所有边,C[i]是第i次选取的可行边在排好序的elist中的下标; ③设一个数组S[1..n],S[i]都是集合,初始时S[i]=[i]。 i: =1;{获取的第i条最小生成树的边} j: =1;{边集数组的下标} Whilei<=n-1Do Begin Fork: =1TonDoBegin{取出第j条边,记下两个顶点分属的集合序号} Ifelist[j].fromvins[k]Thenm1: =k; Ifelist[j].endvins[k]Thenm2: =k; End; Ifm1<>m2ThenBegin{找到的elist第j条边满足条件,作为第i条边保留} C[i]: =j; i: =i+1; s[m1]: =s[m1]+s[m2];{合并两个集合} s[m2]: =[];{另一集合置空} End; j: =j+1;{取下条边,继续判断} End; ④输出最小生成树的各边: elist[C[i]] 3、总结 以上两个算法的时间复杂度均为O(n*n)。 参考程序见Prim.pas和Kruskal.pas。 请大家用以上两种算法完成例1。 第三节最短路径算法 最短路径是图论中的一个重要问题,具有很高的实用价值,也是信息学竞赛中常见的一类中等难度的题目,这类问题很能联系实际,考察学生的建模能力,反映出学生的创造性思维, 因为有些看似跟最短路径毫无关系的问题也可以归结为最短路径问题来求解。 本文就简要分析一下此类问题的模型、特点和常用算法。 在带权图G=(V,E)中,若顶点Vi,Vj是图G的两个顶点,从顶点Vi到Vj的路径长度定义为路径上各条边的权值之和。 从顶点Vi到Vj可能有多条路径,其中路径长度最小的一条路径称为顶点Vi到Vj的最短路径。 一般有两类最短路径问题: 一类是求从某个顶点(源点)到其它顶点(终点)的最短路径;另一类是求图中每一对顶点间的最短路径。 对于不带权的图,只要人为的把每条边加上权值1,即可当作带权图一样处理了。 例4、假设A、B、C、D、E各个城市之间旅费如下图所示。 某人想从城市A出发游览各城市一遍,而所用旅费最少,试编程输出结果。 [问题分析] 解这类问题时,很多同学往往不得要领,采用穷举法把所有可能的情况全部列出,再找出其中旅费最少的那条路径;或者采用递归(深搜)找出所有路径,再找出旅费最少的那条。 但这两种方法都是费时非常多的解法,如果城市数目多的话则很可能要超时了。 实际上我们知道,递归(深搜)之类的算法一般用于求所有解问题(例如求从A出发每个城市都要走一遍一共有哪几种走法? ),所以这些算法对于求最短路径这类最优解问题显然是不合适的。 首先,对于这类图,我们都应该先建立一个邻接矩阵,存放任意两点间的数据(距离、费用、时间等),以便在程序中方便调用,上图的邻接矩阵如下: constdis: array[1..5,1..5]ofinteger=((0,7,3,10,15), (7,0,5,13,12), (3,5,0,6,5), (10,13,6,0,11), (15,12,5,11,0)); 以下介绍几种常见的、更好的算法。 一、宽度优先搜索 宽搜也并不是解决这类问题的优秀算法,这里只是简单介绍一下算法思路,为后面的优秀算法做个铺垫。 具体如下: 1、从A点开始依次展开得到AB、AC、AD、AE四个新结点(第二层结点),当然每个新结点要记录下其旅费; 2、再次由AB展开得到ABC、ABD、ABE三个新结点(第三层结点),而由AC结点可展开得到ACB、ACD、ACE三个新结点,自然由AD可以展开得到ADB、ADC、ADE,由AE可以展开得到AEB、AEC、AED等新结点,对于每个结点也须记录下其旅费; 3、再把第三层结点全部展开,得到所有的第四层结点: ABCD、ABCE、ABDC、ABDE、ABEC、ABED、……、AEDB、AEDC,每个结点也需记录下其旅费; 4、再把第四层结点全部展开,得到所有的第五层结点: ABCDE、ABCED、……、AEDBC、AEDCB,每个结点也需记录下其旅费; 5、到此,所有可能的结点均已展开,而第五层结点中旅费最少的那个就是题目的解了。 由上可见,这种算法也是把所有的可能路径都列出来,再从中找出旅费最少的那条,显而易见也是一种很费时的算法。 二、A*算法 A*算法是在宽度优先搜索算法的基础上,每次并不是把所有可展开的结点展开,而是对所有没有展开的结点,利用一个自己确定的估价函数对所有没展开的结点进行估价,从而找出最应该被展开的结点(也就是说我们要找的答案最有可能是从该结点展开),而把该结点展开,直到找到目标结点为止。 这种算法最关键的问题就是如何确定估价函数,估价函数越准,则能越快找到答案。 A*算法实现起来并不难,只不过难在找准估价函数,大家可以自已找相关资料学习A*算法。 三、等代价搜索法 等代价搜索法也是在宽度优先搜索的基础上进行了部分优化的一种算法,它与A*算法的相似之处都是每次只展开某一个结点(不是展开所有结点),不同之处在于: 它不需要去另找专门的估价函数,而是以该结点到A点的距离作为估价值,也就是说,等代价搜索法是A*算法的一种简化版本。 它的大体思路是: 1、从A点开始依次展开得到AB(7)、AC(3)、AD(10)、AE(15)四个新结点,把第一层结点A标记为已展开,并且每个新结点要记录下其旅费(括号中的数
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 基本 算法 及其 应用 函授 讲稿