算法设计与分析


软件理论与软件工程研究室 蒋波 newdlhot@163.com 0411-84729698 (O) 13052700869 (M) 第一讲第一讲第一讲第一讲 算法的概念及其相关知识算法的概念及其相关知识算法的概念及其相关知识算法的概念及其相关知识 1.1 1.1 1.1 1.1 算法的概念 1.2 1.2 1.2 1.2 算法描述 1.3 1.3 1.3 1.3 分析算法 1.4 1.4 1.4 1.4 递归与递归消去 目录 �算法是解决问题的一种策略(方法或过程)。 1. 算法的定义及其特性 �学术界没有给出精确的定义,我们只能给出 对它的一种描述。有很多对算法的描述。 �算法就是一组有穷的规则,它规定了解决某一 特定类型问题的一系列运算。 �关于算法的定义,近年来又有新的研究。有人 从状态变换的角度给出了一种新的定义。 在分布式计算中,还有关于并行算法的概念。 1.1 1.1 1.1 1.1 算法算法算法算法 �确定性 算法的每一种运算必须要有确切的定义,即每一种运算应该执行何种动作必 须是相当清楚的、无二义性的。 �算法具有以下五个重要特性: �能行性 一个算法是能行的是指算法中有待实现的运算都是基本的运算,每种运算至 少在原理上能由人用纸和笔在有限时间内完成。 �有穷性 一个算法总是在执行了有穷步的运算之后能够终止,且每一步都可在有穷时 间内完成。 �输入 一个算法有0个或多个输入,这些输入是在算法开始之前给出的量,它取自 特定的对象集合。 �输出 一个算法产生一个或多个输出,这些输出是同输入的有某种特定关系的量。 2. 算法学习的内容 �如何设计算法 �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 �如何设计算法 算法学习的内容(算法学习的内容(算法学习的内容(算法学习的内容(1111)))) �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 设计算法的工作是不可能完全自动化。 本课程要使大家学会已被实践证明是有用 的一些基本设计策略。这些策略不仅在计 算机科学,而且在运筹学、电气工程等多 个领域都是非常有用的,利用它们已经设 计出了很多精致有效的算法。一旦掌握了 这些策略,学生自己也一定会设计出更多 新的、有用的算法。 �如何设计算法 算法学习的内容(算法学习的内容(算法学习的内容(算法学习的内容(2222)))) �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 语言是交流思想的工具,设计的算法也 要用语言恰当地表示出来。本课程中基本 采用结构程序设计的方式,并选用C程序设 计语言来简单明了地表示算法。至于结构 程序设计的内容在此不打算具体介绍,而 是将所能收集到的那些主要结构用于本课 程所给出的算法之中。作为应用实例,本 章的最后一节,将详细讨论了一种非常重 要的结构————————递归,并讨论递归的实现与 如何消除递归的问题。 �如何设计算法 算法学习的内容(算法学习的内容(算法学习的内容(算法学习的内容(3333)))) �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 一旦设计出了算法,就应证明它对所有可能 的合法输入都能给出正确的解,这一工作称为 算法确认(algorithm validation)。算法的正 确与否以及算法的优劣,与所采用的描述语言 无关,与运行算法的机器也无关。 但程序的执行时间与机器还是有关的。 一旦证明了算法的正确性,就可将其写成程 序,在将程序放到机器上运行之前,实际上还 应证明程序是正确的,即证明程序对所有可能 的合法输入都能得到正确的结果,这一工作称 为程序证明(Program Proving)。 证明程序是当前很多计算机科学工作者集中 研究的课题,目前还处于相当初期的阶段。 现阶段只能用对程序的测试来代替证明程序。 �如何设计算法 算法学习的内容(算法学习的内容(算法学习的内容(算法学习的内容(4444)))) �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 算法分析(analysis of algorithms)是 对一个算法需要多少计算时间和存储空间 作定量的分析。分析算法不仅可以预计所 设计的算法能在什么样的环境中有效地运 行,而且可以知道在最好、最坏和平均的 情况下执行得怎么样,还可以使读者对解 决同一问题的不同算法的有效性作出比较 与判断。 �如何设计算法 算法学习的内容(算法学习的内容(算法学习的内容(算法学习的内容(5555)))) �如何表示算法 �如何确认算法 �如何分析算法 �如何测试程序 测试程序实际上由调试和作时空分布图两 部分组成。 调试(debugging)程序是在抽象数据集上 执行程序,以确定是否会产生错误结果,若 有,则修改程序。但是,这一工作正如著名 计算机科学家迪伊克斯特拉(E.Dijkstra)所 说的那样,“调试只能指出有错误,而不能 指出它们不存在错误。” 作时空分布图是用各种给定的数据执行调 试认为是正确的程序并测定为计算出结果所 花去的时间和空间,以验证以前所作的分析 是否正确,同时指出实现最优化的有效逻辑 位置。 我们这门课重点讨论算法设计与算法分析 3.几点说明 �凡是算法,都必须满足以上五条特性。只满足前四条特性的一 组规则不能称为算法,只能叫做计算过程。 操作系统就是计算过程的一个重要例子。 �要制定一个算法,一般要经过设计、确认、分析、编码、检查、 调试、计时等阶段,因此学习算法必须掌握这些。 �这里的““““有穷””””概念不是纯数学的,而是在实际上是合理的,可 以接受的。 �算法和程序是不同的概念 Niklaus Wirth: Algorithm + Data Structures = Programs �学习计算机算法,必须有良好的数据结构基础。 3.几点说明 (续) �本课程中所介绍的算法,绝大部分属于求解非数值计算范畴 内的问题。 �算法设计的要求。 一个““““好””””的算法,通常要考虑如下几个目标: �正确性:算法应满足求解具体问题的需求。 对正确性的理解有不同程度 1.不含语法错误; 2.对几组输入数据能够得到满足规格需求的解; 3.对精心选择的典型数据能够得到满足规格需求的解; 4.对一切合法的输入数据都能得到满足规格需求的解。 �可读性:算法要便于交流,要有助于别人对算法的理解。 �健壮性:对于非法的输入,算法要能给出适当的反映, 而不会出现莫名其妙的错误。 �好的时空性:算法的效率要高,同时所占的存储量要低。 �描述算法可以采用多种方式,如自然语言、表格方式等。语言 是交流思想的工具,设计的算法也要用语言恰当地表示出来。 �本课程选用C程序设计语言来简单明了地表示算法。 �本课程中对算法的描述不拘泥于C语言的语法结构,必要时可采 用自然语言补充描述。 �抽象数据类型是算法设计的重要概念。抽象数据类型是由算法 的一个数据模型加上基于该模型的一组运算构成,是描述算法的 有力工具。 1.2 1.2 1.2 1.2 算法描述算法描述算法描述算法描述 �分析算法就是对已经设计好的算法从时间和空间角度定量分析 算法的优劣,目的是构造一个时空特性优良的算法。 �可约定一台““““通用””””机作为分析的基础。该通用机是一台顺序处 理机,它每次执行程序中的一条指令;带有足够容量存储器;在 固定的时间内可以把一个数从任一单元取出或存入。 �要分析一个算法,关键要做两项工作: 1.确定算法由哪些运算组成,且执行这些运算所需要的时间; 2.确定能反映出算法在各种情况下工作的数据集,即是要求编 造出能产生最好、最坏和有代表性情况的数据配置,通过使用这 些数据配置来运行算法,以了解算法的性能。 1.3.1 1.3.1 1.3.1 1.3.1 相关知识相关知识相关知识相关知识 1.3 1.3 1.3 1.3 算法分析算法分析算法分析算法分析 �一般将运算分为两类: 1.基本运算类 如加、减、乘、除包括浮点算术、比较、对变 量赋值和过程调用等。这些运算所用时间虽然不同,但一般都 只花费一个固定量的时间,因此,称它们为其时间是囿界于常 数的运算。 2.与序列长度有关的运算 如两个字符串的比较。一个字符的 比较,其时间囿界于一个常数,但两个字符串的比较时间就取 决于字符串的长度。 �算法分析的两个阶段: 1.事前分析 (a priori analysis):事前分析是算法被执行前 对算法的时空特性做出分析,求出该算法的一个时间和空间限 界函数(通常是某些参数,如输入数据的规模、语句执行频率 等的函数)。 2.事后测试(a posteriori testing):事后测试是实际运行由 该算法做成的程序并收集此算法的执行时间和实际占用空间的 统计资料。 �例 考虑下面三个程序段 x+=y; 程序段a 对于每个程序段,假定语句x+=y仅包含在当前可以看得见的循环之中,那 末,在程序段a中,此语句的频率计数为 1 ,在程序段b中为 n ,在程序段c 中为 n2 。 确定一个算法的数量级是十分重要的,它在本质上反映了一个算法所需要的 计算时间。 For (i=1;i<=n;++i) x+=y;   程序段b For (i=1; i<=n;++i) For (j=1; j<= n;++j) x+=y; 程序段c 在实际的算法分析中,往往要分析出一个算法的计算时间或频率总数,并用 某种函数(例如多项式)来表示。但算法本身可能相当复杂且存在其它许多因 素,使得在事前分析中可能根本就写不出该多项式的完整形式,甚至连最高次 项的系数都不能写出,而只能写出该项的次数并判断出这个多项式与最高次项 的关系。尽管这是件令人非常遗憾的事,但幸运的是这种关系仍反映了算法在 计算时间上的基本特性。因此,在算法的事前分析中,我们将致力于确定这种 关系。 1.3.2 1.3.2 1.3.2 1.3.2 计算时间的渐近表示计算时间的渐近表示计算时间的渐近表示计算时间的渐近表示 假设某算法的计算时间是f(n)f(n)f(n)f(n),其中变量nnnn可以是输入或输出量,也可以是 两者之和,还可以是它们之一的某种测度((((例如,数组的维数,图的边数等))))。 g(n)g(n)g(n)g(n)是在事前分析中确定的某个形式很简单的函数,例如,nnnnmmmm,loglogloglog2222nnnn,2222nnnn,n!n!n!n! 等。它是独立于机器和语言的函数,而f(n)f(n)f(n)f(n)则与机器和语言有关。 函数 名称 1 logn n nlogn n2 n3 2n n! 常数 对数 线性 n个logn 平方 立方 指数 阶乘 表1 常用的渐进函数 多项式关系:Ο(1)(1)(1)(1)<Ο(log(log(log(log2222n)n)n)n)<Ο(n)(n)(n)(n)<Ο(nlog(nlog(nlog(nlog2222n)n)n)n)<Ο(n(n(n(n2222))))<Ο(n(n(n(nnnnn)))) 指数关系:Ο(2(2(2(2nnnn))))<Ο(n!)(n!)(n!)(n!)<Ο(n(n(n(nnnnn)))),且对于任意的mmmm≥0000,有2222nnnn>nnnnmmmm 1.大写Ο符号 大写Ο符号给出了函数f的一个上限。 定义[大写Ο符号]:f(n)=Ο(g(n)),当且仅当存在正的常数 c和n0,使得对于所有的n≥n0,有 f(n)≤c*g(n) 上述定义表明,函数ffff至多是函数gggg的cccc倍,除非nnnn小于nnnn0000。因 此,对于足够大的n(n(n(n(如nnnn≥nnnn0000)))),g g g g 是 f f f f 的一个上限((((不考虑常数因 子 c c c c ))))。 在为函数 f f f f 提供一个上限函数 g g g g 时,通常使用比较简单的函数形 式。比较典型的形式是含有 n n n n 的单个项((((带一个常数系数))))。对于 对数函数lognlognlognlogn,没有给出对数的基数,是因为对于任何大于1111的常 数 a a a a 和 b b b b ,都有: loglogloglogaaaannnn=loglogloglogbbbbnnnn/loglogloglogbbbbaaaa 所以loglogloglogaaaannnn和loglogloglogbbbbnnnn都有一个相对的乘法系数1111/loglogloglogbbbbaaaa,其中 a a a a 是一 个常量。 例3.1[线性函数]f(n)=3n+2。 当n≥2时,3n+2≤3n+n=4n,所以,f(n)=Ο(n)。 f(n)是一个线性变化的函数。 采用其他方式也可以得到同样结论。 例如,对于n>0,有3n+2≤10n,可以通过选择 c=10 以及 n0>0 来满足大Ο定义。对于n≥1时,有3n+2≤3n+2n=5n, 通过选择 c=5 以及 n0=1 ,也可以满足大Ο定义。 由此可见,用来满足大Ο定义的c和n0的值并不重要,只要能 够需说明f(n)=Ο(g(n))即可,因为公式中并未出现c和n0。 同样,对于f(n)=3n+3和f(n)=100n+6,都有f(n) =Ο(n)。 正如所期望的那样,3n+2,3n+3以及100n+6都满足n的大Ο 定义,也即它们都是线性函数(对于一定的n)。 例3.23.23.23.2[平方函数]假定f(n)f(n)f(n)f(n)=10n10n10n10n2222+4n4n4n4n+2222。 对于nnnn≥2222,有f(n)f(n)f(n)f(n)≤10n10n10n10n2222+5n5n5n5n。由于当nnnn≥5555时,有5n5n5n5n≤nnnn2222,因此,对 于nnnn≥nnnn0000=5555,f(n)f(n)f(n)f(n)≤10n10n10n10n2222+nnnn2222=11n11n11n11n2222,所以f(n)f(n)f(n)f(n)=Ο(n(n(n(n2222))))。 考察另外一个具有平方复杂性的例子,令f(n)f(n)f(n)f(n)=100n100n100n100n2222+100n100n100n100n-6666。 容易看出对于所有nnnn,都有f(n)f(n)f(n)f(n)≤100n100n100n100n2222+100n100n100n100n。而对于nnnn≥100100100100,有 100n100n100n100n<nnnn2222。 因此,对于对于nnnn≥nnnn0000=100100100100,有f(n)f(n)f(n)f(n)<101n101n101n101n2222,因而f(n)f(n)f(n)f(n)=ΟΟΟΟ(n(n(n(n2222))))。 例3.33.33.33.3[指数函数]考察一个具有指数复杂性的例子f(n)f(n)f(n)f(n)=6666****2222nnnn+nnnn2222。 可以看出对于nnnn≥4444,有nnnn2222≤2222nnnn,所以,对于nnnn≥4444,有 f(n)f(n)f(n)f(n)≤6666****2222nnnn+2222nnnn=7777****2222nnnn,因此有f(n)f(n)f(n)f(n)=ΟΟΟΟ(2(2(2(2nnnn))))。 例3.43.43.43.4[常数函数]当f(n)f(n)f(n)f(n)是一个常数时,设f(n)f(n)f(n)f(n)=9999或f(n)f(n)f(n)f(n)=2033203320332033。 可以记为f(n)f(n)f(n)f(n)=ΟΟΟΟ(l)(l)(l)(l)。 因为对于f(n)f(n)f(n)f(n)=9999≤9999****1111,只要令cccc=9999以及nnnn0000=0000即可得f(n)f(n)f(n)f(n)=ΟΟΟΟ(l)(l)(l)(l)。 而对于f(n)f(n)f(n)f(n)=2033203320332033≤2033203320332033****1111,只要令cccc=2033203320332033以及nnnn0000=0000,也可得f(n)f(n)f(n)f(n)= ΟΟΟΟ(l)(l)(l)(l)。 例3.5[松散界限]当n≥2时有3n+3≤3n2,所以3n+3=Ο(n2)。 虽然n2是3n+3的一个上限,但不是最小上限,因为可以找到一 个更小的函数(在本例中为线性函数)来满足大Ο定义。 当n≥2时,有10n2+4n+2≤10n4,所以10n2+4n+2=Ο(n4), 但n4同样不是10n2+4n+2的最小上限。 类似地,6n2n+20=Ο(n22n),但n22n不是最小上限,因为可以 找到一个更小的上限为n2n。因此,6n2n+20=Ο(n2n)。 注意在上面的例子中所使用的推导策略是:用次数低的项目(如 n)替换次数高的项(如n2),直到剩下一个单项为止。 上例表明,f(n)=Ο(g(n))仅表明对干所有的n≥n0,cg(n)是 f(n)的一个上限,并未指出该上限是否为最小上限。 显然有n=Ο(n2)、n=Ο(n2.5)、n=Ο(n3)和n=Ο(2n)。 为使f(n)=Ο(g(n))有实际意义,g(n)应尽量地小。因此有 3n+3=Ο(n),而不是3n+3=Ο(n2),尽管后者也是正确的。 例3.63.63.63.6[错误界限]3n3n3n3n+2222≠Ο(l)(l)(l)(l), 因为不存在cccc>0000及nnnn0000,使得对于所有的nnnn≥nnnn0000,有3n3n3n3n+2222<cccc。 可以使用反证法来证明这个结论。假定存在这样的cccc及nnnn0000,使 得对于所有的nnnn≥nnnn0000,有3n3n3n3n+2222<cccc,即有nnnn<(c-2)/3(c-2)/3(c-2)/3(c-2)/3。于是当nnnn> maxmaxmaxmax{nnnn0000,(c-2)/3(c-2)/3(c-2)/3(c-2)/3}时,就会导出矛盾的结论。 同样,有10n10n10n10n2222+4n4n4n4n+2222≠Ο(n)(n)(n)(n),f(n)f(n)f(n)f(n)=3n3n3n3n22222222nnnn+4n24n24n24n2nnnn+8n8n8n8n2222≠Ο (2(2(2(2nnnn))))。 为了证明f(n)f(n)f(n)f(n)=3n3n3n3n22222222nnnn+4n24n24n24n2nnnn+8n8n8n8n2222≠Ο(2(2(2(2nnnn) ) ) ) ,可假定f(n)f(n)f(n)f(n)=Ο (2(2(2(2nnnn)))),因此,总存在一个cccc>0000和nnnn0000,使得对于所有的nnnn≥nnnn0000,有 f(n)f(n)f(n)f(n)≤cccc****2222nnnn。不等式两边同时除以2222nnnn得到3n3n3n3n2222+4n4n4n4n+8n8n8n8n2222/2/2/2/2nnnn≤cccc。可 见这个不等式的左边随着nnnn的增长而增大,而不等式的右边则是 一个常数,因此不等式不能对所有的nnnn都成立。 注意:f(n)f(n)f(n)f(n)=Ο(g(n))(g(n))(g(n))(g(n))并不等价于Ο(g(n))(g(n))(g(n))(g(n))=f(n)f(n)f(n)f(n)。 实际上,Ο(g(n))(g(n))(g(n))(g(n))=f(n)f(n)f(n)f(n)是无意义的。这里的符号““““=””””不表示相 等关系。可将““““=””””读作““““是””””而不是‘‘‘‘等于””””来避免这种误解。 定理3-1:如果f(n)=amnm+…………+a1n+a0且am>0,则f(n) =Ο(nm)。 证明:对于所有的n≥1有: f(n)≤|am|nm+…………+|a1|n+|a0| ≤(|am|+|am-1|/n+…………+|a0|/ nm)nm ≤(|am|+|am-1|+…………+|a0|)nm 所以有f(n)=Ο(nm)。(令c= |am|+|am-1|+…………+|a0| ) 例3.7 把定理3-1应用到例3.1、例3.2和例3.4中去,对于例3.1中的三个函数 都有m=1,因此这三个函数都为Ο(n)。对于例3.2中的函数有m=2,因此所 有函数都为Ο(n2),而对于例3.4中的两个常量来说,m=0,因此这两个常量 都为Ο(1)。 下面要介绍的定理3-2可以用来证明一个上限尽管可能满足大Ο定义,但它 并不是真正需要的上限。 采用定理3-2通常要比采用大Ο定义更容易证明f(n)=Ο(g(n))。 定理3-2[大Ο比率定理]:对于函数f(n)和g(n),若 lim f(n)/g(n)存在, n→∞ 则f(n)=Ο(g(n))当且仅当存在确定的常数c,有lim f(n)/g(n)≤c。 n→∞ 证明:如果f(n)=Ο(g(n)),则存在c>0及某个n0,使得对于所有的n≥n0, 有f(n)/g(n)<c,因此 lim f(n)/g(n) ≤c成立。 n→∞ 反之,若lim f(n)/g(n)≤c,它表明存在一个n0,使得对于所有的n≥n0, n→∞ 所以有f(n)≤max{1,c}*g(n),即f(n)=Ο(g(n)) 。 例3.8 因为lim (3n+2)/n=3,所以3n+2=Ο(n); n→∞ 因为lim (10n2+4n+2)/n2=10,所以10n2+4n+2=Ο(n2); n→∞ 因为lim (6*2n+n2)/2n=6,所以6*2n+n2=Ο(2n); n→∞ 因为lim (2n2-3)/n4=0,所以2n2-3=Ο(n4); n→∞ 因为lim (3n2+5)/n=∞,所以3n2+5≠Ο(n)。 n→∞ 上述定义表明,函数ffff至少是函数 g g g g 的 c c c c 倍,除非 n n n n 小于 nnnn0000。因 此,对于足够大的n(n(n(n(如nnnn≥nnnn0000)))),g g g g 是 f f f f 的一个下限((((不考虑常数因子 cccc ) ) ) )。与大Ο定义的应用一样,通常使用单项形式的 g g g g 函数。 g(n)g(n)g(n)g(n)仅是f(n)f(n)f(n)f(n)的一个下限,与大Ο符号的情形类似,也可能存在 多个函数g(n)g(n)g(n)g(n)满足f(n)f(n)f(n)f(n)=Ω(g(n))(g(n))(g(n))(g(n))。 为了使f(n)f(n)f(n)f(n)=Ω(g(n))(g(n))(g(n))(g(n))更有实际意义,其中的 g(ng(ng(ng(n) ) ) ) 应足够大。因 此,有3n3n3n3n+3333=Ω(n)(n)(n)(n),6666****2222nnnn+nnnn2222=Ω(n(n(n(n2222))))。 而3n3n3n3n+3333=Ω(l)(l)(l)(l),6666****2222nnnn+nnnn2222=Ω(n)(n)(n)(n)不是所希望的,尽管他们也是 正确的。 2. Ω符号 Ω符号给出了函数f的一个下限。 定义[Ω符号]:f(n)=Ω(g(n)),当且仅当存在正的常数c 和n0,使得对于所有的n≥n0,有 f(n)≥c*g(n)。 例3.9 对于所有的n≥0, 有f(n)=3n+2>3n,因此f(n)=Ω(n)。 同样地,f(n)=3n+3>3n,所以有f(n)=Ω(n), f(n)=100n+6>100n,所以100n+6=Ω(n)。 因而,3n+2,3n+3和100n+6都是带有下限的线性函数。 有f(n)=10n2+4n+2>10n2,因此f(n)=Ω(n2)。 同样地,100On2+10On-6=Ω(n2), 由于6*2n+n2>6*2n,所以,6*2n+n2=Ω(2n)。 为了说明3n+2≠Ω(n2),可以先假定3n+2=Ω(n2),因此存 在正数c和n0,使得对于所有的n≥n0,有3n+2≥cn2,即有 c n2/(3n+2)≤l,这个不等式不可能总成立,因为不等式的左 边将会随着n的增大而变得无限大。 定理3-3:如果f(n)=amnm+…………+a1n+a0且am>0,则f(n)=Ω(nm)。 证明留作习题。 例3.10 根据定理3-3可知, 3n+2=Ω(n), 10n2+4n+2=Ω(n2), 100n4+3500n2+82n+8=Ω(n4)。 定理3-4是与定理3-2类似的一个定理,采用定理3-4通常要比采 用Ω定义更容易证明f(n)=Ω(g(n))。 定理3-4 [Ω比率定理]:对于函数f(n)和g(n),若lim g(n)/f(n)存在, n→∞ 则f(n)=Ω(g(n))当且仅当存在确定的常数c,有lim g(n)/f(n)≤c。 n→∞ 证明留作习题。 例3.11 因为lim n/(3n+2)=l/3,所以3n+2=Ω(n); n→∞ 因为lim n2/(10n2+4n+2)=0.1,所以10n2+4n+2=Ω(n2); n→∞ 因为lim 2n/(6*2n+n2)=l/6,所以6*2n+n2=Ω(2n); n→∞ 因为lim n/(6n2+2)=0,所以6n2+2=Ω(n); n→∞ 因为lim n3/(3n2+5)=∞,所以3n2+5≠Ω(n3)。 n→∞ 定义表明,函数f介于函数g的c1倍和c2倍之间,除非n<n0。因 此对于足够大的n(如n≥n0), g既是f的上限,也是f的下限(不 考虑常数因子c)。与大ΟΟΟΟ定义和ΩΩΩΩ定义的应用一样,通常仅使用 单项形式的g函数。 3. ΘΘΘΘ符号 Θ符号适用于同一个函数g既可以作为f的上限,也可以作为f的下限的情形。 定义[Θ符号]:f(n)=Θ(g(n)),当且仅当存在正的常数c1,c2 和n0,使得对于所有的n≥n0,有 c1g(n)≤f(n)≤c2g(n)。 例3.12 从例3.1,3.2,3.3和3.9可以得到: 3n+2=Θ(n); 3n+3=Θ(n); 100n+6=Θ(n); 10n2+4n+2=Θ(n2); 1000n2+100n-6=Θ(n2); 6*2n+n2=Θ(2n)。 由于当n≥16,log2n<10*log2n+4≤11*log2n, 所以,10*log2n+4=Θ(log2n)。 前面曾指出logan等于logbn乘以一个常数,所以可以把 Θ(log2n)写成Θ(logn)。 在例3.6中, 证明了3n+2≠Ο(1),所以3n+2≠Θ(1)。 类似地,可以得到3n+3≠Θ(1),100n+6≠Θ(1)。 因为3n+3≠Ω(n2),所以3n+3≠Θ(n2); 因为10n2+4n+2≠Ο(n),所以10n2+4n+2≠Θ(n); 因为10n2+4n+2≠Ο(1),所以10n2+4n+2≠Θ(1); 因为6*2n+n2≠Ο(n2),所以6*2n+n2≠Θ(n2)。 同样道理,6*2n+n2≠Θ(n100),因为总可以找到这样的n0,使 得当n≥n0时,2n>n100。以及6*2n+n2≠Θ(1)。 前面已经讲到,在实际应用中,通常只使用系数为1的g函数, 而不采用3n+3=ΟΟΟΟ(3n),或10=ΟΟΟΟ(100),或10n2+4n=ΩΩΩΩ (4n2),或6*2n+n2=ΩΩΩΩ(6*2n),或6*2n+n2=ΘΘΘΘ(4*2n)等,即使这 些语句本身都是正确的。 定理3-5:如果f(n)=amnm+…………+a1n+a0且am>0,则f(n)=Θ(nm)。 证明留作习题。 例3.13 由定理3-5可知, 3n+2=Θ(n); 10n2+4n+2=Θ(n2); 100n4 +3500n2+8=Θ(n4)。 定理3-6 [Θ比率定理]:对于函数f(n)和函数g(n),若lim f(n)/g(n)及 n→∞ lim g(n)/f(n)存在,则f(n)=Θ(g(n))当且仅当存在确定的常数c,有 n→∞ lim f(n)/g(n)≤c及lim g(n)/f(n)≤c。 n→∞ n→∞ 证明留着习题。 例3.14 因为lim (3n+2)/n=3且lim n/(3n+2)=1/3<3,所以3n+2=ΘΘΘΘ(n); n→∞ n→∞ 因为lim (10n2+4n+2)/n2=10且lim n2/(10n2+4n+2)=0.1<10, n→∞ n→∞ 所以10n2+4n+2=ΘΘΘΘ(n2); 因为lim (6*2n+n2)/2n=6且lim 2n /(6*2n+n2)=1/6<6, n→∞ n→∞ 所以6*2n+n2=ΘΘΘΘ(2n); 因为lim (6n2+2)/n=∞,所以6n2+2≠Θ≠Θ≠Θ≠Θ(n)。 n→∞ 例3.15 [小写o] 因为3n+2=ΟΟΟΟ(n2)且3n+2≠Ω≠Ω≠Ω≠Ω(n2), 所以3n+2=o(n2),但3n+2≠≠≠≠o(n)。 类似地, 10n2+4n+2=o(n3),但10n2+4n+2≠≠≠≠o(n2)。 4.小写o符号 定义[小写o]:f(n)=o(g(n))当且仅当 f(n)=ΟΟΟΟ(g(n))且f(n)≠Ω≠Ω≠Ω≠Ω(g(n))。 5 特性 下面的定理可用于有关渐进符号的计算。 定理3-7:对于任一实数x>0和任一实数ε>0,下面的结论都是正确的: �存在某个n0,使得对于任何n≥n0,有(logn)x<(logn)x+ε; �存在某个n0,使得对于任何n≥n0,有(logn)x<n; �存在某个n0,使得对于任何n≥n0,有nx<nx+ε; �对于任意实数y,存在某个n0,使得对任何n≥n0,有nx(logn)y<nx+y; �存在某个n0,使得对任何n≥n0,有nx<2n。 以上各条,可直接由函数的定义出发加以证明。 例3.16 根据定理3-7,可以得到如下结论: n3+n2logn=Θ(n3); 对于任意自然数k,2n/nk=Ω(nk); n4十n2.5log20n=Θ(n4); 2nn4log3n十2nn4/logn=Θ(2nn4log3n)。 图3.1列出了一些常用的有 关Ο、Ω和Θ的标记,其中, 除n以外所有符号均为正常数。 图3.2给出了一些关于““““和”””” 与““““积””””的有用的引用规则。对 于图3.2的引用规则,大家不 难举例验证。 图3.1和3.2为今后使用渐进 符号来描述程序的时间复杂性 (或执行步数)做好了准备。 E1 c k E2 Σcini i=0 n E3 Σi i=1 n E4 Σi2 i=1 n E5 Σik,k>0 i=1 n E6 Σri,r>1 i=0 E7 n! n E8 Σ1/i i=1 图3.1 渐近标记 (其中⊕可以Ο、Ω、Θ是之一) f(n) ⊕(1) ⊕(nk) ⊕(n2) ⊕(n3) ⊕(nk+1) ⊕(rn) ⊕((n/e)n) ⊕(logn) 渐近符号 b b I1 {f(n)=⊕(g(n))} → Σf(n)=⊕(Σg(n)) n=a n=a k I2 {fi(n)=⊕(gi(n)),1≤i≤k → Σfi(n)=⊕( max {gi(n)}) 1 1≤i≤k k k I3 {fi(n)=⊕(gi(n)),1≤i≤k → ∏fi(n)=⊕(∏gi(n)) 1 1 I4 {f1(n)=Ο(g1(n)),f2(n)=Θ(g2(n))} → f1(n)+f2(n)=Ο(g1(n)+g2(n)) I5 {f1(n)=Θ(g1(n)),f2(n)=Ω(g2(n))} → f1(n)+f2(n)=Ω(g1(n)+g2(n)) I6 {f1(n)=Ο(g(n)),f2(n)=Θ(g(n))} → f1(n)+f2(n)=Θ(g(n)) 图3.2 3.2 3.2 3.2 关于Ο、Ω和Θ的引用规则 ((((其中⊕可以Ο、Ω、Θ是之一)))) 6 复杂性分析举例 在时间或步数的渐近表示中,利用了图3.1和图3.2的结论。注意,首先要 知道程序完成什么功能,然后分析程序的执行时间和执行步数,再采用渐近 表示记录它们,最后根据图3.1和图3.2得到结果。 有时,可以把Ο(g(n))、Ω(g(n))和Θ(g(n))分别解释成如下集合: Ο(g(n))={f(n)|f(n)=Ο(g(n))} Ω(g(n))={f(n)|f(n)=Ω(g(n))} Θ(g(n))={f(n)|f(n)=Θ(g(n))} 在这种解释下,诸如Ο(g1(n))=Ο(g2(n))和Θ(g1(n))=Θ(g2(n))这样的语 句就有了明确的含义。因为,此时可以将f(n)=Ο(g(n))读作““““f(n)是g(n)的 一个大Ο成员””””,另外两种的读法也类似。 小写o符号通常用于执行步数的分析。执行步数3n+Ο(n)表示3n加上上限 为n的项。在进行这种分析时,可以忽略步数少于Θ(n)的程序部分。 可以扩充Ο、Ω、Θ和o的定义,采用具有多个变量的函数。例如, f(m,n)=Ο(g(n,m))当且仅当存在正常量c、n0和m0,使得对于所有的n≥n0和 所有的m≥m0,有f(m,n)≤c*g(n,m)。 1.3.3 1.3.3 1.3.3 1.3.3 时间复杂度分析时间复杂度分析时间复杂度分析时间复杂度分析 1.常用的整数求和公式 在算法分析中,在确定语句的频率时,经常会遇到以下形式的表达式: ∑ f(i) g(n)≤≤≤≤i≤≤≤≤h(n) 其中,f(i)是一个带有有理数系数且以i为变量的多项式。这种表达式最常 用到的是以下几种形式: ∑ 1 ∑ i ∑ i2 1≤i≤n 1≤i≤n 1≤i≤n 第一式的和是n。 ∑ i =n(n+1)/2=Θ(n2) 1≤i≤n ∑ i2 =n(n+1)(2n+1)/6=Θ(n3) 1≤i≤n 通式是: ∑ iK = nnnnk+1k+1k+1k+1/(/(/(/(kkkk+1111 ) ) ) )+nk/2+低次项=Θ(nk+1) 1≤i≤n  由于它们都是有限求和,因此可列出它们的求和公式。为以后使用方 便,上面直接写出了这些求和公式的结果。 2.时间复杂性的计算 一般而言,较小的问题所需要的运行时间通常要比较大的问题所需要的时 间少。设一个程序P所占用的时间为T,则 T(P)=编译时间+运行时间。 编译时间与实例特征是无关的,且可假设一个编译过的程序可以运行多次 而无需再编译。因此分析程序的时间特性就只需考虑程序的运行时间。 令程序P的运行时间为tp(n),其中n是所要求解问题的实例特征。 由于编写程序时,影响tp的许多因素还是未知的,所以只能对tp进行估算。 由于代码P的主要操作通常包括加、减、乘、除、比较、读、写等,而这些 操作的执行时间是可预知的,从而可用下面的公式计算程序P的运行时间: tp(n)=ca*ADD(n)+cs*SUB(n)+cm*MUL(n)+cd*DIV(n)+………… 其中, ca,cs,cm,cd分别表示一个加、减、乘、除操作所需要的时间;函 数ADD(n),SUB(n),MUL(n),DIV(n)分别表示程序P中所使用的加、减、乘、 除操作的次数。 3.时间复杂性的计算 不过,算术操作的执行时间与数据类型有关,因此有必要按照数据类型 对操作进行分类。 有两个可行的方法可以用来估算一个程序的运行时间: 1.找出一个或多个关键操作,确定这些关键操作的执行所需要的时间; 2.确定程序总的执行步数。 下面分别加以讨论。 3.1 操作计数 估算一个程序的时间复杂性的一种方式就是首先选择一种或多种操作((((如 +,×或比较等)))),然后确定这些操作分别执行的次数,就可以计算出该程 序的执行时间。这种方法是否成功取决于识别关键操作((((这些操作对时间复 杂性的影响较大))))的能力。下面举例说明如何采用这种方法。 例1 [求最大元素]的程序如下: int Max(int a[],int n){ //在a[0..n-1]中找最大的元素,假设数据元素都是整型数 int pos=0; for(int i=1;i1; size--){ int j=Max(a,size); Swap(a[j],a[size-1]); } }//SelectionSort 例4 [冒泡排序]的程序如下: void BubbleSort(datatype a[],int n) {//对数组a[0..n-1]中的n个元素进行冒泡排序,元素类型为datatype Bool flag=true; for(int i=n;i>1 && flag;i--){ flag=false; for(int j=0;ja[j+1]) { Swap(a[j],a[j+1]); Flag=true;//发生了交换 } } }//BubbleSort 在例3中,Max(a,size)需要进行size-1次 比较,n次调用Max(a,size)的总的比较次数 为(n-1)*n/2。元素的移动次数为3(n-1)。 在这个例子中,如果在一次冒泡过程中没有发生数据元素的交换,则说 明数组已经排好了序,程序应该立即终止外层循环。 显然,在这个程序中,内层循环中的比较次数取决于外层的循环控制变 量,最多的情况下进行n-1次比较,最少的情况是进行1次比较。程序总的 比较次数与外层循环进行多少次有关。在待排序的数组已经有序的情况 下,外层循环只进行一次,所以在最好的情况下,比较的次数是n-1次,交 换0次;最坏的情况是待排序的数组是逆序的情况,此时,外层循环共进行 n-1次,且第i躺冒泡的过程中,比较了n-i次,交换了n-i次,所以总的比 较次数为(n-1)*n/2。 分析平均比较的次数是十分困难的事,它取决于待排序数组的分布。通 常情况下通过分析最好和最坏的情况下的操作数,然后按照等概率的情况 确定平均值。 3.2 执行步数 从关于操作计数的讨论可以看出,利用操作计数方法来估算程序的时间复杂 性忽略了所选择操作之外的其它操作的开销,而这些开销的积累值有时较大, 毫无疑问会影响到程序的执行时间。在统计执行步数的方法中,将会统计程序 在执行过程中的所有时间开销。 与操作计数法一样,执行步数也是实例特征的函数,尽管一个特定的程序可 能会有若干个特征(如输入个数,输出个数,输入和输出的大小等),但可以将 执行步数看成是其中一部分特征的函数。通过选择一些感兴趣的特征,例如, 如果要了解程序的时间复杂性是如何随着输入个数的增加而增加的情形时,就 可以将执行步数看成是输入个数的函数。因此,在确定一个程序的执行步数之 前,首先必须确切地知道将要采用的实例特征。这些特征不仅定义了执行步数 表达式中的变量,而且也定义了以多少次计算作为一步。 操作步是独立于所选特征的任意计算单位,10次加法可以视为一步,100次乘 法也可以视为一步,但n次加法不能被视为一步、m/2次加法或p+q次减法也不能 视为一步,其中,n, m,p,q都是实例特征。 由一个程序步所表示的计算量可能与其他形式表示的计算量不同。 例如,下面这条完整语句: return a+b+b*c+(a+b-c)/(a+b)+4; 可以看成是一个程序步,只要它的执行时间独立于所选用的实例特征。也可 以把如下语句看成为一个程序步: x=y; 显然,这两个程序步的计算量是不一样。 可以通过创建一个全局变量count(其初值为0)来确定一个程序或函数为完成 其预定的任务所需要的执行步数。可以将count引入到程序执行语句之中,每当 原始程序中的一条语句被执行时,就为count累加上该语句所需要的执行步数。 当程序运行结束时所得到的count的值就是所需要的执行步数。 定义[程序步]:程序步(program step)可定义为一个语法或语义意义上的程序 片段,该片段的执行时间独立于实例特征。 例5 把计算count引入到求n个数和的程序中,就可以得到下面的程序。 Void Sum(datatype a[],int n) {//求a[0..n-1]中元素之和 datatype tsum=0; count++; //对应于tsum=0的执行步数 for(int i=0;i0) { count++; //对应于return和Rsum调用 return Rsum(a,n-1) + a[n-1]; } count++; //对应于最后的return语句的执行步数 return 0; }//Rsum 在分析递归程序的执行步数时,通常可得到一个计算执行步数的递推式子。 例如,在例6中,有 ttttRsumRsumRsumRsum(n(n(n(n)=2+ t)=2+ t)=2+ t)=2+ tRsumRsumRsumRsum(n-1) (n-1) (n-1) (n-1) 当nnnn大于0000时, , , , t t t tRsumRsumRsumRsum(0)=2 (0)=2 (0)=2 (0)=2 当n=0n=0n=0n=0时 对于本例,可以做如下推算 ttttRsumRsumRsumRsum(n(n(n(n)=2+t)=2+t)=2+t)=2+tRsumRsumRsumRsum(n-1)=4+t(n-1)=4+t(n-1)=4+t(n-1)=4+tRsumRsumRsumRsum(n-2)=(n-2)=(n-2)=(n-2)=…………=2n+t=2n+t=2n+t=2n+tRsumRsumRsumRsum(0)=2(n+1)(0)=2(n+1)(0)=2(n+1)(0)=2(n+1) 其中,n≥0。因此,程序Rsum的执行步数是2(n+1)。 比较以上两个例子,Sum的执行步数是2n+3,而Rsum的执行步数是2(n+1), 但这不能说明Sum就比Rsum慢,因为程序步不代表精确的时间单位。Rsum中的一 步可能要比Sum中一步花更多的时间。 执行步数可用来帮助我们了解程序的执行时间是如何随实例特征的变化而变 化的。比如,我们会看到Sum的执行时间将随着n的增加而呈线性增加趋势。 例7 [矩阵加]考虑两个rows行和cols列的矩阵a和b的相加 Void Add(float a[],float b[],float c[],int rows,int cols) {//求矩阵c[0..rows-1,0..cols-1]=a[0..rows-1,0..cols-1]+b[0..rows-1,0..cols-1] for(int i=0; icols,则 外层循环该成对列的循环是合理的,因为我们可以得到一个较少的执行步数。 注意,在本例中的实例特征是rows和cols。 计算执行步数的一种有效的方法是列表,而不是在程序中增加count。我们 可以首先确定每条语句每一次执行所需要的步数以及该语句总的执行次数 (即频率),然后利用这两个量就可以得到每条语句总的执行步数,把所有 语句的执行步数求和就可以得到整个程序的执行步数。 一条语句的程序步数与该语句每次执行所需要的步数(s/e)之间有重要的区 别,区别就在于程序步数不能反映该语句的复杂程度。例如,语句 x=Sum(a,m) 的程序步数是1,但执行该语句所导致的count值却应该是:1+调用Sum的过程 中的count的值。我们已经知道Sum的执行步数是2n+3,因此执行x=Sum(a,m) 时所需要的步数是2n+4,而不是1。 语句每次执行所需要的步数通常记为s/e,一条语句的s/e就是执行时该语 句所产生的count值。 例8888 列表计算程序sumsumsumsum的执行步数。 语句 s/es/es/es/e 频率 总步数 Void Sum(datatype a[],int n) {//求a[0..n-1]中元素之和 datatype tsum=0; for(int i=0;i0) if(n>0) if(n>0) if(n>0) return Rsum(a,n-1)+a[n-1] return Rsum(a,n-1)+a[n-1] return Rsum(a,n-1)+a[n-1] return Rsum(a,n-1)+a[n-1]; return 0return 0return 0return 0; }//}//}//}//RsumRsumRsumRsum 0000 0000 1111 1111 1111 0 0 0 0 0000 0000 n+1n+1n+1n+1 nnnn 1111 0 0 0 0 0000 0000 n+1n+1n+1n+1 nnnn 1111 0000 合计 2n+22n+22n+22n+2 例10 列表计算程序Add的执行步数。 语句 s/es/es/es/e 频率 总步数 Void Void Void Void Add(a[],b[],c[],intAdd(a[],b[],c[],intAdd(a[],b[],c[],intAdd(a[],b[],c[],int rows,introws,introws,introws,int cols) cols) cols) cols) {//{//{//{//求矩阵c=a+bc=a+bc=a+bc=a+b for(intfor(intfor(intfor(int i=0; i=0 && x=0 && x=0 && x=0 && x=0 && x=0 && x=0 && x=0 && x=0),则执行步数为2n-2j+3,这是因为在j处插入x时,for语句的循环体执 行了n-j次。所以平均执行步数为: n nn nn nn n tavgtavgtavgtavgInsertInsertInsertInsert(nnnn)= = = = ———— ∑(2n-2j+3) = (2n-2j+3) = (2n-2j+3) = (2n-2j+3) = ———— (2 (2 (2 (2∑k+3(n+1)) = n+3k+3(n+1)) = n+3k+3(n+1)) = n+3k+3(n+1)) = n+3 j=0 k=0j=0 k=0j=0 k=0j=0 k=0 计算平均执行步数 1111 n+1n+1n+1n+1 1111 n+1n+1n+1n+1 事后测试是在对算法进行设计、确认、事前分析、编码和调试之后要做的工 作,以确定程序所耗费的精确时间和空间,即作时空性能分布图。由于事后测 试与所用计算机密切相关,故在此只对这一阶段所要进行的基本工作和若干注 意之点概略地作些介绍。 以作时间分布图为例,要确定算法精确的计算时间,首先必须在所用计算机 上配置一台能读出时间的时钟。有了时钟还必须了解它的精确程度以及计算机 所用操作系统的工作方式,因为前者随所用计算机的不同而有相当大的差异, 如果在一台时钟精确度不高的计算机上运行需时很少((((譬如说比时钟的误差值还 小))))的程序,那末,所得的计时图只不过是一些““““噪声””””,时间分布性能完全被淹 没在这些““““噪声””””之中。如果后者是以多道程序或分时方式工作的操作系统,则 在取得算法工作的可靠时间上会出现困难,尤其对于那些时钟计时中包含了换 出磁盘上的用户程序要用的那部分时间的操作系统而言。由于时间随当前记入 系统的用户数而变化,因此无法确定算法本身所花去的时间。 1.3.4 1.3.4 1.3.4 1.3.4 作时空性能分布图作时空性能分布图作时空性能分布图作时空性能分布图 为解决因时钟误差而引起的““““噪声””””问题,下面推荐两种可供选用的方法: 第一种是增加输入规模,直至得到算法所需的可靠的时间总量; 第二种方法是取足够大的rrrr,将此算法反复执行rrrr次,然后用rrrr去除总的时间 得到算法运行的平均时间。 在解决了计时方面的具体技术问题后,就可考虑如何作出时间性能分布图。 对于事前分析为Ω(g(n))(g(n))(g(n))(g(n))时间的算法,应选择输入规模不断增大的数据集。用这 些数据集在计算机上运行程序,从而得到使用这些数据集情况下算法所耗费的 时间,并画出这一数量级的时间曲线。若这曲线与事前分析所得曲线形状基本 吻合,则验证了早先分析的结论。对于事前分析为Ο(g(n))(g(n))(g(n))(g(n))时间的算法,则应在 各种规模的范围内分别按最好、最坏和平均情况的那些数据集独立运行程序, 作出各种情况的时间曲线,并由这些曲线来分析最优的有效逻辑位置。 另外,如果为了解决某一个问题,分别设计了几种具有同一数量级的不同算 法,或者为加快某种算法速度,作了在同一数量级情况下的一些改进,那末, 只要在输入相同数据集的情况下分别作出它们的时间分布图就可比较出哪一个 算法更快些。 第三节完 1.4 递归和消去递归 尽管递归程序在执行时间上往往比非递归程序要付出更多,但有很多问题的数 学模型或算法设计方法本来就是递归的,用递归过程来描述它们不仅非常自然, 而且证明该算法的正确性也比相应的非递归形式容易得多,因此递归不失为是一 种强有力的程序设计方法。 下面举几个使用递归的例子。 例4.14.14.14.1 斐波那契(Fibonacci)(Fibonacci)(Fibonacci)(Fibonacci)序列1111,1111,2222,3333,5555,8888,13131313,21212121,34343434,的定义为: F0=F1=1F0=F1=1F0=F1=1F0=F1=1,FiFiFiFi=Fi-1Fi-1Fi-1Fi-1+Fi-2Fi-2Fi-2Fi-2,i>li>li>li>l 求解这一数学问题的算法描述如下:::: 算法4.1 4.1 4.1 4.1 求斐波那契数 intintintint void void void void F(intF(intF(intF(int n) { n) { n) { n) { // // // //返回第nnnn个斐波那契数//////// if(n<0) return error if(n<0) return error if(n<0) return error if(n<0) return error else else else else if(n<1) return 1 if(n<1) return 1 if(n<1) return 1 if(n<1) return 1 else else else else return (F(n-1)+F(n-2)); return (F(n-1)+F(n-2)); return (F(n-1)+F(n-2)); return (F(n-1)+F(n-2)); }//F }//F }//F }//F 1.4.l 递归 这个程序的优点是它与其数学定义在句法上几乎一样;缺点是此程序的时间 性能很差。其主要原因并不是使用递归引起的,而是由计算方式导致的。在这 个程序中,很多值都被重复计算了多次,例如,F(n-2)计算了两次,F(n-3)计 算了三次,而F(n-4)则计算了五次,………… 。 例4.2[欧几里得算法]已知两个非负整数a和b,且a>b>0,求这两个数的最 大公因数。 解决这个问题的方法是采用辗转相除法: 若b=0,则a和b的最大公因数就是a; 若b>0,则a和b的最大公因数等于b和用b除a的余数的最大公因数。 例如: GCD(22GCD(22GCD(22GCD(22,8)=GCD(88)=GCD(88)=GCD(88)=GCD(8,6)=GCD(66)=GCD(66)=GCD(66)=GCD(6,2)=GCD(22)=GCD(22)=GCD(22)=GCD(2,0)=20)=20)=20)=2。 GCD(21GCD(21GCD(21GCD(21,13)=GCD(1313)=GCD(1313)=GCD(1313)=GCD(13,8)=GCD(88)=GCD(88)=GCD(88)=GCD(8,5)=GCD(55)=GCD(55)=GCD(55)=GCD(5,3)=GCD(33)=GCD(33)=GCD(33)=GCD(3,2)2)2)2) =GCD(2 =GCD(2 =GCD(2 =GCD(2,1)=GCD(11)=GCD(11)=GCD(11)=GCD(1,0)=10)=10)=10)=1。 算法4.2 求最大公因数 int void GCD(int a,int b) { //假设a>b>0// If(b=0) return a; else return GCD(b,a mod b); }//GCD 由于递归的概念首先在数学领域中使用,所以往往产生一种错觉,认为递归 只适用于计算““““数学””””函数。其实不然,递归过程也可用于检索过程等。 例4.3 已知元素x,判断x是否在a(1:n)中。 算法思想:在a(1:n)中检索x, 若存在,返回该元素在a[]中的下标,否则,返回0。 解决这一问题的递归算法可描述如下: 算法4.3 在数组a(1:n)中检索x是否存在 int void Search(int a[],int i,int x) { //设a[1..n]和x是全局变量 //在a[1..n]中若有元素a[k]=x,则返回x第一次出现的下标k,否则返回0 if(i>n) return 0 else if(a[i]==x) return i else return Search(i+1); }//Search 为了确定x是否在a(l:n)中,应首先在主程序中调用Search(1)。 当然该问题也可用迭代形式来描述,但使用递归时就无需使用循环语句了。 对于那些本来就是要用递归设计求解的问题,在设计其算法时,能不能考虑 既发挥递归表示的直观性与算法正确性易于证明的特点,又克服由于使用递归 而带来总开销增加的不足呢?为此,建议采用如下办法: 在算法设计的初期阶段使用递归,一旦所设计的递归算法被证明为正确且确 信是一个好算法时,就可以消去递归,把该算法翻译成与之等价的、只使用迭 代的算法。 这一翻译过程可使用一组简单的转换规则来完成;还可根据具体情况将所得 到的选代算法作进一步的改进,以提高迭代过程的效率。 下面就介绍将直接递归过程翻译成只使用迭代过程的一组规则。对于间接递 归过程的处理只需把这组规则作适当修改即可。 所谓翻译主要是,将递归过程中出现递归调用的地方,用等价的非递归代码 来代替,并对returnreturnreturnreturn语句作适当处理。 这组规则如下: 1.4.2 消去递归 1. 1. 1. 1. 在过程的开始部分,插人说明为栈的代码并将其初始化为空。 使用栈来保存递归过程中的断点信息。断点信息包括如下几个内容: ① 调用的参数; ② 局部变量; ③ 调用的中间结果(函数的值) ④ 返回地址 2. 2. 2. 2. 将标号L1L1L1L1附于第一条可执行语句。然后,对于每一处递归调用都用一组执 行下列规则的指令来代替。 3. 3. 3. 3. 将所有参数和局部变量的值存入栈。栈顶指针设计成一个全程变量。 4. 4. 4. 4. 建立第iiii个新标号LiLiLiLi,并将iiii存入栈。 该标号的iiii值将用来计算返回地址,该标号用在规则7777所描述的程序段中。 5. 5. 5. 5. 计算这次调用的各实在参数((((可能是表达式))))的值,并把这些值赋给相应的形 式参数。 6. 6. 6. 6. 插入一条无条件转向语句转向过程的开始部分,实现循环。 7. 7. 7. 7. 若是函数过程,则对递归过程中含有此次函数调用的语句作如下处理: ① 将该语句的此次函数调用部分用从栈顶取回该函数值的代码来代替; ② 其余部分的代码按原描述方式照抄; ③ 将规则4444中所建立的标号附于这条语句上。 若此过程不是函数,则将4444中建立的标号附于规则6666所产生的转移语句后面的 那条语句上。 以上步骤是消去过程中的所有递归调用,此外,还需要对递归过程中的returnreturnreturnreturn 语句进行处理。注意纯过程结束处的endendendend需看成一条没有返回值的returnreturnreturnreturn语句。 在每个有returnreturnreturnreturn语句的地方,执行下述规则: 8. 8. 8. 8. 如果栈为空,则执行正常返回。 9. 9. 9. 9. 否则,将所有输出参数((((即理解为 out out out out 或 inoutinoutinoutinout 型的参数))))的当前值赋给栈顶 上的那些对应的变量。 10101010. . . . 如果栈中有返回地址标号的下标,就插入一条此下标从栈中退出的代码, 并把这个下标值赋给一个未使用的变量。 11111111. . . . 从栈中退出所有局部变量和参数的值并把它们赋给对应的变量。 12121212. . . . 如果这个过程是函数,则插人以下指令,这些指令用来计算紧接在returnreturnreturnreturn 后面的表达式并将结果值存入栈顶。 13. 13. 13. 13. 用返回地址标号的下标实现对该标号的转向。 在一般情况下,使用上述规则都可将一个直接递归过程正确地翻译成与之等 价的只使用迭代的过程。它的效率通常比原递归模型要高,进一步简化这程序 可使效率再次改进。 下面举一个递归转化为迭代的例子,虽然例中的问题最好是使用迭代来求 解,而且若用递归描述反而变得不很直观,但是,该例子可帮助学习者增加对 以上规则的一些感性认识。 例4.44.44.44.4 写一个求数组A(iA(iA(iA(i:n)n)n)n)中最大元素的过程。 算法4.4 4.4 4.4 4.4 递归求取最大值 intintintint void Max1(int i){ void Max1(int i){ void Max1(int i){ void Max1(int i){ // // // //这是一个函数过程,它返回A(1A(1A(1A(1:n)n)n)n)中最大元素的最大下标 if(ia[j]) k=i;if (a[i]>a[j]) k=i;if (a[i]>a[j]) k=i;if (a[i]>a[j]) k=i; else k=j; } else k=j; } else k=j; } else k=j; } else else else else k=n; k=n; k=n; k=n; return k return k return k return k ; }//Max1}//Max1}//Max1}//Max1 用几个简单的数据实验一下这个算法,就会明白该算法是个递归模型。 在运行时间上,由于过程调用和隐式栈管理方面的消费,使我们自然考虑到要 消去递归。 算法4.54.54.54.5是与算法4.44.44.44.4等价的迭代算法 intintintint void Max2(int i){ void Max2(int i){ void Max2(int i){ void Max2(int i){ intintintint j j j j,kkkk; intintintint n n n n,a[1..n]a[1..n]a[1..n]a[1..n]; //n//n//n//n和数组a[1..n]a[1..n]a[1..n]a[1..n]是全局变量 iniStackiniStackiniStackiniStack S S S S; ////////规则1111 top=0 top=0 top=0 top=0; ////////规则1111 L1L1L1L1:if(ia[j]) k=i; if(a[i]>a[j]) k=i; if(a[i]>a[j]) k=i; if(a[i]>a[j]) k=i; else k=j; else k=j; else k=j; else k=j; }//il) { i=i-1 if(a[i]>a[k]) k=i; };//while return k; //Max3 消去递归的目的是为了获得效率更高且在计算上等效的迭代程序。因为在某 些场合下可能还有更简单的转换规则,所以不必在任何情况下都死搬硬套前面 所叙述的十三条规则,而应具体情况具体分析。例如,若过程只有最后一条语 句是递归调用,则可通过简单地计算过程调用中实在参数的值并转向过程开始 部分来消去递归,且不需要栈。过程GCD就是这样一个例子。 消去递归后得到下面程序。 算法4.7 与算法4.3等价的迭代算法 int GCD1(int a,int b) { L1:if(b=O) return a; else {t=b;b=a mod b;a=t;goto L1;} }//GCD1 稍作整理可得: 算法4.8算法是算法4.7的改进模型 int void GCD2(int a,int b) { while(b<>0) { t=b;b=a mod b;a=t;};//while return a }//GCD2 当然,如果所使用机器的编译程序能将递归过程编译成有效的代码,那末也 不必将递归化为迭代。 第一讲第一讲第一讲第一讲 完了完了完了完了 软件理论与软件工程研究室 蒋波 第三讲第三讲第三讲第三讲 分治法分治法分治法分治法 3.1 分治方法的一般描述 3.2 二分检索 3.3 求最大、最小元素 3.4 归并分类 目录 3.6 选择问题 3.5 快速分类 3.7 斯特拉森矩阵乘法 要使计算机能完成人们预定的工作,首先必须为如何完成该工作设计一个算 法,然后再根据算法编写程序。计算机程序要对所求解问题的每个对象和处理 规则给出正确详尽的描述,其中程序的数据结构和变量用来描述对象;程序结 构、函数和语句用来描述算法。算法和数据结构是程序的两个重要方面。 算法是问题求解过程的精确描述,一个算法由有限条可完全机械地执行的、 有确定结果的指令组成。指令正确地描述了要完成的任务和它们被执行的顺序。 计算机按算法指令所描述的顺序执行算法的指令能在有限的步骤内终止,或终 止于给出问题的解,或终止于指出问题对此输入数据无解。 通常求解一个问题可能会有多种算法可供选择,选择的主要标准首先是算法 的正确性和可靠性,简单性和易理解性。其次是算法所需要的存储空间要尽可 能地少和执行时间要尽可能地短等。 算法设计是一件非常困难的工作。常用的算法设计技术有迭代法、穷举搜索 法、递推法、贪婪法、回溯法、分治法和动态规划法等。另外,为了以更简洁 的形式设计和描述算法,在设计算法时常采用递归技术,用递归描述算法。 本讲中,主要介绍分治法。 引言引言引言引言 3.1 3.1 3.1 3.1 分治方法的一般描述分治方法的一般描述分治方法的一般描述分治方法的一般描述 分治法是最广泛使用的算法设计方法之一,其基本思想: 把大问题分解成一些较小的问题,然后由小问题的解方便地构造出大 问题的解。 当要求解一个输入规模为 n 且 n 的值又相当大的问题时,直接求解 往往是非常困难的,有时甚至根本无法直接求解。解决这类问题的一般 原则是:首先仔细分析问题本身所具有的特性,然后根据这些特性选择 适当的设计策略加以求解。 当将待求解问题具有以下几个特征时,可以采用分治法来求解: 问题的 n 个输入可以分解成 k 个不同子集合且能够得到 k 个不同的、 可独立求解的子问题(1<k≤n);求出每个子问题的解后,能找到适当 的方法将它们合并成整个问题的解。 这种求解的思想是将整个问题分成若干个小问题后分而治之。通常, 由分治法所得到的子问题与原问题应该具有相同的类型。如果得到的子 问题相对来说还较大,则可反复使用分治策略将其分解成更小的同类型 子问题,直至所分解出的子问题都可方便地求出相应的解。 显然,分治法求解问题很自然地可用一个递归过程来表示。 在很多考虑采用分治法求解的问题中,往往把输入分成与原问题类型 相同的两个子问题,即取 k=2 k=2 k=2 k=2 。为了能清晰地反映采用分治策略设计算 法的基本步骤,下面用一个称之为抽象化控制的过程来非形式的描述算 法的控制流向。基于以上目的,此过程中的基本运算由一些未定义具体 含义的过程来表示。在过程中还使用了一个全程变量数组A(1:n)A(1:n)A(1:n)A(1:n),用它 来存放((((或指示) n ) n ) n ) n 个输入。过程 div div div div 是个函数,初始调用为div(1,n)div(1,n)div(1,n)div(1,n)。对 于一般情况,div(p,qdiv(p,qdiv(p,qdiv(p,q) ) ) ) 是求解输入为A(p:q)A(p:q)A(p:q)A(p:q)的子问题函数。 算法1.11.11.11.1 分治策略的抽象化控制描述 void div(p,q) {void div(p,q) {void div(p,q) {void div(p,q) { intintintint n n n n,A[n]A[n]A[n]A[n]; ////////定义成全程变量 intintintint m m m m,pppp,qqqq; //1//1//1//1≤pppp≤qqqq≤nnnn if(small(p,q)) return(answer(p,q)) if(small(p,q)) return(answer(p,q)) if(small(p,q)) return(answer(p,q)) if(small(p,q)) return(answer(p,q)); else else else else { { { { m = divide(p,q) m = divide(p,q) m = divide(p,q) m = divide(p,q); //p//p//p//p≤mmmm<qqqq return (combine(div(p,m) return (combine(div(p,m) return (combine(div(p,m) return (combine(div(p,m),div(mdiv(mdiv(mdiv(m+1,q)))1,q)))1,q)))1,q))); }}}}; }//div}//div}//div}//div 在算法1.11.11.11.1中,small(p,q)small(p,q)small(p,q)small(p,q)是一个布尔值函数,它用以判断输入为A(p:q)A(p:q)A(p:q)A(p:q)的问题 是否小到无需进一步细分就能算出其答案的程度。 若是,则调用能直接计算此规模下的子问题解的函数answer(p,q)answer(p,q)answer(p,q)answer(p,q); 若否,则调用分割函数divide(p,q)divide(p,q)divide(p,q)divide(p,q),返回一个新的分割点mmmm((((整数))))。 于是,原问题被分成输入为A(p:m)A(p:m)A(p:m)A(p:m)和A(m+1:q)A(m+1:q)A(m+1:q)A(m+1:q)的两个子问题。对这两个子问 题分别递归调用divdivdivdiv得到各自的解xxxx和yyyy,再用一个合并函数combine(x,y)combine(x,y)combine(x,y)combine(x,y)将这两个 子问题的解合成原问题((((输入为 A(p,q))A(p,q))A(p,q))A(p,q))的解。 倘若所分成的两个子问题的输入规模大致相等,则divdivdivdiv总的计算时间可用下面 的递归关系式来表示: T(n)= g(n) 当n足够小, 2T(n/2)+f(n) 否则 其中,T(n)T(n)T(n)T(n)是输入规模为nnnn的divdivdivdiv的运行时间,g(n)g(n)g(n)g(n)是输入规模足够小以至于能 直接求解时的运行时间,f(n)f(n)f(n)f(n)是combinecombinecombinecombine的时间。 显然用递归过程描述以分治法为基础的算法是理所当然的,但为了提高效 率,往往需要将这一递归形式转换成迭代形式。算法3.23.23.23.2就是针对算法1.11.11.11.1,运用 上一讲中的转换规则转换成非递归(迭代)并进一步优化后的形式。 算法1.2 1.2 1.2 1.2 分治法抽象化控制的迭代形式。 void div1(p,q) {void div1(p,q) {void div1(p,q) {void div1(p,q) { //div //div //div //div的迭代模型,定义了一个适当大小的工作栈 intintintint s s s s,tttt; intiStack(sqStackintiStack(sqStackintiStack(sqStackintiStack(sqStack)))); ////////定义工作栈sqStacksqStacksqStacksqStack L1L1L1L1:while(!small(p,q)) {while(!small(p,q)) {while(!small(p,q)) {while(!small(p,q)) { m = m = m = m = divied(p,qdivied(p,qdivied(p,qdivied(p,q)))); ////////确定如何分割输入 push(sqStack,(p,q,m,0,2))push(sqStack,(p,q,m,0,2))push(sqStack,(p,q,m,0,2))push(sqStack,(p,q,m,0,2)); ////////处理第一次递归调用 q = mq = mq = mq = m; }}}};//while//while//while//while t = answer(p,q) t = answer(p,q) t = answer(p,q) t = answer(p,q); while(!StackEmptywhile(!StackEmptywhile(!StackEmptywhile(!StackEmpty( ( ( ( sqStacksqStacksqStacksqStack )) { )) { )) { )) { pop(sqStack,(ppop(sqStack,(ppop(sqStack,(ppop(sqStack,(p, q, m, s, ret)), q, m, s, ret)), q, m, s, ret)), q, m, s, ret)); ////////退栈,retretretret为返回地址 if(ret==2) {if(ret==2) {if(ret==2) {if(ret==2) { push(sqStack,(ppush(sqStack,(ppush(sqStack,(ppush(sqStack,(p, q, m, t, 3)), q, m, t, 3)), q, m, t, 3)), q, m, t, 3)); ////////处理第二次递归调用 p = m + 1p = m + 1p = m + 1p = m + 1; go to L1go to L1go to L1go to L1;}}}} else { else { else { else { t = combine(s,t) t = combine(s,t) t = combine(s,t) t = combine(s,t); ////////将两个子问题的解合并成一个解 }}}};//if//if//if//if }}}};//while//while//while//while return t return t return t return t; }//div1 }//div1 }//div1 }//div1 当然,这个算法还可以简化 下面举几个利用分治法求解的实际例子。 例1.1.1.1. 著名的HanoiHanoiHanoiHanoi塔问题描述如下。 有 AAAA,BBBB,CCCC三根针,和一些中间有孔的大小不等金片。初始时,金片由小到 大放在AAAA针上,其中最大的在下面,最小的在最上面。要求将AAAA针上的金片全部 移至BBBB针上,每次只能移动一张金片,且任何时刻不允许将大金片放在小金片 的上面。CCCC针可以作为移动金片的辅助位置。问题要求给出金片的移动过程。 HanoiHanoiHanoiHanoi塔问题的求解过程可采用分治法,其算法由以下三个步骤组成: (1)(1)(1)(1) 将AAAA针上最上面的 n-1 n-1 n-1 n-1 张金片移至CCCC针; (2)(2)(2)(2) 将AAAA针上的第 n n n n 张金片移至BBBB针; (3)(3)(3)(3) 将暂存于CCCC针上的 n-1 n-1 n-1 n-1 张金片移至BBBB针。 显然,这是一个递归过程,可以用递归技术加以实现。虽然移动金片的过程 是一张一张地进行的,但按分而治之的思想,可以将移动上面的n-1n-1n-1n-1张金片和移 动最下面的第nnnn张金片这两件事件分开,从而将nnnn张金片的移动转换成更少张数 金片的移动,并且不必考虑各针下面金片的情况。 从概念上来理解,该算法的正确性是非常明显的。例1111说明分治法是一种很重 要的方法,并且在大多数情况下,还是一个很有效的方法。 算法1.3 Hanoi1.3 Hanoi1.3 Hanoi1.3 Hanoi塔问题的算法描述: void void void void hanoi(inthanoi(inthanoi(inthanoi(int n, char x, char y, char z) n, char x, char y, char z) n, char x, char y, char z) n, char x, char y, char z) // // // //将针xxxx上由小到大编号为1111至nnnn的nnnn个金片移动到zzzz针上,yyyy为辅助针 ////////移动操作movemovemovemove(x,n,zx,n,zx,n,zx,n,z)可定义为将编号为nnnn的金片从xxxx移动到zzzz 1 {1 {1 {1 { 2 if(n==1) 2 if(n==1) 2 if(n==1) 2 if(n==1) 3 3 3 3 move(xmove(xmove(xmove(x, 1, z), 1, z), 1, z), 1, z); 4 else {4 else {4 else {4 else { 5 hanoi(n-1, x, z, y)5 hanoi(n-1, x, z, y)5 hanoi(n-1, x, z, y)5 hanoi(n-1, x, z, y); 6 6 6 6 move(xmove(xmove(xmove(x, n, z), n, z), n, z), n, z); 7 hanoi(n-1, y, x, z)7 hanoi(n-1, y, x, z)7 hanoi(n-1, y, x, z)7 hanoi(n-1, y, x, z); 8 }8 }8 }8 } 9 }// 9 }// 9 }// 9 }// hanoihanoihanoihanoi 注:语句前面的编号是为了说明执行过程而添加的,这些编 号不属于算法本身。 例2.2.2.2. 为参加网球比赛的选手安排比赛日程问题。 设有n(n=2n(n=2n(n=2n(n=2kkkk))))位选手参加网球循环赛,循环赛共进行n-1n-1n-1n-1天,每位选手要与其 他n-ln-ln-ln-l位选手赛一场,且每位选手每天赛一场,不轮空。试按此要求为比赛安排 日程。 设nnnn位选手的顺序编号为1111,2222,…………,nnnn。比赛的日程表是一个nnnn行n-1n-1n-1n-1列的表,iiii 行jjjj列的内容是第iiii号选手在第jjjj天的比赛对手编号。用分治法设计日程表,就是 从其中一半选手(2(2(2(2n-1n-1n-1n-1位))))的比赛日程,导出全体(2(2(2(2nnnn位))))选手的比赛日程。从众所 周知的两位选手的比赛日程出发,重复这个过程,直至nnnn位选手的比赛日程全部 安排好为止。 1 2 3 4 5 6 71 2 3 4 5 6 71 2 3 4 5 6 71 2 3 4 5 6 7 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 2 1 4 3 6 7 8 5 2 1 4 3 6 7 8 5 2 1 4 3 6 7 8 5 2 1 4 3 6 7 8 5 3 4 1 2 7 8 5 6 3 4 1 2 7 8 5 6 3 4 1 2 7 8 5 6 3 4 1 2 7 8 5 6 4 3 2 1 8 5 6 7 4 3 2 1 8 5 6 7 4 3 2 1 8 5 6 7 4 3 2 1 8 5 6 7 5 6 7 8 1 4 3 2 5 6 7 8 1 4 3 2 5 6 7 8 1 4 3 2 5 6 7 8 1 4 3 2 6 5 8 7 2 1 4 3 6 5 8 7 2 1 4 3 6 5 8 7 2 1 4 3 6 5 8 7 2 1 4 3 7 8 5 6 3 2 1 4 7 8 5 6 3 2 1 4 7 8 5 6 3 2 1 4 7 8 5 6 3 2 1 4 8 7 6 5 4 3 2 1 8 7 6 5 4 3 2 1 8 7 6 5 4 3 2 1 8 7 6 5 4 3 2 1 (c)(c)(c)(c)八位选手 比赛日程表 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 2 1 4 3 2 1 4 3 2 1 4 3 2 1 4 3 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 4 3 2 1 4 3 2 1 4 3 2 1 4 3 2 1 (b) (b) (b) (b) 四位选手 比赛日程表 1 1 1 1 1 2 1 2 1 2 1 2 2 1 2 1 2 1 2 1 (a)(a)(a)(a)两位选手 比赛日程表 图3.1 3.1 3.1 3.1 选手比赛日程安排示意图 为了从2222m-1m-1m-1m-1位选手的比赛日程表,导出2222 m m m m位选手的比赛日程表,假定只有8888位 选手((((见图 3.1)3.1)3.1)3.1),若1111至4444号选手之间的比赛日程填在日程表的左上角(4(4(4(4行3333列)))),5555 至8888号选手之间的比赛日程可填在日程表的左下角(4(4(4(4行3333列)))),左下角的内容可由 左上角对应项加上数4444得到。至此,剩下的右上角(4(4(4(4行4444列))))是为编号小的1111至4444号 选手与编号大的5555至8888号选手之间的比赛安排日程。如在第4444天,让1111至4444号选手分 别与5555至8888号选手比赛。以后各天,依次由前一天的日程安排,让5555至8888号选手循 环轮转即可。最后,右下角的比赛日程是由右上角的比赛日程决定,比如,在 右上角的第四天,1111号与5555号选手比赛,则说明5555号选手与1111号选手的第四天的比 赛也就确定了,即在右下角第5555行、第4444列的值为1111。同理,可推算出其他各行、 各列的值,如图3.13.13.13.1所示。由以上分析,不难得到为2222kkkk位选手安排比赛日程的算 法思路,详见以下程序。 算法1.4 1.4 1.4 1.4 比赛日程安排算法 #include <#include <#include <#include >>> #define MAXN 64#define MAXN 64#define MAXN 64#define MAXN 64 intintintint a[MAXN a[MAXN a[MAXN a[MAXN+1][MAXN1][MAXN1][MAXN1][MAXN]; void main() {void main() {void main() {void main() { intintintint twom1,twom,i,j,m,k twom1,twom,i,j,m,k twom1,twom,i,j,m,k twom1,twom,i,j,m,k; printfprintfprintfprintf((((““““指定n(=2n(=2n(=2n(=2的kkkk次幂))))位选手,请输入kkkk。\n\n\n\n””””,&k)&k)&k)&k); scanf(scanf(scanf(scanf(““““%d%d%d%d””””,&k)&k)&k)&k); a[1][1]=2a[1][1]=2a[1][1]=2a[1][1]=2; ////////预设两位选手的比赛日程。 a[2][1]=1a[2][1]=1a[2][1]=1a[2][1]=1; m = 1m = 1m = 1m = 1; twomltwomltwomltwoml = l = l = l = l; while(mx>x>x>aaaakkkk,则只有I3I3I3I3可能有解,在I1I1I1I1中 一定无解(j=0j=0j=0j=0);在与aaaakkkk进行比较后,待求解的问题((((如果有的话))))可以再一次 使用分治方法来求解。如果对所求解的问题((((或子问题))))所选的下标kkkk都是其问题 空间的中间元素的下标((((例如,对于问题IIII,取k=k=k=k=「(n+1)(n+1)(n+1)(n+1)/2222」)))),则所产生的算 法就是通常所说的二分检索。 3.2.1 二分检索算法设计与分析 算法2.1中用C语言描述了上述的二分检索方法。函数过程bin_search有 n+2个输入:a,n 和 x,一个输出j。只要待检索的元素存在,while循环就继 续下去。case语句根据compare(x,a[mid])的结果的三种情况进行选择运行。函 数过程结束时,如果x不在表a中,则j=0,否则 a(j)=x。 算法2.1 2.1 2.1 2.1 二分检索 void void void void bin_search(elemTypebin_search(elemTypebin_search(elemTypebin_search(elemType a[] a[] a[] a[],intintintint n n n n,elemTypeelemTypeelemTypeelemType x x x x,intintintint &j) { &j) { &j) { &j) { ////////给定一个按非递减排列的元素数组a(1:n)a(1:n)a(1:n)a(1:n),n>1n>1n>1n>1,判断xxxx是否出现。 ////////若是,则置jjjj,使得x=a(j)x=a(j)x=a(j)x=a(j),若非,则j=0j=0j=0j=0。函数返回jjjj。 intintintint low low low low,highhighhighhigh,midmidmidmid; low=1low=1low=1low=1;high=nhigh=nhigh=nhigh=n;j = 0j = 0j = 0j = 0; while(low<=high) {while(low<=high) {while(low<=high) {while(low<=high) { mid = (low + high) / 2 mid = (low + high) / 2 mid = (low + high) / 2 mid = (low + high) / 2; //mid//mid//mid//mid取不大于(low + high)(low + high)(low + high)(low + high)÷÷÷÷2222的整数。 switch(compare(xswitch(compare(xswitch(compare(xswitch(compare(x,a[mid])) {a[mid])) {a[mid])) {a[mid])) { case case case case ‘‘‘‘<<<<’’’’ : high = mid -1 : high = mid -1 : high = mid -1 : high = mid -1;breakbreakbreakbreak; //x//x//x//x小于a[mid]a[mid]a[mid]a[mid] case case case case ‘‘‘‘>>>>’’’’ : low = mid +1 : low = mid +1 : low = mid +1 : low = mid +1;breakbreakbreakbreak; //x//x//x//x大于a[mid]a[mid]a[mid]a[mid] case case case case ‘‘‘‘====’’’’ : j = mid : j = mid : j = mid : j = mid;return jreturn jreturn jreturn j;breakbreakbreakbreak; //x//x//x//x等于a[mid]a[mid]a[mid]a[mid] }//switch }//switch }//switch }//switch }//while}//while}//while}//while return jreturn jreturn jreturn j; }//bin_search }//bin_search }//bin_search }//bin_search 判断bin_search bin_search bin_search bin_search 是否为一个算法,除了上面的描述外,还必须使函数 compare(x,a[mid])compare(x,a[mid])compare(x,a[mid])compare(x,a[mid])具有恰当的定义。如果aaaa的元素是整数,实数或字符串,则这 些比较运算都可用适当的指令正确完成的。另外,还需判断bin_searchbin_searchbin_searchbin_search是否能终 止。关于这一点留待证明算法正确性时回答。 在对算法的正确性作出证明以前,为了增加对此算法的置信度,不妨用一个 具体的例子来模拟算法的执行。 例3333 假定在a[1:9]a[1:9]a[1:9]a[1:9]中顺序存放着以下9999个元素:-15, -6, 0, 7, 9, 23, 54, 82, 101-15, -6, 0, 7, 9, 23, 54, 82, 101-15, -6, 0, 7, 9, 23, 54, 82, 101-15, -6, 0, 7, 9, 23, 54, 82, 101。要 求检索下列xxxx的值:101101101101,-14-14-14-14和82828282是否在AAAA中出现。 在模拟算法执行时,只需追踪变量low,high和mid变化。这三个变量的追踪轨 迹由表3.1列出。可以看出,对101,82的检索成功,而对-14的检索不成功。 x= -14x= -14x= -14x= -14 low high mid low high mid low high mid low high mid x=82x=82x=82x=82 low high mid low high mid low high mid low high mid x=101x=101x=101x=101 low high mid low high mid low high mid low high mid 1 9 51 9 51 9 51 9 5 1 4 21 4 21 4 21 4 2 1 1 11 1 11 1 11 1 1 2 1 ?2 1 ?2 1 ?2 1 ? 找不到 1 9 51 9 51 9 51 9 5 6 9 76 9 76 9 76 9 7 8 9 88 9 88 9 88 9 8 找到 1 9 51 9 51 9 51 9 5 6 9 76 9 76 9 76 9 7 8 9 88 9 88 9 88 9 8 9 9 99 9 99 9 99 9 9 找到 关于程序正确性的证明,至今为止还是一个尚未解决的课题。下面仅给出 bin_searchbin_searchbin_searchbin_search正确性的一种““““非形式证明””””。 定理 3.1 3.1 3.1 3.1 函数过程bin_search(a[]bin_search(a[]bin_search(a[]bin_search(a[],nnnn,xxxx,j)j)j)j)能正确地运行。 证明:假定函数compare(x,a[mid])compare(x,a[mid])compare(x,a[mid])compare(x,a[mid])有确定的定义,其结果或为’’’’x>a[mid]x>a[mid]x>a[mid]x>a[mid]’’’’;或 为’’’’xa[mid]x>a[mid]x>a[mid]x>a[mid],则通过low=mid+1low=mid+1low=mid+1low=mid+1语句将lowlowlowlow增加到mid+lmid+lmid+lmid+l来缩小检索范围 且不会影响检索结果。 又因为lowlowlowlow和highhighhighhigh都是整型变量,按上述方式缩小检索区总可在有限步内使 low>highlow>highlow>highlow>high。如果出现这种情况,则说明xxxx不在a[]a[]a[]a[]中,退出循环,jjjj被置0000,算法终 止。 证毕。 bin_search需要的空间很容易确定,它要用n个单元存放数组a[],还要有存 放变量low、high、mid、x和j的5个空间单元。因此所需的空间单元是n+5。至 于它的计算时间,则要分别考虑最好、平均和最坏三种情况。 为了清楚起见,对于检索问题还需将最好情况区分为成功检索的最好情况和 不成功检索的最好情况来加以分析。对于平均和最坏情况的分析也作类似的处 理。 显然,x只有在取a[]任一元素的情况下,才会出现成功的检索,所以,成功 的检索一共有n种可能,而为了测试所有不成功的检索,只需将x取n+1个不同的 值,即x取小于a[1]的某个值,或大于a[n]的某个,或处在a[k]与a[k+1] (1≤≤≤≤k≤≤≤≤n-1)之间的某个值(假如a[k]与a[k+1]存在这样的x)。因此,在算出 bin_search在x这2n+1种取值情况下的执行时间之后,求取它在最坏、平均和 最好情况的计算时间就很容易了。 在对算法的一般情况分析以前,不妨先对例3的实例作出分析,看看算法在频 率计数上有些什么特性。从算法中可以看到,所有的运算基本上都是进行比较 和数据传送。循环语句之外的语句均为赋值语句,其频率计数各为1。在while 循环中,我们集中考虑x和A中的元素比较,而其它运算的频率计数显然与这些 元素比较运算的频率计数具有相同的数量级。假定只需一次比较就可确定case 语句控制是三种情况的哪一种,进而查找这9个元素中的每一个元素所需的比较 次数如表2所示: 表2 例3中查找成功的比较次数 a[i]a[i]a[i]a[i]下标 1111 2222 3333 4444 5555 6666 7777 8888 9999 元素值 -15-15-15-15 -6-6-6-6 0000 7777 9999 23232323 54545454 82828282 101101101101 比较次数 3333 2222 3333 4444 1111 3333 2222 3333 4444 要找到一个元素至少要进行1次比较,至多要进行4次比较。查找这9个数据元 素均能成功,所以取其平均值即可得到每一次成功检索的平均比较次数为: 25÷÷÷÷9≈2.77。 5 2 1 3 6 4 7 8 9 图3.2 n=93.2 n=93.2 n=93.2 n=9的二分检索的二叉比较树 不成功检索的终止方式取决于xxxx的值,总 共有9+l=109+l=109+l=109+l=10种可能的情形。 如果xa[5],所以,下一次则将x与其右结点7所表示的元素比 较;若x0n>0n>0n>0外,其余说明与bin_searchbin_searchbin_searchbin_search相同 intintintint low low low low,highhighhighhigh,midmidmidmid; low=1low=1low=1low=1;high=n+1high=n+1high=n+1high=n+1; //high//high//high//high总比可能的取值大1111 j = 0 j = 0 j = 0 j = 0; while(lowa[n] x>a[n] x>a[n] x>a[n] 时)))),它的元素比较数就是 bin_search1bin_search1bin_search1bin_search1的两倍。然而,对于任何一种成功的检索((((例如,当 x=a[mid]x=a[mid]x=a[mid]x=a[mid]时)))), bin_searchbin_searchbin_searchbin_search平均要比bin_search1bin_search1bin_search1bin_search1多作(log(log(log(log2222n)n)n)n)/2222次比较。bin_search1bin_search1bin_search1bin_search1的最好、平 均和最坏情况时间对于成功和不成功的检索都是ΘΘΘΘ(log(log(log(log2222n)n)n)n)。 证明从略。 作业: 1111.利用将递归程序转化为迭代程序的基本规则,将算法3.13.13.13.1转化为非递归算法 div2div2div2div2,使得又div2div2div2div2通过化简能够得到div1div1div1div1,即算法3.23.23.23.2。 2222.根据二分检索的基本原理,编写一个给定nnnn个可比较大小的元素的二分检 索的递归过程。 3333.对于含有nnnn个内部结点的二叉比较树,试证明:E=I+2n E=I+2n E=I+2n E=I+2n 。 其中,EEEE表示外部路径长度;IIII表示内部路径长度。 提示:分析二叉比较树的特点,假设其深度为k+1k+1k+1k+1,分别计算EEEE和IIII,并利 用前k-1k-1k-1k-1层内部结点与第kkkk层内部结点的关系导出EEEE和IIII的关系。 4444.证明bin_search1bin_search1bin_search1bin_search1的最好、平均和最坏情况的计算时间对于成功和不成功的 检索都是Θ(log(log(log(log2222n)n)n)n)。 提示:仿照书上的计算过程进行证明(计算)。 3.2.2 以比较为基础检索的时间下界 在nnnn个已分类((((已按非递减或非递增方式排好序))))的元素序列上检索某元素是否 存在的问题,是否还存在以元素比较为基础且其计算时间在最坏情况下比二分 检索算法有更低数量的检索算法呢?下面就来讨论这个问题。 假设n个元素A(1:n)有如下关系:A(1)<A(2)<…………<A(n)。要检索某一给定 元素x是否在A中出现。若只允许进行元素间的比较而不允许对它们实施运算, 在这种条件下所设计的算法都称为是以比较为基础的算法。 根据二叉比较树的定义,显然任何以比较为基础的检索算法在执行过程中都 可以用二叉比较树来描述。每个内结点表示一次元素比较,因此任何二叉比较 树中必须含有n个内结点,且分别与n个不同的j值相对应。每个外部结点对应于 一次不成功的检索。图3.3给出了两棵比较树,一个模拟线性检索,一个模拟二 分检索。 图3.3 3.3 3.3 3.3 两个检索算法的比较 (a) (a) (a) (a) 模拟线性检索 x:a[1]x:a[1]x:a[1]x:a[1] 不成功 不成功 不成功 不成功 x:a[2]x:a[2]x:a[2]x:a[2] x:a[n]x:a[n]x:a[n]x:a[n] x:ax:ax:ax:a 不成功 不成功 不成功 不成功 不成功 不成功 不成功 不成功 x:ax:ax:ax:a x:ax:ax:ax:a (b) (b) (b) (b) 模拟二分检索 x:a +1x:a +1x:a +1x:a +1x:a[1]x:a[1]x:a[1]x:a[1] x:a[n]x:a[n]x:a[n]x:a[n]x:a -1x:a -1x:a -1x:a -1 n+1n+1n+1n+1 4444 n+1n+1n+1n+1 2222 n+1n+1n+1n+1 2222 3n+13n+13n+13n+1 4444 n+1n+1n+1n+1 2222 ………… ………… ………… ………… ………… 下面的定理给出了以比较为基础的有序检索问题最坏情况的时间下界。 定理3.3 设A(l:n)含有n(≥1)个不同的元素,且A(1)<A(2)<…………<A(n)。又 设用以比较为基础的判断x是否在A(l:n)出现的任何算法在最坏情况下所需的 最小比较次数是FIND(n),那么, FIND(n)≥「log2(n+1)」。 证明:通过考察模拟求解检索问题的各种可能算法的比较树可知,FIND(n)不 大于树中由根到一个叶子结点的最长路径的距离。在这所有的树中都必定有n个 内部结点与x在A中可能的n种出现情况相对应。如果一棵二叉树的所有内部结点 所在的级数小于或等于级数k,则最多有2k-1个内部结点。因此,n≤2k-1,即 FIND(n)=k≥「log2(n+1)」。 证毕。 由定理3.3可知,任何一种以比较为基础的算法,其最坏情况时间都不可能低 于Ο(log2n),因此,也就不可能存在其最坏情况时间比二分检索数量级还低的 算法。事实上,二分检索所产生的比较树的所有外部结点都在相邻接的两个级 上,而且不难证明这样的二叉树使得由根到结点的最长路径减至最小,因此, 二分检索是解决检索问题的最优的最坏情况算法。 3.3 3.3 3.3 3.3 求最大、最小元素求最大、最小元素求最大、最小元素求最大、最小元素 如果要在含有nnnn个不同元素的集合中同时找出它的最大和最小元素((((元素类型 为eTypeeTypeeTypeeType,可比较大小)))),最简单的方法是将元素逐个进行比较。这种直接算法 可初步描述如下。 算法3.1 3.1 3.1 3.1 直接找最大和最小元素 void void void void StraitMaxMin(eTypeStraitMaxMin(eTypeStraitMaxMin(eTypeStraitMaxMin(eType a[] a[] a[] a[],intintintint n n n n,eTypeeTypeeTypeeType &Max &Max &Max &Max,eTypeeTypeeTypeeType &Min) { &Min) { &Min) { &Min) { // // // // 将A(1A(1A(1A(1:n)n)n)n)中的最大元素置于maxmaxmaxmax中,最小元素置于minminminmin中。 intintintint i i i i; Max=Min=a[1]Max=Min=a[1]Max=Min=a[1]Max=Min=a[1]; for(i=2for(i=2for(i=2for(i=2;i=ni=ni=ni=n;++i) {++i) {++i) {++i) { if(a[i]>max) Max=a[i] if(a[i]>max) Max=a[i] if(a[i]>max) Max=a[i] if(a[i]>max) Max=a[i]; if(a[i]maxA(i)>maxA(i)>maxA(i)>max为假时,才有 必要比较A(i)max) Max=a[i]if(a[i]>max) Max=a[i]if(a[i]>max) Max=a[i]if(a[i]>max) Max=a[i]; elseelseelseelse if(a[i]2 MaxMin需要的元素比较数是多少呢?如果用T(n)表示这个数,则可导出的递归 关系式: 当nnnn是2222的幂时,即对于某个正整数kkkk,n=2n=2n=2n=2kkkk,有 T(n) = 2T(n/2) + 2T(n) = 2T(n/2) + 2T(n) = 2T(n/2) + 2T(n) = 2T(n/2) + 2 = 2(2T(n/4) + 2) + 2 = 2(2T(n/4) + 2) + 2 = 2(2T(n/4) + 2) + 2 = 2(2T(n/4) + 2) + 2 = 4T(n/4) + 4 + 2 = 4T(n/4) + 4 + 2 = 4T(n/4) + 4 + 2 = 4T(n/4) + 4 + 2 = = = = ………… = 2 = 2 = 2 = 2k-1k-1k-1k-1 T(2) + T(2) + T(2) + T(2) + = 2 = 2 = 2 = 2k-1k-1k-1k-1 + 2 + 2 + 2 + 2kkkk –––– 2 2 2 2 = 3n/2-2 = 3n/2-2 = 3n/2-2 = 3n/2-2 注意:当 nnnn是2222的正整数幂时,3n/2-23n/2-23n/2-23n/2-2是最好,平均及最坏情况的比较。它和 直接算法的比较次数2n-22n-22n-22n-2相比,减少了25252525%。 ∑ 2 2 2 2iiii 1111≤iiii≤k-1k-1k-1k-1 可以证明,任何一种以元素比较为基础的找最大和最小元素的算法的比较下 界均为「3n/23n/23n/23n/2」-2-2-2-2。因此,函数MaxMinMaxMinMaxMinMaxMin在这种意义上是最优的。那末,这是 否意味着此算法确实比较好呢?不一定。其原因有如下两个:::: 一是MaxMinMaxMinMaxMinMaxMin要求的存储空间比直接算法多。给出nnnn个元素就有「loglogloglog2222nnnn」+1+1+1+1 级的递归,而每次递归调用需要保留到栈中的有 iiii,jjjj,fmaxfmaxfmaxfmax,fminfminfminfmin和返回地址 五个值。虽然可用前面介绍的递归化迭代的规则去掉递归,但所导出的迭代模 型还需要一个其深度为loglogloglog2222nnnn数量级的栈。 二是当元素a[i]a[i]a[i]a[i]和a[j]a[j]a[j]a[j]的比较时间与iiii和jjjj的比较时间相差不大时,MaxMinMaxMinMaxMinMaxMin并不 可取。为说明问题,假设元素比较与 iiii和 jjjj间的比较时间相同,又设MaxMinMaxMinMaxMinMaxMin的 频率计数为C(n)C(n)C(n)C(n),n=2n=2n=2n=2kkkk,kkkk是某个正整数,并且对i=ji=ji=ji=j和i=j-1i=j-1i=j-1i=j-1用iiii≥j-1j-1j-1j-1的比较代 替,这样,只需对iiii和 j-lj-lj-lj-l作一次比较就足以实现其功能。于是MaxMinMaxMinMaxMinMaxMin的频率计 数为: 2 n=2 C(n)= 2C(n/2) + 3 n>2 解此关系式可得: C(n) = 2C(n/2) + 3C(n) = 2C(n/2) + 3C(n) = 2C(n/2) + 3C(n) = 2C(n/2) + 3 = 4C(n/4) + 6 + 3 = 4C(n/4) + 6 + 3 = 4C(n/4) + 6 + 3 = 4C(n/4) + 6 + 3 = = = = ………… = 2 = 2 = 2 = 2k-1k-1k-1k-1C(2) + 3 C(2) + 3 C(2) + 3 C(2) + 3 = 2 = 2 = 2 = 2kkkk + 3 + 3 + 3 + 3****2222k-1k-1k-1k-1-3-3-3-3 = 5n/2 = 5n/2 = 5n/2 = 5n/2 ––––3 3 3 3 。 而StraitMaxMinStraitMaxMinStraitMaxMinStraitMaxMin的比较数是3(n-1)(3(n-1)(3(n-1)(3(n-1)(包括实现forforforfor循环所要的比较))))。尽管它比 5n/2-35n/2-35n/2-35n/2-3大一些,但由于递归算法中iiii,jjjj,fmaxfmaxfmaxfmax,fminfminfminfmin的进出栈所带来的开销, 因此MaxMinMaxMinMaxMinMaxMin在这种情况下反而比直接比较算法StraitMaxMinStraitMaxMinStraitMaxMinStraitMaxMin还要慢一些。 根据以上分析可以得出结论:如果a[]a[]a[]a[]的元素间的比较的复杂程度远比整型变 量的比较代价大,则分治方法产生效率较高((((实际上是最优))))的算法;反之,就 得到一个效率较低的程序。因此,分治策略只能看成是一个较好的然而并不总 是能成功的算法设计指导。 算法3.33.33.33.3是找最小元素和最大元素的非递归算法,请读者分析并验证其时间 复杂度。 ∑ 2i 0≤i≤k-2 Void Void Void Void MinMax(elemTypeMinMax(elemTypeMinMax(elemTypeMinMax(elemType w[] w[] w[] w[],intintintint n n n n,intintintint &Min &Min &Min &Min,intintintint &Max) { &Max) { &Max) { &Max) { // // // //求w[0w[0w[0w[0:n-1]n-1]n-1]n-1]中的最小最大元素。如果少于1111个元素,则返回falsefalsefalsefalse,否则返回truetruetruetrue。 //Min //Min //Min //Min保存最小元素的下标;MaxMaxMaxMax保存最大元素的下标。 if(n<1) return falseif(n<1) return falseif(n<1) return falseif(n<1) return false; ////////对nnnn≤1111,做特殊处理。 if(n==1) {Min = Max = 0if(n==1) {Min = Max = 0if(n==1) {Min = Max = 0if(n==1) {Min = Max = 0;return truereturn truereturn truereturn true;} } } } ////////当n=1n=1n=1n=1时,最大最小元素下标都为0000。 intintintint s s s s; ////////对MinMinMinMin和MaxMaxMaxMax进行初始化并定义循环起点。 if(nif(nif(nif(n%2) 2) 2) 2) {Min = Max = 0Min = Max = 0Min = Max = 0Min = Max = 0;S = 1S = 1S = 1S = 1;}////////当nnnn为奇数时;设最大最小元素的下标为0000。 ////////并设循环起点s=1s=1s=1s=1。 else else else else { ////////当nnnn为偶数时,比较w[0]w[0]w[0]w[0]和w[1]w[1]w[1]w[1],设定最大最小元素,并设循环起点s=2s=2s=2s=2。 if(w[0]>=w[1]) { Min = 1if(w[0]>=w[1]) { Min = 1if(w[0]>=w[1]) { Min = 1if(w[0]>=w[1]) { Min = 1;Max = 0Max = 0Max = 0Max = 0;}}}} else { Min = 0 else { Min = 0 else { Min = 0 else { Min = 0;Max = 1Max = 1Max = 1Max = 1;}}}} S = 2 S = 2 S = 2 S = 2;} ////////比较余下的偶数个数据元素(成对比较) for(i=sfor(i=sfor(i=sfor(i=s;iw[i+1]) { if(w[i]>w[Max]) Max = iif(w[i]>w[i+1]) { if(w[i]>w[Max]) Max = iif(w[i]>w[i+1]) { if(w[i]>w[Max]) Max = iif(w[i]>w[i+1]) { if(w[i]>w[Max]) Max = i;if(w[i+1]w[Max]) Max = i+1if(w[i+1]>w[Max]) Max = i+1if(w[i+1]>w[Max]) Max = i+1if(w[i+1]>w[Max]) Max = i+1;if(w[i]mid) { if(h>mid) { if(h>mid) { if(h>mid) { for(k=j for(k=j for(k=j for(k=j;k=highk=highk=highk=high;++k) {b[i] = a[k]++k) {b[i] = a[k]++k) {b[i] = a[k]++k) {b[i] = a[k];++i++i++i++i;}} //}} //}} //}} //处理剩余的元素 else { for(k=helse { for(k=helse { for(k=helse { for(k=h;k=midk=midk=midk=mid;++k) {b[i] = a[k]++k) {b[i] = a[k]++k) {b[i] = a[k]++k) {b[i] = a[k];++i++i++i++i;}} //}} //}} //}} //处理剩余的元素 }}}};//if//if//if//if for(k=low for(k=low for(k=low for(k=low;k=highk=highk=highk=high;++k) { //++k) { //++k) { //++k) { //将已归并的集合复制到AAAA a[k]=b[k] a[k]=b[k] a[k]=b[k] a[k]=b[k]; }}}};//for//for//for//for }//Merge}//Merge}//Merge}//Merge 在主程序中调用函数MergeSort(1,n)MergeSort(1,n)MergeSort(1,n)MergeSort(1,n),就可将数组AAAA中的nnnn 个元素按关键字重新排列成非递减序列并存于AAAA中。 例4.14.14.14.1 用归并分类算法,将含有十个元素的数组 A=(310,285,179,652,351,423,861,254,450,520)A=(310,285,179,652,351,423,861,254,450,520)A=(310,285,179,652,351,423,861,254,450,520)A=(310,285,179,652,351,423,861,254,450,520)按非递减分类。 函数过程MergeSortMergeSortMergeSortMergeSort首先把A(1:10)A(1:10)A(1:10)A(1:10)分成两个各有五个元素的子集合,然后把 A(1:5)A(1:5)A(1:5)A(1:5)分成大小为3333和2222的两个子集合,再把A(1:3)A(1:3)A(1:3)A(1:3)分成大小为2222和1111的子集合,最 后将A(1:2)A(1:2)A(1:2)A(1:2)分成各含一个元素的两个子集合,至此就开始归并。此时的状态可 排成下列形式: (310|285|179|652(310|285|179|652(310|285|179|652(310|285|179|652,351|423351|423351|423351|423,861861861861,254254254254,450450450450,520)520)520)520) 其中,直线表示子集合的边界线。归并A(1)A(1)A(1)A(1)和A(2)A(2)A(2)A(2)得: (285(285(285(285,310|179|652310|179|652310|179|652310|179|652,351|423351|423351|423351|423,861861861861,254254254254,450450450450,520)520)520)520) 再归并A(1:2)A(1:2)A(1:2)A(1:2)和A(3)A(3)A(3)A(3)得: (179(179(179(179,285285285285,310|652310|652310|652310|652,351|423351|423351|423351|423,861861861861,254254254254,450450450450,520)520)520)520) 然后将A(4:5)A(4:5)A(4:5)A(4:5)分成两个各含一个元素的子集合,再将两个子集合归并得: (179(179(179(179,285285285285,310|351310|351310|351310|351,652|423652|423652|423652|423,861861861861,254254254254,450450450450,520)520)520)520) 接着归并A(1:3)A(1:3)A(1:3)A(1:3)和A(4:5)A(4:5)A(4:5)A(4:5)得: (179(179(179(179,285285285285,310310310310,351351351351,652|423652|423652|423652|423,861861861861,254254254254,450450450450,520)520)520)520) 此时算法就返回到MergeSortMergeSortMergeSortMergeSort首次递归调用的后继语句的开始处,即准备执 行第二条递归调用语句。又通过反复地递归调用和归并,将A(6:10)A(6:10)A(6:10)A(6:10)分好类,其 结果如下: (179(179(179(179,285285285285,310310310310,351351351351,652|254652|254652|254652|254,423423423423,450450450450,520520520520,861)861)861)861) 对这两个各含5555个元素的已分好类的集合再经过最后的归并得到如下结果: (179(179(179(179,254254254254,285285285285,310310310310,351351351351,423423423423,450450450450,520520520520,652652652652,861)861)861)861) 图3.5所示的是在n=10的情况下,由MergeSort所产生的一系列递归调用的树 的表示。每个结点中的一组值是参变量low和high的值。注意:集合的分割一直 进行到产生出只含单个元素的子集合为止。图3.6所示的则是一棵表示 MergeSort对函数过程Mergeergeergeerge调用的树型结构。 每个结点的值依次是参变量low,mid和highghghgh的值。例如,含有值1,2,3的结 点表示A(1:2)和A(3)中元素的归并。 1,10 1,5 1,3 1,2 1,1 4,5 4,4 5,53,3 2,2 6,10 6,8 6,7 6,6 7,7 图3.5 MergeSort(1,10)调用过程的树型表示 10,109,9 9,10 8,8 1,1,2 1,2,3 1,3,5 1,5,10 4,4,5 图3.6 Merge调用过程的树型表示 6,6,7 6,7,8 9,9,10 6,8,10 如果归并运算的时间与n成正比,则归并分类的计算时间可用递归关系式描述如 下: a n=1a n=1a n=1a n=1,aaaa是常数 T(n)= T(n)= T(n)= T(n)= 2T(n/2) + 2T(n/2) + 2T(n/2) + 2T(n/2) + cncncncn n>1 n>1 n>1 n>1,cccc是常数 当 nnnn是2222的正整数幂,即n=2n=2n=2n=2kkkk时,可以通过逐次代入求出其解: T(n) = 2(2T(n/4) + cn/2) + T(n) = 2(2T(n/4) + cn/2) + T(n) = 2(2T(n/4) + cn/2) + T(n) = 2(2T(n/4) + cn/2) + cncncncn = 4T(n/4) + 2cn = 4T(n/4) + 2cn = 4T(n/4) + 2cn = 4T(n/4) + 2cn = 4(2T(n/8) + cn/4) + 2cn = 4(2T(n/8) + cn/4) + 2cn = 4(2T(n/8) + cn/4) + 2cn = 4(2T(n/8) + cn/4) + 2cn = = = = ………… = 2 = 2 = 2 = 2kkkkT(1) + T(1) + T(1) + T(1) + kcnkcnkcnkcn = an + cnlog = an + cnlog = an + cnlog = an + cnlog2222nnnn 如果2222kkkk<nnnn<2222k+1k+1k+1k+1,易于看出T(n)T(n)T(n)T(n)≤T(2T(2T(2T(2k+1k+1k+1k+1))))。因此 T(n)T(n)T(n)T(n)=Ο(nlog(nlog(nlog(nlog2222n)n)n)n)。 2 2 2 2 改进的归并分类算法 尽管算法4.24.24.24.2相当充分地反映了使用分治策略对数据对象分类的长处,但仍存 在着一些明显的不足,从而限制了分类效率的进一步提高。下面来逐一分析这 些不足之处,并提出相应的改进措施,以期能给出一个更有效的归并分类模型。 在使用例4.1的数据集模拟执行MergeSort的过程中,可以看到,每当集合被 分成只含两个元素的子集合时,还需要使用二次递归调用将这子集合分成单个 元素的集合。这表明该算法执行到将集合分成含元素相当少的子集合时,很多 时间不是用在实际的分类而是消耗在处理递归上。如果不让递归一直进行到最 低一级,就可对这种情况做出改进。 从抽象化控制过程来看,只要使Small(p,q)在输入规模q-p+1适当小时取真 值,而在这种情况下采用另一个能在小规模集合上有效工作的分类算法就能克 服低层递归调用所带来消耗过大的问题。 本节开始所给出的插入分类算法尽管时间复杂度为Ο(n2),但当 n很小(譬如 小于16)时却能极快地工作,因此,将它作为归并分类算法中处理小规模集合情 况的子过程是很适宜的。 另外,归并分类算法使用了辅助数组B(1:n)B(1:n)B(1:n)B(1:n),这是一个明显的不足之处。但 是,由于不可能在两个已分类集合的原来的位置上进行适当的归并,所以这nnnn个 位置的附加空间对于本算法是必需的。而且算法还必须在这附加空间上竭力地 工作,在每次调用MergeMergeMergeMerge时,把存放在B(low:high)B(low:high)B(low:high)B(low:high)中的结果复制到A(low:high)A(low:high)A(low:high)A(low:high) 中去。 不过,使用一个以整数表示的链接信息数组Link(l:n)Link(l:n)Link(l:n)Link(l:n)来代换暂时存放元素的 辅助数组BBBB可以节省一些附加空间。这个链接数组在1111,2222,…………,nnnn的范围内取值。 这些整数看成是AAAA的元素的指针,在分类表中它指向下一个元素所在的下标位置。 因此,分类表就成了一个指针的序列,而分类表的归并则不必移动元素本身, 只要改变相应的链值即可进行。用0000表示表的结束。 下面是LinkLinkLinkLink值的一个集合,它包含了两个已分类子集合的元素链接信息表。QQQQ 和RRRR分别表示两个表的起始位置,这里Q=2Q=2Q=2Q=2,R=5R=5R=5R=5。 Link (1) (2) (3) (4) (5) (6) (7) (8)Link (1) (2) (3) (4) (5) (6) (7) (8)Link (1) (2) (3) (4) (5) (6) (7) (8)Link (1) (2) (3) (4) (5) (6) (7) (8) 6 4 7 1 3 0 8 0 6 4 7 1 3 0 8 0 6 4 7 1 3 0 8 0 6 4 7 1 3 0 8 0 Q=2Q=2Q=2Q=2的表是(2(2(2(2,4444,1111,6)6)6)6),R=5R=5R=5R=5的表是(5(5(5(5,3333,7777,8)8)8)8)。 QQQQ和RRRR分别描述了AAAA中两个 已分类子集合,它们有A(2)A(2)A(2)A(2)≤A(4)A(4)A(4)A(4)≤A(1)A(1)A(1)A(1)≤A(6)A(6)A(6)A(6)和A(5)A(5)A(5)A(5)≤A(3)A(3)A(3)A(3)≤A(7)A(7)A(7)A(7)≤A(8)A(8)A(8)A(8)。 下面是采取了以上两种改进措施的归并分类模型。 算法4.3 4.3 4.3 4.3 使用了链接的归并分类模型。 Void MergeSort1(int low, Void MergeSort1(int low, Void MergeSort1(int low, Void MergeSort1(int low, intintintint high, high, high, high, intintintint p) { p) { p) { p) { // // // //利用辅助数组Link(low,high)Link(low,high)Link(low,high)Link(low,high),将数组A(low:high)A(low:high)A(low:high)A(low:high)按非递减分类。 //Link//Link//Link//Link中值表示按分类次序给出AAAA下标的表,并把PPPP置于指示这表的开始处 eTypeeTypeeTypeeType a[low..high] a[low..high] a[low..high] a[low..high]; //a[low:high]//a[low:high]//a[low:high]//a[low:high]定义成全局变量 intintintint Link[low..high] Link[low..high] Link[low..high] Link[low..high]; if(high-low+1)if(high-low+1)if(high-low+1)if(high-low+1)<16 {16 {16 {16 {InsertionSort(aInsertionSort(aInsertionSort(aInsertionSort(a[], Link[], low, high, p)[], Link[], low, high, p)[], Link[], low, high, p)[], Link[], low, high, p);}}}} else { mid=(low + high)/2 else { mid=(low + high)/2 else { mid=(low + high)/2 else { mid=(low + high)/2; MergeSort1(low,mid,q)MergeSort1(low,mid,q)MergeSort1(low,mid,q)MergeSort1(low,mid,q); ////////返回qqqq表 MergeSort1(mid+1,high,r)MergeSort1(mid+1,high,r)MergeSort1(mid+1,high,r)MergeSort1(mid+1,high,r); ////////返回 rrrr表 Merge1(q,r,p)Merge1(q,r,p)Merge1(q,r,p)Merge1(q,r,p);} //} //} //} //将表q q q q 和rrrr归并成表pppp }//MergeSort1 }//MergeSort1 }//MergeSort1 }//MergeSort1 初次调用MergeSort1MergeSort1MergeSort1MergeSort1时,把要分类的元素((((即关键字))))先放到A(1:n)A(1:n)A(1:n)A(1:n)中,并且将 Link(l:n)Link(l:n)Link(l:n)Link(l:n)置成0000。初次调用语句为MergeSort1(1,n,p)MergeSort1(1,n,p)MergeSort1(1,n,p)MergeSort1(1,n,p),pppp作为按分类次序给出AAAA中 元素的指示表的指针返回。 这里的InsertionSortInsertionSortInsertionSortInsertionSort算法4.14.14.14.1的改型,它把对A(low:high)A(low:high)A(low:high)A(low:high)的分类表变成起始点 在PPPP的链接信息表。每当被分类的项数小于16161616时就调用它。 修改过的归并过程如下: 算法4.4 4.4 4.4 4.4 使用链接表归并已分类的集合。 Void Merge1(q, r, P)Void Merge1(q, r, P)Void Merge1(q, r, P)Void Merge1(q, r, P) //q//q//q//q和rrrr是全局数组Link(1:n)Link(1:n)Link(1:n)Link(1:n)中两个表的指针。这两个表可用来获得数组 //A(1:n)//A(1:n)//A(1:n)//A(1:n)中已分类元素的子集合。此算法执行后构造出一个由pppp所指示的新 ////////表,利用此表可以将AAAA中元素按非递减分类,同时qqqq和rrrr所指示的表随之 ////////消失。假定Link(0) Link(0) Link(0) Link(0) 已被定义,且假定表由0000表示结束。 eTypeeTypeeTypeeType a[n] a[n] a[n] a[n],Link[n]Link[n]Link[n]Link[n];intintintint n n n n;////////定义成全局变量 intintintint i i i i,jjjj,kkkk; i=qi=qi=qi=q;j=rj=rj=rj=r;k=0k=0k=0k=0; ////////新表在Link(0)Link(0)Link(0)Link(0)处开始 while(i!=0 and j!=0) { //while(i!=0 and j!=0) { //while(i!=0 and j!=0) { //while(i!=0 and j!=0) { //当两表皆非空时作 if(a[i]<=a[j]) { if(a[i]<=a[j]) { if(a[i]<=a[j]) { if(a[i]<=a[j]) { // // // //找较小的关键字 Link[k]=iLink[k]=iLink[k]=iLink[k]=i;k=ik=ik=ik=i;i=link[i]i=link[i]i=link[i]i=link[i];} //} //} //} //加一个新关键字到Link[]Link[]Link[]Link[]中 else { Link[k]=jelse { Link[k]=jelse { Link[k]=jelse { Link[k]=j;k=jk=jk=jk=j;j=link[j]j=link[j]j=link[j]j=link[j];}}}} } } } };//while//while//while//while if(i==0) {Link[k]=j if(i==0) {Link[k]=j if(i==0) {Link[k]=j if(i==0) {Link[k]=j;}}}} else {Link[k]=i else {Link[k]=i else {Link[k]=i else {Link[k]=i;}}}} P=Link[0] P=Link[0] P=Link[0] P=Link[0]; }//Merge1}//Merge1}//Merge1}//Merge1 为了帮助理解这个新的归并分类模型,下面来看一个例子。 例4.24.24.24.2 假设要对下述 8888个元素序列(50(50(50(50,10101010,25252525,30303030,15151515,70707070,35353535,55)55)55)55)分类。 要求使用新算法模拟执行。这里略去在少于16161616个元素时应使用InsertionSortInsertionSortInsertionSortInsertionSort分类 的要求。LinkLinkLinkLink数组初始化为 0000。 表4.14.14.14.1显示了在每一次调用MergeSort1MergeSort1MergeSort1MergeSort1结束后LinkLinkLinkLink数组的变化情况。各行上的 PPPP值分别指向最近一次Merge1Merge1Merge1Merge1结束时所产生的LinkLinkLinkLink中的那个表。右边是这些表 所表示的相应的已分类元素子集合。 例如,在最后一行,P=2P=2P=2P=2表示链表(2(2(2(2,5555,3333,4444,7777,1111,8888,6)6)6)6)在2222处开始,而 此链表意味着A(2)A(2)A(2)A(2)≤A(5)A(5)A(5)A(5)≤A(3)A(3)A(3)A(3)≤A(4)A(4)A(4)A(4)≤A(7)A(7)A(7)A(7)≤A(1)A(1)A(1)A(1)≤A(8)A(8)A(8)A(8)≤A(6)A(6)A(6)A(6)。 表4.1 例5.1中Link数组的变化过程 a[]a[]a[]a[] (0)(0)(0)(0) (1)(1)(1)(1) (2)(2)(2)(2) (3)(3)(3)(3) (4)(4)(4)(4) (5)(5)(5)(5) (6)(6)(6)(6) (7)(7)(7)(7) (8)(8)(8)(8) ---- 50505050 10101010 25252525 30303030 15151515 70707070 35353535 55555555 LinkLinkLinkLink 0000 0000 0000 0000 0000 0000 0000 0000 0000 q r pq r pq r pq r p 1 2 21 2 21 2 21 2 2 2222 0000 1111 0000 0000 0000 0000 0000 0000 (10,50)(10,50)(10,50)(10,50) 3 4 33 4 33 4 33 4 3 3333 0000 1111 4444 0000 0000 0000 0000 0000 (10,50),(25,30)(10,50),(25,30)(10,50),(25,30)(10,50),(25,30) 2 3 22 3 22 3 22 3 2 2222 0000 3333 4444 1111 0000 0000 0000 0000 (10,25,30,50)(10,25,30,50)(10,25,30,50)(10,25,30,50) 5 6 55 6 55 6 55 6 5 5555 0000 3333 4444 1111 6666 0000 0000 0000 (10,25,30,50),(15,70)(10,25,30,50),(15,70)(10,25,30,50),(15,70)(10,25,30,50),(15,70) 7 8 77 8 77 8 77 8 7 7777 0000 3333 4444 1111 6666 0000 8888 0000 (10,25,30,50),(15,70) ,(35,55)(10,25,30,50),(15,70) ,(35,55)(10,25,30,50),(15,70) ,(35,55)(10,25,30,50),(15,70) ,(35,55) 5 7 55 7 55 7 55 7 5 5555 0000 3333 4444 1111 7777 0000 8888 6666 (10,25,30,50),(15,35,55,70)(10,25,30,50),(15,35,55,70)(10,25,30,50),(15,35,55,70)(10,25,30,50),(15,35,55,70) 2 5 22 5 22 5 22 5 2 2222 8888 5555 4444 7777 3333 0000 1111 6666 (10,15,25,30,35,50,55,70)(10,15,25,30,35,50,55,70)(10,15,25,30,35,50,55,70)(10,15,25,30,35,50,55,70) 除了以上改进之外,还可采用由底向上的方式设计算法,从以取消对栈空间。 3 3 3 3 以比较为基础分类的时间下界 尽管作了以上改进,但不难看出,新归并分类算法的时间复杂度在最坏情况 下仍为Ο(nlog(nlog(nlog(nlog2222n)n)n)n)。事实上,任何以关键字比较为基础的分类算法,它的最坏情 况的时间下界都为Ω(nlog(nlog(nlog(nlog2222n)n)n)n),因此,从数量级的角度上来看,归并分类算法是 最坏情况的最优算法。 利用第2小节所描述的二叉比较树,也很容易给出以比较为基础的分类算法时 间下界的证明。在这里,假设参加分类的n个关键字A(l),…………,A(n)各不相同, 因此任意两个关键字A(i)和A(j)的比较必导致A(i)<A(j)或者A(i)>A(j)的结 果。在描述算法各种可能执行的比较树中,每个内结点用比较对““““i:j””””来代表 A(i)和A(j)的比较,A(i)<A(j)时进入左分枝,A(j)>A(j)时进入右分枝。各 外部结点表示此算法的终止。从根到外结点的每一条路径分别与一种唯一的排 列相对应。由于n个关键字有n!种排列,而每种排列可以是某种特定输入下的 分类结果,因此比较树必定至少有n!个外部结点,每个外结点表示一种可能的 分类序列。图4.3给出了对三个关键字分类的一棵二叉比较树。边上的不等式表 示此条件成立时控制的转向。 1:2 1,2,3 2:3 1:3 2:3 1:3 1,3,2 3,1,2 2,1,3 2,3,1 3,2,1 a(1)a(2) a(3)>a(2)a(3)a(3) a(1)a(3) a(1)a(3) 对于任何一个以比较为 基础的算法,在描述其执 行的那棵比较树中,由根 到某外部结点的路径长度 表示生成该外部结点中那 个分类序列所需要的比较 次数。因此这棵树中最长 路径的长度((((即此树的高度)))) 就是该算法在最坏情况下 所作的比较次数。从而, 要求出所有以比较为基础 的对nnnn个关键字分类的算法 最坏情况下界,只需求出 这些算法对应的比较树的 最小高度,设它为T(n)T(n)T(n)T(n)。 如果一棵二叉树的所有内 部结点的级数均小于或等 于kkkk,对kkkk进行归纳可以证 得该树至多有2222kkkk个外部结 点((((比内部结点数多1)1)1)1)。 令T(n)=k,T(n)=k,T(n)=k,T(n)=k,则 nnnn!≤2222T(n)T(n)T(n)T(n) 而当nnnn>1111时有 nnnn!≥n(n-1)(n-2)n(n-1)(n-2)n(n-1)(n-2)n(n-1)(n-2)…………((((「n/2n/2n/2n/2」))))≥(n/2)(n/2)(n/2)(n/2)n/2n/2n/2n/2 因此,对于nnnn≥4444有 T(n)T(n)T(n)T(n)≥(n/2)log(n/2)log(n/2)log(n/2)log2222(n/2) (n/2) (n/2) (n/2) ≥(n/4)log(n/4)log(n/4)log(n/4)log2222nnnn。 故以比较为基础的分类算法的最坏情况的时间下 界为Ω(nlog(nlog(nlog(nlog2222n)n)n)n)。 3.5 3.5 3.5 3.5 快速分类快速分类快速分类快速分类 1 1 1 1 快速分类算法 由著名计算机科学家霍尔((((C.A.R.HoareC.A.R.HoareC.A.R.HoareC.A.R.Hoare))))给出的快速分类算法也是根据分治策 略设计的一种高效率的分类算法。它虽然也是把集合A(1:n)A(1:n)A(1:n)A(1:n)分成两个子集,但与 归并分类算法有所不同是,被分成的两个子集中的一个子集的所有元素都必须 小于等于另一个子集的任何一个元素,这样,两个子集就不再需要归并。这种 分解是通过重新整理A(1:n)A(1:n)A(1:n)A(1:n)中元素的排列顺序来实现的,其实现的基本思想如下: 选取AAAA的某个元素tttt,然后将AAAA的其它元素重新排列,使 得在tttt以前出现的所有 元素都小于或等于tttt,而所有在tttt后面出现的所有元素都大于tttt。称这种重新整理 为划分(Partitioning)(Partitioning)(Partitioning)(Partitioning),元素tttt称为划分元素(Partition element)(Partition element)(Partition element)(Partition element)。快速分类就是通 过不断地对产生的文件进行划分来实现元素的重新排列。 函数PartitionPartitionPartitionPartition完成对集合A(m:P-1)A(m:P-1)A(m:P-1)A(m:P-1)的划分,其中假定第一个元素A(m)A(m)A(m)A(m)是划分 元素((((这种假定是非本质的,只是为了方便说明。事实上,选择划分元素不能是 最大或最小的元素))))。A(p)A(p)A(p)A(p)不属于A(m:P-1)A(m:P-1)A(m:P-1)A(m:P-1),且假定A(P)A(P)A(P)A(P)≥A(m)A(m)A(m)A(m),引进A(p)A(p)A(p)A(p)是为 了在特殊情况下能控制程序顺利进行。因此,在对PartitionPartitionPartitionPartition初次调用,即m=1m=1m=1m=1, p-1=np-1=np-1=np-1=n时,则必须将A(n+1)A(n+1)A(n+1)A(n+1)定义成大于或等于A(l:n)A(l:n)A(l:n)A(l:n)的所有元素(假定为++++∞)。 函数InterChange(x,yInterChange(x,yInterChange(x,yInterChange(x,y))))实现xxxx和yyyy的交换功能。 算法5.1 5.1 5.1 5.1 用A(m)A(m)A(m)A(m)划分集合A(m:P-1)A(m:P-1)A(m:P-1)A(m:P-1) void Partition(m,p)void Partition(m,p)void Partition(m,p)void Partition(m,p) ////////在集合A(m)A(m)A(m)A(m),A(m+1)A(m+1)A(m+1)A(m+1),…………,A(p-1)A(p-1)A(p-1)A(p-1)中的元素按如下方式重新排列: ////////若最初t=A(m)t=A(m)t=A(m)t=A(m),则在重排完成之后,对于mmmm和p-lp-lp-lp-l之间的某个qqqq,有A(q)=tA(q)=tA(q)=tA(q)=t, ////////并使得对于mmmm≤kkkk<qqqq,有A(k)A(k)A(k)A(k)≤tttt,而对于qqqq<kkkk<PPPP,有A(k)A(k)A(k)A(k)≥tttt ////////退出过程时,pppp带着划分元素所在的下标位置,即qqqq的值返回。 eTypeeTypeeTypeeType a[n] a[n] a[n] a[n]; ////////定义为全局变量 intintintint m m m m,pppp,iiii;v=a[m]v=a[m]v=a[m]v=a[m];i=mi=mi=mi=m; //A(m)//A(m)//A(m)//A(m)是划分元素 while(iv ) //p} while(a[p]>v ) //p} while(a[p]>v ) //p} while(a[p]>v ) //p由右向左移,至少做一次。 if(ij,则第k小元素在A(j+1:n)中的第k-j小元素。根据这一思路导出的 算法6.1 如下。此函数把第k小元素放在A(k) ,并划分剩余元素使得 A(i)≤A(k),1≤i<k且 A(i)≥A(k),k<i≤n。 1 1 1 1 选择问题算法 算法6.1 6.1 6.1 6.1 找第kkkk小的元素 void void void void Select(eTypeSelect(eTypeSelect(eTypeSelect(eType a[] a[] a[] a[],intintintint n n n n,intintintint &k) { &k) { &k) { &k) { // // // //在数组A(1:n)A(1:n)A(1:n)A(1:n)找第kkkk小的元素ssss并存放在A(k)A(k)A(k)A(k)中(1111≤kkkk≤nnnn),即A(k)=sA(k)=sA(k)=sA(k)=s。 ////////将剩余元素重新排列,使得A(m)A(m)A(m)A(m)≤A(k)A(k)A(k)A(k),1111≤mmmm<kkkk // // // //且A(m)A(m)A(m)A(m)≥A(k)A(k)A(k)A(k), kkkk<mmmm≤nnnn。假设A(n+1)=+A(n+1)=+A(n+1)=+A(n+1)=+∞。 intintintint k k k k,mmmm,rrrr,jjjj,tttt; m = 1m = 1m = 1m = 1;r = n+1r = n+1r = n+1r = n+1; a[n+1] = +a[n+1] = +a[n+1] = +a[n+1] = +∞; do { //do { //do { //do { //最多进行kkkk次划分即可找到第kkkk小元素。 j = rj = rj = rj = r; ////////将剩余元素的最大下标+1+1+1+1作为划分的上界。 Partition(mPartition(mPartition(mPartition(m,j)j)j)j); ////////返回jjjj,jjjj使得A(j)A(j)A(j)A(j)是第jjjj小的元素 if(j=k) return k = jif(j=k) return k = jif(j=k) return k = jif(j=k) return k = j; if(k0c>0c>0c>0,使得下式成立。 TTTTAAAA(n) (n) (n) (n) ≤ cncncncn + ( + ( + ( + (∑TTTTk-ik-ik-ik-iAAAA (n-i) + (n-i) + (n-i) + (n-i) + ∑TTTTkkkkAAAA (i-1)) (i-1)) (i-1)) (i-1)) nnnn≥2222 1 1 1 1 ≤iiii<k kk kk kk k<iiii≤nnnn 1111 nnnn 因此, R(n) R(n) R(n) R(n) ≤ cncncncn + max { + max { + max { + max {∑R (n-i) + R (n-i) + R (n-i) + R (n-i) + ∑R(i-1)}R(i-1)}R(i-1)}R(i-1)} k 1 k 1 k 1 k 1 ≤iiii<k kk kk kk k<iiii≤nnnn n-1 n-1 n-1 n-1 n-1 n-1 n-1 n-1 = cncncncn + max{ + max{ + max{ + max{∑R (i) + R (i) + R (i) + R (i) + ∑R(i)} R(i)} R(i)} R(i)} nnnn≥2222 (6.1)(6.1)(6.1)(6.1) k n-k+1 k k n-k+1 k k n-k+1 k k n-k+1 k 1111 nnnn 1111 nnnn 选择cccc≥R(1)R(1)R(1)R(1),并对nnnn进行归纳法,证明对于所有的nnnn≥2222,有 R(n)R(n)R(n)R(n)≤4cn4cn4cn4cn。 归纳基础:对于n=2n=2n=2n=2,(6.1)(6.1)(6.1)(6.1)式给出: R(n) R(n) R(n) R(n) ≤ 2c + max ( 2c + max ( 2c + max ( 2c + max (∑R (1) , R (1) , R (1) , R (1) , ∑R(1))R(1))R(1))R(1)) ≤2.5c2.5c2.5c2.5c<4cn4cn4cn4cn 1111 2222 R(m) R(m) R(m) R(m) ≤ cm + max { cm + max { cm + max { cm + max {∑R (m-i) + R (m-i) + R (m-i) + R (m-i) + ∑R(i-1)}R(i-1)}R(i-1)}R(i-1)} k 1 k 1 k 1 k 1 ≤iiii<k kk kk kk k<iiii≤mmmm m-1 m-1 m-1 m-1 m-1 m-1 m-1 m-1 = cm + max{ cm + max{ cm + max{ cm + max{∑R (i) + R (i) + R (i) + R (i) + ∑R(i)} R(i)} R(i)} R(i)} k m-k+1 k k m-k+1 k k m-k+1 k k m-k+1 k 1111 mmmm 1111 nnnn 归纳假设: 假定R(n)≤4cn,对于所有的n,2≤n<m成立。 归纳步骤:对于n=mn=mn=mn=m,(6.1)(6.1)(6.1)(6.1)式给出: 由于R(n)R(n)R(n)R(n)是nnnn的非递减函数,故可以得到当mmmm为偶数而k=m/2k=m/2k=m/2k=m/2时,或者当mmmm为 奇数而k=(m+1)/2k=(m+1)/2k=(m+1)/2k=(m+1)/2时,取 m-1 m-1 m-1 m-1 m-1 m-1 m-1 m-1 ∑R(i) + R(i) + R(i) + R(i) + ∑R(i)R(i)R(i)R(i) m-k+1 km-k+1 km-k+1 km-k+1 k 的极大值。因此,若mmmm为偶数,则 当iiii<mmmm时,R(i)R(i)R(i)R(i)<4ci4ci4ci4ci 取R(i)R(i)R(i)R(i)的极大值4ci4ci4ci4ci即可得左式。 m-1 m-1 m-1 m-1 R(m) R(m) R(m) R(m) ≤ cm + cm + cm + cm + ∑R(i) R(i) R(i) R(i) m/2 m/2 m/2 m/2 m-1 m-1 m-1 m-1 ≤cm + (cm + (cm + (cm + (∑ i) i) i) i) < 4cm 4cm 4cm 4cm m/2 m/2 m/2 m/2 2222 mmmm 8c8c8c8c mmmm 若mmmm是奇数,则 m-1m-1m-1m-1 R(m) R(m) R(m) R(m) ≤ cm + cm + cm + cm + ∑R(i) R(i) R(i) R(i) (m+1)/2 (m+1)/2 (m+1)/2 (m+1)/2 m-1 m-1 m-1 m-1 ≤cm + (cm + (cm + (cm + (∑ i) i) i) i) < 4cm4cm4cm4cm (m+1)/2 (m+1)/2 (m+1)/2 (m+1)/2 2222 mmmm 8c8c8c8c mmmm 由于TTTTAAAA(n)(n)(n)(n)≤R(n)R(n)R(n)R(n),所以TTTTAAAA(n)(n)(n)(n)≤4cn4cn4cn4cn,故TTTTAAAA(n)(n)(n)(n)是ОООО(n)(n)(n)(n)。证毕。 选择算法所需要的附加空间是ОООО(l)(l)(l)(l)。((((交换划分元素用)))) 2 2 2 2 最坏情况时间是ОООО(n)的选择算法 通过精细地挑选划分元素vvvv,可以得到一个最坏情况时间复杂度是ОООО(n)(n)(n)(n)的选择 算法。 划分元素最好能处在待排序列的中间位置,但确定这个中间位置是困难的。 为了得到这样一个算法,划分元素vvvv必须被选择成比一部分元素小而比另一部 分元素大。使用二次取中间值规则可以选出满足以上要求的元素vvvv。这一规则是: 将参加划分的nnnn个元素分成「n/rn/rn/rn/r」组,每组有rrrr个元素,其中rrrr是一个大于1111的正 整数。剩余的n-rn-rn-rn-r****[n/r][n/r][n/r][n/r]个元素忽略不计。 先对[n/r][n/r][n/r][n/r]组中每组的rrrr个元素分类并找出 其中间值元素mmmmiiii,1111≤iiii≤「n/rn/rn/rn/r」; 然后再从这「n/rn/rn/rn/r」个mmmmiiii中找出它们的中 间值mmmmmmmm,并将mmmmmmmm作为划分元素。 图6.16.16.16.1给出了n=35n=35n=35n=35,r=7r=7r=7r=7时的mmmmiiii和mmmmmmmm的选 择示意图。图中,BBBB1111,…………,BBBB5555是五个元素 组,每组的七个元素沿列而下已排成一个 非递减序列。每列中间的元素就是mmmmiiii。而 且这些列也按mmmmiiii的非递减进行了排列。因 此,第3333列的mmmmiiii就是mmmmmmmm ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ◎ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ B1 B2 B3 B4 B5 小于等于mmmmmmmm的元素 图6.1 n=356.1 n=356.1 n=356.1 n=35,r=7r=7r=7r=7时的二次取中间值示意图 mmmmmmmm 非 递 减 次 序 中 间 元 素 大于等于mmmmmmmm的元素 算法6.2 6.2 6.2 6.2 使用二次取中规则的选择算法的说明性描述。 Void Select2(elemType a[]Void Select2(elemType a[]Void Select2(elemType a[]Void Select2(elemType a[],intintintint k k k k,intintintint n) { n) { n) { n) { ////////在集合AAAA中找第kkkk小元素 ① 若nnnn≤rrrr,则采用插入法直接对AAAA分类并返回第kkkk小元素。 ② 把A[1:n]A[1:n]A[1:n]A[1:n]分成大小为rrrr的「n/rn/rn/rn/r」个子集合,忽略剩余的元素。 ③ 设M =M =M =M ={mmmmllll,mmmm2222,…………,mmmm「n/rn/rn/rn/r」}是上面「n/rn/rn/rn/r」个子集合的中间值的集合。 ④ v = Select2(Mv = Select2(Mv = Select2(Mv = Select2(M,「「n/rn/rn/rn/r」/2/2/2/2」,「n/rn/rn/rn/r」)))) ⑤ 用partitionpartitionpartitionpartition划分AAAA,vvvv作为划分元素。 ⑥ 假设vvvv在位置jjjj。 ⑦ case {case {case {case { :k=jk=jk=jk=j:return(v)return(v)return(v)return(v); :kk) { p = j - 115 if(j-m+1>k) { p = j - 115 if(j-m+1>k) { p = j - 115 if(j-m+1>k) { p = j - 1;}}}} 16 else {k = k - (j-m+1) 16 else {k = k - (j-m+1) 16 else {k = k - (j-m+1) 16 else {k = k - (j-m+1) ;m = j + 1m = j + 1m = j + 1m = j + 1;}}}} 17 }17 }17 }17 };//while//while//while//while 18 }//Select318 }//Select318 }//Select318 }//Select3 本算法只是依据算法6.26.26.26.2的一个描述,大家只需掌握算法思路即可。 3.7 斯特拉森矩阵乘法斯特拉森矩阵乘法斯特拉森矩阵乘法斯特拉森矩阵乘法 二维数组无论在数值计算还是在非数值计算领域中都是一种相当基本而又极 其重要的抽象数据结构,矩阵则是它的数学表示形式。因此,在研究矩阵的基 本运算时,尽可能改进运算的效率无疑是件非常重要的工作。 矩阵加和矩阵乘是两种最基本的矩阵运算。 设AAAA和BBBB是两个nnnn×nnnn矩阵,这两个矩阵相加指的是它们对应元素相加,其相加 的结果作为其和矩阵的对应元素值,因此它们的和矩阵仍是一个nnnn×nnnn矩阵,记 为C=A+BC=A+BC=A+BC=A+B。其时间显然为Θ(n(n(n(n2222))))。 如果将矩阵AAAA和BBBB的乘积记为C=ABC=ABC=ABC=AB,那么CCCC也是一个nnnn×nnnn矩阵,乘积CCCC的第iiii 行第jjjj列的元素C(i,j)C(i,j)C(i,j)C(i,j)等于AAAA的第iiii行元素和BBBB的第jjjj列的对应元素相乘积的和,可表 示为: C(i,j) = C(i,j) = C(i,j) = C(i,j) = ΣA(i,k) A(i,k) A(i,k) A(i,k) **** B(k,j) B(k,j) B(k,j) B(k,j) (1111≤iiii,jjjj≤nnnn) (7.1)(7.1)(7.1)(7.1) 1 1 1 1≤kkkk≤nnnn 按上式计算C(i,j)C(i,j)C(i,j)C(i,j),需要做nnnn次乘法和n-1n-1n-1n-1次加法,而乘积矩阵CCCC有nnnn2222个元素, 因此,由矩阵乘定义直接产生的矩阵乘算法的时间为Θ(n(n(n(n3333))))。 人们长期以来对改进矩阵乘法的效率作过不少尝试,设计了一些改进算法, 但在计算时间上都仍旧囿界于nnnn3333这一数量级。直到1969196919691969年斯特拉森((((V.StrassenV.StrassenV.StrassenV.Strassen)))) 利用分治策略并加上一些处理技巧设计出一种矩阵乘算法以后,才在计算时间 数量级上取得突破。他所设计的算法的计算时间是ОООО(n(n(n(n2.812.812.812.81))))。此结果在第一次发 表时曾震动了数学界。 下面介绍斯特拉森矩阵算法的基本思想与主要处理技巧。 为简单起见,假定nnnn是2222的正整数幂,即存在一非负整数kkkk,使得n=2n=2n=2n=2kkkk。在nnnn不 是2222的幂的情况下,,,,则可对AAAA和BBBB增加适当的全零行和全零列,使其变成维长是2222的 幂的方阵。 按照分治设计策略,首先可以将AAAA和BBBB都分成四个(n/2)(n/2)(n/2)(n/2)×(n/2)(n/2)(n/2)(n/2)的矩阵,于是AAAA 和BBBB就可以看成是两个以(n/2)(n/2)(n/2)(n/2)×(n/2)(n/2)(n/2)(n/2)矩阵为元素的2222×2222矩阵。对这两个2222×2222矩阵 采用通常的矩阵乘法运算((((即通过(7.1)(7.1)(7.1)(7.1)式计算乘积矩阵元素)))),可得 A A A A11111111 A A A A12121212 B B B B11111111 B B B B12121212 C C C C11111111 C C C C12121212 × = = = = (7.27.27.27.2) A A A A21212121 A A A A22222222 B B B B21212121 B B B B22222222 C C C C11111111 C C C C12121212 其中,CCCC11 11 11 11 = A= A= A= A11111111BBBB11111111+AAAA12121212BBBB21212121 C C C C12 12 12 12 = A= A= A= A11111111BBBB12121212+AAAA12121212BBBB22 22 22 22 (7.3) (7.3) (7.3) (7.3) C C C C21 21 21 21 = A= A= A= A21212121BBBB11111111+AAAA22222222BBBB21212121 C C C C22 22 22 22 = A= A= A= A21212121BBBB12121212+AAAA22222222BBBB22222222 使用通常的矩阵乘法和加法计算(n/2)(n/2)(n/2)(n/2)×(n/2)(n/2)(n/2)(n/2)矩阵CCCCl1l1l1l1,CCCCl2l2l2l2,CCCC21212121和CCCC22222222的各元素 的值以及C=ABC=ABC=ABC=AB各乘积元素的值,可以直接证明 CCCC11111111 C C C C12121212 C = AC = AC = AC = A×B = B = B = B = C C C C11111111 C C C C12121212 如果分块子矩阵的级((((这里是n/2n/2n/2n/2级方阵))))大于 2222,则可以继续将这些子矩阵分 成更小的方阵,直至每个子方阵只含一个元素,以至可直接计算其乘积为止。 这样的算法显然是由分治策略设计而得的。 为了用(7.3)(7.3)(7.3)(7.3)式计算ABABABAB,需要执行以(n/2)(n/2)(n/2)(n/2)×(n/2)(n/2)(n/2)(n/2)矩阵为元素的八次乘法和四次 加法。由于每两个n/2n/2n/2n/2级方阵相加可在对于某个常数cccc而言的cncncncn2222时间内完成,如 果所得到的分治算法的时间用T(n)T(n)T(n)T(n)表示,则可以得到下面的递归关系式: b nb nb nb n≤2222 T(n) = T(n) = T(n) = T(n) = 8T(n/2) + dn 8T(n/2) + dn 8T(n/2) + dn 8T(n/2) + dn2 2 2 2 n n n n>2222 其中,bbbb和dddd是常数。 求解这个递归关系式得到T(n)=T(n)=T(n)=T(n)=ОООО(n(n(n(n3333)))),与通常的矩阵乘算法计算时间具有相 同的数量级。由于矩阵乘法比矩阵加法的花费要大((((ОООО(n(n(n(n3333))))对ОООО(n(n(n(n2222)))))))),斯特拉森发 现了在分治设计的基础上使用一种减少乘法次数而让加减法次数相应增加的处 理方法来计算(7.3)(7.3)(7.3)(7.3)式中的CCCCijijijij。其处理方法是,先用七个乘法和十个加((((减))))法来算 出下面七个(n/2)(n/2)(n/2)(n/2)×(n/2)(n/2)(n/2)(n/2)矩阵: P = (AP = (AP = (AP = (Allllllll + A + A + A + A12121212))))×(B(B(B(Bllllllll + B + B + B + B22222222)))) Q = (A Q = (A Q = (A Q = (A2l2l2l2l + A + A + A + A22222222))))×BBBBllllllll R = A R = A R = A R = Allllllll×(B(B(B(Bl2l2l2l2 - B - B - B - B22222222)))) S = A S = A S = A S = A22222222×(B(B(B(B2l2l2l2l –––– B B B B11111111) (7.4)) (7.4)) (7.4)) (7.4) T = (A T = (A T = (A T = (Allllllll + A + A + A + A12121212))))×BBBB22222222 U = (A U = (A U = (A U = (A2l2l2l2l - A - A - A - A11111111))))×(B(B(B(Bllllllll + B + B + B + B12121212)))) V = (A V = (A V = (A V = (Al2l2l2l2 –––– A A A A22222222))))×(B(B(B(B2l2l2l2l + B + B + B + B22222222)))) 然后用八个加((((减))))法算出这些CCCCijijijij: C11 = P + S C11 = P + S C11 = P + S C11 = P + S –––– T + V T + V T + V T + V C12 = R + T (7.5) C12 = R + T (7.5) C12 = R + T (7.5) C12 = R + T (7.5) C21 = Q + S C21 = Q + S C21 = Q + S C21 = Q + S C22 = P + R C22 = P + R C22 = P + R C22 = P + R –––– Q + U Q + U Q + U Q + U 以上共用7777次乘法和18181818次加((((减))))法。 由T(n)T(n)T(n)T(n)所得出的递归关系式是:::: b b b b, nnnn≤2222 T(n) = (7.6)T(n) = (7.6)T(n) = (7.6)T(n) = (7.6) 7T(n/2) + an 7T(n/2) + an 7T(n/2) + an 7T(n/2) + an2222 n n n n>2222 其中,aaaa和bbbb是常数。 求解这个递归关系式,得 T(n) = anT(n) = anT(n) = anT(n) = an2222(1 + 7/4 + (7/4)(1 + 7/4 + (7/4)(1 + 7/4 + (7/4)(1 + 7/4 + (7/4)2222 + + + + ………… + (7/4) + (7/4) + (7/4) + (7/4)k-1k-1k-1k-1))))+7777kkkk(1)(1)(1)(1) ≤cncncncn2222(7/4)(7/4)(7/4)(7/4)lognlognlognlogn + 7 + 7 + 7 + 7lognlognlognlogn c c c c是一个常数 = cn= cn= cn= cnlog4+log7-log4log4+log7-log4log4+log7-log4log4+log7-log4 + n + n + n + nlog7log7log7log7 = (c + 1)n = (c + 1)n = (c + 1)n = (c + 1)nlog7log7log7log7 = = = = Ο(n(n(n(nlog7log7log7log7) ) ) ) ≈ Ο(n(n(n(n2.812.812.812.81)))) 作业:::: 1.1.1.1. 设计一个““““二分””””检索算法,,,,将原集合分成1/31/31/31/3和2/32/32/32/3大小的两个子集,将这个算 法与对半分的二分检索算法相比较,分析所设计算法的时间复杂度。 2.2.2.2. 过程MergesortMergesortMergesortMergesort的最坏情况的时间复杂度是Ο(nlogn(nlogn(nlogn(nlogn))))。它的最好情况下的时 间复杂度是多少?根据你的结论,能够说明归并分类的时间复杂度是 Θ ( ( ( (nlognnlognnlognnlogn))))吗? 在斯特拉森之后,有很多人继续设法改进他的结果,值得指出的是 J.E.HopcroftJ.E.HopcroftJ.E.HopcroftJ.E.Hopcroft和L.R.KerrL.R.KerrL.R.KerrL.R.Kerr已经证明了二个2222×2222矩阵相乘必须要用7777次乘法,因此 要进一步获得改进,则需考虑3333×3333或4444×4444等更高级数的分块子矩阵或者用完全 不同的设计策略。 最后,要提请读者注意的是,斯特拉森矩阵乘法目前还只具有理论意义,因 为只有当nnnn相当大时它才优于通常的矩阵乘法。经验表明,当nnnn取为120120120120时,斯 特拉森矩阵乘法与通常的矩阵乘法在计算时间上仍无显著的差别。尽管如此, 它还是给出了有益的启示:即使是由定义出发所直接给出的明显算法并非总是 最好的。 斯特拉森矩阵乘法可能为获得更有效和在计算机上切实可行的算法奠定了基 础。 第三讲 完了 软件理论与软件工程研究室 蒋波 第四讲 贪心方法 4.1 贪心方法的一般方法 4.2 背包问题 4.3 带有限期的作业排序 4.4 最优归并模式 目录 4.6 单源点最短路径 4.5 最小生成树 4.1 4.1 4.1 4.1 贪心方法的一般方法贪心方法的一般方法贪心方法的一般方法贪心方法的一般方法 在现实世界中,有这样一类问题:它有nnnn个输入,而它的解就由这nnnn个输入的 某个子集组成,不过这个子集必须满足某些事先给定的条件。把那些必须满足 的条件称为约束条件;而把满足约束条件的子集称为该问题的可行解。 问题的简单描述: In={nIn={nIn={nIn={n个输入}}}}; InaInaInaIna是InInInIn的子集; InaInaInaIna满足约定的条件; InaInaInaIna构成问题的解。 为了衡量可行解的优劣,事先也给出了一定的标准,这些标准一般以函数形 式给出,这些函数称为目标函数。那些使目标函数取极值((((极大或极小))))的可行 解,称为最优解。 这一类求取最优解的问题,根据描述约束条件和目标函数的数学模型的特性 或求解问题方法的不同,进一步又可划分为线性规划、整数规划、非线性规划、 动态规划等问题。尽管各类规划问题都有一些相应的求解方法,但其中的某些 问题,还可用一种更直接的方法来求解,这种方法就是贪心方法(贪婪法)。 显然,满足约束条件的子 集可能不止一个,一般来说 可行解不是唯一的。 贪心方法是一种改进了的分级处理方法。它首先根据题意,选取一种量度标 准。然后按这种量度标准对这nnnn个输入排序,并按次序每次输入一个输入量。 如果该输入与在这种量度意义下当前已构成的部分最优解组合在一起,不能产 生一个可行解,则该输入不能加到部分解中。这种能够得到某种量度意义下的 最优解的分级处理方法称为贪心方法。 要注意的是,对于一个给定的问题,往往可能有好几种量度标准。初看起 来,这些量度标准似乎都是可取的。但实际上在贪心处理中,利用量度标准中 的大多标准所得到的该量度意义下的最优解往往不是问题的最优解,而是次优 解。尤其值得指出的是,把目标函数作为量度标准所得到的解也不一定是问题 的最优解。 因此,选择能产生问题最优解的最优量度标准是使用贪心法设计求解的核 心问题。 在一般情况下,要选出最优量度标准并不是一件容易的事,不过,一旦能 选择出某个问题的最优量度标准,那么用贪心方法求解这个问题则特别有效。 贪心方法可以用下面的抽象化控制来描述。 算法1.1 1.1 1.1 1.1 贪心方法的抽象化控制 void Greedy(a[]void Greedy(a[]void Greedy(a[]void Greedy(a[],n) {n) {n) {n) { //A(1:n) //A(1:n) //A(1:n) //A(1:n)包含nnnn个输入 solution = solution = solution = solution = Φ // // // //将解向量solutionsolutionsolutionsolution初始化为空 for(j=1for(j=1for(j=1for(j=1;j=nj=nj=nj=n;++j) {++j) {++j) {++j) { x = Select(a[]) x = Select(a[]) x = Select(a[]) x = Select(a[]); if Feasible(solutionif Feasible(solutionif Feasible(solutionif Feasible(solution,x) {solution=union(solutionx) {solution=union(solutionx) {solution=union(solutionx) {solution=union(solution,x)x)x)x);}}}} } } } };//for//for//for//for return solution return solution return solution return solution; }// Greedy}// Greedy}// Greedy}// Greedy 函数SelectSelectSelectSelect的功能是按某种最优量度标准从A(1:n)A(1:n)A(1:n)A(1:n)中选择一个输入,把它的 值赋给xxxx并从输入集合AAAA中消去它。FeasibleFeasibleFeasibleFeasible是一个布尔函数,它判定xxxx是否可 以包含在解向量中。unionunionunionunion将xxxx与解向量结合并修改目标函数。过程GreedyGreedyGreedyGreedy描 述了用贪心策略设计算法的主要工作和基本控制路线。一旦给出一个特定的问 题,就可将SelectSelectSelectSelect,FeasibleFeasibleFeasibleFeasible和 UnionUnionUnionUnion具体化并付诸实现。 本节介绍使用贪心设计策略来解决更复杂的问题————————背包问题。 背包问题的描述如下: 已知有nnnn种物品和一个可容纳mmmm重量的背包,每种物品iiii的重量为wwwwiiii。假定将 物品iiii的一部分xxxxiiii放人背包就会得到ppppiiiixxxxiiii的效益,0000≤xxxxiiii≤1111,ppppiiii>0000。采用怎样的 装包方法才会使装入背包物品的总效益最大呢?显然,由于背包容量是mmmm,因 此,要求所有选中要装入背包的物品总重量不超过mmmm。如果这nnnn件物品的总重 量不超过mmmm,则把所有物品装入背包自然获得最大效益。如果这些物品重量的 和大于mmmm,则在这种情况下该如何装包呢?这是本节所要解决的问题。根据以 上讨论,可将问题形式描述如下: 极 大 化: Σppppiiiixxxxiiii (4.1)(4.1)(4.1)(4.1) 1 1 1 1 ≤iiii≤nnnn 约束条件:Σ wwwwiiiixxxxiiii ≤m m m m (4.2)(4.2)(4.2)(4.2) 1 1 1 1 ≤iiii≤nnnn 0 0 0 0≤xxxxiiii≤1111,ppppiiii>0000,wwwwiiii>0000,1111≤iiii≤nnnn (4.3)(4.3)(4.3)(4.3) 其中,(4.1)(4.1)(4.1)(4.1)式是目标函数,(4.2)(4.2)(4.2)(4.2)和(4.3)(4.3)(4.3)(4.3)是约束条件。 满足约束条件的任一集合(x(x(x(x1111, , , , …………, , , , xxxxnnnn))))是一个可行解((((即能装下)))); 使目标函数取最大值的可行解是最优解。 4.2 4.2 4.2 4.2 背包问题背包问题背包问题背包问题 例2.1 考虑下面的背包问题。n=3,m=20,(pl,p2,p3)=(25,24,15), (w1,w2,w3)=(18,15,10)。 序号 (x1,x2,x3) Σwixi 1≤i≤n Σpixi 1≤i≤n ① (1/2,1/3,1/4) 16.5 24.25 ② (1,2/15,0) 20 28.2 ③ (0,2/3,1) 20 31 ④ (0,1,1/2) 20 31.5 表2.1 2.1 2.1 2.1 例2.12.12.12.1问题的四个可行解 在这四个可行解中,第④个解的效益值最大。下面将看到,该解是背包问 题在这一情况下的最优解。 为了获取背包问题的最优解,必须要把物品放满背包。由于可以取物品 i 的一部分放到背包中去,因此这一要求是可以达到的。 如果用贪心策略来求解背包问题,则正如第llll节中所说的一样,首先要选出 最优的量度标准。不妨先取目标函数作为量度标准,即每装入一件物品就使背 包获得最大可能的效益值增量。在这种量度标准下的贪心方法就是按效益值的 非增次序(由大到小)将物品一件件放到背包中去。如果正在考虑中的物品放 不进去,则可只取其一部分来装满背包。但这最后一次的放法可能不符合使背 包每次获得最大效益增量的量度标准,此时,可以换成一种能获得最大增量的 物品,将它((((或它的一部分))))放人背包,从而使最后一次装包也符合量度标准的 要求。例如,假定还剩有两个单位的空间,而在背包外还有两种物品,这两种 物品有(p(p(p(piiii=4,w=4,w=4,w=4,wiiii=4)=4)=4)=4)和((((ppppjjjj=3,w=3,w=3,w=3,wjjjj=2)=2)=2)=2),则使用jjjj就比用iiii要好些。下面对例2.l2.l2.l2.l的数据使 用这种选择策略。 物品1111有最大的效益值(p(p(p(p1111=25)=25)=25)=25),因此首先将物品1111放人背包,这时xxxx1111=1=1=1=1,且获 得25252525的效益。背包容量中只剩下两个单位空着。物品2222有次大的效益值 (P(P(P(P2222=24)=24)=24)=24),但wwww2222=15=15=15=15,在背包中装不下物品2222,使用xxxx2222=2/15=2/15=2/15=2/15就正好装满背包。不 难看出,物品2222的2/152/152/152/15效益值=3.2=3.2=3.2=3.2比物品3333的2/102/102/102/10效益值=3.0=3.0=3.0=3.0高。所以此种选择策 略得到②的解,总效益值是28.228.228.228.2。它是一个次优解。由此例可知,按物品效益 值的非增次序装包不能得到最优解。 为什么上述贪心策略不能获得最优解呢?原因在于背包可用容量消耗过快。 由此,很自然地启发我们用容量作为量度,让背包容量尽可能慢地被消耗。这 就要求按物品重量的非递减次序(由小到大)把物品放入背包。例2.12.12.12.1的解③就 是使用这种贪心策略得到的,它仍是一个次优解。这种策略也只能得到次优 解,其原因在于容量虽然慢慢地被消耗,但效益值没能迅速地增加。这又启发 我们应采用在效益值的增长速率和容量的消耗速率之间取得平衡的量度标准。 即每一次装入的物品应使它占用的每一单位容量获得当前最大的单位效益。这 就需使物品的装入次序按ppppiiii////wwwwiiii比值的非增次序来考虑。在这种策略下的量度标 准是:已装入物品的累计效益值与所用容量之比,且每次装入的结果要使得累 计效益值与所用容量的比值,有最多的增加或最少的减小((((第二次和以后的装入 可能使该比值减小))))。将此贪心策略应用于例2.l2.l2.l2.l的数据,得到解④。 如果将物体事先按ppppiiii////wwwwiiii的非增次序分好类,则过程Greedy_KnapsackGreedy_KnapsackGreedy_KnapsackGreedy_Knapsack可得到 在这一策略下背包问题的解。如果将物品分类的时间不算在内,则此算法所用 时间为ОООО(n)(n)(n)(n)。 可以选择的策略:1 1 1 1 按效益非递减; 2 2 2 2 按重量非递减; 3 3 3 3 按ppppiiii////wwwwiiii 非递减 算法2.1 2.1 2.1 2.1 背包问题的贪心算法 void Greedy_Knapsack(float p[],float w[],float m[],float void Greedy_Knapsack(float p[],float w[],float m[],float void Greedy_Knapsack(float p[],float w[],float m[],float void Greedy_Knapsack(float p[],float w[],float m[],float x[],intx[],intx[],intx[],int n) { n) { n) { n) { //p(1:n)//p(1:n)//p(1:n)//p(1:n)和w(1:n)w(1:n)w(1:n)w(1:n)分别含有按p(i)/w(i)p(i)/w(i)p(i)/w(i)p(i)/w(i)≥p(i+1)/w(i+l)p(i+1)/w(i+l)p(i+1)/w(i+l)p(i+1)/w(i+l)排序的 nnnn件物品的 ////////效益值和重量。mmmm是背包的容量大小,而x(1:n)x(1:n)x(1:n)x(1:n)是解向量 intintintint i i i i;float cufloat cufloat cufloat cu; //cu//cu//cu//cu是背包的剩余容量 for(i=1for(i=1for(i=1for(i=1;i=ni=ni=ni=n;++i) x[i] = 0++i) x[i] = 0++i) x[i] = 0++i) x[i] = 0; ////////将解向量初始化为零 cu = mcu = mcu = mcu = m; ////////将背包剩余容量cucucucu设成mmmm for(i=1 for(i=1 for(i=1 for(i=1;i=ni=ni=ni=n;++i) {++i) {++i) {++i) { if(w[i]>cu) break if(w[i]>cu) break if(w[i]>cu) break if(w[i]>cu) break; else {x[i] = 1else {x[i] = 1else {x[i] = 1else {x[i] = 1;cu = cu - w[i]cu = cu - w[i]cu = cu - w[i]cu = cu - w[i];}}}} } } } };//for//for//for//for if(i if(i if(i if(i≤n) { x(i) = cu/w(i)n) { x(i) = cu/w(i)n) { x(i) = cu/w(i)n) { x(i) = cu/w(i); return x[]return x[]return x[]return x[]; }// Greedy_Knapsack}// Greedy_Knapsack}// Greedy_Knapsack}// Greedy_Knapsack 值得指出的是,如果把物品事先按效益值 的非增次序或重量的非递减序分好类,再使用算 法2.1就可分别得到量度标准为最优(使每次效益 增量最大或使容量消耗最慢)的解。由背包问题 量度选取的研究可知,选取最优的量度标准,实 质上为用贪心方法求解问题的核心。 下面来证明使用第三种策略的贪心算法所得到的解是一个最优解。 证明的基本思想是:把这贪心解与任一最优解相比较,如果这两个解不 同,就去找开始不同的第一个xxxxiiii,然后设法用贪心解的这个xxxxiiii去代换最优解的 那个xxxxiiii,并证明最优解在分量代换前后的总效益无任何变化。反复进行这种代 换,直到新产生的最优解与贪心解完全一样,从而证明了贪心解是最优解。 这种证明最优解的方法今后将经常使用,因此请注意掌握它。 定理2.1 2.1 2.1 2.1 如果pppp1111/w/w/w/w1111≥≥≥≥pppp2222/w/w/w/w2222≥…≥≥…≥≥…≥≥…≥ppppnnnn/w/w/w/wnnnn,则算法Greedy_KnapsackGreedy_KnapsackGreedy_KnapsackGreedy_Knapsack对 于给定的背包问题实例可生成一个最优解。 证明:设x=(xx=(xx=(xx=(x1111, , , , …………, , , , xxxxnnnn))))是Greedy_KnapsackGreedy_KnapsackGreedy_KnapsackGreedy_Knapsack所生成的解。如果所有的xxxxiiii等于 1111,显然这个解就是最优解((((效益值为ΣΣΣΣppppiiii))))。于是,设jjjj是使 xxxxjjjj≠≠≠≠1111的最小下标。由 算法可知,对于llll≤≤≤≤iiii<jjjj,xxxxiiii=1=1=1=1;对于jjjj<iiii≤≤≤≤nnnn,xxxxiiii=0=0=0=0;对于jjjj,0000≤≤≤≤xxxxjjjj<1111。如果xxxx不是 一个最优解,则必定存在一个可行解y=(yy=(yy=(yy=(y1111, , , , …………, , , , yyyynnnn)))),使得ΣΣΣΣppppiiiiyyyyiiii>ΣΣΣΣppppiiiixxxxiiii。不失一 般性,可以假定ΣΣΣΣwwwwiiiiyyyyiiii=m=m=m=m。设kkkk是使得yyyykkkk≠≠≠≠xxxxkkkk的最小下标。显然,这样的kkkk必定 存在。由上面的假设,可以推得yyyykkkk<xxxxkkkk。这可从三种可能发生的情况,即kkkk< jjjj,k=jk=jk=jk=j或kkkk>jjjj分别得证明。 ① 若kkkk<jjjj,则xxxxkkkk=1=1=1=1。因yyyykkkk≠xxxxkkkk,从而yyyykkkk<xxxxkkkk。 ② 若k=jk=jk=jk=j,则 由于Σwwwwiiiixxxxiiii=m=m=m=m,且对于llll≤iiii<jjjj,有xxxxiiii====yyyyiiii=1=1=1=1,而对于jjjj<iiii≤nnnn,有 xxxxiiii=0=0=0=0。 若yyyykkkk>xxxxkkkk,显然有Σwwwwiiiiyyyyiiii>mmmm,与yyyy是可行解矛盾。 若yyyykkkk====xxxxkkkk,与假设yyyykkkk≠xxxxkkkk矛盾,故yyyykkkk<xxxxkkkk。 ③ 若kkkk>jjjj,则Σwwwwiiiiyyyyiiii>mmmm,这是不可能的。 现在,假定把yyyykkkk增加到xxxxkkkk,那么必须从(y(y(y(yk+1k+1k+1k+1, , , , …………, , , , yyyynnnn))))中减去同样多的量,使得 所用的总容量仍是mmmm。这导致一个新解z=(zz=(zz=(zz=(z1111, , , , …………, , , , zzzznnnn)))),其中zzzziiii=x=x=x=xiiii,1111≤iiii≤kkkk,并且 Σwwwwiiii(y(y(y(yiiii-z-z-z-ziiii)=)=)=)=wwwwkkkk(z(z(z(zkkkk-y-y-y-ykkkk))))。 kkkk<iiii≤n n n n 因此,对于zzzz有 Σppppiiiizzzziiii = = = = Σppppiiiiyyyyiiii + ( + ( + ( + (zzzzkkkk-y-y-y-ykkkk)w)w)w)wkkkkppppkkkk/w/w/w/wkkkk - - - -Σ(y(y(y(yiiii-z-z-z-ziiii)w)w)w)wiiiiPPPPiiii/w/w/w/wiiii 1 1 1 1≤iiii≤n 1n 1n 1n 1≤iiii≤n kn kn kn k <iiii≤n n n n ≥Σ ppppiiiiyyyyiiii + [( + [( + [( + [(zzzzkkkk-y-y-y-ykkkk)wk)wk)wk)wk - - - - Σ(y(y(y(yiiii-z-z-z-ziiii)w)w)w)wiiii]]]]****ppppiiii/w/w/w/wkkkk 1 1 1 1≤iiii≤n kn kn kn k <iiii≤nnnn = = = = Σppppiiiiyyyyiiii 1 1 1 1≤iiii≤nnnn 若Σppppiiiizzzziiii>Σppppiiiiyyyyiiii,则yyyy不可能是最优解;若相等,且z=xz=xz=xz=x,则xxxx就是最优解;若 zzzz≠xxxx,则需重复上述讨论,或者证明yyyy不是最优解,或者把yyyy转换成xxxx,从而证明 了xxxx也是最优解。证毕。 在这一节将应用贪心设计策略来解决操作系统中单机、无资源约束且每个作 业可在等量的时间内完成的作业调度问题。即,假定只能在一台机器上处理nnnn 个作业,每个作业均可在单位时间内完成;又假定每个作业iiii都有一个截止期 限ddddiiii>0(0(0(0(它是整数)))),当且仅当作业iiii在它的期限截止以前被完成时,则获得ppppjjjj> 0000的效益。 这个问题的一个可行解是:这nnnn个作业的一个子集合J[]J[]J[]J[],J[]J[]J[]J[]中的每个作业都 能在各自截止期限之前完成。可行解的效益值是J[]J[]J[]J[]中作业的效益之和,即Σ ppppiiii(i(i(i(i∈J)J)J)J)。 具有最大效益值的可行解就是最优解。 例3.13.13.13.1 n=4 n=4 n=4 n=4,(p(p(p(p1111,p,p,p,p2222,p,p,p,p3333,p,p,p,p4444)=(100,10,15,20))=(100,10,15,20))=(100,10,15,20))=(100,10,15,20)和(d(d(d(d1111,d,d,d,d2222,d,d,d,d3333,d,d,d,d4444)=(2,l,2,1))=(2,l,2,1))=(2,l,2,1))=(2,l,2,1)。 这个问题的可行解和它们的效益值如表3.13.13.13.1,其中,解⑦是最优的。 所允许的处理次序是:先处理作业4444,再处理作业llll。于是,在时间0000开始处 理作业4444,而在时间2222完成对作业1111的处理。 截止时间不是作业运行所需要的时间,而是指作业必须完成的时间限界,既 在此之前应该完成。 4.3 4.3 4.3 4.3 带有限期的作业排序(带有限期的作业排序(带有限期的作业排序(带有限期的作业排序(自己看自己看自己看自己看)))) 表3.1 例3.1的可行解与效益值(截止时间为时间2) 序号 可行解 处理顺序 效益值 ① (1) 1 100 ② (2) 2 10 ③ (3) 3 15 ④ (4) 4 20 ⑤ (1,2) 2,1 110 ⑥ (1,3) 1,3或3,1 115 ⑦ (1,4) 4,1 120 ⑧ (2,3) 2,3 25 ⑨ (3,4) 4,3 35 4.3.l 4.3.l 4.3.l 4.3.l 带有限期的作业排序算法带有限期的作业排序算法带有限期的作业排序算法带有限期的作业排序算法 为了拟制出一个最优解的算法,应制定如何选择下一个作业的量度标准, 利用贪心策略,使得所选择的下一个作业在这种量度下达到最优。不妨首先 就把目标函数Σppppiiii(i(i(i(i∈J)J)J)J)作为量度。使用这一量度,下一个要计入的作业将是 使得,在满足所产生的JJJJ是一个可行解的限制条件下,让Σppppiiii(i(i(i(i∈J)J)J)J)达到最大 增加的作业。这就要求按PPPPiiii的非增次序来考虑这些作业。利用上例中的数据 来应用这一准则。 开始时,J=J=J=J=фффф,ΣΣΣΣppppiiii = 0 = 0 = 0 = 0,由于作业1111有最大效益且J={1}J={1}J={1}J={1}是一个可行解, iiii∈J J J J 于是把作业1111计入JJJJ。 下一步考虑作业4444,J={1,4}J={1,4}J={1,4}J={1,4}也是可行解。然后考虑作业3333,因为{1,3,4}{1,3,4}{1,3,4}{1,3,4}不是 可行解,故作业3333被舍弃。最后考虑作业2222,由于{1,2,4}{1,2,4}{1,2,4}{1,2,4}也不可行,作业2222被舍 弃。最终所留下的是效益值为120120120120的解J={1,4}J={1,4}J={1,4}J={1,4}。它是这个问题的最优解。 现在,对上面所叙述的贪心方法,以算法3.13.13.13.1的形式给出其粗略的描述。 void void void void GreedyJob(intGreedyJob(intGreedyJob(intGreedyJob(int d[] d[] d[] d[],intintintint j[] j[] j[] j[],intintintint n) { n) { n) { n) { ////////作业按pppp1111≥, , , , …………, , , , ≥ppppnnnn的次序输入,它们的期限值d[i]>1d[i]>1d[i]>1d[i]>1, //1//1//1//1≤iiii≤nnnn,nnnn≥1111。 //J[ ]//J[ ]//J[ ]//J[ ]是在它们的截止期限前完成的作业的集合 1111 J = {1}J = {1}J = {1}J = {1}; 2222 for(i=2for(i=2for(i=2for(i=2;i=ni=ni=ni=n;++i) {++i) {++i) {++i) { 3333 if(J if(J if(J if(J∪{i}{i}{i}{i}的所有作业都能在它们的截止期限前完成) {J=J) {J=J) {J=J) {J=J∪{i}{i}{i}{i}; 4444 }if}if}if}if 5555 } } } } 6666 }// }// }// }// GreedyJobGreedyJobGreedyJobGreedyJob 算法3.1 作业排序算法的概略描述 上述算法的思路是简洁明确的,下面给出一个定理,说明该算法一定能得 到一个最优解。 定理3.1 算法3.1所描述的贪心方法对于作业排序问题总是得到一个最优 解。 证明: 设((((ppppiiii,d,d,d,diiii)))),1111≤iiii≤nnnn,是作业排序问题的任一实例,JJJJ是由贪心方法所选择的作 业的集合,IIII是一个最优解的作业集合。可证明JJJJ和 IIII具有相同的效益值,从而JJJJ 也是最优的。 假定IIII≠JJJJ,因为若I=JI=JI=JI=J,则JJJJ即为最优解。 容易看出,如果IIII是JJJJ子集,则IIII就不可能是最优的。由贪心法的工作方式也排 斥了JJJJ是IIII的子集的可能。因此,至少存在着这样的两个作业aaaa和bbbb,使得aaaa∈JJJJ且 aaaa∈IIII,bbbb∈IIII且bbbb∈JJJJ。设aaaa是使得aaaa∈JJJJ且aaaa∈IIII的一个具有最高效益值的作业。由贪 心方法可以得出,对于在IIII中又不在JJJJ中的所有作业bbbb,都有ppppaaaa≥ppppbbbb。这是因为若 ppppbbbb>ppppaaaa,则贪心方法会先于作业aaaa考虑作业bbbb并且把bbbb计入到JJJJ中去。 现在,设SSSSJJJJ和SSSSIIII分别是JJJJ和IIII的可行调度表。设iiii是既属于JJJJ又属于IIII的一个作业。 又设IIII在 SSSSJJJJ中的tttt到t+1t+1t+1t+1时刻被调度,而在SSSSIIII中则在tttt’’’’到tttt’’’’+1+1+1+1时刻被调度。如果tttt< tttt’’’’,则可以将SSSSIIII中[tttt’’’’,t,t,t,t’’’’+1+1+1+1]时刻所调度的那个作业((((如果有的话))))与iiii相交换。如 果JJJJ中在「tttt’’’’,t,t,t,t’’’’+1+1+1+1」时刻没有作业被调度,则iiii移到[t[t[t[t’’’’,t,t,t,t’’’’+l]+l]+l]+l]时刻调度。所形成的这 个调度表也是可行的。如果tttt’’’’<tttt,则可在SSSSIIII中作类似的调换。用这种方法,就能 得到调度表SSSS’’’’JJJJ和SSSS’’’’IIII,使JJJJ和IIII中共有的所有作业在相同的时间被调度。考虑SSSS’’’’JJJJ中 [ttttaaaa,t,t,t,taaaa+1+1+1+1]时刻,在这时刻内((((上面所定义的))))作业aaaa被调度。 为了便于具体实现算法3.23.23.23.2,考虑对于一个给定的JJJJ,如何确定它是否为可行解 的问题,一个明显的方法是检验JJJJ中作业的所有可能的排列,对于任一种次序排 列的作业序列,判断这些作业是否能在其限期前完成。假定JJJJ中有kkkk个作业,这 就需要检查kkkk!个排列。对于所给出的一个排列σσσσ====IIII1111IIII2222…………IIIIkkkk,由于完成作业 iiiijjjj(1(1(1(1≤≤≤≤jjjj≤≤≤≤k)k)k)k)的最早时间是jjjj,因此,只要判断出σσσσ排列中的每个作业的ddddijijijij≥≥≥≥jjjj,就可得 知σσσσ是一个允许的调度序列,从而JJJJ就是一个可行解。反之,如果σσσσ排列中有一个 ddddijijijij<JJJJ,则σσσσ是一个行不通的调度序列,因为至少作业iiiijjjj不会在它的限期前完成, 故必须去检查JJJJ的另外形式的排列。事实上,对于JJJJ的可行性可以通过只检验JJJJ中 作业的一种特殊的排列来确定,这个排列是按期限的非递减次序来完成的。 设作业bbbb在SSSS’’’’IIII中的这一时刻被调度。根据对aaaa的选择,ppppaaaa≥≥≥≥ppppbbbb。在SSSS’’’’IIII的「ttttaaaa,,,,ttttaaaa+1+1+1+1」 时刻去掉作业bbbb而调度作业aaaa,这就给出了一张关于作业集合IIII’’’’ = I = I = I = I ––––{b}{b}{b}{b}∪{a}{a}{a}{a}可行 的调度表。显然IIII’’’’的效益值不小于IIII的效益值,并且 IIII’’’’比IIII少了一个与JJJJ不同的作 业。 重复使用上述转换,将使IIII能在不减效益值的情况下转换成JJJJ,因此JJJJ也必定是 最优解。 证毕。 定理3.2 3.2 3.2 3.2 设JJJJ是kkkk个作业的集合,σ=I=I=I=I1111IIII2222…………IIIIkkkk是JJJJ中作业的一种排列,它使得 ddddilililil≤ddddi2i2i2i2≤…………≤ddddikikikik。JJJJ是一个可行解,当且仅当JJJJ中的作业可以按照σ的次序而又 不违反任何一个期限的情况来处理。 证明::::显然,如果JJJJ中的作业可以按照σσσσ的次序而又不违反任何一个期限的情 况来处理,则JJJJ就是一个可行解。 现证,若JJJJ是可行解,则σσσσ表示可以处理这些作业的一种允许的调度序列。由 于JJJJ可行,则必存在σσσσ’’’’=r=r=r=r1111rrrr2222…………rrrrkkkk,使得ddddijijijij≥≥≥≥jjjj,1111≤≤≤≤jjjj≤≤≤≤kkkk。假设σ≠σσ≠σσ≠σσ≠σ’’’’,那么,令aaaa是使 得rrrraaaa≠≠≠≠iiiiaaaa的最小下标。设rrrrbbbb====iiiiaaaa,显然bbbb>aaaa。在σσσσ’’’’中将rrrraaaa与rrrrbbbb相交换,因为ddddrararara> ddddrbrbrbrb,故作业可依新产生的排列σσσσ’’’’’’’’=s=s=s=s1111ssss2222…………sssskkkk的次序处理而不违反任何一个期限。 连续使用该方法,就可将σσσσ’’’’转换成σσσσ且不违反任何期限。 证毕。 即使这些作业有不同的处理时间titititi≥≥≥≥0000,上述定理亦真。其证明留作习题。 根据定理3.23.23.23.2,可将带限期的作业排序问题作如下处理:首先将作业1111存入解数 组JJJJ中,然后处理作业2222到作业nnnn。假设已处理了i-1i-1i-1i-1个作业,其中有kkkk个作业已计 入J(1), J(2), J(1), J(2), J(1), J(2), J(1), J(2), …………, J(k)), J(k)), J(k)), J(k))之中,且有 D(J(l)) D(J(l)) D(J(l)) D(J(l)) ≤D(J(2))D(J(2))D(J(2))D(J(2))≤…………≤D(J(k))D(J(k))D(J(k))D(J(k)),现在处理作业iiii。 为了判断J J J J ∪{i}{i}{i}{i}是否可行,只需看能否找出按期限的非递减插入作业 iiii的适当 位置,使得作业iiii在此处插入后有D(J( r ))D(J( r ))D(J( r ))D(J( r ))≥rrrr,llll≤rrrr≤k+1k+1k+1k+1。找作业iiii可能的插入位 置按如下方式进行:将D(J(k))D(J(k))D(J(k))D(J(k)),D(J(k-l))D(J(k-l))D(J(k-l))D(J(k-l)),…………,D(J(1))D(J(1))D(J(1))D(J(1))逐个与D(i)D(i)D(i)D(i)比较,直到找 到位置qqqq,它使得D(i) D(i) D(i) D(i) <D(J(1))D(J(1))D(J(1))D(J(1));qqqq<1111≤kkkk且D(J(q))D(J(q))D(J(q))D(J(q))≤D(i)D(i)D(i)D(i)。 此时,若D(J(1))D(J(1))D(J(1))D(J(1))>1111,qqqq<1111≤kkkk,则说明这k-qk-qk-qk-q个作业均可延迟一个单位时间处 理,即可将这些作业在JJJJ中均后移一个位置而不违反各自的期限值。 在以上条件成立时,只要D(i)D(i)D(i)D(i)>qqqq,就可将作业iiii在位置q+lq+lq+lq+l处插入,从而得到 一个按期限的非递减序排列的含有k+1k+1k+1k+1个作业的可行解。以上过程可反复进行到 对第nnnn个作业处理完毕,所得到的贪心解由定理3.13.13.13.1可知是一个最优解。该处理过 程可用算法3.23.23.23.2来描述。算法中引进了一个虚构的作业0000,它被放在J(0)J(0)J(0)J(0),且 D(J(0))=0D(J(0))=0D(J(0))=0D(J(0))=0。引入这一虚构作业是为了便于将作业插入位置1111。 算法3.23.23.23.2 带有限期和效益的单位时间的作业排序贪心算法 line void line void line void line void JS(intJS(intJS(intJS(int D, D, D, D, intintintint J, J, J, J, intintintint n, n, n, n, intintintint k) { k) { k) { k) { //D(1), //D(1), //D(1), //D(1), …………, D(n), D(n), D(n), D(n)是期限值,nnnn≥1111。作业已按ppppllll≥pppp2222…………≥ppppnnnn被排序。J(i)J(i)J(i)J(i)是 ////////最优解中的第iiii个作业,1111≤iiii≤kkkk。终止时,D(J(i))D(J(i))D(J(i))D(J(i))≤D(J(i+1))D(J(i+1))D(J(i+1))D(J(i+1)),1111≤iiii<kkkk 1111 intintintint i i i i,rrrr; 2222 d[0] = j[0] = 0 //d[0] = j[0] = 0 //d[0] = j[0] = 0 //d[0] = j[0] = 0 //初始化 3333 k = 1k = 1k = 1k = 1;j[1] = 1j[1] = 1j[1] = 1j[1] = 1; ////////计入作业1111 4444 for(i=2 for(i=2 for(i=2 for(i=2;i=ni=ni=ni=n;++i) ++i) ++i) ++i) {//{//{//{//按pppp的非增次序考虑作业。找iiii的位置并检查插入的可行性 5555 r = kr = kr = kr = k; 6666 while((d[j[r]] > d[i]) && (d[j[r]]!= r)) {while((d[j[r]] > d[i]) && (d[j[r]]!= r)) {while((d[j[r]] > d[i]) && (d[j[r]]!= r)) {while((d[j[r]] > d[i]) && (d[j[r]]!= r)) { 7777 r = r - l r = r - l r = r - l r = r - l; 8888 }}}};// while // while // while // while 9999 if((d[j[r]] <= d[i]) && (d[i] > r)) { // if((d[j[r]] <= d[i]) && (d[i] > r)) { // if((d[j[r]] <= d[i]) && (d[i] > r)) { // if((d[j[r]] <= d[i]) && (d[i] > r)) { //把 iiii插入JJJJ中 10101010 for(i=kfor(i=kfor(i=kfor(i=k;i=r+li=r+li=r+li=r+l;--i) {--i) {--i) {--i) { 11111111 j[i+1] = j[1) j[i+1] = j[1) j[i+1] = j[1) j[i+1] = j[1); 12121212 };//for};//for};//for};//for 13131313 j[r+1] = i j[r+1] = i j[r+1] = i j[r+1] = i;k = k+1k = k+1k = k+1k = k+1; 14141414 }if}if}if}if 15151515 } } } };//for//for//for//for 16161616 }//JS }//JS }//JS }//JS 对于JSJSJSJS,有两个赖之以测量其复杂度的参数,即作业数nnnn和包含在解中的作业 数ssss。第6666~8888行的循环至多迭代kkkk次,每次迭代的时间为O(l)O(l)O(l)O(l)。若第9999行的条件为 真,则执行10101010~13131313行语句。这些要求ОООО(k-r)(k-r)(k-r)(k-r)时间去插入作业iiii。 因此,对于4444~15151515行的循环,其每次选代的总时间是ОООО(k)(k)(k)(k)。该循环共选代n-1n-1n-1n-1 次。如果ssss是kkkk的终值,即ssss是最后所得解的作业数,则算法JSJSJSJS所需要的总时间是 ОООО(sn(sn(sn(sn))))。 由于ssss≤nnnn,因此JSJSJSJS的最坏情况时间是ОООО(n(n(n(n2222))))。这种情况在pppp1111=d=d=d=d1111=n-i+1(1=n-i+1(1=n-i+1(1=n-i+1(1≤iiii≤n)n)n)n) 时就会出现。 进而可以证明最坏情况计算时间为Θ(n(n(n(n2222))))。在计算空间方面,除了DDDD所需要的 空间外,为了存放解JJJJ,还需要 Θ(s)(s)(s)(s)的空间量。 要指出的是,JSJSJSJS并不需要具体的效益值,只要知道ppppiiii≥ppppi+li+li+li+l(1(1(1(1≤iiii≤n)n)n)n)即可。 4.3.2 4.3.2 4.3.2 4.3.2 一种更快的作业排序算法一种更快的作业排序算法一种更快的作业排序算法一种更快的作业排序算法 通过使用不相交集合的Union与Find算法(使用压缩规则的查找算法)以及 使用一个不同的方法来确定部分解的可行性,可以把JS的计算时间由ОООО(n2)降 低到数量级相当接近于ОООО(n)。如果J是作业的可行子集,那么可以使用下述规 则来确定这些作业中的每一个作业的处理时间:若还没给作业i分配处理时 间,则分配给它时间片[α-1,α],其中α应尽量取大且时间片[α-1,α] 是空的。此规则就是尽可能推迟对作业i的处理。于是,在将作业一个一个地 装配到J中时,就不必为接纳新作业而去移动J中那些已分配了时间片的作业。 如果正被考虑的新作业不存在在像上面那样定义的α,这个作业就不能计入J。 这样处理的正确性证明留作习题。 例3.2 设n=5,(p1,…………,p5)=(20,15,10,5,1)和(d1,…………,d5)=(2,2,l,3,3)。 使用上述可行性规则,可得结果如表3.2所示。 表3.2 例3.2的执行结果 J 已分配的时间片 正被考虑的作业 动作 фффф 无 1 分配[1,2] {1} [1,2] 2 分配[0,1] {1,2} [0,1],[1,2] 3 不合适,舍弃 {1,2} [0,1],[1,2] 4 分配[2,3] {1,2,4} [0,1],[1,2],[2,3] 5 舍弃 最优解是J={1,2,4} 由于只有nnnn个作业且每个作业花费一个单位时间,因此只需考虑这样一些时间 片[i-1,ii-1,ii-1,ii-1,i],1111≤iiii≤bbbb,其中 b=minb=minb=minb=min{n,max{dn,max{dn,max{dn,max{diiii}}}}}。为简便起见,以后用 iiii来表 示时间片[i-1,ii-1,ii-1,ii-1,i]。 易于看出,这nnnn个作业的期限值只能是{l,2,{l,2,{l,2,{l,2,…………,b},b},b},b}中的某些((((或全部))))元素。实现 上述调度规则的一种方法是把这bbbb个期限值分成一些集合。对于任一期限值iiii, 设nnnniiii是使得nnnnjjjj≤iiii的最大整数且是空的时间片。 为避免极端情况,引进一个虚构的期限值0000和时间片[-1,0][-1,0][-1,0][-1,0]。当且仅当nnnnjjjj====nnnnjjjj 时,期限值iiii和jjjj在同一个集合中,即所要处理作业的期限值如果是 iiii或jjjj,则当前 可分配的最接近的时间片是nnnniiii。显然,若iiii<jjjj,则iiii,i+li+li+li+l,i+2i+2i+2i+2,…………,jjjj都在同一个 集合中。 因此,上述方法就是作出一些以期限值为元素的集合,且使同一集合中的元 素都有一个当前共同可用的最接近的空时间片。对于每个期限值iiii,0000≤iiii≤bbbb, 当前最接近的空时间片可用线性表元素F(i)F(i)F(i)F(i)表示,即F(i)=F(i)=F(i)=F(i)=nnnniiii。使用集合表示法, 把每个集合表示成一棵树。根结点就认为是这个集合。最初,这b+1b+1b+1b+1个期限值 最接近的空时间片是F(i)= iF(i)= iF(i)= iF(i)= i,0000≤iiii≤bbbb,并且有b+1b+1b+1b+1个集合与b+1b+1b+1b+1个期限值相对应。 用P(i)P(i)P(i)P(i)把期限值iiii链接到它的集合树上。开始时P(i)=-1P(i)=-1P(i)=-1P(i)=-1,0000≤iiii≤b b b b 。如果要调度具 有期限dddd的作业,那么就需要去寻找包含期限值min{n,d}min{n,d}min{n,d}min{n,d}的那个根。如果这个根 是jjjj且只要F(j)F(j)F(j)F(j)≠0000,则FQFQFQFQ=是最接近的空时间片。在使用了这一时间片后,其 根为jjjj的集合应与包含期限值F(j)-1F(j)-1F(j)-1F(j)-1的集合合并。 过程FJSFJSFJSFJS描述了这个更快的算法。易于看出它的计算时间是ОООО(n(n(n(nα(2n(2n(2n(2n,n))n))n))n)) ((((参看AckermannAckermannAckermannAckermann函数的逆函数))))。它用于FFFF和PPPP的空间至多为2n2n2n2n个字节。 line void line void line void line void FJS(intFJS(intFJS(intFJS(int d[] d[] d[] d[],intintintint n n n n,intintintint b b b b,intintintint j[] j[] j[] j[],intintintint k) { k) { k) { k) { // // // //找最优解J=J(1)J=J(1)J=J(1)J=J(1),…………,J(k).J(k).J(k).J(k).假定ppppllll≥pppp2222≥…………≥ppppnnnn, //b=min//b=min//b=min//b=min{nnnn,max(D(i))max(D(i))max(D(i))max(D(i))} l l l l intintintint f[b] f[b] f[b] f[b],p[b]p[b]p[b]p[b]; 2 for(i=02 for(i=02 for(i=02 for(i=0;i=ni=ni=ni=n;++i) { //++i) { //++i) { //++i) { //将树置初值 3 f[i]=i3 f[i]=i3 f[i]=i3 f[i]=i;p[i]=-1p[i]=-1p[i]=-1p[i]=-1; 4 }4 }4 }4 };//for//for//for//for 5 k=05 k=05 k=05 k=0;////////初始化J J J J 6 for(i=06 for(i=06 for(i=06 for(i=0;i=ni=ni=ni=n;++i) { //++i) { //++i) { //++i) { //使用贪心规则 7 j=Find(min(n7 j=Find(min(n7 j=Find(min(n7 j=Find(min(n,d[i]))d[i]))d[i]))d[i])); 8 if(f[j]!=0) { k=k+l8 if(f[j]!=0) { k=k+l8 if(f[j]!=0) { k=k+l8 if(f[j]!=0) { k=k+l;j[k]=ij[k]=ij[k]=ij[k]=i; ////////选择作业i i i i 9 i = Find(f[j]-1)9 i = Find(f[j]-1)9 i = Find(f[j]-1)9 i = Find(f[j]-1);Union(lUnion(lUnion(lUnion(l,j)j)j)j); 10 f[j]=f[1] 10 f[j]=f[1] 10 f[j]=f[1] 10 f[j]=f[1] ; 11 }11 }11 }11 };//if //if //if //if 12 }12 }12 }12 };//for //for //for //for 13 }//FJS 13 }//FJS 13 }//FJS 13 }//FJS 例3.43.43.43.4 设n=7n=7n=7n=7,(P(P(P(P1111,…………,PPPP7777)=(35,30,25,20,15,10,5))=(35,30,25,20,15,10,5))=(35,30,25,20,15,10,5))=(35,30,25,20,15,10,5)和(d(d(d(d1111,…………,dddd7777)=(4,2,4,3,4,8,3))=(4,2,4,3,4,8,3))=(4,2,4,3,4,8,3))=(4,2,4,3,4,8,3)。 利用算法FJSFJSFJSFJS求解上述作业排序问题的最优解。 最优解J=J=J=J={1111,2222,3333,4444,6666}。过程如表3.33.33.33.3所示。 {1,2,3,4} 0 1 1 1 3 5 6 7 3 {1,2,3} 0 1 1 3 3 5 6 7 2 {1,2} 0 1 2 3 4 5 6 7 1 {1} 0 1 2 3 4 5 6 7 无 J(ФФФФ)树 F(0)F(1)F(2)F(3)F(4)F(5)F(6)F(7) 考虑作业 -1 0 1 2 3 4 5 6 7 -1 -1 -1 -1 -1 -1 -1 -1 0 1 2 3 5 6 7 -1 -1 -2 -1 -1 -1 34 -1 0 1 3 5 6 7 -1 -2 -1 -1 -1 3412 -1 0 1 5 6 7 -4 1 -1 -1 -1 34 12 3 表3.3 3.3 3.3 3.3 例3.43.43.43.4的执行过程 结构不变 7 舍弃 0 0 1 1 3 5 6 6 6 {1,2,3,4} 0 0 1 1 3 5 6 7 5 舍弃 {1,2,3,4} 0 0 1 1 3 5 6 7 4 J(ФФФФ) 树 F(0)F(1)F(2)F(3)F(4)F(5)F(6)F(7) 考虑作业 续表 1 1 5 6 7 -5 1 -1 -1 -1 34 10 3 2 1 1 5 6 7 -5 1 -1 -1 -1 1 4 1 0 32 1 1 5 6 -5 1 -1 -2 61 4 1 0 32 7 在前面已学过两个分别包含nnnn个和mmmm个记录的已分类文件可以在ОООО(n+m)(n+m)(n+m)(n+m)时间 内归并在一起而得到一个分类文件。当要把两个以上的已分类文件归并在一起 时,可以通过成对地重复归并已分类的文件来完成。 例如,假定XXXX1111,XXXX2222,XXXX3333,XXXX4444是要归并的文件,则可以首先把XXXX1111和XXXX2222归并成文 件YYYY1111,然后将YYYY1111和XXXX3333归并成YYYY2222,最后将YYYY2222和XXXX4444归并,从而得到想要的分类文 件;也可以先把XXXX1111和XXXX2222归并成YYYY1111,然后把XXXX3333和XXXX4444归并成YYYY2222,最后归并YYYY1111和YYYY2222而 得到想要的分类文件。给出nnnn个文件,则有许多种把这些文件成对地归并成一个 单一分类文件的方法。不同的配对法要求不同的计算时间。现在所要论及的问 题是确定一个把nnnn个已分类文件归并在一起的最优方法((((即需要最少比较的方法))))。 例4.14.14.14.1 X X X X1111,XXXX2222和XXXX3333是各自有30303030个记录、20202020个记录和10101010个记录的三个已分类文 件,归并XXXX1111和XXXX2222需要50505050次记录移动,再与XXXX3333归并则还要60606060次移动,其所需要的 记录移动总量是110110110110。如果首先归并X2X2X2X2和X3(X3(X3(X3(需要30303030次移动)))),然后归并XXXX1111((((需要 60606060次移动)))),则所要作的记录移动总数仅为90909090。因此,第二个归并模式比第一个 要快些。 4.4 4.4 4.4 4.4 最优归并模式最优归并模式最优归并模式最优归并模式 试图得到最优归并模式的贪心方法是容易表达的。由于归并一个具有 nnnn个记录 的文件和一个具有mmmm个记录的文件可能需要n+mn+mn+mn+m次记录移动,因此对于量度标 准的一种明显的选择是:每一步都归并记录数最小的两个文件。例如,有五个 文件(F(F(F(Fllll, , , , …………, F, F, F, F5555)))),它们的记录数为(20,30,10,5,30)(20,30,10,5,30)(20,30,10,5,30)(20,30,10,5,30),由以上的贪心策略就会产生以 下的归并模式:F3F3F3F3和F4F4F4F4归并成ZZZZ1111(|Z(|Z(|Z(|Z1111|=15)|=15)|=15)|=15);归并ZZZZ1111和FFFFllll得到ZZZZ2222(|Z(|Z(|Z(|Z2222|=35)|=35)|=35)|=35);把FFFF2222和 FFFF5555归并成ZZZZ3333(|Z(|Z(|Z(|Z3333|=60)|=60)|=60)|=60);归并ZZZZ2222和ZZZZ3333而得到答案ZZZZ4444。记录移动总量是205205205205。可以证 明这是给定问题实例的最优归并模式。 像刚才所描述的归并模式称为二路归并模式((((每一个归并步包含两个文件的归 并))))。二路归并模式可以用二元归并树来表示。 95 Z4 35 15 60 Z1 20 30 Z2 Z3 105 F1 F2 F3 30 F4 F5 图4.1 例4.1的二元归并树 图4.14.14.14.1显示了一棵表示上面五个文件所得到 的最优归并模式的二元归并树。叶结点被画成 方块形,表示这五个已知的文件。这些结点称 为外部结点。剩下的结点被画成圆圈,称为内 部结点。每个内部结点恰好有两个儿子,它表 示把它的两个儿子所表示的文件归并而得到的 文件。每个结点中的数字都是那个结点所表示 的文件的长度((((即记录数))))。 一个最优二路归并模式与一棵具有最小权外部路径的二元树相对应,算法 4.14.14.14.1 的函数TreeTreeTreeTree使用上面所叙述的规则去获得nnnn个文件的二元归并树。这算法把nnnn个 树的表LLLL作为输入。树中每一结点有三个信息段,LChildLChildLChildLChild,RChildRChildRChildRChild和WeightWeightWeightWeight。 起初,LLLL中的每一棵树正好有一个结点。这个结点是一个外部结点,而且其 LChildLChildLChildLChild和RChildRChildRChildRChild信息段为0000,而WeightWeightWeightWeight是要归并的nnnn个文件之一的长度。在这个 算法运行期间,对于LLLL中的任何一棵具有根结点TTTT的树,Weight(T)(Weight(T)(Weight(T)(Weight(T)(树TTTT中外部结 点的长度的和))))表示要归并的文件的长度。 过程TreeTreeTreeTree用了三个子算法,GetNode(TGetNode(TGetNode(TGetNode(T)))),Least(L)Least(L)Least(L)Least(L)和Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T)。 函数GetNode(TGetNode(TGetNode(TGetNode(T))))为构造这棵树提供一个新结点。 函数Least(L)Least(L)Least(L)Least(L)找出LLLL中一棵其根具有最小的WeightWeightWeightWeight值的树,并把这棵树从LLLL中 删去。 函数Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T)把根为TTTT的树插入到表LLLL中。 定理4.14.14.14.1将证明贪心过程Tree(Tree(Tree(Tree(算法4.1)4.1)4.1)4.1)将产生一棵最优的二元归并树。 第4级外部结点F4距离根结点Z4的路径长度为3(一个i级结点距离根结点的路径 长度为i-1),因此文件F4的记录都要移动3次,一次得到Z1,一次得到Z2,最后移 动一次就得到Z4。如果di是由根到代表文件Fi的外部结点的距离; qi是Fi的长度,则这棵二元归并树的记录移动总量是: n i=1 ∑diqi 算法4.14.14.14.1 生成二元归并树算法 line void Tree(list line void Tree(list line void Tree(list line void Tree(list L,intL,intL,intL,int n) { n) { n) { n) { //L //L //L //L是如上所述的nnnn个单结点二元树的表 l for(i=1l for(i=1l for(i=1l for(i=1;i<=n-1i<=n-1i<=n-1i<=n-1;++i) {++i) {++i) {++i) { 2 2 2 2 GetNode(TGetNode(TGetNode(TGetNode(T) ) ) ) ; ////////用于归并两棵树 3 3 3 3 LChild(TLChild(TLChild(TLChild(T) = Least(L)) = Least(L)) = Least(L)) = Least(L); ////////最小的长度 4 4 4 4 RChild(TRChild(TRChild(TRChild(T) = Least(L)) = Least(L)) = Least(L)) = Least(L); 5 Weight(T) = 5 Weight(T) = 5 Weight(T) = 5 Weight(T) = Weight(LChild(TWeight(LChild(TWeight(LChild(TWeight(LChild(T)) + )) + )) + )) + Weight(RChild(TWeight(RChild(TWeight(RChild(TWeight(RChild(T)))))))); 6 Insert(L6 Insert(L6 Insert(L6 Insert(L,T)T)T)T) 7 }//for7 }//for7 }//for7 }//for 8 return Least(L)8 return Least(L)8 return Least(L)8 return Least(L); ////////留在LLLL中的树是归并树 9 }//Tree9 }//Tree9 }//Tree9 }//Tree 例4.24.24.24.2 当LLLL最初表示其长度为2222,3333,5555,7777,9999,13131313的六个文件时,算法TTTTreereereeree是如 何工作的。 图4.24.24.24.2显示出在forforforfor循环的每一次选代结束时的表LLLL。在算法结束时所产生的二 元归并树可以用来确定归并了哪些文件。归并是在这棵树中““““最低””””((((有最大的深 度))))的那些文件上进行的。 迭代后 开始 L 2 3 5 7 9 13 5 7 9 13 1 2 3 5 7 9 13 2 5 10 2 3 5 13 3 5 10 2 3 5 7 9 16 4 7 9 16 13 23 5 10 2 3 5 5 39 7 9 16 13 23 5 10 2 3 5 图4.2 例4.1表L中树的构造过程 现在来分析算法4.14.14.14.1需要的计算时间。主循环执行n-1n-1n-1n-1次。如果保持LLLL按照这些 根中的WeightWeightWeightWeight值的非递减,则Least(L)Least(L)Least(L)Least(L)只需要ОООО(1)(1)(1)(1)时间,Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T)在ОООО(n)(n)(n)(n)时间 内被执行。因此所花费的时间总量是ОООО(n(n(n(n2222))))。在LLLL被表示成一个min-min-min-min-堆的情况 下,其中根的值不超过它的孩子们的值((((见堆的定义)))),则Least(L)Least(L)Least(L)Least(L)和Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T) 可以在ОООО(logn(logn(logn(logn))))时间内完成。在这种情况下TreeTreeTreeTree的计算时间是ОООО(nlogn(nlogn(nlogn(nlogn))))。将第6666行 的Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T)和第 4444行的Least(L)Least(L)Least(L)Least(L)结合起来还可以加快一些速度。Least(L)Least(L)Least(L)Least(L)和 Insert(L,T)Insert(L,T)Insert(L,T)Insert(L,T)的算法以及其计算时间的分析留作习题。 定理 4.1 4.1 4.1 4.1 若LLLL最初包含nnnn>1111个单个结点的树,这些树有WeightWeightWeightWeight值为((((qqqqllll, q, q, q, q2 2 2 2 ,,,,…………, , , , qqqqnnnn )))),则算法TreeTreeTreeTree对于具有这些长度的nnnn个文件生成一棵最优的二元归并树。 证明 通过施归纳法于nnnn来证明。对于n=1n=1n=1n=1,返回一棵没有内部结点的树且这棵树显 然是最优的。假定该算法对于所有的(q(q(q(qllll,q,q,q,q2222,,,,…………,,,,qqqqmmmm)))),llll≤mmmm<nnnn,都生成一棵最优二 元归并树,现在来证明对于所有的(q(q(q(q1111,q,q,q,q2222,,,,…………,,,,qqqqnnnn))))也生成最优的树。不失一般性, 假定qqqq1111≤qqqq2222≤…………≤qqqqnnnn,且qqqq1111和qqqq2222是在for for for for 循环的第一次迭代期间由第3333行和第4444行 中的算法LeastLeastLeastLeast所找到的两棵树的WeightWeightWeightWeight信息段的值。 于是就生成了图4.34.34.34.3的子树TTTT。 设TTTT’’’’是一棵对于(q(q(q(q1111, q, q, q, q2222, , , , ………… , , , ,qqqqnnnn))))的最优二元归并树。设pppp是距离根最远的一个 内部结点。如果PPPP的儿子不是qqqq1111和qqqq2222,则可以用qqqq1111和qqqq2222来代换pppp现在的孩子而不 增加TTTT’’’’的带权外部路径长度。因此TTTT也是一棵最优归并树中的子树。于是在TTTT’’’’ 中如果用其权为qqqq1111+q+q+q+q2222的一个外部结点来代换TTTT,则所产生的树TTTT’’’’’’’’是关于中 (q(q(q(q1111+q+q+q+q2222, q, q, q, q3333, , , , …………, , , , qqqqnnnn))))的一棵最优归并树。由归纳假设,在用其权为qqqq1111+q+q+q+q2222的那个外 部结点代换了TTTT以后,过程TreeTreeTreeTree转化成去求取一棵关于(q(q(q(q1111+q+q+q+q2222, q, q, q, q3333, , , , …………, , , , qqqqnnnn))))的最优 归并树。因此TreeTreeTreeTree生成一棵关于(q(q(q(q1111, q, q, q, q2222, , , , …………, , , , qqqqnnnn))))的最优归并树。 证毕。 q1+q2 q1 q2 T 图4.3 最简单的二元归并树 生成归并树的贪心方法也适用于kkkk路归并的情况。 在这种情况下,相应的归并树是一棵kkkk元树。由于所 有的内部结点的度数必须为kkkk,因此对于nnnn的某些值, 就不与kkkk元归并树相对应。例如,当k=3k=3k=3k=3时,就不存在 具有n=2n=2n=2n=2个外部结点的kkkk元归并树。所以有必要引进一 定量的““““虚””””外部结点。每一个虚结点被赋以0000值的qqqqiiii。 这个虚值不会影响所产生的kkkk元村的带权外部路径长 度。本讲习题11111111表明其所有内部结点都具有度数为kkkk 的kkkk元树的存在性,只有当外部结点数nnnn满足等式n n n n mod(k-l)=1mod(k-l)=1mod(k-l)=1mod(k-l)=1时才成立。因此至多应增加k-2k-2k-2k-2个虚结点。 生成最优归并树的贪心规则是:在每一步,选取kkkk棵 具有最小长度的子树用于归并。关于它的最优性证 明,则留作习题。 定义::::设G=(V,E)G=(V,E)G=(V,E)G=(V,E)是一个无向连通图。如果GGGG的生成子图T=(V,ET=(V,ET=(V,ET=(V,E’’’’))))是一棵树, 则称TTTT是GGGG的一棵生成树(Spanning Tree)(Spanning Tree)(Spanning Tree)(Spanning Tree)。 例5.15.15.15.1 图5.15.15.15.1显示了4444个结点的完全图以及它的三棵生成树。 图5.1 一个无向图和它的三棵生成树 4.5 4.5 4.5 4.5 最小生成树最小生成树最小生成树最小生成树 应用生成树可以得到关于一个电网的一组独立的回路方程。第一步是要得到 这个电网的一棵生成树。设BBBB是那些不在生成树中的电网的边的集合,从BBBB中取 出一条边添加到这生成树上就生成一个环。从BBBB中取出不同的边就生成不同的环。 把克希霍夫((((KirchoffKirchoffKirchoffKirchoff))))第二定律用到每一个环上,就得到一个回路方程。用这种 方法所得到的环是独立的((((即这些环中没有一个可以用那些剩下的环的线性组合 来得到)))),这是因为每一个环包含一条从BBBB中取来的边((((生成树固定的情况下)))),而 这条边不包含在任何其它的环中。因此,这样所得的这组回路方程也是独立的。 可以证明,通过一次取BBBB中的一条边放进所产生的生成树中而得到的这些环组成 一个环基,从而这图中所有其它的环都能够用这个基中的这些环的线性组合构 造出来。 生成树在其它方面也有广泛的应用。一种重要的应用是由生成树的性质所产 生的,这一性质是,生成树是GGGG的这样一个最小子图TTTT,它使得V(T)=V(G)V(T)=V(G)V(T)=V(G)V(T)=V(G)且TTTT 连通并具有最少的边数。任何一个具有nnnn个结点的连通图都必须至少有n-1n-1n-1n-1条 边,而所有具有n-1n-1n-1n-1条边的nnnn结点连通图都是树。如果GGGG的结点代表城市,边代表 连接两个城市的可能的交通线,则连接这nnnn个城市所需要的最少交通线是n-ln-ln-ln-l条。 GGGG的那些生成树代表所有可行的选择。 然而,在实际情况下,这些边都有分配给它们的权。这些权可以代表建造的 成本、交通线的长度以及其它。假定给出一个带权图((((假定所有的权都是正数)))), 则人们就会希望在结构上选择一组交通线,它们连接所有的城市并且有最小的 总成本或者有最小的总长度。显然,在这两种情况下所选择的连线都必须构成 一棵树。感兴趣的是找出GGGG中具有最小成本的生成树。图5.25.25.25.2显示了一个图和它 的最小成本生成树中的一棵生成树。 图5.2 一个带权无向图和它的一棵最小生成树 1 19 2 3 45 6 16 21 11 33 14 18 6 10 5 1 2 3 45 6 16 11 18 6 5 获得最小成本生成树的贪心方法应该是一条边一条边地构造这棵树。根据某 种量度标准来选择将要计入的下一条边。最简单的量度标准是选择使得迄今为 止所计入的那些边的成本的和有最小增量的那条边。有两种可能的方法来解释 这一量度标准。第一种方法使得迄今所选择的边的集合AAAA构成一棵树,即将要计 入到AAAA中的下一条边(u,v)(u,v)(u,v)(u,v)是一条不在AAAA中且使得A A A A ∪{(u,v)}{(u,v)}{(u,v)}{(u,v)}也是一棵树的最小成 本的边。这种选择准则产生一棵最小成本生成树的证明留作习题。其相应的算 法称为PrimPrimPrimPrim算法。 例5.25.25.25.2 图5.35.35.35.3是一个连通带权无向图,试按照primprimprimprim思想构造最小生成树。 图5.3 Prim方法构造最小生成树的例子 1 30 2 3 4 5 6 10 45 40 20 55 25 15 35 50 1 2 3 4 5 6 10 20 25 15 35 所得到的最小生成树的代价为105105105105。 由上述可知,PrimPrimPrimPrim方法是如何运行的以及使用这一方法来获得求取最小生成 树的类CCCC语言算法。该算法是在只计入了GGGG中一条最小成本边的一棵树的情况下 开始的,然后一条边一条边地加进这棵树中。所要加的下一条边(i,j)(i,j)(i,j)(i,j)是这样的一 条边,其iiii是已计入到这棵树中的一个结点,jjjj是还没有计入的一个结点,且(i,j) (i,j) (i,j) (i,j) 的成本COST(i,j)COST(i,j)COST(i,j)COST(i,j)是所有的满足上述要求的一些边中成本最小的边。为了有效地 求出这条边(i,j)(i,j)(i,j)(i,j),把还没计入这树中的每一个结点jjjj和值near(j)near(j)near(j)near(j)联系起来。near(j)near(j)near(j)near(j) 是树中的这样一个结点,它使得COST(j,near(j))COST(j,near(j))COST(j,near(j))COST(j,near(j))是对near(j)near(j)near(j)near(j)所有选择中的最小值。 而对于已经在树中的所有结点jjjj,定义near(j)=0near(j)=0near(j)=0near(j)=0。用使near(j)near(j)near(j)near(j)≠0000(jjjj不在树中) 且COST(j,near(j))COST(j,near(j))COST(j,near(j))COST(j,near(j))为最小值的结点来确定要计入的下一条边。 在函数PPPPrimrimrimrim((((算法5.1)5.1)5.1)5.1)中,第3333行选取一条最小成本边。第4444~10101010行给变量置初 值以便表示仅包含一条边(k,(k,(k,(k,llll))))的树。在11111111~21212121行中,逐条边地构造生成树的剩余 部分。第12121212行选取(j,near(j))(j,near(j))(j,near(j))(j,near(j))作为要计入的下一条边。第 16161616~20202020行修改 near()near()near()near()。 算法5.15.15.15.1 Prim Prim Prim Prim最小生成树算法 line void line void line void line void Prim(edge[],COST[][],intPrim(edge[],COST[][],intPrim(edge[],COST[][],intPrim(edge[],COST[][],int n,&T[][],intn,&T[][],intn,&T[][],intn,&T[][],int minCOSTminCOSTminCOSTminCOST) {) {) {) { //edge() //edge() //edge() //edge()是GGGG的边集。COST(n,n)COST(n,n)COST(n,n)COST(n,n)是nnnn结点图GGGG的成本邻接矩阵,矩阵元素 //COST(i,j)//COST(i,j)//COST(i,j)//COST(i,j)是一个正实数,如果不存在边(i,j)(i,j)(i,j)(i,j),则为++++∞∞∞∞。计算一棵最小生 ////////成树并把它作为一个集合存放到数组T(1..n-1,2)T(1..n-1,2)T(1..n-1,2)T(1..n-1,2)中。(T(i,1)(T(i,1)(T(i,1)(T(i,1),T(i,2))T(i,2))T(i,2))T(i,2))是最小 ////////成本生成树的一条边。最小成本生成树的总成本最后赋给minCOSTminCOSTminCOSTminCOST。 1 float COST[n,n]1 float COST[n,n]1 float COST[n,n]1 float COST[n,n];float float float float minCOSTminCOSTminCOSTminCOST; 2 2 2 2 intintintint near[] near[] near[] near[],nnnn,iiii,jjjj,kkkk,mmmm,T[n-1][2]T[n-1][2]T[n-1][2]T[n-1][2]; 3 (k,m) = 3 (k,m) = 3 (k,m) = 3 (k,m) = 具有最小成本的边; 4 4 4 4 minCOSTminCOSTminCOSTminCOST====COSTCOSTCOSTCOST(k,m)(k,m)(k,m)(k,m); 5 (T(1,1),T(1,2)) = (k,m)5 (T(1,1),T(1,2)) = (k,m)5 (T(1,1),T(1,2)) = (k,m)5 (T(1,1),T(1,2)) = (k,m) 6 for(i=16 for(i=16 for(i=16 for(i=1;i=i=i=i=nnnn;++i) {//++i) {//++i) {//++i) {//将nearnearnearnear置初值 7 if(COST(i,m)。因此, Dist(w)=Dist(u)+C(u,w)Dist(w)=Dist(u)+C(u,w)Dist(w)=Dist(u)+C(u,w)Dist(w)=Dist(u)+C(u,w)。 由上述可知,单源最短路径问题存在一个简单算法((((算法6.1)6.1)6.1)6.1),该算法实际上 只求出从vvvv0000到GGGG中所有其余各结点的最短路径长度。这个算法被称为DijkstraDijkstraDijkstraDijkstra算 法。 这些路径的实际生成则需要对此算法作少量的补充((((留下作为习题))))。在过程 Shortest_Path(Shortest_Path(Shortest_Path(Shortest_Path(算法6.1)6.1)6.1)6.1)中,假定GGGG中的nnnn个结点被标记上1111到nnnn,集合SSSS作为一个 位数组存放,如果结点iiii不在SSSS中则S(i)=0S(i)=0S(i)=0S(i)=0;如果在SSSS中,则S(i)=1S(i)=1S(i)=1S(i)=1。假定这个图用 它的成本邻接矩阵来表示,cost(i,j)cost(i,j)cost(i,j)cost(i,j)是边的权。在边不在E(G)E(G)E(G)E(G)中的情况 下,cost(i,j)cost(i,j)cost(i,j)cost(i,j)被置以某个大数++++∞。对于 i=ji=ji=ji=j,cost(i,j)cost(i,j)cost(i,j)cost(i,j)可以被置成不影响这个算法 结果的任一非负数,比如0000。 DijkstraDijkstraDijkstraDijkstra算法的描述如下: 算法3.103.103.103.10 生成最短路径的贪心算法 void Shortest_Path (void Shortest_Path (void Shortest_Path (void Shortest_Path (intintintint v v v v,float cost[][]float cost[][]float cost[][]float cost[][],float dist[]float dist[]float dist[]float dist[],intintintint n){ n){ n){ n){ //G //G //G //G是一个 nnnn结点有向图,它由其成本邻接矩阵cost(n,n)cost(n,n)cost(n,n)cost(n,n)表示 dist(j)dist(j)dist(j)dist(j) // // // //被置以结点vvvv到结点jjjj的最短路径长度,这里 1111≤jjjj≤nnnn。dist(v)dist(v)dist(v)dist(v)被置成0000 booleanbooleanbooleanboolean S[n] S[n] S[n] S[n];float cost[n][n]float cost[n][n]float cost[n][n]float cost[n][n];float dist[n]float dist[n]float dist[n]float dist[n]; intintintint u u u u,nnnn,numnumnumnum,iiii,wwww l forl forl forl for(i=1i=1i=1i=1;i=ni=ni=ni=n;++i++i++i++i){ //{ //{ //{ //将集合SSSS初始化为空 2 S[i] = 02 S[i] = 02 S[i] = 02 S[i] = 0;dist[i]= cost[v][i]dist[i]= cost[v][i]dist[i]= cost[v][i]dist[i]= cost[v][i]; 3 }3 }3 }3 };// // // // 4 S[v]=14 S[v]=14 S[v]=14 S[v]=1;dist[v]=0dist[v]=0dist[v]=0dist[v]=0; ////////结点vvvv计入S S S S 5 for(num=25 for(num=25 for(num=25 for(num=2;m<=n-1m<=n-1m<=n-1m<=n-1;++num) { //++num) { //++num) { //++num) { //确定由结点vvvv出发的n-1n-1n-1n-1条路 6 6 6 6 选取结点uuuu,它使得dist[u] = mindist[u] = mindist[u] = mindist[u] = min{dist[w]dist[w]dist[w]dist[w]} s(w)=0s(w)=0s(w)=0s(w)=0 7 S[u]=17 S[u]=17 S[u]=17 S[u]=1; ////////将结点uuuu并入S S S S 8 for 8 for 8 for 8 for 所有S[w]=0S[w]=0S[w]=0S[w]=0的结点w do { //w do { //w do { //w do { //修改距离 9 dist[w] 9 dist[w] 9 dist[w] 9 dist[w] ←min(dist[w]min(dist[w]min(dist[w]min(dist[w],dist[u] + cost[u][w]) dist[u] + cost[u][w]) dist[u] + cost[u][w]) dist[u] + cost[u][w]) 10 }10 }10 }10 };//for //for //for //for 11 }11 }11 }11 };//for //for //for //for 12 }//Shortest_Path 12 }//Shortest_Path 12 }//Shortest_Path 12 }//Shortest_Path 容易看出这个算法是正确的。 在nnnn个顶点的图上,算法所花费的时间是ОООО(n(n(n(n2222))))。 为了看出这点,注意第1111行的forforforfor循环需用Θ(n)(n)(n)(n)时间。第5555行的forforforfor循环则要执行 n-2n-2n-2n-2次,而这个循环的每一次执行在第6666行选择下一个结点并在第8888~10101010行再一次 修改distdistdistdist需要ОООО(n)(n)(n)(n)时间。因此,这个循环的总时间是ОООО(n(n(n(n2222))))。在提供一个当前不 在SSSS中的结点的表TTTT的情况下,则表的结点数在任何时刻都是n-numn-numn-numn-num。这会加快 第6666行和第8888~10101010行的速度,但渐近时间还是保持为ОООО(n(n(n(n2222))))。 任何最短路径算法都必须至少检查这个图中的每一条边一次,这是因为任何 条一边都可能在一条最短路径中,故算法的最小时间便是ОООО(e)(e)(e)(e)。由于用邻接矩 阵来表示图,要确定哪些边在GGGG中正好需用ОООО(n(n(n(n2222))))时间,故使用这种表示法的任 何最短路径算法必定花费ОООО(n(n(n(n2222))))时间。 于是,对于这种表示法,算法Shortest_PathShortest_PathShortest_PathShortest_Path在一个常因子范围内是最优的。 即使换成为一些邻接表,也只是第8888~10101010行的forforforfor循环的总时间可能降低到ОООО (e) (e) (e) (e) ,这是因为distdistdistdist只能改变那些与uuuu邻近的结点。第6666行总的时间仍保持为ОООО(n(n(n(n2222))))。 例6.2 求图6.2中vl到其余各个结点的最短路径。 图6.2 一个带权的有向图 v1 v2 v3 v4 v5 v6 v7 20 50 70 10 25 40 55 50 25 50 7030 图6.26.26.26.2的成本邻接矩阵为:::: 0 20 50 30 +0 20 50 30 +0 20 50 30 +0 20 50 30 +∞ + + + +∞ + + + +∞ ++++∞ 0 25 + 0 25 + 0 25 + 0 25 +∞ + + + +∞ 70 + 70 + 70 + 70 +∞ ++++∞ + + + +∞ 0 40 25 50 + 0 40 25 50 + 0 40 25 50 + 0 40 25 50 +∞ ++++∞ + + + +∞ + + + +∞ 0 55 + 0 55 + 0 55 + 0 55 +∞ + + + +∞ ++++∞ + + + +∞ + + + +∞ + + + +∞ 0 10 70 0 10 70 0 10 70 0 10 70 ++++∞ + + + +∞ + + + +∞ + + + +∞ + + + +∞ 0 50 0 50 0 50 0 50 ++++∞ + + + +∞ + + + +∞ + + + +∞ + + + +∞ + + + +∞ 0 0 0 0 以这7777个结点有向图的有关数据为输入,执行过程Shortest_PathShortest_PathShortest_PathShortest_Path。将算法第5555 行forforforfor循环每一次选代所选取的结点和distdistdistdist的值以表6.16.16.16.1的形式列出。可以看出, 只有当这7777个结点中的6666个结点在SSSS内时算法才会终止。 0 20 50 30 +0 20 50 30 +0 20 50 30 +0 20 50 30 +∞ + + + +∞ + + + +∞ 0 20 45 30 +0 20 45 30 +0 20 45 30 +0 20 45 30 +∞ 90 + 90 + 90 + 90 +∞ 0 20 45 30 85 90 +0 20 45 30 85 90 +0 20 45 30 85 90 +0 20 45 30 85 90 +∞ 0 20 45 30 70 90 +0 20 45 30 70 90 +0 20 45 30 70 90 +0 20 45 30 70 90 +∞ 0 20 45 30 70 80 1400 20 45 30 70 80 1400 20 45 30 70 80 1400 20 45 30 70 80 140 0 20 45 30 70 80 1300 20 45 30 70 80 1300 20 45 30 70 80 1300 20 45 30 70 80 130 1 1 1 1 1,2 1,2 1,2 1,2 1,2,4 1,2,4 1,2,4 1,2,4 1,2,4,3 1,2,4,3 1,2,4,3 1,2,4,3 1,2,4,3,5 1,2,4,3,5 1,2,4,3,5 1,2,4,3,5 1,2,4,3,5,6 1,2,4,3,5,6 1,2,4,3,5,6 1,2,4,3,5,6 ———— 2222 3333 4444 5555 6666 置初值 1111 2222 3333 4444 5555 dist dist dist dist (1) (2) (3) (4) (5) (6) (7)(1) (2) (3) (4) (5) (6) (7)(1) (2) (3) (4) (5) (6) (7)(1) (2) (3) (4) (5) (6) (7)S S S S 选取的结点迭代 表6.1 Shortest_Path 执行踪迹 不难证明在一个连通无向图GGGG中,由结点vvvv到其余各结点最短路径的边构成 GGGG的一棵生成树((((或称最短路径生成树))))。显然,对于不同的根结点vvvv,这样的 生成树可能是不同的。图6.36.36.36.3显示了一个图 GGGG,它的最小成本生成树以及一棵 由结点 llll出发的最短路径生成树。 1 2 3 4 8 5 7 6 55 20 45 25 40 20 15 35 10 50 30 1 32 4 8 5 7 6 10 5 25 2040 15 30 1 2 3 4 8 5 7 6 55 25 45 5 10 15 30 图6.3 图和生成树 (a)一个图; (b)最小成本生成树; (c)由结点1出发的最短路径生成树 练习: 1 1 1 1 背包问题 ① 求以下情况背包问题的最优解。 n=7,M=15 n=7,M=15 n=7,M=15 n=7,M=15 (p (p (p (pllll,,,,…………,p,p,p,p7777)=(0,5,15,7,6,18,3) )=(0,5,15,7,6,18,3) )=(0,5,15,7,6,18,3) )=(0,5,15,7,6,18,3) (w (w (w (w1111,,,,…………,w,w,w,w7777)=(2,3,5,7,1,4,) )=(2,3,5,7,1,4,) )=(2,3,5,7,1,4,) )=(2,3,5,7,1,4,) ② 将以上数据情况的背包问题记为IIII。设FG(I)FG(I)FG(I)FG(I)是物品按PPPPiiii的非增次序输入时由 Greedy_KnapsackGreedy_KnapsackGreedy_KnapsackGreedy_Knapsack所生成的解,FO(I)FO(I)FO(I)FO(I)是一个最优解。试求FO(I)/FG(I)FO(I)/FG(I)FO(I)/FG(I)FO(I)/FG(I)? ③ 当物品按wiwiwiwi的非递减序输入时,重复②的讨论。 2 2 2 2 根据讲义图5.35.35.35.3,画出primprimprimprim算法构造最小生成树的过程,给出TTTT数组和nearnearnearnear数 组的变化过程。 3 3 3 3 以邻接距阵作为图的存储结构,具体给出PrimPrimPrimPrim算法的CCCC语言实现,并列表分 析其时间复杂度。 4 4 4 4 以邻接距阵作为图的存储结构,具体给出KruskalKruskalKruskalKruskal算法的CCCC语言实现,并列表 分析其时间复杂度。 5 5 5 5 以邻接距阵作为图的存储结构,具体给出DijkstraDijkstraDijkstraDijkstra算法的CCCC语言实现,并列表 分析其时间复杂度。 第四讲 完了 软件理论与软件工程研究室 蒋波 第五讲动态规划法 5.1 动态规划法的一般方法 5.2 多段图 5.3 每对结点之间的最短路径 5.4 最优二分检索树 目录 5.6 可靠性设计 5.5 0/1背包问题 5.7 货郎担问题 5.8 流水线调度问题 5.1 5.1 5.1 5.1 动态规划法的一般方法动态规划法的一般方法动态规划法的一般方法动态规划法的一般方法 在实际生活中,有这么一类问题,它们的活动过程可以分为若干个阶段,而 且在任一阶段i后的行为都仅依赖于i阶段的过程状态,而与i阶段之前的过程是 如何达到i阶段的状态的方式无关,这样的过程就构成一个多阶段决策过程。在 50年代,贝尔曼(Richard Bellman)等人根据这类问题的多阶段决策的特性,提 出了解决这类问题的“最优性原理”,从而创建了最优化问题的一种新的算法 设计方法,称之为动态规划法。 在多阶段决策过程的每一阶段,都可能有多种可供选择的决策,但必须从中 选取一种决策。一旦各个阶段的决策选定之后,就构成了解决这一问题的一个 决策序列。决策序列不同,所导致的问题的结果也不同。动态规划法的目标就 是要在所有允许选择的决策序列中选取一个会获得问题最优解的决策序列,即 最优决策序列。 显然,用枚举的方法从所有可能的决策序列中选取最优决策序列是一种最笨 拙的方法。贝尔曼认为,利用最优性原理(Principle Of Optimality)以及所获 得的递推关系式去求取最优决策序列可以使枚举量急剧下降。这个原理指出, 过程的最优决策序列具有如下性质:无论过程的初始状态和初始决策是什么, 其余的决策都必须相对于初始决策所产生的状态构成一个最优决策序列。 如果 所求解问题的最优性原理成立,则说明用动态规划法方法有可能解决该问题, 而解决问题的关键在于获取各阶段间的递推关系式。 例1.11.11.11.1 【多段图问题】多段图G=(V,E)G=(V,E)G=(V,E)G=(V,E)是一个有向图。它具有如下特性:图中 的结点被划分成k>2k>2k>2k>2个不相交的集合VVVViiii,1111≤iiii≤kkkk,其中VVVV1111和VVVVkkkk分别只有一个结 点s(s(s(s(源点))))和t(t(t(t(汇点))))。图中所有的边均具有如下性质:若uuuu∈VVVViiii, 则 vvvv∈VVVVi+1i+1i+1i+1,1111≤i均附有成本C(u,v)C(u,v)C(u,v)C(u,v)。从ssss到tttt的一条路径成本是 这条路径上边的成本和。多段图问题(Multistage Graph Problem)(Multistage Graph Problem)(Multistage Graph Problem)(Multistage Graph Problem)是求由ssss到tttt的 最小成本路径。每个集合VVVViiii定义图中的一段。由于EEEE的约束,每条从ssss到tttt的路径 都是从第1111段开始,在第kkkk段终止。图1.11.11.11.1所示的就是一个5555段图。 1 2 3 4 5 6 7 8 9 10 11 12 V1 V2 V3 V4 V5 9 7 3 2 11 11 8 7 1 2 2 4 6 45 4 3 5 6 5 2 图1.1 一个k段图的例子(k=5) 对于每一条由ssss到tttt的路径,可以把它看成是在k-2k-2k-2k-2个阶段中作出的某个决策序 列的相应结果。第iiii次决策就是确定VVVVi+1i+1i+1i+1中的哪个结点在该路径上,1111≤iiii≤k-2(k-2(k-2(k-2(在 第k-1k-1k-1k-1阶段无须做出决策,毫无疑问,只有汇点在这个路径上))))。 下面证明最优性原理对多段图成立。 假设s,vs,vs,vs,v2222,v,v,v,v3333, , , , …………, v, v, v, vk-1k-1k-1k-1,t,t,t,t是一条由ssss到tttt的最短路径。再设从源点s(s(s(s(初始状态))))开始, 已作出了到结点vvvv2222的决策((((初始决策)))),则vvvv2222就是初始决策所产生的状态。如果把 vvvv2222看成是原始问题的一个子问题的初始状态,求解这个子问题就是找出一条由vvvv2222 到tttt的最短路径。这条最短路径显然是vvvv2222,v,v,v,v3333, , , , …………, v, v, v, vk-1k-1k-1k-1,t,t,t,t。如若不然,可假设 vvvv2222,q,q,q,q3333,,,,…………,q,q,q,qk-1k-1k-1k-1,t,t,t,t是一条由vvvv2222到tttt的更短路径,则s,vs,vs,vs,v2222,q,q,q,q3333,,,,…………,q,q,q,qk-1k-1k-1k-1,,t,,t,,t,,t是一条比路径s, s, s, s, vvvv2222,v,v,v,v3333, , , , …………, v, v, v, vk-1k-1k-1k-1,t,t,t,t更短的由ssss到tttt的路径。与假设矛盾,故最优性原理成立。因此它为 使用动态规划法方法来解多段图问题提供了可能。 s tv3 vk-1v2 ………… q3 qk-1 例1.21.21.21.2 【0/10/10/10/1背包问题】此问题除了限定xxxxjjjj必须取0000或1111值外,其余条件及目标函 数均与贪婪法中所介绍的背包问题的约定类同。用Knap(l,j,X)Knap(l,j,X)Knap(l,j,X)Knap(l,j,X)来表示这个问题。 极大化 ∑ppppiiiixxxxiiii 1 1 1 1≤iiii≤j j j j 约束条件∑wwwwiiiixxxxiiii≤XXXX x x x xiiii=0=0=0=0或1111,1111≤iiii≤jjjj。 (1.1) (1.1) (1.1) (1.1) 1 1 1 1≤iiii≤j j j j 0/1 0/1 0/1 0/1背包问题就是Knap(l,n,M)Knap(l,n,M)Knap(l,n,M)Knap(l,n,M)。设yyyy1111,y,y,y,y2222, , , , …………, , , , yyyynnnn分别是xxxx1111,x,x,x,x2222,,,,…………,,,,xxxxnnnn的0/10/10/10/1值的最优 序列。 若yyyy1111=0=0=0=0,则yyyy2222,y,y,y,y3333, , , , …………, , , , yyyynnnn必须相对于Knap(2,n,M)Knap(2,n,M)Knap(2,n,M)Knap(2,n,M)问题构成一个最优序列。若不 然,则yyyy1111,y,y,y,y2222, , , , …………, , , , yyyynnnn就不是Knap(1,n,M)Knap(1,n,M)Knap(1,n,M)Knap(1,n,M)的最优序列。 若yyyy1111=1=1=1=1,则yyyy2222,y,y,y,y3333, , , , …………, , , , yyyynnnn必须是Knap(2,n,M-wKnap(2,n,M-wKnap(2,n,M-wKnap(2,n,M-w1111))))的最优序列。如若不然,则必有 另一0/10/10/10/1序列zzzz2222,z,z,z,z3333,,,,…………,,,,zzzznnnn使得 ∑wwwwiiiizzzziiii≤M-wM-wM-wM-w1 1 1 1 且 ∑ppppiiiizzzziiii >∑ppppiiiiyyyyiiii。 2222≤iiii≤n 2n 2n 2n 2≤iiii≤n 2n 2n 2n 2≤iiii≤n n n n 因此,序列yyyy1111,z,z,z,z2222,,,,…………,,,,zzzznnnn是一个对问题Knap(1,n,M)Knap(1,n,M)Knap(1,n,M)Knap(1,n,M)具有更大效益值的序列,与 假设矛盾。所以,最优性原理成立。 能用动态规划法求解的问题的最优化决策序列可表示如下: : : : 设SSSS0000是问题的初始状态,假定需要作nnnn次决策xxxxiiii,1111≤iiii≤nnnn。设XXXX1111={r={r={r={r1,11,11,11,1, r, r, r, r1,21,21,21,2, , , , …………, , , , rrrr1,p1,p1,p1,p1111}}}}是xxxx1111的可能决策值的集合,而SSSS1,j1,j1,j1,j1111 是在选取决策值rrrrl,jl,jl,jl,j1111 以后所产生的状态, llll≤jjjj1111≤pppp1111。又设Γ1,j1,j1,j1,j1111 是相应于状态SSSSl,jl,jl,jl,j1111 的最优决策序列。那么,相应于SSSS0000的最优 决策序列就是{r{r{r{rl,jl,jl,jl,j1111 Γ1,j1,j1,j1,j1111 | l| l| l| l≤jjjj1111≤pppp1111}}}}中最优的序列,记为 OPT{rOPT{rOPT{rOPT{rllll, j, j, j, j1111 Γ1, j1, j1, j1, j1111}= r}= r}= r}= rllllΓ1111。 llll≤jjjj1111≤pppp1111 如果已作了k-1k-1k-1k-1次决策,1111≤k-1∈E)E)E)E) 因为,若∈EEEE,有COST(k-1,j)=C(j,t)COST(k-1,j)=C(j,t)COST(k-1,j)=C(j,t)COST(k-1,j)=C(j,t),若∈EEEE,有COST(k-1,j)=COST(k-1,j)=COST(k-1,j)=COST(k-1,j)=∞∞∞∞, 所以,可以通过如下步骤解(2.1)(2.1)(2.1)(2.1)式并求出COST(1,s)COST(1,s)COST(1,s)COST(1,s):首先对于所有jjjj∈VVVVk-2k-2k-2k-2计算 COST(k-2,j)COST(k-2,j)COST(k-2,j)COST(k-2,j),然后对所有jjjj∈VVVVk-3k-3k-3k-3,计算COST(k-3,j)COST(k-3,j)COST(k-3,j)COST(k-3,j)等等,最后算出COST(1,s)COST(1,s)COST(1,s)COST(1,s)。 下面对图1.l1.l1.l1.l的5555段图给出具体实现这一系列计算的步骤: 5.2 5.2 5.2 5.2 多段图多段图多段图多段图 COST(3,6) = min{6+COST(4,9), 5+COST(4,10)} = 7COST(3,6) = min{6+COST(4,9), 5+COST(4,10)} = 7COST(3,6) = min{6+COST(4,9), 5+COST(4,10)} = 7COST(3,6) = min{6+COST(4,9), 5+COST(4,10)} = 7 ((((已知COST(4,9)=4COST(4,9)=4COST(4,9)=4COST(4,9)=4,COST(4,10)=2)COST(4,10)=2)COST(4,10)=2)COST(4,10)=2) COST(3,7) = min{4+COST(4,9), 3+COST(4,10)}= 5 COST(3,7) = min{4+COST(4,9), 3+COST(4,10)}= 5 COST(3,7) = min{4+COST(4,9), 3+COST(4,10)}= 5 COST(3,7) = min{4+COST(4,9), 3+COST(4,10)}= 5 COST(3,8) = 7COST(3,8) = 7COST(3,8) = 7COST(3,8) = 7 COST(2,2) = min{4+COST(3,6), 2+COST(3,7), 1+COST(3,8)}= 7 COST(2,2) = min{4+COST(3,6), 2+COST(3,7), 1+COST(3,8)}= 7 COST(2,2) = min{4+COST(3,6), 2+COST(3,7), 1+COST(3,8)}= 7 COST(2,2) = min{4+COST(3,6), 2+COST(3,7), 1+COST(3,8)}= 7 COST(2,3) = 9 COST(2,3) = 9 COST(2,3) = 9 COST(2,3) = 9 COST(2,4) = 18COST(2,4) = 18COST(2,4) = 18COST(2,4) = 18 COST(2,5) = 15COST(2,5) = 15COST(2,5) = 15COST(2,5) = 15 COST(1,1) = min{9+COST(2,2), 7+COST(2,3),3+COST(2,4), 2+COST(2,5)}= 16 COST(1,1) = min{9+COST(2,2), 7+COST(2,3),3+COST(2,4), 2+COST(2,5)}= 16 COST(1,1) = min{9+COST(2,2), 7+COST(2,3),3+COST(2,4), 2+COST(2,5)}= 16 COST(1,1) = min{9+COST(2,2), 7+COST(2,3),3+COST(2,4), 2+COST(2,5)}= 16 于是,由ssss到tttt的最小成本路径的成本为16161616。 若计算每个COST(i,j)COST(i,j)COST(i,j)COST(i,j)的同时,记录每个状态((((结点j)j)j)j)所作的决策,即使c(j,v)+ c(j,v)+ c(j,v)+ c(j,v)+ COST(i+1,v)COST(i+1,v)COST(i+1,v)COST(i+1,v)取最小值的vvvv值并用D(i,j)D(i,j)D(i,j)D(i,j)存储,则可方便地求出该最小成本路径。 1 2 3 4 5 6 7 8 9 10 11 12 V1 V2 V3 V4 V5 9 7 3 2 11 11 8 7 1 2 2 4 6 45 4 3 5 6 5 2 对于图1.l1.l1.l1.l而言,可得到:(D(D(D(D的第一下标是集合编号,,,,第二下标是顶点编号)))) D(3,6) = 10D(3,6) = 10D(3,6) = 10D(3,6) = 10, D(3,7) = 10, D(3,8)=10D(3,7) = 10, D(3,8)=10D(3,7) = 10, D(3,8)=10D(3,7) = 10, D(3,8)=10 D(2,2)=7 D(2,2)=7 D(2,2)=7 D(2,2)=7 , D(2,3) = 6D(2,3) = 6D(2,3) = 6D(2,3) = 6, D(2,4) = 8D(2,4) = 8D(2,4) = 8D(2,4) = 8, D(2,5)=8D(2,5)=8D(2,5)=8D(2,5)=8 D(1,1)=2 D(1,1)=2 D(1,1)=2 D(1,1)=2 设这条最小成本路径是s=l,vs=l,vs=l,vs=l,v2222,v,v,v,v3333,,,,…………,,,,vvvvk-lk-lk-lk-l,t,t,t,t=12=12=12=12。立即可知, vvvv2222=D(1,l)=2=D(1,l)=2=D(1,l)=2=D(1,l)=2 v v v v3333=D(2,D(l,1))=7=D(2,D(l,1))=7=D(2,D(l,1))=7=D(2,D(l,1))=7 v v v v4444=D(3,D(2,D(1,1)))=D(3,7)=10=D(3,D(2,D(1,1)))=D(3,7)=10=D(3,D(2,D(1,1)))=D(3,7)=10=D(3,D(2,D(1,1)))=D(3,7)=10。 即这条最小成本路径的顶点序列为1,2,7,10,121,2,7,10,121,2,7,10,121,2,7,10,12。 为了使写出的算法更简单一些,可事先对结点集VVVV的结点按下述方式排序: 首先将ssss结点编成1111号,然后对VVVV2222中的结点顺序编号,VVVV3333的结点接着VVVV2222中的最 后一个编号继续向下顺序编号,最后将tttt编成nnnn号。经过这样编号,VVVVi+1i+1i+1i+1中结点的 编号均大于VVVViiii中结点的编号((((见图1.1)1.1)1.1)1.1)。 于是,COSTCOSTCOSTCOST和DDDD都可按n-1,n-2,n-1,n-2,n-1,n-2,n-1,n-2,…………,1,1,1,1的次序计算,而无需考虑COSTCOSTCOSTCOST,PPPP和DDDD中 标识结点所在段数的第一个下标,因此它们的第一个下标可在算法中略去。据 此所导出的算法是过程函数FGraphFGraphFGraphFGraph。 算法2.12.12.12.1 多段图的向前处理算法 line void line void line void line void FGraphFGraphFGraphFGraph ( ( ( (edgeTypeedgeTypeedgeTypeedgeType E, E, E, E, intintintint k, k, k, k, intintintint n, n, n, n, intintintint P[]) { P[]) { P[]) { P[]) { // // // //输人按段顺序给结点编号的有nnnn个结点的kkkk段图。EEEE是边集, //c(i,j)//c(i,j)//c(i,j)//c(i,j)是边的成本。P(1:k)P(1:k)P(1:k)P(1:k)是最小成本路径。 1 float COST[n]1 float COST[n]1 float COST[n]1 float COST[n];intintintint D[n-1], r, j D[n-1], r, j D[n-1], r, j D[n-1], r, j; 2 COST[n]=02 COST[n]=02 COST[n]=02 COST[n]=0; 3 for(j=n-13 for(j=n-13 for(j=n-13 for(j=n-1;j>=1j>=1j>=1j>=1;--j) { //--j) { //--j) { //--j) { //计算COST(j) COST(j) COST(j) COST(j) 4 4 4 4 设rrrr是一个这样的结点,∈EEEE且使c(j,r)+COST[r]c(j,r)+COST[r]c(j,r)+COST[r]c(j,r)+COST[r]取最小值; 5 COST[j] = c[j,r] + COST[r]5 COST[j] = c[j,r] + COST[r]5 COST[j] = c[j,r] + COST[r]5 COST[j] = c[j,r] + COST[r]; 6 D[j]=r6 D[j]=r6 D[j]=r6 D[j]=r; 7 }7 }7 }7 };////////找一条最小成本路径 8 P[1]=18 P[1]=18 P[1]=18 P[1]=1;p[k]=np[k]=np[k]=np[k]=n; 9 for(j=29 for(j=29 for(j=29 for(j=2;j<=k-1j<=k-1j<=k-1j<=k-1;++j){ //++j){ //++j){ //++j){ //找路径上的第jjjj个结点 10 P[j] = D[P[j-l]]10 P[j] = D[P[j-l]]10 P[j] = D[P[j-l]]10 P[j] = D[P[j-l]]; 11 }11 }11 }11 };//for //for //for //for 12 }//12 }//12 }//12 }//FGraphFGraphFGraphFGraph 函数过程FGraphFGraphFGraphFGraph的复杂度分析相当简单。如果GGGG用邻接表表示,那么第4444行的 rrrr可以在与结点jjjj的度成正比的时间内算出。因此,如果GGGG有eeee条边,则第3333~7777行 的forforforfor循环的时间是Θ(n+e)(n+e)(n+e)(n+e),第9999~11111111行的forforforfor循环时间是Θ(k)(k)(k)(k)。总的计算时间在 Θ(n+e)(n+e)(n+e)(n+e)以内。除了输人所需要的空间外,还需要给COSTCOSTCOSTCOST,DDDD和PPPP分配空间。 kkkk段图问题也能用向后处理法求解。设BP(i,j)BP(i,j)BP(i,j)BP(i,j)是一条由源点ssss到VVVViiii中结点jjjj的最 小成本路径,BCOST(i,j)BCOST(i,j)BCOST(i,j)BCOST(i,j)是BP(i,j)BP(i,j)BP(i,j)BP(i,j)的成本。由向后处理法得到: BCOST(i,j)=minBCOST(i,j)=minBCOST(i,j)=minBCOST(i,j)=min{BCOST(i-1,v)+c(v,j)BCOST(i-1,v)+c(v,j)BCOST(i-1,v)+c(v,j)BCOST(i-1,v)+c(v,j)} (2.2)(2.2)(2.2)(2.2) (v (v (v (v∈VVVVi-1i-1i-1i-1,∈E)E)E)E) 因为,若<1,j><1,j><1,j><1,j>∈EEEE,有BCOST(2,j)=c(1,j)BCOST(2,j)=c(1,j)BCOST(2,j)=c(1,j)BCOST(2,j)=c(1,j),若<1,j><1,j><1,j><1,j>∈EEEE,有BCOST(2,j)=BCOST(2,j)=BCOST(2,j)=BCOST(2,j)=∞∞∞∞, 所以,可用(2.2)(2.2)(2.2)(2.2)式首先对i=3i=3i=3i=3计算BCOSTBCOSTBCOSTBCOST,然后对i=4i=4i=4i=4计算BCOSTBCOSTBCOSTBCOST等,最后计算 出BCOST(k,t)BCOST(k,t)BCOST(k,t)BCOST(k,t)((((共kkkk段))))。 对图1.11.11.11.1的5555段图可进行如下计算: BCOST(3,6) = min{BCOST(2,2)+4,BCOST(2,3)+2}= 9 BCOST(3,6) = min{BCOST(2,2)+4,BCOST(2,3)+2}= 9 BCOST(3,6) = min{BCOST(2,2)+4,BCOST(2,3)+2}= 9 BCOST(3,6) = min{BCOST(2,2)+4,BCOST(2,3)+2}= 9 BCOST(3,7) = 11 BCOST(3,7) = 11 BCOST(3,7) = 11 BCOST(3,7) = 11 BCOST(3,8) = 10 BCOST(3,8) = 10 BCOST(3,8) = 10 BCOST(3,8) = 10 BCOST(4,9) = min{BCOST(3,6)+6,BCOST(3,7)+4}= 15 BCOST(4,9) = min{BCOST(3,6)+6,BCOST(3,7)+4}= 15 BCOST(4,9) = min{BCOST(3,6)+6,BCOST(3,7)+4}= 15 BCOST(4,9) = min{BCOST(3,6)+6,BCOST(3,7)+4}= 15 BCOST(4,10) = min{COST(3,6)+5,BCOST(3,7)+3,BCOST(3,8)+5}=14 BCOST(4,10) = min{COST(3,6)+5,BCOST(3,7)+3,BCOST(3,8)+5}=14 BCOST(4,10) = min{COST(3,6)+5,BCOST(3,7)+3,BCOST(3,8)+5}=14 BCOST(4,10) = min{COST(3,6)+5,BCOST(3,7)+3,BCOST(3,8)+5}=14 BCOST(4,11) = 16 BCOST(4,11) = 16 BCOST(4,11) = 16 BCOST(4,11) = 16 BCOST(5,12) = min{BCOST(4,9)+4,BCOST(4,10)+2,BCOST(4,11)+5}=16 BCOST(5,12) = min{BCOST(4,9)+4,BCOST(4,10)+2,BCOST(4,11)+5}=16 BCOST(5,12) = min{BCOST(4,9)+4,BCOST(4,10)+2,BCOST(4,11)+5}=16 BCOST(5,12) = min{BCOST(4,9)+4,BCOST(4,10)+2,BCOST(4,11)+5}=16 获取ssss到tttt的一条最小成本路径的向后处理算法是函数BGraphBGraphBGraphBGraph。它与FGraphFGraphFGraphFGraph一 样,略去了BCOSTBCOSTBCOSTBCOST,PPPP和DDDD的第一个下标。只要GGGG用它的逆邻接表表示,即对于 每一个结点vvvv,有一个使得∈EEEE的结点wwww的链接表。这个算法和FGraphFGraphFGraphFGraph有 一样的计算复杂度。 算法2.22.22.22.2多段图的向后处理算法 LineLineLineLine void void void void BGRAPH(edgeTypeBGRAPH(edgeTypeBGRAPH(edgeTypeBGRAPH(edgeType E, E, E, E, intintintint k, k, k, k, intintintint n, n, n, n, intintintint BP[]) { BP[]) { BP[]) { BP[]) { // // // //和FGraphFGraphFGraphFGraph功能相同 float BCOST[n]float BCOST[n]float BCOST[n]float BCOST[n];intintintint D[n-1] D[n-1] D[n-1] D[n-1],rrrr,jjjj; 1 BCOST[1] = 01 BCOST[1] = 01 BCOST[1] = 01 BCOST[1] = 0; 2 For(j=22 For(j=22 For(j=22 For(j=2;j<=nj<=nj<=nj<=n;++j) { ++j) { ++j) { ++j) { 计算BCOST(j)BCOST(j)BCOST(j)BCOST(j) 3 3 3 3 设rrrr是一个这样的结点,∈EEEE且使BCOST[r]+c[r,j]BCOST[r]+c[r,j]BCOST[r]+c[r,j]BCOST[r]+c[r,j]取最小值; 4 BCOST[j] = BCOST[r] + c[r,j]4 BCOST[j] = BCOST[r] + c[r,j]4 BCOST[j] = BCOST[r] + c[r,j]4 BCOST[j] = BCOST[r] + c[r,j]; 5 D[j] = r5 D[j] = r5 D[j] = r5 D[j] = r; 6 };//for6 };//for6 };//for6 };//for // // // //找一条最小成本路径 7 BP[1] = 17 BP[1] = 17 BP[1] = 17 BP[1] = 1;BP[k] = nBP[k] = nBP[k] = nBP[k] = n; 8 for(j=k-18 for(j=k-18 for(j=k-18 for(j=k-1;j>=2j>=2j>=2j>=2;--j) {//--j) {//--j) {//--j) {//找路径上的第jjjj个结点 9 BP[j]=D[BP[j+1]]9 BP[j]=D[BP[j+1]]9 BP[j]=D[BP[j+1]]9 BP[j]=D[BP[j+1]] 10 }10 }10 }10 };//for//for//for//for 11 }//11 }//11 }//11 }//BGraphBGraphBGraphBGraph 不难看出,即使对多段图的更一般形式,即图中允许有这样的边,其中 uuuu∈VVVViiii,vvvv∈VVVVjjjj且iV(i+1,k)>V(i+1,k)>V(i+1,k)>的形式,这里jjjj≤≤≤≤kkkk,1111≤≤≤≤iiii≤≤≤≤rrrr。当jjjj≤≤≤≤kkkk且1111≤≤≤≤i赋予N(i,k-j)N(i,k-j)N(i,k-j)N(i,k-j)的成本值((((即净利)))),它表示给项目iiii分配k-jk-jk-jk-j个资 源。当jjjj≤≤≤≤nnnn且i=ri=ri=ri=r即边具有的形式时,每一条这样的边被赋予 maxmaxmaxmax{N(r,p)N(r,p)N(r,p)N(r,p)}的成本值。 1111≤pppp≤n-1 n-1 n-1 n-1 图2.12.12.12.1显示出将4444个资源分配给3333个项目的资源分配问题所产生的4444段图。显 然,资源的最优分配方案由ssss到tttt的一条最大成本路径所确定。只要改变所有边的 成本符号立即就可将此问题转换成最小成本问题,利用前面所给出的任何一个 多段图算法都可算出此问题的最优解。 图2.1 给3个项目分配资源的4段图 Max{N(3,0),N(3,1),N(3,2)} s=V(1,0) t=V(4,4) V(2,0) V(2,1) V(2,2) V(2,3) V(2,4) V(3,0) V(3,1) V(3,2) V(3,3) V(3,4) N(1,0) N(1,0) N(1,2) N(1,3) N(1,4) N(2,0) N(2,1) N(2,2) N(2,0) N(2,0) N(2,1) Max{N(3,0), N(3,1)} N(4,0) 设G=(V,E)G=(V,E)G=(V,E)G=(V,E)是一个有nnnn个结点的有向图,CCCC是图GGGG的成本邻接矩阵,其中 C(i,i)=0C(i,i)=0C(i,i)=0C(i,i)=0,1111≤≤≤≤iiii≤≤≤≤nnnn;当iiii≠≠≠≠jjjj时,若∈E(G)E(G)E(G)E(G)时,则C(i,j)C(i,j)C(i,j)C(i,j)表示边的长度((((或成 本))));若∈E(G)E(G)E(G)E(G)时,C(i,j)=C(i,j)=C(i,j)=C(i,j)=∞∞∞∞。 每对结点之间的最短路径问题(all pairs shortest path problem)(all pairs shortest path problem)(all pairs shortest path problem)(all pairs shortest path problem)是求满足下述 条件的矩阵AAAA,要求AAAA中的任何元素A(i,j)A(i,j)A(i,j)A(i,j)是代表结点iiii到结点jjjj的最短路径长度。 这样的矩阵AAAA可通过nnnn次调用单源点最短路径问题的算法Shortest_PathShortest_PathShortest_PathShortest_Path而获得。 由于每次调用Shortest_PathShortest_PathShortest_PathShortest_Path要花ОООО(n(n(n(n2222))))的时间,因此求出矩阵AAAA所花的时间为 ОООО(n(n(n(n3333))))。 下面利用动态规划法方法来设计这个问题的另一种算法,虽然它要求的计算 时间仍是ОООО(n(n(n(n3333)))),但在边成本上比Shortest_PathShortest_PathShortest_PathShortest_Path要求的约束条件要弱。它只要求 GGGG不含负长度的环即可,而不是要求所有的C(i,i)C(i,i)C(i,i)C(i,i)≥≥≥≥0000。 需要指出的是,若允许GGGG包含一个负长度的环,则在此环上任意两结点间的最 短长度为----∞∞∞∞。 5.3 5.3 5.3 5.3 每对结点之间的最短路径每对结点之间的最短路径每对结点之间的最短路径每对结点之间的最短路径 考察GGGG中一条由iiii到jjjj的最短路径,iiii≠≠≠≠jjjj。这条路径由iiii出发,通过一些中间结点 ((((也可能没有)))),在jjjj处结束。可以假定这条路径不含环,因为如果有环,则可去 掉这个环且不增加这条路径的长度((((不含有负长度的环))))。 如果kkkk是这条最短路径上的一个中间结点,那么由iiii到kkkk和由kkkk到jjjj的这两条子路 径应分别是由iiii到kkkk和由kkkk到jjjj的最短路径。否则,这条由iiii到jjjj的路径就不是具有最 小长度的路径。于是,最优性原理成立。这表明有使用动态规划法的可能性。 如果kkkk是编号最高的中间结点,那么由iiii到kkkk的这条最短路径上就不会有比编号 k-1k-1k-1k-1更大的结点通过。同样,在kkkk到jjjj的那条最短路径上也不会有比编号k-1k-1k-1k-1更大的 结点通过。 因此,可以把求取一条由iiii到jjjj的最短路径看成是如下的过程: 首先需要决策哪一个结点是该路径上具有最大编号的中间结点kkkk,然后就再去 求取由iiii到kkkk和由kkkk到jjjj这两对结点间的最短路径。当然,这两条路径都不可能有比 k-1k-1k-1k-1还大的中间结点。 根据上述分析,可以得到求解本问题的递推关系式。用AAAAkkkk(i,j(i,j(i,j(i,j))))表示从iiii到jjjj且中 间经过的结点的编号都不大于kkkk的最短路径长度。由于GGGG中不存在编号比nnnn还大 的结点,因此A(i,j)=AA(i,j)=AA(i,j)=AA(i,j)=Annnn(i,j)(i,j)(i,j)(i,j)。即由结点iiii到结点jjjj的最短路径上所通过的结点编号 都不大于nnnn。当然这条路径可能通过结点nnnn也可能不通过结点nnnn。 如果它通过结点nnnn,则AAAAnnnn(i,j)= A(i,j)= A(i,j)= A(i,j)= An-1n-1n-1n-1(i,n)+A(i,n)+A(i,n)+A(i,n)+An-1n-1n-1n-1(n,j)(n,j)(n,j)(n,j)。 如果它不通过结点nnnn,则没有编号大于n-1n-1n-1n-1的结点,所以,AAAAnnnn(i,j)=A(i,j)=A(i,j)=A(i,j)=An-1n-1n-1n-1(i,j)(i,j)(i,j)(i,j)。 组合起来就得到: AAAAnnnn(i,j) = min { A(i,j) = min { A(i,j) = min { A(i,j) = min { An-1n-1n-1n-1(i,j) , A(i,j) , A(i,j) , A(i,j) , An-1n-1n-1n-1(i,n)+A(i,n)+A(i,n)+A(i,n)+An-1n-1n-1n-1(n,j) } (n,j) } (n,j) } (n,j) } (3.1)(3.1)(3.1)(3.1) 同理,对于1111≤≤≤≤kkkk≤≤≤≤nnnn,有下面的递推关系式成立: AAAAkkkk(i,j(i,j(i,j(i,j) = min { A) = min { A) = min { A) = min { Ak-1k-1k-1k-1(i,j) , A(i,j) , A(i,j) , A(i,j) , Ak-1k-1k-1k-1(i,k)+A(i,k)+A(i,k)+A(i,k)+Ak-1k-1k-1k-1(k,j) } (k,j) } (k,j) } (k,j) } (3.2)(3.2)(3.2)(3.2) 考虑结点iiii和结点j j j j 间中间不经过任何结点的最短路径,显然有 AAAA0000(i,j) = C(i,j)(i,j) = C(i,j)(i,j) = C(i,j)(i,j) = C(i,j),1111≤≤≤≤i, ji, ji, ji, j≤≤≤≤nnnn, 通过先计算AAAA1111,然后计算AAAA2222,AAAA3333,…………,AAAAnnnn,就可得到每个结点对之间的最短 路径长度。 下面的例子表明如果图中含有负长度的环,则(3.2)(3.2)(3.2)(3.2)式不成立。 例3.1 3.1 3.1 3.1 图3.13.13.13.1显示了一个有向图和它的AAAA0000矩阵。 对于图3.13.13.13.1所示的图,AAAA2222(1,3)(1,3)(1,3)(1,3)≠≠≠≠minminminmin{AAAA1111(l,3), A(l,3), A(l,3), A(l,3), A1111(1,2)+A(1,2)+A(1,2)+A(1,2)+A1111(2,3)(2,3)(2,3)(2,3)}= 2= 2= 2= 2。其原因在于 出现了长度为-1-1-1-1的环1,2,11,2,11,2,11,2,1。所以,由1111到3333的最短路径1,2,1,21,2,1,21,2,1,21,2,1,2,…………,1,2,31,2,31,2,31,2,3的长度可 以作得任意小。 1 2 3 图3.1 含有负长度环的图 0 1 ∞ -2 0 1 ∞ ∞ 0 -2 1 1 函数All_PathsAll_PathsAll_PathsAll_Paths给出了求所有结点对间最短路径长度的算法。 由公式3.13.13.13.1及AAAA0000(j,j)=0(j,j)=0(j,j)=0(j,j)=0可知:AAAAkkkk(i,k(i,k(i,k(i,k)=A)=A)=A)=Ak-1k-1k-1k-1(i,k)(i,k)(i,k)(i,k)和AAAAkkkk(k,j(k,j(k,j(k,j)=A)=A)=A)=Ak-1k-1k-1k-1(k,j)(k,j)(k,j)(k,j)。 因此在构造AAAAkkkk时,第kkkk列和第kkkk行的数组元素值不变。从而在7777~11111111行的迭代 中,当jjjj>kkkk且iM=max{COST(i,j) | M=max{COST(i,j) | M=max{COST(i,j) | ∈E(G)}E(G)}E(G)}E(G)}。易于看出AAAAnnnn(i,j)(i,j)(i,j)(i,j)≤(n-1)(n-1)(n-1)(n-1)****MMMM。为便于 All_PathsAll_PathsAll_PathsAll_Paths能在计算机上正常执行,对于那些∈E(G)E(G)E(G)E(G)且iiii≠jjjj的COST(i,j)COST(i,j)COST(i,j)COST(i,j)置以 一个大于(n-1)(n-1)(n-1)(n-1)****MMMM的初值((((而不是∞))))。如果在结束时A(i,j)A(i,j)A(i,j)A(i,j)>(n-1)(n-1)(n-1)(n-1)****MMMM,则表明GGGG中 没有从iiii到jjjj的有向路径。 过程All_PathAll_PathAll_PathAll_Path所需的时间很容易确定。第9999行迭代了nnnn3333次,而且整 个循环与矩阵AAAA中的数据无关,因此过程的计算时间是ΘΘΘΘ(n(n(n(n3333))))。至于 求由iiii到jjjj的最短路径所需增设的内容,则留作一道习题。 前面给出了二分检索树的定义。根据定义,要求树中所有的结点是互异的。 对于一个给定的标识符集合,可能有若干棵不同的二分检索树。图4.14.14.14.1给出了 关于保留字的一个子集的两棵二分检索树。 图4.1两棵二分检索树 while if for repeat loop repeat while if for loop 5.4 5.4 5.4 5.4 最优二分检索树最优二分检索树最优二分检索树最优二分检索树 为了确定标识符xxxx是否在一棵二分检索树中出现,将xxxx先与根比较,如果XXXX比根 中标识符小,则检索在左子树中继续;如果xxxx等于根中标识符,则检索成功地终 止;否则检索在右子树中继续下去。上述步骤可以形式化为过程SearchSearchSearchSearch。 算法4.14.14.14.1 检索一棵二分检索树 Line void Search (Line void Search (Line void Search (Line void Search (BinaryTreeBinaryTreeBinaryTreeBinaryTree T T T T,elemTypeelemTypeelemTypeelemType x x x x,intintintint i) { i) { i) { i) { // // // //在二分检索树TTTT上查找xxxx,树的每个结点有三个信息段:LChildLChildLChildLChild,IDentIDentIDentIDent // // // //和RChildRChildRChildRChild。如果xxxx不在TTTT中,则置i=0i=0i=0i=0,否则将iiii置成使得IDent(iIDent(iIDent(iIDent(i)=x)=x)=x)=x 1 i = T1 i = T1 i = T1 i = T; 2 while(i!2 while(i!2 while(i!2 while(i!=0) {0) {0) {0) { 3 if(x=3 if(x=3 if(x=3 if(x=IDent[iIDent[iIDent[iIDent[i] {return ] {return ] {return ] {return IDent[iIDent[iIDent[iIDent[i]}]}]}]};////////检索成功 4 else4 else4 else4 else 5 if(x<5 if(x<5 if(x<5 if(xax>ax>ax>annnn的标识符xxxx。 显然,在同一EEEEiiii中的所有标识符,其检索都在同一个外部结点处终止。而在不 同EEEEiiii中的标识符则在不同的外部结点处终止。如果EEEEiiii中的结点在kkkk级,则WhileWhileWhileWhile循 环进行k-1k-1k-1k-1次迭代。于是,这个结点的成本分担额是Q(i)Q(i)Q(i)Q(i)****(leve(E(leve(E(leve(E(leve(Eiiii)-1))-1))-1))-1)。 上面所讨论导的二分检索树的预期成本可用公式(4.1)(4.1)(4.1)(4.1)表示如下: ΣΣΣΣP(i)P(i)P(i)P(i)****level(alevel(alevel(alevel(aiiii) + ) + ) + ) + ΣΣΣΣQ(i)Q(i)Q(i)Q(i)****((((level(Elevel(Elevel(Elevel(Eiiii)-l)-l)-l)-l) ) ) ) (4.1)(4.1)(4.1)(4.1) 1111≤iiii≤n 0n 0n 0n 0 ≤iiii≤n n n n 定义标识符集(a(a(a(a1111,a,a,a,a2222,,,,…………,a,a,a,annnn))))的最优二分检索树是一棵使(4.1)(4.1)(4.1)(4.1)式取最小值的二分检 索树。 例4.1 标识符集(a1,a2,a3)=(do,if,stop)可能的二分检索树,如图4.3所示。 图4.3三种标识符的各种二分检索树 stop (a) do if (b) if stopdo do (c) stop if if stop (d) do do (e) if stop 在每个内、外部结点具有相同概率P(i)=Q(i)=l/7P(i)=Q(i)=l/7P(i)=Q(i)=l/7P(i)=Q(i)=l/7的情况下,有:::: cost(cost(cost(cost(树a)=15/7 a)=15/7 a)=15/7 a)=15/7 cost( cost( cost( cost(树b)=13/7 b)=13/7 b)=13/7 b)=13/7 cost( cost( cost( cost(树c)=15/7 c)=15/7 c)=15/7 c)=15/7 cost( cost( cost( cost(树d)=15/7 d)=15/7 d)=15/7 d)=15/7 cost( cost( cost( cost(树e)=15/7e)=15/7e)=15/7e)=15/7 正如所料想的那样,树bbbb是最优的。 在检索成功的概率分别为P(1)=0.5P(1)=0.5P(1)=0.5P(1)=0.5,P(2)=0.1P(2)=0.1P(2)=0.1P(2)=0.1,P(3)=0.05 P(3)=0.05 P(3)=0.05 P(3)=0.05 和检索不成功的概率 分别为 Q(0)=0.15Q(0)=0.15Q(0)=0.15Q(0)=0.15,Q(l)=0.1Q(l)=0.1Q(l)=0.1Q(l)=0.1,Q(2)=0.05Q(2)=0.05Q(2)=0.05Q(2)=0.05,Q(3)=0.05Q(3)=0.05Q(3)=0.05Q(3)=0.05的情况下,则有 cost(cost(cost(cost(树a)=2.65a)=2.65a)=2.65a)=2.65 cost( cost( cost( cost(树b)=l.9 b)=l.9 b)=l.9 b)=l.9 cost( cost( cost( cost(树c)=1.5c)=1.5c)=1.5c)=1.5 cost( cost( cost( cost(树d)=2.15d)=2.15d)=2.15d)=2.15 cost( cost( cost( cost(树e)=l.6e)=l.6e)=l.6e)=l.6 显然树CCCC是最优的。 为了应用动态规划法求解一棵最优二分检索树的问题,需要把构造这样的一 棵树TTTT看成是一系列决策的结果,而且还要能列出求取最优决策序列的递推式。 解决该问题的一种可能方法是:对于那些aaaaiiii,1111≤≤≤≤iiii≤≤≤≤nnnn,需决策将其中的哪一个作 为TTTT的根结点。如果选择aaaakkkk,那么,aaaa1111,,,,aaaa2222, , , , …………, a, a, a, ak-1k-1k-1k-1这些内部结点和EEEE0000, E, E, E, Ellll, , , , ………… , E , E , E , Ek-1k-1k-1k-1 这些类的外部结点显然都将位于TTTT的根的左子树LLLL中,而其余的结点则将在TTTT的 右子树RRRR中。 定义: COST(L) = : COST(L) = : COST(L) = : COST(L) = ΣΣΣΣP(i)P(i)P(i)P(i)****level(alevel(alevel(alevel(aiiii) + ) + ) + ) + ΣΣΣΣQ(i)Q(i)Q(i)Q(i)****(level(E(level(E(level(E(level(Eiiii)-1) )-1) )-1) )-1) 1 1 1 1 ≤iP>P>P>PYYYY) ) ) ) xxxxnnnn = 0= 0= 0= 0; //P//P//P//PXXXX是SSSSnnnn的最末序偶 9 else 9 else 9 else 9 else xxxxnnnn = 1= 1= 1= 1; //P//P//P//PYYYY是SSSSnnnn的最末序偶 10 10 10 10 回溯确定xxxxnnnn, , , , xxxxnnnn-llll, , , , …………, x, x, x, x1111; 11 }//DKP 11 }//DKP 11 }//DKP 11 }//DKP 对于上述内容,可以用一种非形式的算法DKPDKPDKPDKP来描述。为了实现DKPDKPDKPDKP,需要 给出表示序偶集SSSSiiii和SSSS1111iiii的结构形式,而且要将Merge_PurgeMerge_PurgeMerge_PurgeMerge_Purge过程具体化,使它能 按要求归并SSSSi-1i-1i-1i-1和SSSS1111iiii,且清除一些序偶。此外,还要给出一个沿着SSSSn-1n-1n-1n-1,,,,…………,SSSS1111回 溯以确定xxxxnnnn,,,,…………,x,x,x,x1111的0000或1111值的算法。 可以用两个一维数组PPPP和WWWW来存放所有的序偶(P(P(P(P1111,W,W,W,W1111))))。PPPP1111的值放在PPPP中,WWWW1111的 值放在WWWW中。序偶集SSSS0000,S,S,S,S1111, , , , …………, , , , SSSSnnnn互相邻接地存放。各个集合可以用指针F(i)F(i)F(i)F(i)来指 示,这里0000≤iiii≤nnnn。对于0000≤iP[next-1] { (P[next],W[next])=(16 if(pp>P[next-1] { (P[next],W[next])=(16 if(pp>P[next-1] { (P[next],W[next])=(16 if(pp>P[next-1] { (P[next],W[next])=(pp,wwpp,wwpp,wwpp,ww)))) 17 next=next+117 next=next+117 next=next+117 next=next+1; 18 }18 }18 }18 }; 19 while((k<=h) and P[k]<=P[next-1]) { //19 while((k<=h) and P[k]<=P[next-1]) { //19 while((k<=h) and P[k]<=P[next-1]) { //19 while((k<=h) and P[k]<=P[next-1]) { //清除 20 k=k+120 k=k+120 k=k+120 k=k+1; 21 }21 }21 }21 }; 22 }22 }22 }22 } // // // //将SSSSi-1i-1i-1i-1中剩余的元素并入SSSSiiii 23 while(k∈EEEE则ccccijijijij====∞∞∞∞。令|V|=n|V|=n|V|=n|V|=n,并假定nnnn>1111。GGGG的一条周 游路线是包含VVVV中每个结点的一个有向环。周游路线的成本是此路线上所有边的 成本和。货郎担问题(traveling salesperson problem)(traveling salesperson problem)(traveling salesperson problem)(traveling salesperson problem)是求取具有最小成本的周游 路线问题。 有很多实际问题可归结为货郎担问题。例如,邮路问题就是一个货郎担问题。 假定有一辆邮车要到nnnn个不同的地点收集邮件,这种情况可以用n+1n+1n+1n+1个结点的图 来表示。一个结点表示此邮车出发并要返回的那个邮局,其余的nnnn个结点表示要 收集邮件的nnnn个地点。由地点iiii到地点jjjj的距离则由边上所赋予的成本来表示。 邮车所行经的路线是一条周游路线,希望求出具有最小长度的周游路线。 5.7 5.7 5.7 5.7 货郎担问题货郎担问题货郎担问题货郎担问题((((售货员问题售货员问题售货员问题售货员问题)))) 第二个例子是在一条装配线上用一个机械手去紧固待装配部件上的螺帽问题。 机械手由其初始位置((((该位置在第一个要紧固的螺帽的上方))))开始,依次移动到其 余的每一个螺帽,最后返回到初始位置。机械手移动的路线就是以螺帽为结点 的一个图中的一条周游路线。一条最小成本周游路线将使这机械手完成其工作 所用的时间取最小值。 注意,只有机械手移动的时间总量是可变化的。 第三个例子是产品的生产安排问题。假设要在同一组机器上制造nnnn种不同的产 品,生产是周期性进行的,即在每一个生产周期这nnnn种产品都要被制造。要生产 这些产品有两种开销,一种是制造第iiii种产品时所耗费的资金(1(1(1(1≤iiii≤n)n)n)n),称为生 产成本,另一种是这些机器由制造第iiii种产品变到制造第jjjj种产品时所耗费的开支 ccccijijijij,称为转换成本。显然,生产成本与生产顺序无关。于是,我们希望找到一种 制造这些产品的顺序,使得制造这nnnn种产品的转换成本和为最小。由于生产是周 期进行的,因此在开始下一周期生产时也要开支转换成本,它等于由最后一种 产品变到制造第一种产品的转换成本。于是,可以把这个问题看成是一个具有 nnnn 个结点,边成本为ccccijijijij的图的货郎担问题。 货郎担问题要从图GGGG的所有周游路线中求取具有最小成本的周游路线,而由始 点出发的周游路线一共有(n-l)!(n-l)!(n-l)!(n-l)!条,即等于除始结点外的n-1n-1n-1n-1个结点的排列数,因 此货郎担问题是一个排列问题。排列问题比子集合的选择问题((((例如,0/10/10/10/1背包问 题就是这类问题))))通常要难于求解得多,这是因为nnnn个物体有n!n!n!n!种排列,只有2222nnnn个 子集合(n!>(n!>(n!>(n!>ОООО(2(2(2(2nnnn))))))))。通过枚举(n-1)!(n-1)!(n-1)!(n-1)!条周游路线,从中找出一条具有最小成本的周 游路线的算法,其计算时间显然为ОООО(n!)(n!)(n!)(n!)。为了改善计算时间必须考虑新的设计 策略来拟制更好的算法。动态规划法就是待选择的设计策略之一。但货郎担问 题是否能使用动态规划法设计求解呢?下面就来讨论这个问题。 不失一般性,假设周游路线是开始于结点1111并终止于结点1111的一条简单路径。 每一条周游路线都由一条边<1,k><1,k><1,k><1,k>和一条由结点kkkk到结点1111的路径所组成,其中 kkkk∈V-{1}V-{1}V-{1}V-{1};而这条由结点kkkk到结点1111的路径通过V-{1,k}V-{1,k}V-{1,k}V-{1,k}的每个结点各一次。容易 看出,如果这条周游路线是最优的,那么这条由kkkk到1111的路径必定是通过V-{1,k}V-{1,k}V-{1,k}V-{1,k} 中所有结点的由kkkk到1111的最短路径,因此最优性原理成立。 设g(i,S)g(i,S)g(i,S)g(i,S)是由结点iiii开始,通过SSSS中的所有结点,在结点1111终止的一条最短路径长 度。g(1, V-{1})g(1, V-{1})g(1, V-{1})g(1, V-{1})是一条最优的周游路线长度。于是可以得出: g(1,V-{1})=min {cg(1,V-{1})=min {cg(1,V-{1})=min {cg(1,V-{1})=min {c1k1k1k1k+g(k,V-{1,k}) (7.1)+g(k,V-{1,k}) (7.1)+g(k,V-{1,k}) (7.1)+g(k,V-{1,k}) (7.1) 2 2 2 2 ≤kkkk≤n n n n 将(7.1)(7.1)(7.1)(7.1)式一般化可得 g(i,S)= min {g(i,S)= min {g(i,S)= min {g(i,S)= min {ccccijijijij+g(j+g(j+g(j+g(j, S-{j}) (7.2), S-{j}) (7.2), S-{j}) (7.2), S-{j}) (7.2) j j j j∈SSSS 如果对于kkkk的所有选择,知道了g(k,V-{1,k})g(k,V-{1,k})g(k,V-{1,k})g(k,V-{1,k})的值,由(7.1)(7.1)(7.1)(7.1)式就可方便地求解出 g(1, V-{1})g(1, V-{1})g(1, V-{1})g(1, V-{1}),而这些gggg值则可通过(7.2)(7.2)(7.2)(7.2)式得到。显然,g(i,g(i,g(i,g(i,ФФФФ)=c)=c)=c)=ci1i1i1i1,1<2,4><2,4><2,4>。以后的剩余段由g(4,{3})g(4,{3})g(4,{3})g(4,{3}) 来获得。J(4,{3})=3J(4,{3})=3J(4,{3})=3J(4,{3})=3。因此这条最优周游路线是1,2,4,3,11,2,4,3,11,2,4,3,11,2,4,3,1。 设NNNN是用(7.1)(7.1)(7.1)(7.1)式计算g(1,V-{l})g(1,V-{l})g(1,V-{l})g(1,V-{l})以前需要计算的g(i,S)g(i,S)g(i,S)g(i,S)的数目。|S||S||S||S|的取值在不同 的决策阶段分别对应为0,1,0,1,0,1,0,1,…………,n-2,n-2,n-2,n-2。对于|S||S||S||S|的每一种取值,iiii都有n-1n-1n-1n-1种选择。不 包含1111和iiii在内的大小为kkkk的不同SSSS的个数是 。因此, n-2n-2n-2n-2 N = N = N = N = ∑(n-l) =(n-l)2(n-l) =(n-l)2(n-l) =(n-l)2(n-l) =(n-l)2n-2n-2n-2n-2 k=0 k=0 k=0 k=0 又因为在|S|=k|S|=k|S|=k|S|=k时,由(7.2)(7.2)(7.2)(7.2)求g(i,S)g(i,S)g(i,S)g(i,S)的值要进行k-lk-lk-lk-l次比较,所以用(7.1)(7.1)(7.1)(7.1)和(7.2)(7.2)(7.2)(7.2)求 最优周游路线的算法要求的计算时间为ΘΘΘΘ(n(n(n(n22222222nnnn))))。由此可知,用动态规划法设计 的算法比枚举法求最优周游路线在所用的时间上有所改进,但这种方法的严重 缺点是空间要求要有ОООО(n2(n2(n2(n2nnnn)))),这太大了,因而实际上是不可取的。在以后的章 节里,将对货郎担问题作进一步的讨论。 n-2n-2n-2n-2 kkkk n-2n-2n-2n-2 kkkk 处理一个作业通常需要执行若干个不同的任务。对于在多道程序环境中运行 的一些计算机程序,也需要执行输入、运算,然后排队打印输出等任务。假设 在一条流水线上有nnnn个作业,每个作业要求执行mmmm个任务:TTTT1i1i1i1i,TTTT2i2i2i2i,…………,TTTTmimimimi, 1111≤iiii≤nnnn。并且任务TTTTijijijij只能在设备PPPPjjjj上执行,1111≤jjjj≤mmmm。除此之外,还假定对于 任一作业iiii,在任务TTTTj-1,ij-1,ij-1,ij-1,i没完成以前,不能对任务TTTTijijijij开始处理,而且同一台设备 在任何时刻不能同时处理一个以上的任务。如果假设完成任务TTTTijijijij所要求的时间 是ttttijijijij,1111≤jjjj≤mmmm,1111≤iiii≤nnnn,那么如何将这nnnn×mmmm个任务分配给这mmmm台设备,才能 使这nnnn个作业在以上要求下顺利完成呢?这就是流水线作业调度问题。 例8.18.18.18.1 考虑在三台设备上调度两个作业,每个作业包含三项任务,完成这些任 务要求的时间由矩阵JJJJ给出,J = J = J = J = 。这两个作业的两种可能调度如图8.18.18.18.1 所示。((((第2222个下标表示作业序号)))) 2 0 3 3 5 2 5.8 5.8 5.8 5.8 流水线调度问题流水线调度问题流水线调度问题流水线调度问题 图8.1 8.1 8.1 8.1 例8.18.18.18.1的两种可能的调度 时 间 1 2 3 4 5 6 7 8 9 1 0 1 1 1 2   1 2 3 4 5 6 7 8 9 1 0 1 1 p1                     T11                   P2 T22 T21 T2 2             T22 T21           P3           T31 T32       T32   T31 流水线上的作业调度,有两种基本方式:一种是非抢先调度,它要求在任何 一台设备上处理一个任务一直到完成才能处理另一任务。另一种是抢先调度, 它没有以上要求,图8.18.18.18.1左部表示一种抢先调度,而右部则表示一种非抢先调度。 作业iiii的完成时间ffffiiii(S(S(S(S))))是在SSSS调度方案下作业iiii的所有任务得以完成的时间。在图 8.18.18.18.1左部,f,f,f,f1111(S)=10(S)=10(S)=10(S)=10,ffff2222(S)=12(S)=12(S)=12(S)=12。在图8.18.18.18.1右部,f,f,f,f1111(S)=11(S)=11(S)=11(S)=11,ffff2222(S)=5(S)=5(S)=5(S)=5。调度SSSS的完成时间 F(S)F(S)F(S)F(S)由下式给出: F(S) = F(S) = F(S) = F(S) = max{fmax{fmax{fmax{fiiii(S(S(S(S)} (8.1))} (8.1))} (8.1))} (8.1) 1 1 1 1≤iiii≤n n n n 平均流动时间MFT(S)MFT(S)MFT(S)MFT(S)定义为 MFT(S) = (1/n)MFT(S) = (1/n)MFT(S) = (1/n)MFT(S) = (1/n)**** ∑ffffiiii(S(S(S(S) (8.2)) (8.2)) (8.2)) (8.2) 1 1 1 1≤iiii≤n n n n 一组给定作业的最优完成时间OFTOFTOFTOFT调度是一种非抢先调度SSSS,它对所有非抢 先调度而言,F(S)F(S)F(S)F(S)的值最小。可以很容易地给出抢先最优完成时间(POFT)(POFT)(POFT)(POFT),最 优平均完成时间(OMFT)(OMFT)(OMFT)(OMFT)和抢先最优平均完成时间(POMFT)(POMFT)(POMFT)(POMFT)等调度的相应定义。 当mmmm>2222时,得到OFTOFTOFTOFT和POFTPOFTPOFTPOFT调度的一般问题是难于计算的问题,得到 OMFTOMFTOMFTOMFT调度的一般问题也是难于计算的问题((((见第8888讲))))。本节只讨论当m=2m=2m=2m=2时获 取OFTOFTOFTOFT调度这种特殊情况的算法。 为方便起见,用aaaajjjj表示tttt1i1i1i1i,bbbbjjjj表示tttt2i2i2i2i。在两台设备情况下容易证明:在两台设 备上处理的任务若不按作业的排列次序处理,则在调度完成时间上不比按次序 处理弱((((注意,若mmmm>2222则不然))))。因此,调度方案的好坏完全取决于这些作业在 每台设备上被处理的排列次序。当然,每个任务都应在最早的可能时间开始执 行。图8.28.28.28.2所示的调度就完全由作业的排列次序5,1,3,2,45,1,3,2,45,1,3,2,45,1,3,2,4所确定。为讨论简单起 见,假定aaaaiiii≠≠≠≠0000,llll≤≤≤≤iiii≤≤≤≤nnnn。事实上,如果允许有aaaaiiii=0=0=0=0的作业,那么最优调度可通过 以下方法构造出来:首先对于所有aaaaiiii≠≠≠≠0000的作业求出一种最优调度的排列,然后 把所有aaaaiiii=0=0=0=0的作业以任意次序加到这一排列的前面。 a2 b4 b2 b1 b5 p2 a4 a3 a1 a5 p1 图8.2 一种调度 容易看出,最优调度的排列具有下述性质:在给出了这个排列的第一个作业 后,剩下的排列相对于这两台设备在完成第一个作业时所处的状态而言是最优 的。假设对作业1,2, 1,2, 1,2, 1,2, …………, k, k, k, k的一种调度排列为σ1111,,,,σ2222, , , , …………, , , , σkkkk。对于这种调度,设 hhhh1111和hhhh2222分别是在设备PPPP1111和PPPP2222上完成任务(T(T(T(T11111111,T,T,T,T12121212, , , , …………, T, T, T, T1k1k1k1k))))和(T(T(T(T21212121,T,T,T,T22222222, , , , …………, T, T, T, T2k2k2k2k))))的时 间;又设t= ht= ht= ht= h2222-h-h-h-hllll,那么,在对作业1,2, 1,2, 1,2, 1,2, …………, k, k, k, k作了一系列决策之后,这两台设备 所处的状态可以完全由tttt来表征。它表示如果要在设备PPPP1111和PPPP2222上处理一些别的作 业,则必须在这两台设备同时处理前kkkk个作业的不同的任务后,设备PPPP2222还要用大 小为tttt的时间段处理前kkkk个作业中没处理完的任务,即在tttt这段时间及其以前,设 备PPPP2222不能用来处理别的作业的任务。记这些别的作业组成的集合为SSSS,假设g(S,t)g(S,t)g(S,t)g(S,t) 是上述tttt下SSSS的最优调度长度,于是作业集合{1,2, 1,2, 1,2, 1,2, …………, n, n, n, n}的最优长度是 g({1,2,g({1,2,g({1,2,g({1,2,…………,n}, 0),n}, 0),n}, 0),n}, 0)。 由最优调度排列所具有的性质可得: g ({1,2,g ({1,2,g ({1,2,g ({1,2,…………,n}, 0) = ,n}, 0) = ,n}, 0) = ,n}, 0) = min{amin{amin{amin{aiiii + g({1,2, + g({1,2, + g({1,2, + g({1,2,…………,n} - {i},b,n} - {i},b,n} - {i},b,n} - {i},biiii)} (8.1))} (8.1))} (8.1))} (8.1) 1 1 1 1 ≤iiii≤nnnn 将(8.1)(8.1)(8.1)(8.1)式推广到一般情况,对于任意的SSSS和iiii可得(8.2)(8.2)(8.2)(8.2)式。式中要求: g(g(g(g(ФФФФ,t)=max{t,0},t)=max{t,0},t)=max{t,0},t)=max{t,0}且aaaaiiii≠0000,1111≤iiii≤nnnn。 g(S,t) = g(S,t) = g(S,t) = g(S,t) = min{amin{amin{amin{aiiii + g(S-{i}, b + g(S-{i}, b + g(S-{i}, b + g(S-{i}, biiii+max{t-a+max{t-a+max{t-a+max{t-aiiii,0})} (8.2),0})} (8.2),0})} (8.2),0})} (8.2) i i i i∈SSSS (8.2) (8.2) (8.2) (8.2)式max{t-amax{t-amax{t-amax{t-aiiii,0},0},0},0}这一项由以下推导得出。由于任务TTTT2222在max{amax{amax{amax{ajjjj ,t} ,t} ,t} ,t}这段时间及 其以前不能用设备pppp2222处理,因此: h2-h1 = h2-h1 = h2-h1 = h2-h1 = bbbbiiii+max{a+max{a+max{a+max{ajjjj ,t} ,t} ,t} ,t} –––– aaaaiiii = b = b = b = biiii+max{t-a+max{t-a+max{t-a+max{t-aiiii,0},0},0},0}。 本来,可以使用一种与求解(7.2)(7.2)(7.2)(7.2)式类似的方法求解g(S,t)g(S,t)g(S,t)g(S,t),但它有这么多不同 的SSSS,而对这些SSSS都要计算g(S,t)g(S,t)g(S,t)g(S,t),因此这样计算最优调度长度g({1,2, g({1,2, g({1,2, g({1,2, …………, n}, n}, n}, n},0)0)0)0)至 少要花费ОООО(2(2(2(2nnnn))))的时间。下面介绍另一种代数方法来求解(8.2)(8.2)(8.2)(8.2)式,可得到一组非 常简单的规则,使用这组规则可以在ОООО(nlogn(nlogn(nlogn(nlogn))))的时间内产生最优调度。 考虑这些作业的一子集SSSS的任意一种调度RRRR。假设直到tttt这段时间PPPP2222都不能用来 处理SSSS中的作业。如果iiii和jjjj是这调度中排在前面的两个作业,则由(8.2)(8.2)(8.2)(8.2)式就得到: g(S,t) = g(S,t) = g(S,t) = g(S,t) = aaaaiiii + g(S-{i} , b + g(S-{i} , b + g(S-{i} , b + g(S-{i} , biiii+max{t-a+max{t-a+max{t-a+max{t-aiiii,0}),0}),0}),0}) = = = = aaaaiiii+a+a+a+ajjjj + g(S-{i,j}+ g(S-{i,j}+ g(S-{i,j}+ g(S-{i,j},bbbbjjjj + max{b + max{b + max{b + max{bi i i i + + + + max{t-amax{t-amax{t-amax{t-aiiii ,0}-a ,0}-a ,0}-a ,0}-ajjjj ,0}) (8.3) ,0}) (8.3) ,0}) (8.3) ,0}) (8.3) 令ttttijijijij = = = = bbbbjjjj + max{b + max{b + max{b + max{bi i i i + + + + max{t-amax{t-amax{t-amax{t-aiiii ,0}-a ,0}-a ,0}-a ,0}-ajjjj ,0} ,0} ,0} ,0} = = = = bbbbjjjj + b + b + b + biiii - - - - aaaajjjj + + + + max{max{t-amax{max{t-amax{max{t-amax{max{t-aiiii ,0} , ,0} , ,0} , ,0} , aaaajjjj - b- b- b- biiii}}}} = = = = bbbbjjjj + b + b + b + biiii - - - - aaaajjjj + + + + max{t-amax{t-amax{t-amax{t-aiiii , , , ,aaaajjjj - b- b- b- bi i i i ,0},0},0},0} = = = = bbbbjjjj + b + b + b + biiii - - - - aaaajjjj –––– aaaaiiii + max{t , + max{t , + max{t , + max{t , aaaaiiii + + + + aaaajjjj - b- b- b- biiii , , , , aaaaiiii} (8.4)} (8.4)} (8.4)} (8.4) 如果作业iiii和jjjj在RRRR中互易其位,那么完成时间 gggg’’’’(S,t) = (S,t) = (S,t) = (S,t) = aaaaiiii + + + + aaaajjjj + g(S-{i,j} , + g(S-{i,j} , + g(S-{i,j} , + g(S-{i,j} , ttttijijijij)))) 其中,ttttijijijij = = = = bbbbjjjj + b + b + b + biiii - - - - aaaajjjj –––– aaaaiiii + max{t , + max{t , + max{t , + max{t , aaaaiiii + + + + aaaajjjj - b- b- b- biiii , , , , aaaaiiii}}}}。 将g(S,t)g(S,t)g(S,t)g(S,t)与gggg’’’’(S,t)(S,t)(S,t)(S,t)相比较可以看出,如果下面的(8.3)(8.3)(8.3)(8.3)式成立,则g(S,t)g(S,t)g(S,t)g(S,t)≤≤≤≤gggg’’’’(S,t)(S,t)(S,t)(S,t)。 max{t , max{t , max{t , max{t , aaaaiiii+a+a+a+ajjjj-b-b-b-biiii , , , , aaaaiiii}}}}≤≤≤≤max{tmax{tmax{tmax{t , , , , aaaaiiii+a+a+a+ajjjj-b-b-b-bjjjj , , , , aaaaiiii} (8.5) } (8.5) } (8.5) } (8.5) 为了使(8.3)(8.3)(8.3)(8.3)式对于tttt的所有取值都成立,则需要 max{amax{amax{amax{aiiii+a+a+a+ajjjj-b-b-b-biiii , , , , aaaaiiii}}}}≤≤≤≤max{amax{amax{amax{aiiii+a+a+a+ajjjj-b-b-b-bjjjj , , , , aaaaiiii}}}} 即, aaaaiiii+a+a+a+ajjjj+max+max+max+max{-b{-b{-b{-biiii ,- ,- ,- ,-aaaajjjj}}}}≤≤≤≤aaaaiiii+a+a+a+ajjjj+max{-b+max{-b+max{-b+max{-bjjjj ,- ,- ,- ,-aaaaiiii}}}},或 max{-bmax{-bmax{-bmax{-biiii ,- ,- ,- ,-aaaajjjj}}}}≤≤≤≤ max{- max{- max{- max{-bbbbjjjj ,- ,- ,- ,-aaaaiiii} (8.6)} (8.6)} (8.6)} (8.6) 由(8.4)(8.4)(8.4)(8.4)式可以断定存在一种最优调度,在这调度中,对于每一邻近的作业对 (i,j)(i,j)(i,j)(i,j),有 min{bmin{bmin{bmin{biiii , , , ,aaaajjjj}}}}≥≥≥≥min{bmin{bmin{bmin{bjjjj , , , ,aaaaiiii}}}}。不难证明,具有这一性质的所有调度都有相同的长度。 由此足以产生对于每个邻近作业对都满足(8.6)(8.6)(8.6)(8.6)式的任何调度。根据(8.6)(8.6)(8.6)(8.6)式作以 下的观察就能得到具有这一性质的调度。如果min{amin{amin{amin{a1111,a,a,a,a2222,,,,…………,a,a,a,annnn , b , b , b , b1111,b,b,b,b2222,,,,…………,,,,bbbbnnnn}}}}是 aaaaiiii,那么作业iiii就应是最优调度中的第一个作业。如果min{amin{amin{amin{a1111,a,a,a,a2222,,,,…………,a,a,a,annnn , b , b , b , b1111,b,b,b,b2222,,,,…………,,,,bbbbnnnn}}}} 是bbbbjjjj,那么作业jjjj就应是最优调度中的最后一个作业。这使我们能作出一个决 策,以便决定nnnn个作业中的一个作业应放的位置。然后又将(8.6)(8.6)(8.6)(8.6)式用于其余的n-n-n-n- llll个作业,再正确地决定另一作业的位置,等等。因此,由(8.6)(8.6)(8.6)(8.6)式导出的调度规 则是:::: ① 把全部aaaaiiii和bbbbiiii分类成非降序列; ② 按照这一分类次序考察此序列:如果序列中下一个数是aaaajjjj且作业jjjj还没调 度,那么在还没使用的最左位置调度作业jjjj;如果下个数是bbbbjjjj且作业jjjj还没调度, 那么在还没使用的最右位置调度作业jjjj。如果已经调度了作业jjjj,则转到该序列 的下一个数。 要指出的是,上述规则也正确地把aaaaiiii=0=0=0=0的那些作业放在适当位置上,因此对 这样的作业不用分开考虑。 例8.18.18.18.1 设n=4n=4n=4n=4,(a(a(a(a1111,a,a,a,a2222,a,a,a,a3333,a,a,a,a4444)=(3,4,8,10))=(3,4,8,10))=(3,4,8,10))=(3,4,8,10)和(b(b(b(bllll,b,b,b,b2222,b,b,b,b3333,b,b,b,b4444)=(6,2,9,15))=(6,2,9,15))=(6,2,9,15))=(6,2,9,15),对这些aaaa和bbbb分类后 的序列是(b(b(b(b2222,a,a,a,a1111,a,a,a,a2222,b,b,b,b1111,a,a,a,a3333,b,b,b,b3333,a,a,a,a4444,b,b,b,b4444)=(2,3,4,6,8,9,10,15))=(2,3,4,6,8,9,10,15))=(2,3,4,6,8,9,10,15))=(2,3,4,6,8,9,10,15),设σ1111,,,,σ2222,,,,σ3333,,,,σ4444是最优调度。 由于最小数是bbbb2222,置σ4444=2=2=2=2。下一个数是aaaa1111,置σ1111=1=1=1=1。接着的最小数是aaaa2222,由于 作业2222已被调度,转向再下一个数bbbb1111。作业1111已被调度,再转向下一个数aaaa3333,置σ 2222=3=3=3=3。最后剩σ3333是空的,而当前只有作业4444还没调度,从而σ3333=4=4=4=4。 练习: 1 1 1 1 修改过程All_PathsAll_PathsAll_PathsAll_Paths使其输出每对结点(i,j)(i,j)(i,j)(i,j)间的最短路径,该新算法的时间和空 间复杂度是多少? 2 2 2 2 ① 证明算法OBSTOBSTOBSTOBST的计算时间是ОООО(n(n(n(n2222))))。 ② 在已知根R(i,j)(0R(i,j)(0R(i,j)(0R(i,j)(0≤≤≤≤i0) { while(k>0) { while(k>0) { while(k>0) { if( if( if( if(还剩未检验的X(k)X(k)X(k)X(k)使得X(k)X(k)X(k)X(k)∈T(X[l],T(X[l],T(X[l],T(X[l],…………,X[k-1] and,X[k-1] and,X[k-1] and,X[k-1] and B(X[1], B(X[1], B(X[1], B(X[1],…………,X[k])=True) {,X[k])=True) {,X[k])=True) {,X[k])=True) { if((X[1], if((X[1], if((X[1], if((X[1],…………,X[k]),X[k]),X[k]),X[k])是一条已抵达一答案结点的路径) { ) { ) { ) { print(X[1],print(X[1],print(X[1],print(X[1],…………,X[k]) },X[k]) },X[k]) },X[k]) };//if //if //if //if ++k ++k ++k ++k; // // // //考虑下一个集合 else {--kelse {--kelse {--kelse {--k;} //} //} //} //回溯到先前的集合 }}}};////////if if if if } } } }; }// }// }// }// BackTrack BackTrack BackTrack BackTrack 注意:集合T()T()T()T()将提供作为解向量的第一个分量X(1)X(1)X(1)X(1)的所有可能值,解向量 则取使限界函数BBBB1111(X(1))(X(1))(X(1))(X(1))为真的那些X(1)X(1)X(1)X(1)的值。还要注意的是,这些元素是如何 按深度优先方式生成的。随着kkkk值的增加,解向量的各分量不断生成,直到找到 一个解或者不再剩有未经检验的X(k)X(k)X(k)X(k)为止。当kkkk值减少时,算法必须重新开始生 成那些可能在以前剩下而没经检验的元素。因此,还需拟制一个子算法,使它 按某种次序来生成这些元素 X(k)X(k)X(k)X(k)。如果只想要一个解,则可在printprintprintprint后面设置一 条 returnreturnreturnreturn语句。 算法6.26.26.26.2提供了回溯算法的一种递归表示。由于它基本上是一棵树的后根遍 历算法,因此,按照这种方法描述回溯法是自然的。这个递归模型的初始调用 为 BackTrackBackTrackBackTrackBackTrack(1)(1)(1)(1)。 算法6.26.26.26.2 递归回溯算法 void void void void RBackTrackRBackTrackRBackTrackRBackTrack(k) {(k) {(k) {(k) { // // // //此算法是对回溯法抽象地递归描述。进入算法时,解向量 // // // // X(1:n)X(1:n)X(1:n)X(1:n)的前k-1k-1k-1k-1个分量X(1)X(1)X(1)X(1),…………,X(k-1)X(k-1)X(k-1)X(k-1)已赋值 intintintint n n n n,X[n]X[n]X[n]X[n]; // // // //定义成全局变量 forforforfor((((对每个X[k]|X[k]X[k]|X[k]X[k]|X[k]X[k]|X[k]∈T(X[1],T(X[1],T(X[1],T(X[1],…………,X[k-1]) and,X[k-1]) and,X[k-1]) and,X[k-1]) and B(X[1], B(X[1], B(X[1], B(X[1],…………,X[k]=true) {,X[k]=true) {,X[k]=true) {,X[k]=true) { if(X[1], if(X[1], if(X[1], if(X[1],…………,X[k]),X[k]),X[k]),X[k])是一条已抵达一答案结点的路径)))) { { { {print(X[1],print(X[1],print(X[1],print(X[1],…………,X[k]),X[k]),X[k]),X[k]);}//if}//if}//if}//if RBackTrack RBackTrack RBackTrack RBackTrack(k+1)(k+1)(k+1)(k+1); } } } }; }//}//}//}//RBackTrackRBackTrackRBackTrackRBackTrack 解向量((((xxxx1111,,,,…………,,,,xxxxnnnn))))作为一个全程数组X(1:n)X(1:n)X(1:n)X(1:n)来处理。该元组的第kkkk个位置上满足BBBB 的所有元素逐个被生成,并被连接到当前向量((((X(l),X(l),X(l),X(l),…………,X(k-1)),X(k-1)),X(k-1)),X(k-1)),每次X(k)X(k)X(k)X(k)都要附 带一种检查,即判断一个解是否已被找到。因此,这个算法被递归调用。当退 出forforforfor循环时,不再剩有X(k)X(k)X(k)X(k)的值,从而结束此层递归并继续上次未完成的调用。 要指出的是,当kkkk大于nnnn时,T(X(1),T(X(1),T(X(1),T(X(1),…………,X(k-1)),X(k-1)),X(k-1)),X(k-1))返回一个空集,因此,根本不进 入forforforfor循环。还要指出的是,这个程序输出所有的解,而且组成解的元组的大小 是可变的。如果只想要一个解,则可加上一个标志作为一个参数,以指明首次 成功的情况。 l.2 l.2 l.2 l.2 效率估计 上面所给出的两个回溯程序的效率主要取决于以下四种因素: ① 生成下一个X(k)X(k)X(k)X(k)的时间; ② 满足显式约束条件的X(k)X(k)X(k)X(k)的数目; ③ 限界函数BBBBiiii的计算时间; ④ 对于所有的 iiii,满足 BBBBiiii的X(k)X(k)X(k)X(k)的数目。 如果这些限界函数大量地减少了所生成的结点数,则认为它们是好的。不 过,一些好的限界函数往往需要较多的计算时间,而我们所希望的不只是减少 所生成的结点,而是要减少总的计算时间。因此选择限界函数时,通常在好坏 与时间消费上采取折衷的方案。有许多问题,其状态空间树的规模太大,要想 生成其全部结点实际上是不允许的,因此应该使用限界函数并且希望在一段适 当的时间内至少会找出一个解。不过对于许多问题((((例如n-n-n-n-皇后问题))))至今还没听 说有完善的限界方法。 对于许多问题,可以按任意次序使用包含各个解分量xxxxiiii可能取值的有穷集SSSSiiii。 为了提高有效检索的效率,一般可采用一种称之为重新排列的方法。其基本思 想是:在其它因素相同的情况下,从具有最少元素的集合中作下一次选择。这 种策略虽已证明对n-n-n-n-皇后问题无效,而且还可构造出一些证明用此方法是无效 的例子,但从信息论的观点看,从最小集合中作下一次选择,平均来说更为有 效。 在图6.76.76.76.7中,对同一个问题用两棵回溯检索树显示了这一方法的潜在能力。如 果能去掉图6.7(6.7(6.7(6.7(a)a)a)a)中树的第一级的一个结点,那么,实际上就从所考虑的情况中 去掉了12121212个元组。如果从图 6.7( 6.7( 6.7( 6.7(b)b)b)b)中树的第一级上去掉一个结点,则只删去8888个 元组。更完善的重新排列策略将在后面和动态状态空间树一起研究。 图6.7 重新排列 如上所述,有四种因素决定回溯算法所需要的时间。一旦选定了一种状态空 间树结构,这四种因素的前三种因素相对来说就与所要解决问题的实例无关, 只有第四种因素,即所生成的结点数将因问题的实例不同而异。对于某一实 例,回溯算法可能只生成 ОООО(n)(n)(n)(n)个结点,而对于另一不同的实例,由于它与原实 例密切相关,故也可能生成这棵状态空间树的几乎全部结点。如果解空间的结 点数是2222nnnn和n!n!n!n!,则回溯算法的最坏情况时间一般将分别是ОООО(p(n)2(p(n)2(p(n)2(p(n)2nnnn))))或ОООО(q(n)n!)(q(n)n!)(q(n)n!)(q(n)n!)。 p(n)p(n)p(n)p(n)和q(n)q(n)q(n)q(n)都是nnnn的多项式。尽管回溯法对同一问题的不同实例在计算时间上可 能出现极大差异,但当nnnn很大时,对于某些实例而言,回溯算法确实可在很短时 间内求出其解。因此,回溯法仍不失为一种有效的算法设计策略,只是在决定 采用回溯算法正式计算某实例之前,应预先估算出回溯算法在此实例情况下的 工作效能。 用回溯算法去处理一棵树所要生成的结点数,可以用蒙特卡罗((((Monte Carlo)Monte Carlo)Monte Carlo)Monte Carlo) 方法估算出来。这种估计方法的一般思想是:在状态空间树中生成一条随机路 径。设XXXX是这条随机路径上的一个结点,且XXXX在状态空间树的第iiii级。在结点XXXX处 用限界函数确定没受限界的孩子结点的数目mmmmiiii,再在这mmmmiiii结点中随机地选择一 个结点作为这条路径上的下一个结点。当所选择是一个叶子结点,或者该结点 的所有孩子结点都已被限界时,该路径的生成即结束。 用这些mmmmiiii可以估算出这棵状态空间树中不受限界结点的总数mmmm。此数在准备 检索出所有答案结点时非常有用。在这种情况下,需要生成所有的不受限结点。 当只想求一个解时,由于只需生成mmmm个结点的很少一部分,回溯算法就可以得到 一个解。因此,mmmm不是一个理想的估计值。由mmmmiiii估算mmmm,需要假定这些限界函数 是固定的,即在算法执行期间当其信息逐渐增加时,限界函数不变,而且同一 个函数正好用于这棵状态空间树的同一级的所有结点。这一假定对于大多数回 溯算法并不适用,在大多数情况下,随着检索的进行限界函数会变得更强一 些,在这些情况中,对mmmm的估计值将大于考虑了限界函数的变化后所能得到的值。 沿用限界函数固定的假定,可以看到第2222级没受限的结点数为mmmm1111。如果该检索树 是同一级上结点有相同度的树,那么就可预计到每一个2222级结点平均有mmmm2222个没限 界的子结点,从而得出在第3333级上有mmmm1111****mmmm2222个结点。第4444级预计没受限的结点数是 mmmm1111****mmmm2222****mmmm3333。一般地,在i+1i+1i+1i+1级上预计的结点数是mmmm1111****mmmm2222****…………mmmmiiii。于是,在求解给 定问题的实例IIII中所要生成的不受限界结点的估计数m=1+mm=1+mm=1+mm=1+m1111+ m+ m+ m+ m1111****mmmm2222+ + + + mmmm1111****mmmm2222****mmmm3333++++…………。 过程EstimateEstimateEstimateEstimate是一个确定mmmm值的算法。它从状态空间树的根出发选择一条随 机路径。函数SizeSizeSizeSize返回集合TTTTkkkk的大小。函数ChooseChooseChooseChoose从TTTTkkkk中随机地挑选一个元素。 mmmm和rrrr是产生不受限结点估计数所用的临时工作单元。 算法6.36.36.36.3 估计回溯法的效率 void Estimate() {void Estimate() {void Estimate() {void Estimate() { // // // //程序沿着状态空间树中一条随机路径产生该树中不受限界结点的估计数 m = lm = lm = lm = l;r = 1r = 1r = 1r = 1;k = 1k = 1k = 1k = 1; while(1) { while(1) { while(1) { while(1) { T[k]={X[k]|X[k] T[k]={X[k]|X[k] T[k]={X[k]|X[k] T[k]={X[k]|X[k]∈T(X[1],T(X[1],T(X[1],T(X[1],…………,X[k-1]) and,X[k-1]) and,X[k-1]) and,X[k-1]) and B B B Bkkkk(X[1],(X[1],(X[1],(X[1],…………,X[k])} ,X[k])} ,X[k])} ,X[k])} if(Size(T[k])=0) break if(Size(T[k])=0) break if(Size(T[k])=0) break if(Size(T[k])=0) break; r r r r**** = Size(T[k]) = Size(T[k]) = Size(T[k]) = Size(T[k]); m += r m += r m += r m += r; X[k]=Choose(T[k]) X[k]=Choose(T[k]) X[k]=Choose(T[k]) X[k]=Choose(T[k]); ++k ++k ++k ++k; } } } };//while //while //while //while return m return m return m return m; }//Estimate}//Estimate}//Estimate}//Estimate 对算法EstimateEstimateEstimateEstimate稍加修改就可得到更准确的结点估计数。这只需增加一个forforforfor 循环语句,选取数条不同的随机路径((((一般可取20202020条)))),在求出沿每条路径的估计 值后,取平均值即得。 8- 8- 8- 8-皇后问题实际上很容易一般化为n-n-n-n-皇后问题,即要求找出在一个nnnn××××nnnn棋盘上 放置nnnn个皇后并使其不能互相攻击的所有方案。令((((xxxx1111,,,,…………,,,,xxxxnnnn))))表示一个解,其中xxxxiiii 是把第iiii个皇后放在第iiii行的列数(列号)。由于没有两个皇后可以放人同一列, 因此,所有这些xxxxiiii将是截然不同的。那么应如何去测试两个皇后是否在同一条斜 角线上呢? 如果设想棋盘的方格像二维数组A(1..n,1..n)A(1..n,1..n)A(1..n,1..n)A(1..n,1..n)的下标那样标记,那么可以看到, 对于在同一条斜角线上的由左上方到右下方的每一个元素有相同的““““行----列””””值, 同样,在同一条斜角线上的由右上方到左下方的每一个元素则有相同的 ““““行++++列”””” 值。假设有两个皇后被放置在((((i,j)i,j)i,j)i,j)和((((k,p)k,p)k,p)k,p)位置上,那么根据上述说明,当且仅当 iiii-j=k-p-j=k-p-j=k-p-j=k-p或i+j=k+pi+j=k+pi+j=k+pi+j=k+p时,它们才在同一条斜角线上。 将这两个等式分别变换成 j-p=i-k j-p=i-k j-p=i-k j-p=i-k 和j-p=k-ij-p=k-ij-p=k-ij-p=k-i,那么,当且仅当||||j-p|=|i-k|j-p|=|i-k|j-p|=|i-k|j-p|=|i-k|时,两 个皇后在同一条斜角线上。 过程 Place(k)Place(k)Place(k)Place(k)返回一个布尔值,当第kkkk个皇后能够放在(k)(k)(k)(k)的当前值处时,返回 值为真。这个过程测试两种情况,即X(k)X(k)X(k)X(k)是否不同于前面X(1),X(1),X(1),X(1),…………,X(k-l),X(k-l),X(k-l),X(k-l)的值以 及在同一条斜角线上是否根本就没有别的皇后。该过程的计算时间是ОООО(k-1)(k-1)(k-1)(k-1)。 6.2 6.2 6.2 6.2 8-8-8-8-皇后问题皇后问题皇后问题皇后问题 算法6.46.46.46.4 可以放置一个新皇后吗? void Place(k) {void Place(k) {void Place(k) {void Place(k) { ////////如果一个皇后能放在第kkkk行和X(k)X(k)X(k)X(k)列,则返回truetruetruetrue;否则返回falsefalsefalsefalse。 //X//X//X//X是一个全程数组,进入此过程时已置了kkkk个值。ABS(r)ABS(r)ABS(r)ABS(r)返回 rrrr的绝对值。 intintintint X[k] X[k] X[k] X[k];intintintint k k k k; // X(1..n) // X(1..n) // X(1..n) // X(1..n)定义成全局变量 i=1i=1i=1i=1; while(i0) { While(k>0) { While(k>0) { While(k>0) { // // // //对所有的行,执行以下语句 ++++++++X[k]X[k]X[k]X[k]; // // // //移到下一列 While((X[k]<=n) and !Place(k)) { //While((X[k]<=n) and !Place(k)) { //While((X[k]<=n) and !Place(k)) { //While((X[k]<=n) and !Place(k)) { //此处能放这个皇后吗 ++++++++X[k]X[k]X[k]X[k]; }}}};//while//while//while//while if(X[k]<=n) { if(X[k]<=n) { if(X[k]<=n) { if(X[k]<=n) { // // // //找到一个位置 if(k==n) { print(X)if(k==n) { print(X)if(k==n) { print(X)if(k==n) { print(X);} //} //} //} //若是一个完整的解,则输出XXXX数组 else {++kelse {++kelse {++kelse {++k;X[k]=0X[k]=0X[k]=0X[k]=0;} //} //} //} //转向下一行 else --kelse --kelse --kelse --k; } // } // } // } //回溯 } } } };////////while while while while }// }// }// }// NQueensNQueensNQueensNQueens 此时,读者可能对于过程 NQueensNQueensNQueensNQueens怎么会优于硬性处理感奇怪。原因是这样 的,如果硬性要求个8888××××8888的棋盘安排出8888块位置,就有CCCC888864646464种可能的方式,即要检 查将近4.44.44.44.4××××101010109999个8-8-8-8-元组。然而过程NQueensNQueensNQueensNQueens只允许把皇后放置在不同的行和列 上,因此至多需要作8!8!8!8!次检查,即至多只检查40320403204032040320个8-8-8-8-元组。 可以用过程EstimateEstimateEstimateEstimate来估算NQueensNQueensNQueensNQueens所生成的结点数。要指出的是,过程 EstimateEstimateEstimateEstimate所需要的假定也适用于NQueensNQueensNQueensNQueens,即使用固定的限界函数且在检索进行 时函数不改变。另外,在状态空间树的同一级的所有结点都有相同的度。图 6.86.86.86.8 显示了由过程EstimateEstimateEstimateEstimate求结点估计数所用的五个8888×8888棋盘。如同所要求的那样, 棋盘上每一个皇后的位置是随机选取的。对于每种选择方案,都记下了可以将 一个皇后合法地放在各行中列的数目((((即状态树的每一级没受限的结点数))))。它们 都列在每个棋盘下方的向量中。向量后面的数字表示过程EstimateEstimateEstimateEstimate由这些量值所 产生的值。这五次试验的平均值是1625162516251625。 8 8 8 8----皇后状态空间树的结点总数是:::: 7 7 7 7 jjjj 1 + 1 + 1 + 1 + ∑((((∏(8-i)) = 69281(8-i)) = 69281(8-i)) = 69281(8-i)) = 69281 j=0 j=0 j=0 j=0 j=0 j=0 j=0 j=0 因此,不受限结点的估计数大约只是8-8-8-8-皇后状态空间树的结点总数的2.342.342.342.34%。 列 图6.8 8皇后问题的五种方案及不受限结点的估计值 子集和数问题是假定有nnnn个不同的正数((((通常称为权)))),要求找出这些数中所有 使得某和数为MMMM的组合。例6.26.26.26.2和例6.46.46.46.4说明了如何用大小固定或变化的元组来表 示这个问题。本节将利用大小固定的元组来研究一种回溯解法,在此情况下, 解向量的元素X(i)X(i)X(i)X(i)取1111或0000值,它表示是否包含了权数W(i)W(i)W(i)W(i)。 生成图6.46.46.46.4中任一结点的子结点是很容易的。对于iiii级上的一个结点,其左子结 点对应于X(i)=1X(i)=1X(i)=1X(i)=1,右子结点对应于X(i)=0X(i)=0X(i)=0X(i)=0。对于限界函数的一种简单选择是,当 且仅当 k k k k n n n n ∑W(i)X(i) + W(i)X(i) + W(i)X(i) + W(i)X(i) + ∑W(i)W(i)W(i)W(i)≥M M M M 时,B(X(l),B(X(l),B(X(l),B(X(l),…………,X(k))=true,X(k))=true,X(k))=true,X(k))=true。 i=1 i=1 i=1 i=1 i=k+1 i=k+1 i=k+1 i=k+1 显然,如果这个条件不满足,X(1),X(1),X(1),X(1),…………,X(k),X(k),X(k),X(k)就不能导致一个答案结点。如果假 定这些W(i)W(i)W(i)W(i)一开始就是按非降次序排列的,那么这些限界函数可以被强化。在 这种情况下,如果 k k k k ∑W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)>MMMM i=1 i=1 i=1 i=1 则X(l),X(l),X(l),X(l),…………,X(k),X(k),X(k),X(k)就不能导致一个答案结点。因此将要使用的限界函数是 BBBBkkkk((1),((1),((1),((1),…………,X(k)),X(k)),X(k)),X(k))=truetruetruetrue 当且仅当 k k k k n n n n kkkk ∑W(i)X(i) + W(i)X(i) + W(i)X(i) + W(i)X(i) + ∑W(i)W(i)W(i)W(i)≥M M M M 且 ∑W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)W(i)X(i) + W(k+1)≤M (6.1)M (6.1)M (6.1)M (6.1) i=1 i=1 i=1 i=1 i=k+1 i=k+1 i=k+1 i=k+1 i=1 i=1 i=1 i=1 6.3 6.3 6.3 6.3 子集和数问题子集和数问题子集和数问题子集和数问题 由于在即将拟制的算法中不会使用BBBBnnnn,因此不必担心这个函数中会出现 W(n+1)W(n+1)W(n+1)W(n+1)。至此已说明了直接使用6.6.6.6.llll节介绍的两种回溯方案中任何一种方案所需 要的一切。为简单起见,将算法6.26.26.26.2修改成适应求子集和数的需要便得到递归算 法SumOfSubSumOfSubSumOfSubSumOfSub。 算法6.66.66.66.6 子集和数问题的递归回溯算法 voidvoidvoidvoid SumOfSub SumOfSub SumOfSub SumOfSub(s,k,r) {(s,k,r) {(s,k,r) {(s,k,r) { ////////找w(1:n)w(1:n)w(1:n)w(1:n)中和数为MMMM的所有子集。进入此过程时x(1),x(1),x(1),x(1),…………,X(k-1),X(k-1),X(k-1),X(k-1)的值已确定。 k-1 n k-1 n k-1 n k-1 n //s=//s=//s=//s=∑W(i)X(i)W(i)X(i)W(i)X(i)W(i)X(i)且r=r=r=r=∑W(j)W(j)W(j)W(j)。W(j)W(j)W(j)W(j)按非递减排列。假定w(1)w(1)w(1)w(1)≤MMMM,∑W(i)W(i)W(i)W(i)≥MMMM j=1 j=1 j=1 j=1 j=k j=k j=k j=k l l l l int int int int M M M M,nnnn;float W[n]float W[n]float W[n]float W[n];boolboolboolbool X[n] X[n] X[n] X[n]; //M //M //M //M、W[n]W[n]W[n]W[n]、X[n]X[n]X[n]X[n]定义成全局变量 2 2 2 2 float rfloat rfloat rfloat r,ssss;intintintint k k k k,jjjj; // // // //生成左子结点。注意,由于BBBBkkkk-1-1-1-1=true=true=true=true,因此s + w[k]s + w[k]s + w[k]s + w[k]≤MMMM 3 3 3 3 X[k]=1 X[k]=1 X[k]=1 X[k]=1; 4 4 4 4 if(s+w[k]==M) { // if(s+w[k]==M) { // if(s+w[k]==M) { // if(s+w[k]==M) { //子集找到 5 5 5 5 print(X[j],j=1 to k)print(X[j],j=1 to k)print(X[j],j=1 to k)print(X[j],j=1 to k);}}}} // // // //由于W(j)W(j)W(j)W(j)>0000,1111≤≤≤≤jjjj≤≤≤≤kkkk,所以不存在递归调用 6 6 6 6 elseelseelseelse 7 7 7 7 if(sif(sif(sif(s+W[k]W[k]W[k]W[k]+W[k+1]<=M) { //W[k+1]<=M) { //W[k+1]<=M) { //W[k+1]<=M) { //BBBBkkkk=true=true=true=true 8888 SumOfSubSumOfSubSumOfSubSumOfSub(s+W[k](s+W[k](s+W[k](s+W[k],k+1k+1k+1k+1,r-W[k])r-W[k])r-W[k])r-W[k]);}}}} 9 9 9 9 ////////endifendifendifendif 10 10 10 10 ////////endifendifendifendif ////////生成右孩子和计算 BkBkBkBk的值 // // // // 11 11 11 11 if((s+r-W[k]>=M) and (sif((s+r-W[k]>=M) and (sif((s+r-W[k]>=M) and (sif((s+r-W[k]>=M) and (s+w(kw(kw(kw(k+1)<=M)) { //1)<=M)) { //1)<=M)) { //1)<=M)) { //BBBBkkkk=true=true=true=true 12 X[k]=012 X[k]=012 X[k]=012 X[k]=0; 13131313 SumOfSub SumOfSub SumOfSub SumOfSub(s,k+l,r-W[k])(s,k+l,r-W[k])(s,k+l,r-W[k])(s,k+l,r-W[k]); 14 14 14 14 } } } };//if //if //if //if 15}//15}//15}//15}//SumOfSub SumOfSub SumOfSub SumOfSub k k k k n n n n 过程SumOfSubSumOfSubSumOfSubSumOfSub将∑W(i)X(i)W(i)X(i)W(i)X(i)W(i)X(i)和∑W(i)W(i)W(i)W(i)分别保存在变量ssss和rrrr中以避免每次 i=1 i=1 i=1 i=1 i=k+1 i=k+1 i=k+1 i=k+1 都要计算之。此算法假定W(1)W(1)W(1)W(1)≤MMMM和∑W(i)W(i)W(i)W(i)≥MMMM,(1(1(1(1≤iiii≤n)n)n)n)。初次调用是 SumOfSubSumOfSubSumOfSubSumOfSub(0,l,W[i])(0,l,W[i])(0,l,W[i])(0,l,W[i])。着重要指出的是,算法没有明显地使用测试条件kkkk>nnnn终止 递归。之所以不需要这一测试条件,是因为在算法入口处ssss≠MMMM且s+rs+rs+rs+r≥MMMM,因此 rrrr≠0000,从而kkkk也不可能大于nnnn。在第 7 7 7 7行由于s+w(k)s+w(k)s+w(k)s+w(k)<MMMM并且s+rs+rs+rs+r≥MMMM,因此可以 得出rrrr≠w(k)w(k)w(k)w(k),从而 k+1k+1k+1k+1≤nnnn。还需注意的是,如果ssss+W(k)=M(W(k)=M(W(k)=M(W(k)=M(第4444行)))),则 X(k+1),X(k+1),X(k+1),X(k+1),…………,X(n),X(n),X(n),X(n)应为0000。这些0000在第5555行的输出中被略去。在第7777行没作 k k k k n n n n ∑W(i)X(i)+W(i)X(i)+W(i)X(i)+W(i)X(i)+∑W(i)W(i)W(i)W(i)≥MMMM的测试,这是因为已经知道s+rs+rs+rs+r≥MMMM和X(k)=1X(k)=1X(k)=1X(k)=1。 i=1 i=1 i=1 i=1 i=k+1 i=k+1 i=k+1 i=k+1 例6.6 6.6 6.6 6.6 图6.96.96.96.9给出了过程SumOfSubSumOfSubSumOfSubSumOfSub对n=6n=6n=6n=6,M=30M=30M=30M=30和W(1:6)=(5,1O,12,13,15,18)W(1:6)=(5,1O,12,13,15,18)W(1:6)=(5,1O,12,13,15,18)W(1:6)=(5,1O,12,13,15,18) 的处理时所生成的一部分状态空间树。矩形结点列出了对SumOfSubSumOfSubSumOfSubSumOfSub的每次调用 时s,k,rs,k,rs,k,rs,k,r的值。圆形结点表示其和为MMMM的一个子集被输出出来的场所。在结点A,BA,BA,BA,B 和CCCC处,输出分别为(1,1,0,0,1)(1,1,0,0,1)(1,1,0,0,1)(1,1,0,0,1),(1,0,1,1)(1,0,1,1)(1,0,1,1)(1,0,1,1)和(0,0,(0,0,(0,0,(0,0,l,0,0,l)l,0,0,l)l,0,0,l)l,0,0,l)。要指出的是,图6.96.96.96.9所示 的树只包含20202020个矩形结点,而n=6n=6n=6n=6的满状态空间树中对SumOfSubSumOfSubSumOfSubSumOfSub调用的结点可 以有22226666-1=63-1=63-1=63-1=63个((((由于根本不需要从一个叶结点作出调用,因此这一计数排除了64646464 个叶结点))))。 下页图为:图6.9 6.9 6.9 6.9 由SumOfSubSumOfSubSumOfSubSumOfSub所生成状态空间树的一部分 k n k n k n k n 过程SumOfSubSumOfSubSumOfSubSumOfSub将∑W(i)X(i)W(i)X(i)W(i)X(i)W(i)X(i)和∑W(i)W(i)W(i)W(i)分别保存在变量ssss和rrrr中以避免每次都要 i=1 i=k+1 i=1 i=k+1 i=1 i=k+1 i=1 i=k+1 计算之。此算法假定W(1)W(1)W(1)W(1)≤MMMM和∑W(i)W(i)W(i)W(i)≥MMMM,(1(1(1(1≤iiii≤n)n)n)n)。 初次调用是SumOfSubSumOfSubSumOfSubSumOfSub(0,l,W[i])(0,l,W[i])(0,l,W[i])(0,l,W[i])。必须指出的是,算法没有明显地使用测试条 件kkkk>nnnn终止递归。之所以不需要这一测试条件,是因为在算法入口处ssss≠MMMM且 s+rs+rs+rs+r≥MMMM,因此rrrr≠0000,从而kkkk也不可能大于nnnn。在第 7 7 7 7行由于s+w(k)s+w(k)s+w(k)s+w(k)<MMMM并且 s+rs+rs+rs+r≥MMMM,因此可以得出rrrr≠w(k)w(k)w(k)w(k),从而 k+1k+1k+1k+1≤nnnn。还需注意的是,如果ssss+ W(k)=M(W(k)=M(W(k)=M(W(k)=M(第4444行)))),则X(k+1),X(k+1),X(k+1),X(k+1),…………,X(n),X(n),X(n),X(n)应为0000。这些0000在第5555行的输出中被略去。在第 7777行没作 k nk nk nk n ∑W(i)X(i)+W(i)X(i)+W(i)X(i)+W(i)X(i)+∑W(i)W(i)W(i)W(i)≥MMMM的测试,这是因为已经知道s+rs+rs+rs+r≥MMMM和X(k)=1X(k)=1X(k)=1X(k)=1。 i=1 i=1 i=1 i=1 i=k+1 i=k+1 i=k+1 i=k+1 已知一个图GGGG和mmmm>0000种颜色,在只准使用这mmmm种颜色对GGGG的结点着色的情况 下,是否能使图中任何相邻的两个结点都具有不同的颜色呢?这个问题称为 m-m-m-m- 着色判定问题。在m-m-m-m-着色最优化问题中,则是求可对图GGGG着色的最小整数mmmm。称 mmmm为图GGGG的色数。 对于图着色的研究是从m-m-m-m-可着色性问题的著名特例————————四色问题开始的。四 色问题要求证明平面或球面上的任何地图的所有区域都至多可用四种颜色来着 色,并使任何两个有一段公共边界的相邻区域没有相同的颜色。这个问题可转 换成对一平面图的4-4-4-4-着色判定问题((((平面图是一个能画于平面上而边无任何交叉 的图))))。将地图的每个区域变成一个结点,若两个区域相邻,则相应的结点用一 条边连接起来。图6.106.106.106.10显示了一幅有5555个区域的地图以及与该地图对应的平面图。 多年来,虽然已证明用5555种颜色足以对任一幅地图着色,但是一直找不到一定要 求多于4444种颜色的地图。直到1976197619761976年,这个问题才由爱普尔((((K.1.Apple)K.1.Apple)K.1.Apple)K.1.Apple),黑肯 ((((W.W.W.W.HakenHakenHakenHaken))))和考西((((J.Koch)J.Koch)J.Koch)J.Koch)利用电子计算机的帮助得以解决。他们证明了4444种颜 色足以对任何地图着色。在这一节,不是只考虑那些由地图产生出来的图,而 是所有的图。讨论在至多使用mmmm种颜色的情况下,可对一给定的图着色的所有不 同方法。 6.4 6.4 6.4 6.4 图的着色图的着色图的着色图的着色 假定用图的邻接矩阵Graph(1:nGraph(1:nGraph(1:nGraph(1:n,1:n)1:n)1:n)1:n)来表示一个图GGGG,其中若((((i,j)i,j)i,j)i,j)是GGGG的一 条边,则Graph(i,j)=trueGraph(i,j)=trueGraph(i,j)=trueGraph(i,j)=true,否则Graph(i,j)=falseGraph(i,j)=falseGraph(i,j)=falseGraph(i,j)=false。因为要拟制的算法只关心一条 边是否存在,所以使用布尔值。颜色用整数1,2,1,2,1,2,1,2,…………,,,,mmmm表示,解则用n-n-n-n-元组 ((((X(1),X(1),X(1),X(1),…………,X(n)),X(n)),X(n)),X(n))来给出,其中X(i)X(i)X(i)X(i)是结点iiii的颜色。使用和算法6.26.26.26.2相同的递归回溯 表示,得到算法MColoringMColoringMColoringMColoring。此算法使用的基本状态空间树是一棵度数为mmmm,高 为n+1n+1n+1n+1的树。在iiii级上的每一个结点有mmmm个孩子,它们与X(i)X(i)X(i)X(i)的mmmm种可能的赋值相 对应,1111≤≤≤≤iiii≤≤≤≤nnnn。在n+1n+1n+1n+1级上的结点都是叶结点。图6.116.116.116.11给出了n=3n=3n=3n=3且m=3m=3m=3m=3时的状态 空间树。 图6.10 一幅地图和它的平面图表示 1 2 4 5 3 1 2 3 4 5 图6.11 当n=3,m=3时MColoring的状态空间树 算法6.76.76.76.7 找一个图的所有m-m-m-m-着色方案 voidvoidvoidvoid MColoring MColoring MColoring MColoring((((intintintint k) k) k) k) // // // //这是图着色的一个递归回溯算法。图GGGG用它的布尔邻接矩阵Graph(1:nGraph(1:nGraph(1:nGraph(1:n,1:n)1:n)1:n)1:n) // // // //表示。它计算并打印出符合以下要求的全部解,把整数1,2,1,2,1,2,1,2,…………,,,,mmmm分配给图中 // // // //各个结点且使相邻近的结点的有不同的整数。kkkk是下一个要着色结点的下标。 intintintint m m m m,nnnn,X[n]X[n]X[n]X[n];boolboolboolbool Graph[n][n] Graph[n][n] Graph[n][n] Graph[n][n]; // m // m // m // m、nnnn、X[n]X[n]X[n]X[n]定义成全局变量 intintintint k k k k; while(1) { // while(1) { // while(1) { // while(1) { //产生对X(k)X(k)X(k)X(k)所有的合法赋值。 NextValueNextValueNextValueNextValue(k)(k)(k)(k); // // // //将一种合法的颜色分配给X[k] X[k] X[k] X[k] if(X[k]==0) { break if(X[k]==0) { break if(X[k]==0) { break if(X[k]==0) { break;} //} //} //} //没有可用的颜色了 if(k==n) {print(X)if(k==n) {print(X)if(k==n) {print(X)if(k==n) {print(X);} //} //} //} //至多用了mmmm种颜色分配给nnnn个结点 else {else {else {else {MColoringMColoringMColoringMColoring(k+1)(k+1)(k+1)(k+1); // // // //所有m-m-m-m-着色方案均在此反复递归调用中产生 } } } };////////whilewhilewhilewhile }//}//}//}//McoloringMcoloringMcoloringMcoloring 在最初调用McoloringMcoloringMcoloringMcoloring(1)(1)(1)(1)之前,应对图的邻接矩阵置初值并对数组XXXX置 0000值。 在确定了X(1)X(1)X(1)X(1)到X(k-1)X(k-1)X(k-1)X(k-1)的颜色之后,过程NextValueNextValueNextValueNextValue从这mmmm种颜色中挑选 一种符合要求的颜色,并把它分配给X(k)X(k)X(k)X(k),若无可用的颜色,则返回X(k)=0X(k)=0X(k)=0X(k)=0。 算法6.86.86.86.8 生成下一种颜色 voidvoidvoidvoid NextValue NextValue NextValue NextValue(k) {(k) {(k) {(k) { // // // //进入此过程前X(1),X(1),X(1),X(1),…………,X(k-1),X(k-1),X(k-1),X(k-1)已分得了区域[[[[l,m]l,m]l,m]l,m]中的整数且相邻近的结 // // // //点有不同的整数。本过程在区域[0,[0,[0,[0,m]m]m]m]中给X(k)X(k)X(k)X(k)确定一个值:如果还剩下 // // // //一些颜色,它们与结点kkkk邻接的结点分配的颜色不同,那末就将其中最高 // // // //标值的颜色分配给结点kkkk;如果没剩下可用的颜色,则置 X(k)X(k)X(k)X(k)为0000。 intintintint m m m m,nnnn,X[n]X[n]X[n]X[n];boolboolboolbool Graph[n][n] Graph[n][n] Graph[n][n] Graph[n][n];// m// m// m// m、nnnn、X[n]X[n]X[n]X[n]定义成全局变量 int int int int jjjj,kkkk; while(1) { while(1) { while(1) { while(1) { X[k]=(X[k]+l) mod (m+l) X[k]=(X[k]+l) mod (m+l) X[k]=(X[k]+l) mod (m+l) X[k]=(X[k]+l) mod (m+l); // // // //试验下一个最高标值的颜色 if(X[k]==0) { returnif(X[k]==0) { returnif(X[k]==0) { returnif(X[k]==0) { return;} } } } // // // //全部颜色用完 for(j=1for(j=1for(j=1for(j=1;j<=nj<=nj<=nj<=n;++j) { //++j) { //++j) { //++j) { //检查此颜色是否与邻近结点的颜色不同 if(Graph[k][j] and X[k]==X[j]) breakif(Graph[k][j] and X[k]==X[j]) breakif(Graph[k][j] and X[k]==X[j]) breakif(Graph[k][j] and X[k]==X[j]) break; // // // //如果((((k,j)k,j)k,j)k,j)是一条边,并且邻近的结点有相同的颜色 }// }// }// }//forforforfor if(j==n+l) return if(j==n+l) return if(j==n+l) return if(j==n+l) return; // // // //找到一种新颜色 } } } }////////while //while //while //while //否则试着找另一种颜色//////// }// }// }// }// NextValueNextValueNextValueNextValue n-1n-1n-1n-1 算法6.76.76.76.7的计算时间上界可以由状态空间树的内部结点数∑mmmmiiii得到。在 i=0i=0i=0i=0 每个内部结点处,为了确定它的子结点所对应的合法着色,由NextValueNextValueNextValueNextValue所花费 的时间是ОООО(mn(mn(mn(mn))))。因此,总的时间由∑mmmmiiiin=n(mn=n(mn=n(mn=n(mn+1n+1n+1n+1-m)/(m-1)-m)/(m-1)-m)/(m-1)-m)/(m-1)=ОООО(n(n(n(n****mmmmnnnn))))所限界。 图6.126.126.126.12给出了一个包含四个结点的简单图。下面是一棵由过程MColoringMColoringMColoringMColoring生 成的树。到叶子结点的每一条路径表示一种至多使用3333种颜色的着色法。 注意:正好用3种颜色的解只有12种。 图6.12 一个4结点图和所有可能的3着色 设G=(V,E)G=(V,E)G=(V,E)G=(V,E)是一个nnnn结点的连通图。一个哈密顿环是一条沿着图GGGG的nnnn条边环行 的路径,它访问每个结点一次并且返回到它的开始位置。换言之,如果一个哈 密顿环在某个结点vvvv1111∈VVVV处开始,且GGGG中结点按照vvvv1111,v,v,v,v2222,,,,…………,,,,VVVVn+ln+ln+ln+l的次序被访问,则 边(V(V(V(Viiii,V,V,V,Vi+1i+1i+1i+1)))),1111≤≤≤≤iiii≤≤≤≤nnnn,均在图GGGG中,且除了vvvv1111和vvvvnnnn+l+l+l+l是同一个结点外,其余的结点 均各不相同。 6.5 6.5 6.5 6.5 哈密顿环哈密顿环哈密顿环哈密顿环 1 2 6 3 4 578 1 2 3 4 5 G1G1G1G1 G2G2G2G2 图6.13 包含哈密顿环与不包含哈密顿环的例图 在图6.136.136.136.13中,图G1G1G1G1含有一个哈密顿环1,2,8,7,6,5,4,3,l1,2,8,7,6,5,4,3,l1,2,8,7,6,5,4,3,l1,2,8,7,6,5,4,3,l。图GGGG2222不包含哈密顿环。 似乎没有一种容易的方法能确定一个已知图是否包含哈密顿环。本节考察找 一个图中所有哈密顿环的回溯算法。这个图可以是有向图也可以是无向图。只 有不同的环才会被输出。 用向量(x(x(x(x1111, , , , …………, , , , xxxxnnnn))))表示用回溯法求得的解,其中xxxxiiii是找到的环中第iiii个被访问的 结点。如果已选定xxxx1111, , , , …………, x, x, x, xk-1k-1k-1k-1,那么下一步要做的工作是如何找出可能作XXXXkkkk的结 点集合。若k=lk=lk=lk=l,则X(1)X(1)X(1)X(1)可以是这nnnn个结点中的任一结点,但为了避免将同一个环 重复打印nnnn次,可事先指定X(l)=1X(l)=1X(l)=1X(l)=1。若1111<kkkk<nnnn,则X(k)X(k)X(k)X(k)可以是不同于X(1), X(1), X(1), X(1), X(2), X(2), X(2), X(2), …………, X(k-1), X(k-1), X(k-1), X(k-1)且和X(k-1)X(k-1)X(k-1)X(k-1)有边相连的任一结点vvvv。X(n)X(n)X(n)X(n)只能是唯一剩下的且必 须与X(n-1)X(n-1)X(n-1)X(n-1)和X(1)X(1)X(1)X(1)皆有边相连的结点。 过程NextValueNextValueNextValueNextValue给出了在求哈密顿环的过程中如何找下一个结点的算法。 算法6.96.96.96.9 生成下一个结点 void void void void NextValue(intNextValue(intNextValue(intNextValue(int k) { k) { k) { k) { //X(l), //X(l), //X(l), //X(l),…………,X(k-1),X(k-1),X(k-1),X(k-1)是一条有k-lk-lk-lk-l个不同结点的路径。若X(k)=0X(k)=0X(k)=0X(k)=0,则表示再无结 ////////点可分配给X(k)X(k)X(k)X(k)。若还有与X(1),X(1),X(1),X(1),…………,X(k-1),X(k-1),X(k-1),X(k-1)不同且与X(k-1)X(k-1)X(k-1)X(k-1)有边相连结的 ////////结点则将其中标数最高的结点置于X(k)X(k)X(k)X(k)。若k=nk=nk=nk=n,则还需与X(1)X(1)X(1)X(1)相连结。 intintintint n n n n,X[n]X[n]X[n]X[n];boolboolboolbool Graph[n][n] Graph[n][n] Graph[n][n] Graph[n][n]; // n// n// n// n、X[n]X[n]X[n]X[n]定义成全局变量 intintintint k k k k,jjjj; while(1) {while(1) {while(1) {while(1) { X[k]=(x[k] + 1) mod (m+1) // X[k]=(x[k] + 1) mod (m+1) // X[k]=(x[k] + 1) mod (m+1) // X[k]=(x[k] + 1) mod (m+1) //下一个结点 if(X[k]==0) { returnif(X[k]==0) { returnif(X[k]==0) { returnif(X[k]==0) { return;}}}} if(Graph[X[k-1]],X[k]) { // if(Graph[X[k-1]],X[k]) { // if(Graph[X[k-1]],X[k]) { // if(Graph[X[k-1]],X[k]) { //有边相连吗 for(j=1for(j=1for(j=1for(j=1;j<=k-1j<=k-1j<=k-1j<=k-1;++j) { //++j) { //++j) { //++j) { //检查与前k-1k-1k-1k-1个结点是否相同 if(X[j]==X[k]) breakif(X[j]==X[k]) breakif(X[j]==X[k]) breakif(X[j]==X[k]) break; ////////有相同结点,出此循环 }}}};//for//for//for//for if(j==k) { // if(j==k) { // if(j==k) { // if(j==k) { //若为真,则是一个不同结点 if((kn) {7 if(k>n) {7 if(k>n) {7 if(k>n) {fpfpfpfp=cp=cp=cp=cp;fwfwfwfw====cwcwcwcw;k=nk=nk=nk=n;X=YX=YX=YX=Y;} //} //} //} //修改解 8 else {Y[k]=08 else {Y[k]=08 else {Y[k]=08 else {Y[k]=0;} //} //} //} //超出MMMM,物品KKKK不适合 9 //if9 //if9 //if9 //if 10 while 10 while 10 while 10 while Bound(cp,cw,k,MBound(cp,cw,k,MBound(cp,cw,k,MBound(cp,cw,k,M)<=)<=)<=)<=fpfpfpfp) { //) { //) { //) { //上面置了fpfpfpfp后,Bound=Bound=Bound=Bound=fpfpfpfp 11 while((k<>0) and Y[k]<>1) {11 while((k<>0) and Y[k]<>1) {11 while((k<>0) and Y[k]<>1) {11 while((k<>0) and Y[k]<>1) { 12 --k12 --k12 --k12 --k; ////////找最后放入背包的物品 13 }13 }13 }13 }; 14 if(k==0) return14 if(k==0) return14 if(k==0) return14 if(k==0) return; ////////算法在此处结束 15 Y[k]=015 Y[k]=015 Y[k]=015 Y[k]=0;cwcwcwcw-=W[k]-=W[k]-=W[k]-=W[k];cp-=P[k] //cp-=P[k] //cp-=P[k] //cp-=P[k] //移掉第kkkk项 16 }16 }16 }16 };//while//while//while//while 17 ++k17 ++k17 ++k17 ++k; 18 }18 }18 }18 };//while//while//while//while 19 }//Bknap119 }//Bknap119 }//Bknap119 }//Bknap1 n 当fpfpfpfp≠-l-l-l-l时,X(i)(1X(i)(1X(i)(1X(i)(1≤iiii≤n)n)n)n)是这样的一些元素,它们使得∑P(i)X(iP(i)X(iP(i)X(iP(i)X(i)=)=)=)=fpfpfpfp。 i=1 在4444~6666行的whilewhilewhilewhile循环中,回溯算法作一连串到可行左孩子的移动。 k-1 k-1 k-1 k-1 k-1k-1k-1k-1 Y(i)(1Y(i)(1Y(i)(1Y(i)(1≤iiii≤k)k)k)k)是到当前结点的路经。cwcwcwcw====∑W(i)Y(i)W(i)Y(i)W(i)Y(i)W(i)Y(i)且cp=cp=cp=cp=∑P(i)Y(iP(i)Y(iP(i)Y(iP(i)Y(i))))。在第7777行, i=1 i=1 i=1 i=1 i=1 i=1 i=1 i=1 如果kkkk>nnnn,则必有cpcpcpcp>fpfpfpfp,因为若cpcpcpcp<fpfpfpfp,则在上一次使用限界函数时((((第10101010~ 16161616行))))就会终止到此叶结点的路径。如果kkkk≤nnnn,则W(k)W(k)W(k)W(k)不适合,从而必须作一次 到右孩子的移动。所以在第8888行,Y(k)Y(k)Y(k)Y(k)被置成0000。如果在第10101010行中BoundBoundBoundBound≤fpfpfpfp,由 于现今的这条路径不能导出比迄今所找到的最好解还要好的解,因此该路径可 终止。在第11111111~13131313行,沿着到最近结点的路径回溯,而由这最近结果可以作迄 今尚未试验过的移动。如果不存在这样的结点,则算法在第14141414行终止。反之, Y(k)Y(k)Y(k)Y(k),cwcwcwcw和cpcpcpcp则对应于一次右孩子移动作出相应的修改。计算这个新结点的界。 继续倒转去处理第10101010~16161616行,一直到作出有可能得到一个大于fpfpfpfp值的解的右孩 子结点为止。否则fpfpfpfp就是背包问题的最优效益值。 注意,第10101010行的限界函数并不是固定的,这是因为当检索这棵树的更多结点 时,fpfpfpfp就改变。因此限界函数动态地被强化。 例6.76.76.76.7 考虑以下情况的背包问题:M:M:M:M=110110110110,n=8n=8n=8n=8,P=(11,21,31,33,43,53,55,65)P=(11,21,31,33,43,53,55,65)P=(11,21,31,33,43,53,55,65)P=(11,21,31,33,43,53,55,65), W=(1,11,21,23,33,43,45,55)W=(1,11,21,23,33,43,45,55)W=(1,11,21,23,33,43,45,55)W=(1,11,21,23,33,43,45,55) 。 图6.146.146.146.14显示了对向量YYYY作出各种选择的情况下所生成的树,这棵树的第iiii级对 应于将1111或0000赋值给Y(i)Y(i)Y(i)Y(i),表示或者含有重量W(i)W(i)W(i)W(i)或者拒绝接纳重量W(i)W(i)W(i)W(i)。一个结 点内所载的两个数是重量((((cwcwcwcw))))和效益(cp)(cp)(cp)(cp),给出了该结点的下一级的两个赋值。 注意,若结点不含有重量和效益值,则表示此两类值与它们父亲的相同。每个 右孩子以及根结点外面的数是对应于那个结点的上界。左孩子的上界与它父亲 的相同。算法6.126.126.126.12的变量fpfpfpfp在结点 AAAA、BBBB、CCCC和DDDD的每一处被修改。每次修改fpfpfpfp 时也修改XXXX。终止时,fpfpfpfp=159=159=159=159和 X=(l,1,1,0,1,1,0,0)X=(l,1,1,0,1,1,0,0)X=(l,1,1,0,1,1,0,0)X=(l,1,1,0,1,1,0,0)。在状态空间树的22229999-1=511-1=511-1=511-1=511个 结点中只生成了其中的35353535个结点。注意到由于所有的P(i)P(i)P(i)P(i)都是整数而且所有可行 解的值也是整数,因此[Bund(p,w,k,M)][Bund(p,w,k,M)][Bund(p,w,k,M)][Bund(p,w,k,M)]是一个更好的限界函数,使用此限界函 数结点EEEE和FFFF就不必再扩展,从而生成的结点数可减少到 28282828。 每次在第10101010行调用BoundBoundBoundBound时,在过程BoundBoundBoundBound中基本上重复了第4444~6666行的循环, 因此可对算法Bknap1Bknap1Bknap1Bknap1作进一步的改进。为了取消Bknap1Bknap1Bknap1Bknap1第4444~6666行所做的工作, 需要把BoundBoundBoundBound改变成一个具有边界效应的函数。这两个新算法Bound1Bound1Bound1Bound1和 Bkknap2Bkknap2Bkknap2Bkknap2以算法6.136.136.136.13和6.146.146.146.14的形式写出。这两个算法与算法6.116.116.116.11和6.126.126.126.12中同名的 变量含意完全一样。 算法6.136.136.136.13 有边界效应的限界函数 void Bound1(pvoid Bound1(pvoid Bound1(pvoid Bound1(p,wwww,kkkk,MMMM,pppppppp,wwwwwwww,i)i)i)i) // // // //新近移到的左孩子所对应的效益为 pppppppp,重量为wwwwwwww。 ////////iiii是上一个不适合的物品。 ////////如果所有物品都试验过了,则iiii的值是n+l.n+l.n+l.n+l. intintintint n n n n,P[n]P[n]P[n]P[n],W[n]W[n]W[n]W[n],Y[n]Y[n]Y[n]Y[n]; ////////定义成全局变量 intintintint k k k k,,,,llll;float pfloat pfloat pfloat p,wwww,pppppppp,wwwwwwww,MMMM,bbbb; pp=ppp=ppp=ppp=p;wwwwwwww=w=w=w=w; for(i=k+1for(i=k+1for(i=k+1for(i=k+1;i<=ni<=ni<=ni<=n;++i) {++i) {++i) {++i) { if((ww+W[iif((ww+W[iif((ww+W[iif((ww+W[i])<=M) {])<=M) {])<=M) {])<=M) {wwwwwwww+=W[i]+=W[i]+=W[i]+=W[i];pp+=P[i]pp+=P[i]pp+=P[i]pp+=P[i];Y[i]=1Y[i]=1Y[i]=1Y[i]=1;}}}} else return else return else return else return pp+(M-wwpp+(M-wwpp+(M-wwpp+(M-ww))))****P[i]/W[i]P[i]/W[i]P[i]/W[i]P[i]/W[i]; }}}};//for//for//for//for return pp return pp return pp return pp; }//Bound1}//Bound1}//Bound1}//Bound1 图6.14 算法6,12所生成的树 算法6.146.146.146.14 改进的背包算法 void Bknap2(M, n, W, P, void Bknap2(M, n, W, P, void Bknap2(M, n, W, P, void Bknap2(M, n, W, P, fwfwfwfw, , , , fpfpfpfp, X) {, X) {, X) {, X) { // // // //与Bknap1Bknap1Bknap1Bknap1同 intintintint n, k, n, k, n, k, n, k, Y[nY[nY[nY[n], i, j, ], i, j, ], i, j, ], i, j, X[nX[nX[nX[n]]]]; float float float float W[nW[nW[nW[n], ], ], ], P[nP[nP[nP[n], M, ], M, ], M, ], M, fwfwfwfw, , , , fpfpfpfp, pp, , pp, , pp, , pp, wwwwwwww, , , , cwcwcwcw, cp, cp, cp, cp; cwcwcwcw=cp=k=0=cp=k=0=cp=k=0=cp=k=0;fpfpfpfp=1=1=1=1; while(1)while(1)while(1)while(1) while (Bound1(cp, while (Bound1(cp, while (Bound1(cp, while (Bound1(cp, cwcwcwcw, k, M, pp, , k, M, pp, , k, M, pp, , k, M, pp, wwwwwwww, , , , j)j)j)j)≤≤≤≤fpfpfpfp) {) {) {) { while((k<>0) and Y[k]<>1)) { while((k<>0) and Y[k]<>1)) { while((k<>0) and Y[k]<>1)) { while((k<>0) and Y[k]<>1)) { --k --k --k --k; }}}};//while//while//while//while if(kif(kif(kif(k==0) { return==0) { return==0) { return==0) { return;}}}} Y[k]=0 Y[k]=0 Y[k]=0 Y[k]=0;cwcwcwcw+=W[k] +=W[k] +=W[k] +=W[k] ;cp+=P[k]cp+=P[k]cp+=P[k]cp+=P[k]; }}}};//while//while//while//while cp=pp cp=pp cp=pp cp=pp;cwcwcwcw====wwwwwwww;k=jk=jk=jk=j; ////////等价于Bknap1Bknap1Bknap1Bknap1中4444~6666行的循环语句 if(k>n) { if(k>n) { if(k>n) { if(k>n) { fpfpfpfp=cp=cp=cp=cp;fwfwfwfw====cwcwcwcw;k=nk=nk=nk=n;X=YX=YX=YX=Y;}}}} else {Y[k]=0 else {Y[k]=0 else {Y[k]=0 else {Y[k]=0;}}}} } } } };//while//while//while//while }//Bknap2}//Bknap2}//Bknap2}//Bknap2 到目前为止,所讨论的都是在静态状态空间树环境下工作的回溯算法,现在 讨论如何利用动态状态空间树来设计背包问题的回溯算法。下面介绍的一种回 溯算法的核心思想是以第三讲中贪心算法所得的贪心解为基础来动态地划分解 空间,并且力图去得到0/10/10/10/1背包问题的最优解。首先用约束条件0000≤xxxxiiii≤1111来代换 xxxxiiii=0=0=0=0或1111的整数约束条件,于是得到一个放宽了条件的背包问题。 Max Max Max Max ∑ppppiiiixxxxiiii 1 1 1 1≤iiii≤nnnn 约束条件 ∑wwwwiiiixxxxiiii≤MMMM (6.3) 0 (6.3) 0 (6.3) 0 (6.3) 0≤xxxxiiii≤1111,1111≤iiii≤nnnn 1 1 1 1 ≤iiii≤nnnn 用贪心方法解(6.3)(6.3)(6.3)(6.3)式这个背包问题,如果所得贪心解的所有xxxxiiii都等于0000或1111,显 然这个解也是相应0/10/10/10/1背包问题的最优解。如若不然则必有某xxxxiiii使得0000<xxxxiiii<1111。利 用这个xxxxiiii把(6.2)(6.2)(6.2)(6.2)式的解空间划分成两个子空间,使xxxxiiii=0=0=0=0在一个子空间,xxxxiiii=l=l=l=l在另 一子空间。于是表示此解空间的状态空间树的左子树是以xxxxiiii=0=0=0=0为根的左分枝的子 树,右子树是以xxxxiiii=1=1=1=1为右分枝的子树。 一般说来,在状态空间树的每个结点ZZZZ处用贪心方法求解(6.3)(6.3)(6.3)(6.3)是在有附加限制 的情况下进行的,这附加限制是已对由根到结点ZZZZ的那条路径进行了赋值。换言 之,是在给出了根到结点ZZZZ那条路径各边所对应的那些xxxxiiii的值((((它们各自取0000或1111值)))) 之后,再用贪心方法求解(6.3)(6.3)(6.3)(6.3)。如果贪心解全是0000或1111,则找到了此情况下的最 优解。如若不然,则必有一个xxxxiiii使得0000<xxxxiiii<1111。那么取xxxxiiii=0=0=0=0为结点ZZZZ的左分枝, xxxxiiii=1=1=1=1为ZZZZ的右分枝。这种划分(6.2)(6.2)(6.2)(6.2)式解空间方法之所以合理是因为在贪心解含有 非整数xxxxiiii的情况下不仅防止了把这个贪心解作为0/10/10/10/1背包问题的可行解,并且通 过强迫这个xxxxiiii 取0000或1111值,可以快速得到0/10/10/10/1背包问题的一个可行的贪心解。 由于贪心算法要求ppppjjjj/w/w/w/wjjjj≤ppppj+1j+1j+1j+1/w/w/w/wj+lj+lj+lj+l,因此可以预料大多数标记数较小的物品((((即 代表某物品的jjjj值小,因此密度高))))在最优装法中会装入背包。当xxxxiiii置为0000时,不需 防止贪心算法装入任何jjjj<iiii的物品((((除非xxxxiiii已被置成了0)0)0)0),但当xxxxiiii置为1111时,可能有 某些jjjj<iiii的物品将不能放入背包,因此预计可能在xxxxiiii=0000的情况下达到最优解。以 上分析促使我们在设计回溯算法时考虑先试验xxxxiiii=0=0=0=0的方案,于是在构造动态状态 空间树时就应选取左分枝与 xxxxiiii=0=0=0=0相对应,右分枝与xxxxiiii=1=1=1=1对应。 例6.86.86.86.8 利用例6.76.76.76.7的数据试验上述动态划分下回溯算法的执行。 与根结点对应的贪心解,即(6.3)(6.3)(6.3)(6.3)式的贪心解是: X=(1,1,1,l,1,1,21/43,0,0): X=(1,1,1,l,1,1,21/43,0,0): X=(1,1,1,l,1,1,21/43,0,0): X=(1,1,1,l,1,1,21/43,0,0),效益 值为164.88164.88164.88164.88。根结点的两棵子树分别对应于xxxx6666=0=0=0=0和xxxx6666=0 (=0 (=0 (=0 (见图6.15)6.15)6.15)6.15)。 结点2222处的贪心解为X=(1,l,l,l,1,0,21/45,0)X=(1,l,l,l,1,0,21/45,0)X=(1,l,l,l,1,0,21/45,0)X=(1,l,l,l,1,0,21/45,0),效益值为164.66164.66164.66164.66。结点2222处的解空间 用xxxx7777=0=0=0=0和xxxx7777=1=1=1=1来划分。下一个E_E_E_E_结点是结点3333,此处的解有xxxx8888=21/55=21/55=21/55=21/55。此时划分 具有xxxx8888=0=0=0=0和xxxx8888=1=1=1=1。 在结点4444处的解全是整数,因此没有必要进一步扩展该结点。至此,这个0/10/10/10/1背 包问题所找到的最好解是X=(lX=(lX=(lX=(l,,,,l,l,l,l,0,0,0)l,l,l,l,0,0,0)l,l,l,l,0,0,0)l,l,l,l,0,0,0),效益值为139139139139。 结点5555是下一个E_E_E_E_结点,这个结点的贪心解是: X=(l,l,1,22/23,0,0,0,l): X=(l,l,1,22/23,0,0,0,l): X=(l,l,1,22/23,0,0,0,l): X=(l,l,1,22/23,0,0,0,l),效益值 为159.56159.56159.56159.56。这个划分现在具有xxxx4444=0=0=0=0和 xxxx4444=1=1=1=1。 在结点6666处的贪心解是156.66156.66156.66156.66且xxxx5555=2/3=2/3=2/3=2/3。紧接着结点7777就变成为E_E_E_E_结点,此处的 解是(l,1,l,0,0,0,0,1)(l,1,l,0,0,0,0,1)(l,1,l,0,0,0,0,1)(l,1,l,0,0,0,0,1),效益值是128128128128。在这里贪心解全是整数,结点7777不再扩展。 在结点8888处,贪心解是157.71157.71157.71157.71且xxxx3333=4/7=4/7=4/7=4/7。结点9999处的解全是整数且有值140140140140。结 点10101010处的贪心解是(1,0,l,0,1,0,0,1)(1,0,l,0,1,0,0,1)(1,0,l,0,1,0,0,1)(1,0,l,0,1,0,0,1),它的值是150150150150。下一个E_E_E_E_结点是结点11111111。它 的值是159.52159.52159.52159.52且 xxxx3333=20/21=20/21=20/21=20/21。这划分现在是在xxxx3333=0=0=0=0和xxxx3333=1=1=1=1上。 此背包问题回溯处理的剩余部分留作习题。 许多实验数据结果表明,在使用静态树的情况下,运行背包问题的回溯算法 所用的时间一般比用一棵动态树时要少。然而,动态划分方法在求解整数规划 问题上是非常有用的。一般,整数规划的数学表示为: 极小化 Max Max Max Max ∑cccciiiixxxxjjjj 1≤j≤n 约束条件 ∑aaaaijijijijxxxxjjjj≤bbbbiiii (1 (1 (1 (1≤iiii≤m) (6.4)m) (6.4)m) (6.4)m) (6.4) 1 1 1 1 ≤jjjj≤nnnn 且这些xxxxjjjj是非负整数。 如果将(6.4)(6.4)(6.4)(6.4)中的那些xxxxiiii的整数约束条件用约束条件xxxxiiii≥≥≥≥0000来代替,则得到一个至 少有一个解值和(6.4)(6.4)(6.4)(6.4)式最优解的值一样大的线性规划问题。线性规划问题可用 单纯形法求解((((线性规划的单纯形法可在任何一本有关最优化方法的书中找到))))。 如果解不全是整数,则选择一个非整数xxxxiiii划分这个解空间。假定在状态空间树任 一结点 ZZZZ处,与此线性规划相应的最优解中xxxxiiii的值是vvvv,且vvvv不是一个整数。ZZZZ的 左孩子与xxxxiiii≤≤≤≤[v[v[v[v]]]]相对应,而ZZZZ的右孩子则与xxxxiiii≤≤≤≤[v[v[v[v]]]]相对应。由于所导出的状态空间 树可能有无限的深度((((注意::::在由根到结点ZZZZ的路径上,解空间能够在一个xxxxiiii处被 划分多次,这是因为每一个xxxxiiii都可以有任一非负整数值)))),因此,几乎总是用分 枝一限界法来进行检索。 图6.156.156.156.15用例6.76.76.76.7的数据生成的 部分动态状态空间树 作业 • 修改算法6.16.16.16.1和6.26.26.26.2,使它们只求出问题的一个解而不用求出全部解。 • 重新定义过程Place(k)Place(k)Place(k)Place(k),使它的返回值或者是第kkkk个皇后可以放于其上的合法 列号,,,,或者是一个非法值,这样可提高过程N_QueensN_QueensN_QueensN_Queens的效率。按以上策略重 写这两个过程。 • 对于n-n-n-n-皇后问题,可以发现一些解是另一些解的简单反射或旋转的结果。例 如,n=4n=4n=4n=4时右图的两个解在反射意义下是等效的。为了找出所有不等效的 解,此算法只用置X(1)= 2,3,X(1)= 2,3,X(1)= 2,3,X(1)= 2,3,…………,[n/2],[n/2],[n/2],[n/2]。修改过程N_QueensN_QueensN_QueensN_Queens,使其只计算不等 效的解。 1111 1111 2222 2222 3333 3333 4444 4444 习题3 图 4-皇后问题的等效解 4.4.4.4. 对第4444题所得到的n-n-n-n-皇后问题算法在n=8n=8n=8n=8,9999,10101010情况下投入运行,将每个nnnn 值下所找出解的数目列成表。 5.5.5.5. 分派问题一般陈述如下:给nnnn个人分派nnnn件工作,把工作iiii分配给第iiii个人的成 本为cost(i,icost(i,icost(i,icost(i,i))))。设计一个回溯算法,在给每个人分派一件不同工作的情况 下,使得总成本最小。 6.6.6.6. 6. 6. 6. 6. 设W=(5,7,10,12,15,18,20)W=(5,7,10,12,15,18,20)W=(5,7,10,12,15,18,20)W=(5,7,10,12,15,18,20)和M=35M=35M=35M=35,使用过程SumOfSubSumOfSubSumOfSubSumOfSub找出WWWW中使得和数 等于MMMM的全部子集并画出所生成的部分状态空间树。 软件理论与软件工程研究室 蒋波 第七讲 分枝-限界法 7.1 分枝-限界法的一般方法 7.2 0/1背包问题 7.3 货郎担问题 目录 7.1 7.1 7.1 7.1 分枝分枝分枝分枝----限界法的一般方法限界法的一般方法限界法的一般方法限界法的一般方法 在图的检索方法中,BFSBFSBFSBFS和D-D-D-D-检索这两种方法都是对当前E-E-E-E-结点((((正在扩展的 结点))))检测完毕之后,再检测以队或栈结构形式存放在活结点((((已经生成但其子结 点尚未全部生成的结点))))表中的其它结点。将这两种方法一般化后就成为分枝____限 界策略。分枝____限界法是在生成当前E-E-E-E-结点的全部子结点后再生成其它活结点的 子结点,与此同时用限界函数帮助避免生成不包含答案结点子树的状态空间((((根 结点到其它结点的所有路径一起构成了状态空间))))的一种检索方法。在这个总的 原则下,根据对状态空间树中结点检索次序的不同又可将分枝____限界设计策略分 为几种不同的检索方法。其中,类似于BFSBFSBFSBFS的状态空间检索称为 FIFOFIFOFIFOFIFO检索 (First In First Out)(First In First Out)(First In First Out)(First In First Out),它的活结点表采用队列加以存储;类似于 D-D-D-D-检索的状态空 间检索称为 LIFOLIFOLIFOLIFO检索(Last In Fist Out)(Last In Fist Out)(Last In Fist Out)(Last In Fist Out),它的活结点表采用堆栈加以存储。 例1 (4-1 (4-1 (4-1 (4-皇后问题)))) 本例考察用FIFOFIFOFIFOFIFO分枝____限界算法检索4-4-4-4-皇后问题的状态空间 树((((见图7.1)7.1)7.1)7.1)的基本过程。 问题的初始状态只有一个活结点,即结点llll,它表示没有皇后被放在棋盘上。 扩展结点llll,生成子结点2222,18181818,34343434和50505050。这些结点分别表示皇后1111放置在第1111行的 1111,2222,3333,4444列情况下的状态,且为当前的活结点。 图7.1 4-皇后问题解空间的树结构,结点按深度优先检索编号 如果按序扩展这些结点,则下一个E-E-E-E-结点就是结点2222。扩展结点2222后生成结点 3333,8888和13131313。利用限界函数((((同行、列、对角线上只能放置1111个皇后)))),结点3(3(3(3(放在 第二列))))立即被杀死。于是,仅将结点8888和13131313加到活结点队列。结点18181818变成下一个 E-E-E-E-结点,生成结点19(19(19(19(放在第一列)))),24(24(24(24(放在第三列))))和29(29(29(29(放在第四列)))),限界函 数杀死结点19191919和24242424,结点29292929被加到活结点队列。下一个E-E-E-E-结点是34343434,…………。 图7.27.27.27.2显示了由FIFOFIFOFIFOFIFO分枝____限界检索生成图7.17.17.17.1所示的树的一部分。 由限界函数杀死的那些 结点的下方有一个 BBBB字。 结点内的数与图7.17.17.17.1所示的 树的结点内的数对应。结 点外的数字给出了用FIFOFIFOFIFOFIFO 分枝____限界法生成结点的次 序。在到达答案结点31313131 时,仅剩下活结点38(38(38(38(它可 导致另一答案结点39)39)39)39)和54545454。 比较图7.17.17.17.1和图7.27.27.27.2可以看 出,对于这个问题而言回 溯法占优势。 图7.27.27.27.2 由FIFOFIFOFIFOFIFO分枝----限界法生成的4-4-4-4-皇后问题状态空间树 在LIFOLIFOLIFOLIFO和FIFOFIFOFIFOFIFO分枝____限界法中,对下一个E-E-E-E-结点的选择规则相当死板,而且 在某种意义上是““““盲目的””””。这种选择规则对于有可能快速检索到一个答案结点 的结点没有给出任何优先权。因此,在例1111中,尽管在生成结点30303030后,这个结点 显然只要走一步就可到达答案结点31313131,但死板的FIFOFIFOFIFOFIFO规则却要求先扩展已生成 的所有其它活结点((((即结点35353535、51515151、56565656、14)14)14)14)。 对活结点使用一个““““有智力的””””排序函数c(c(c(c(····))))来选取下一个E-E-E-E-结点往往可以加快 到达某答案结点的检索速度。在4-4-4-4-皇后的例子中,如果用一个能使结点30303030比所有 其它活结点取得更高优先级的排序函数,那么结点30303030就会在结点29292929之后成为E-E-E-E- 结点。接着扩展E-E-E-E-结点就生成答案结点31313131。因此,那些剩下的活结点则无需再 变成E-E-E-E-结点。 要给可能导致答案结点的活结点赋以优先级,必然要附加若干计算工作,即 要付出代价。对于任一结点XXXX,要付出的代价可以使用两种标准来度量: ① 在生成一个答案结点之前,子树XXXX需要生成的结点数。 ② 在子树XXXX中离XXXX最近的那个答案结点到XXXX的路径长度。 使用后一种度量,图7.27.27.27.2中树的根结点付出的代价是4(4(4(4(结点31313131在树的第5555级))))。 结点(18(18(18(18和34)34)34)34),(29(29(29(29和35)35)35)35)以及(30(30(30(30和38)38)38)38)的代价分别是3333,2222和1111。所有在2222,3333和4444级 上剩余结点的代价应分别大于3333,2222和llll。 1.1 LC-1.1 LC-1.1 LC-1.1 LC-检索 以这些代价作为选择下一个E-E-E-E-结点的依据,则 E-E-E-E-结点依次为llll,18181818,29292929和30303030。 由结点1111生成结点2222,18181818,34343434,50505050,而由结点18181818则生成19191919,24242424,29292929,由29292929生成 30303030,32323232,最后由30303030生成31313131,等到答案结点。 容易看出:如果使用度量①,则对于每一种分枝____限界算法,总是生成最小数 目的结点;如果使用度量②,则要成为E-E-E-E-结点的结点只是由根到最近的那个答 案结点路径上的那些结点。以后用c(c(c(c(····))))表示““““有智力的””””排序函数,又称为结点成 本函数。它的定义如下:如果XXXX是答案结点,则c(X)c(X)c(X)c(X)是由状态空间树的根结点到 结点XXXX的成本((((即所用的代价,它可以是级数、计算复杂度等))));如果XXXX不是答案 结点且子树XXXX不包含任何答案结点,则c(X)=c(X)=c(X)=c(X)=∞;否则c(X)c(X)c(X)c(X)等于子树XXXX中具有最小 成本的答案结点的成本。但要指出的是,要得到结点成本函数c(c(c(c(····))))所用的计算工 作量与解原问题具有相同的复杂度,这是因为计算一个结点的代价通常要检索 包含一个答案结点的子树XXXX才能确定,而这正是解决此问题所要作的检索工作, 因此要得到精确的成本函数一般是不现实的。在算法中检测活结点的次序通常 根据能大致估计结点成本的函数g(g(g(g(····))))来确定。 设g(X)g(X)g(X)g(X)是由 XXXX到达一个答案结点所需做的附加工作的估计函数。只用g(X)g(X)g(X)g(X)来给 结点排序是否合适呢?只单纯使用函数g(X)g(X)g(X)g(X)并不合适,因为这样会导致算法偏 向于作纵深检查。 为了看出这一点,不妨设结点XXXX是当前的E-E-E-E-结点且它的子结点为YYYY,由于通常 要求g(Y)0) {<>0) {<>0) {<>0) { 20 20 20 20 print(ansprint(ansprint(ansprint(ans)))); 21 21 21 21 ansansansans====Parent(ansParent(ansParent(ansParent(ans)))); 22 }22 }22 }22 }; 23 return23 return23 return23 return; 24 //if 24 //if 24 //if 24 //if 25 25 25 25 DeleteQ(XDeleteQ(XDeleteQ(XDeleteQ(X)))); 26 if(c26 if(c26 if(c26 if(c’’’’(X)=U) { >=U) { >=U) { >=U) { 19 print (19 print (19 print (19 print (‘‘‘‘least cost=least cost=least cost=least cost=’’’’,U)U)U)U); 20 20 20 20 while(answhile(answhile(answhile(ans=0) do =0) do =0) do =0) do 21 21 21 21 print(ansprint(ansprint(ansprint(ans)))); 22 22 22 22 ansansansans====Parent(ansParent(ansParent(ansParent(ans)))); 23 }23 }23 }23 };// // // // 24 return24 return24 return24 return; 25 }//if 25 }//if 25 }//if 25 }//if 26 Least(E)26 Least(E)26 Least(E)26 Least(E); 27 }27 }27 }27 };//while //while //while //while 28 }//LCBB 28 }//LCBB 28 }//LCBB 28 }//LCBB 1.6 1.6 1.6 1.6 效率分析 由以上讨论可知,上、下界函数选择的好坏是决定极小化问题的各种分枝 ____限 界算法效率的主要因素,但它们对算法效率究竟有多大影响呢?这一点正是本 节最后所要讨论的问题。关于上、下界函数的选择一般可提出以下问题: ① 对UUUU选择一个更好的初值是否能减少所生成的结点数? ② 扩展一些cccc’’’’()()()()>UUUU的结点是否能减少所生成的结点数? ③ 假定有两个成本估计函数cccc1111’’’’((((····))))和cccc2222’’’’((((····)))),对于状态空间树的每一个结点XXXX, 若有cccc1111’’’’(X)(X)(X)(X)≤cccc2222’’’’(X)(X)(X)(X)≤c(X)c(X)c(X)c(X),则称cccc2222’’’’((((····))))比cccc1111’’’’((((····))))好。是否用较好的成本估计函数cccc2222’’’’((((····)))) 比用cccc1111’’’’((((····))))生成的结点数要少呢? 对于以上问题,读者可凭直觉立即得出一些““““是””””或““““非””””的答案,但由下述定 理可以看出有的结论可能刚好与你的直觉相反。假定下面出现的分枝____限界算法 均用来求最小成本答案结点,c(X)c(X)c(X)c(X)是XXXX子树中最小成本答案结点的成本。 定理7.47.47.47.4 设 UUUU1111和UUUU2222是状态空间树TTTT中最小成本答案结点的两个初始上界且 UUUU1111=w[j] {c=c-w[j]j++) {if(c>=w[j] {c=c-w[j]j++) {if(c>=w[j] {c=c-w[j]j++) {if(c>=w[j] {c=c-w[j];LBB=LBB+p[j]LBB=LBB+p[j]LBB=LBB+p[j]LBB=LBB+p[j];} } } } } } } } return return return return; }}}};//if //if //if //if c=c-w[i] c=c-w[i] c=c-w[i] c=c-w[i];LBB=LBB+p[i]LBB=LBB+p[i]LBB=LBB+p[i]LBB=LBB+p[i]; };};};}; UBB=LBB UBB=LBB UBB=LBB UBB=LBB; }//}//}//}//LUBoundLUBoundLUBoundLUBound 算法7.7(a)7.7(a)7.7(a)7.7(a) 生成一个新结点 void void void void NewNode(parNewNode(parNewNode(parNewNode(par,levlevlevlev,tttt,capcapcapcap,profprofprofprof,ubububub) { ) { ) { ) { // // // //生成一个新结点iiii并将它加到活结点表 GetNode(iGetNode(iGetNode(iGetNode(i)))); Parent[i]=parParent[i]=parParent[i]=parParent[i]=par;Level[i]=Level[i]=Level[i]=Level[i]=levlevlevlev;TAG[i]=tTAG[i]=tTAG[i]=tTAG[i]=t; CU[i]=capCU[i]=capCU[i]=capCU[i]=cap;PE[i]=PE[i]=PE[i]=PE[i]=profprofprofprof;UB[i]=UB[i]=UB[i]=UB[i]=ubububub; Add(IAdd(IAdd(IAdd(I)))); }// }// }// }//NewNodeNewNodeNewNodeNewNode 算法7.7(b)7.7(b)7.7(b)7.7(b) 打印答案 void Finish(Lvoid Finish(Lvoid Finish(Lvoid Finish(L,ANSANSANSANS,N) {N) {N) {N) { // // // //输出解 float Lfloat Lfloat Lfloat L;intintintint TAG TAG TAG TAG,ParentParentParentParent; //TAG//TAG//TAG//TAG,ParentParentParentParent定义成全局变量 printf(printf(printf(printf(‘‘‘‘ValueValueValueValue of Optimal Filling is of Optimal Filling is of Optimal Filling is of Optimal Filling is’’’’,L)L)L)L); printf(printf(printf(printf(‘‘‘‘ObjectsObjectsObjectsObjects in Knapsack are in Knapsack are in Knapsack are in Knapsack are’’’’) ) ) ) for(jfor(jfor(jfor(j=n=n=n=n;j=1j=1j=1j=1;j--) { j--) { j--) { j--) { if(TAG[ansif(TAG[ansif(TAG[ansif(TAG[ans]==l) ]==l) ]==l) ]==l) printf(jprintf(jprintf(jprintf(j)))); ansansansans====Parent[ansParent[ansParent[ansParent[ans]]]]; }}}} }//Finish }//Finish }//Finish }//Finish LCKnapLCKnapLCKnapLCKnap的参量是pppp,wwww,M,nM,nM,nM,n和εεεε。nnnn是物品数;p(i)p(i)p(i)p(i)和w(i)w(i)w(i)w(i)分别是物品iiii的效益 和重量,物品排列的次序满足p(i)/w(i)p(i)/w(i)p(i)/w(i)p(i)/w(i)≥≥≥≥p(i+1)/w(i+1)p(i+1)/w(i+1)p(i+1)/w(i+1)p(i+1)/w(i+1),1111≤≤≤≤iiii≤≤≤≤nnnn;MMMM是背包容量; 与LCBBLCBBLCBBLCBB一样,此算法也用了一个很小的正常数,εεεε是它的形式参数。算法中局 部量LLLL是在迄今所找到的最好解的值与LUBoundLUBoundLUBoundLUBound算出的最大下界减去εεεε后的值中 取大值。第1111~5555行对可用结点表、活结点表和检索树的根结点置初值。根结点EEEE 是第一个E-E-E-E-结点。第6666~24242424行的循环依次检查所生成的每个活结点。此循环在以 下两种情况下终止,或者不再剩有活结点((((第22222222行)))),或者为了扩展((((即取下一个 E-E-E-E-结点))))而选择的结点EEEE有UB(E)UB(E)UB(E)UB(E)≤≤≤≤L(L(L(L(第24242424行))))。在后一种情况下,终止之所以正确 是由于选作下一个E-E-E-E-结点的结点具有最大UB(E)UB(E)UB(E)UB(E)值,因此其它的任何活结点XXXX都 有UB(X)UB(X)UB(X)UB(X)≤≤≤≤UB(E)UB(E)UB(E)UB(E)≤≤≤≤LLLL,这样的每一个都不可能导致其值比LLLL还大的解。在此循环 中,新的E-E-E-E-结点EEEE得到检查,有两种可能的结果:① 它是一个叶结点 (Level(E)=n+l)(Level(E)=n+l)(Level(E)=n+l)(Level(E)=n+l),则在第9999~11111111行确定它是否是一个答案结点,若是,它还有可能 代表最优解;② 它不是一个叶结点,那么它就会生成两个子结点,左子结点XXXX 对应于xxxxiiii=1=1=1=1,右子结点YYYY对应于xxxxiiii=0=0=0=0,这里i=Level(E)i=Level(E)i=Level(E)i=Level(E)。当且仅当背包有足够的空 间能放下物品iiii,即capcapcapcap≥≥≥≥w(i)w(i)w(i)w(i)时,这个左子结点是可行的,它可能导致一个答案 结点。在左子结点可行的情况下,由LUBoundLUBoundLUBoundLUBound算出它的上界并由此可得 UB(X)=U(E)UB(X)=U(E)UB(X)=U(E)UB(X)=U(E)。因为UB(E)UB(E)UB(E)UB(E)>L(L(L(L(第24242424行))))或者L=UBB-L=UBB-L=UBB-L=UBB-εεεε<UBB(UBB(UBB(UBB(第5555行)))),所以将XXXX加 入活结点表。注意,由于左子结点和EEEE的下界和上界相同,因此不用再计算它的 下、上界。对于EEEE的右子结点YYYY,因为EEEE是可行的,所以它总是可行的。不过它 的下界和上界值可能和EEEE的不同,因此要在第16161616行调用一次LUBoundLUBoundLUBoundLUBound来获取 UB(Y)=UBBUB(Y)=UBBUB(Y)=UBBUB(Y)=UBB。如果UBBUBBUBBUBB≤≤≤≤L,L,L,L,则杀死结点YYYY;反之则在第18181818行将其加入活结点表并 在第19191919行修改LLLL的值。 算法7.8 7.8 7.8 7.8 背包问题的LCLCLCLC分枝----限界算法 line void line void line void line void LCKnap(pLCKnap(pLCKnap(pLCKnap(p,wwww,MMMM,nnnn,εεεε) {) {) {) { // // // //这是0/10/10/10/1背包问题的最小成本分枝----限界算法,它使用大小固定的元组表示。 ////////假设p(1)/w(1)p(1)/w(1)p(1)/w(1)p(1)/w(1)≥≥≥≥p(2)/w(2)p(2)/w(2)p(2)/w(2)p(2)/w(2)≥…≥≥…≥≥…≥≥…≥p(n)/w(n)p(n)/w(n)p(n)/w(n)p(n)/w(n) float float float float p[np[np[np[n], ], ], ], w[nw[nw[nw[n], M, L, LBB, UBB, cap, ], M, L, LBB, UBB, cap, ], M, L, LBB, UBB, cap, ], M, L, LBB, UBB, cap, profprofprofprof; intintintint ansansansans, , , , X,nX,nX,nX,n; 1 Init; //1 Init; //1 Init; //1 Init; //初始化可用结点表及活结点表 2 2 2 2 GetNode(EGetNode(EGetNode(EGetNode(E); //); //); //); //根结点 3 Parent[E]=0; 3 Parent[E]=0; 3 Parent[E]=0; 3 Parent[E]=0; Level[ELevel[ELevel[ELevel[E]=1; CU(E)=M; PE(E)=0;]=1; CU(E)=M; PE(E)=0;]=1; CU(E)=M; PE(E)=0;]=1; CU(E)=M; PE(E)=0; 4 4 4 4 LUBound(pLUBound(pLUBound(pLUBound(p, w, M, n, 0, 1, LBB, UBB), w, M, n, 0, 1, LBB, UBB), w, M, n, 0, 1, LBB, UBB), w, M, n, 0, 1, LBB, UBB) 5 L=LBB-5 L=LBB-5 L=LBB-5 L=LBB-εεεε; UB(E)=UBB;; UB(E)=UBB;; UB(E)=UBB;; UB(E)=UBB; 6 while(1) {6 while(1) {6 while(1) {6 while(1) { 7 i=7 i=7 i=7 i=Level[ELevel[ELevel[ELevel[E]; cap=CU[E]; ]; cap=CU[E]; ]; cap=CU[E]; ]; cap=CU[E]; profprofprofprof=PE[E]=PE[E]=PE[E]=PE[E]; 8 case8 case8 case8 case 9 :i=n+1; //9 :i=n+1; //9 :i=n+1; //9 :i=n+1; //解结点 10 10 10 10 if(profif(profif(profif(prof>=L) {L=>=L) {L=>=L) {L=>=L) {L=profprofprofprof;ansansansans=E=E=E=E; 11 }//if11 }//if11 }//if11 }//if 12 :else //E12 :else //E12 :else //E12 :else //E有两个子结点 13 if(cap>=w[i]) { //13 if(cap>=w[i]) { //13 if(cap>=w[i]) { //13 if(cap>=w[i]) { //左子结点可行 14 14 14 14 NewNode(ENewNode(ENewNode(ENewNode(E, i+1, 1, cap-, i+1, 1, cap-, i+1, 1, cap-, i+1, 1, cap-w[iw[iw[iw[i], ], ], ], prof+p[lprof+p[lprof+p[lprof+p[l], UB[E])], UB[E])], UB[E])], UB[E]); 15 }; 15 }; 15 }; 15 }; // // // //看右子结点是否会活 16 16 16 16 LUBound(pLUBound(pLUBound(pLUBound(p, w, cap, , w, cap, , w, cap, , w, cap, profprofprofprof, n, i+1, LBB, UBB), n, i+1, LBB, UBB), n, i+1, LBB, UBB), n, i+1, LBB, UBB); 17 if(UBB>L) { //17 if(UBB>L) { //17 if(UBB>L) { //17 if(UBB>L) { //右子结点会活 18 18 18 18 NewNode(ENewNode(ENewNode(ENewNode(E,i+1i+1i+1i+1,0000,capcapcapcap,profprofprofprof,UBB)UBB)UBB)UBB); 19 L=max(L19 L=max(L19 L=max(L19 L=max(L,LBB-LBB-LBB-LBB-εεεε)))); 20 }//if 20 }//if 20 }//if 20 }//if 21 //case21 //case21 //case21 //case 22 if(22 if(22 if(22 if(不再有活结点) { exit) { exit) { exit) { exit;//if} //if} //if} //if} 23 Largest(E)23 Largest(E)23 Largest(E)23 Largest(E); ////////下一个E-E-E-E-结点是UBUBUBUB值最大的结点 24 until(UB(E)<=L) { 24 until(UB(E)<=L) { 24 until(UB(E)<=L) { 24 until(UB(E)<=L) { 25 Finish(L25 Finish(L25 Finish(L25 Finish(L,ansansansans,n)n)n)n);} } } } 26 }//26 }//26 }//26 }//LCKnapLCKnapLCKnapLCKnap 2.2 FIFO2.2 FIFO2.2 FIFO2.2 FIFO分枝----限界求解 例7.3(FIFOBB) 7.3(FIFOBB) 7.3(FIFOBB) 7.3(FIFOBB) 背包问题采用(7.1)(7.1)(7.1)(7.1)式表示,考虑用FIFOBB(FIFOBB(FIFOBB(FIFOBB(算法7.3)7.3)7.3)7.3)来求解例 7.27.27.27.2的背包实例,其工作情况如下:最开始根结点((((即图7.117.117.117.11中结点1)1)1)1)是E-E-E-E-结点, 活结点队为空。由于结点1111不是答案结点,因此UUUU置初值为u(1)+u(1)+u(1)+u(1)+εεεε=-32+=-32+=-32+=-32+εεεε。扩展 结点1111,生成它的两个子结点2222和3333并将它们依次加入活结点队,结点2222成为下一 个E-E-E-E-结点,生成它的子结点4444和5555并将它们加入队。结点了成为下一个E-E-E-E-结点, 生成它的子结点6666和7777,结点6666加入队,由于结点7777的cccc’’’’(7)=-30(7)=-30(7)=-30(7)=-30>UUUU,因此立即被杀 死。下次扩展结点4444,生成它的子结点8888和9999并将它们加入队,修改U=U=U=U=uuuu(9)+(9)+(9)+(9)+εεεε=-=-=-=- 38+38+38+38+εεεε。接着要成为E-E-E-E-结点的结点是5555和6666,由于它们的cccc’’’’值均大于UUUU,因此都被杀 死。结点8888是下一个E-E-E-E-结点,生成结点10101010和11111111,结点10101010不可行,于是被杀死;结 点11111111的cccc’’’’(11)=-32(11)=-32(11)=-32(11)=-32>UUUU,因此也被杀死。接着扩展结点9999,当生成结点12121212时将UUUU和 ansansansans的内容分别修改成-38-38-38-38和12121212,结点12121212加入活结点队,生成结点13131313,但cccc’’’’(13)(13)(13)(13)> UUUU,因此13131313立即被杀死。此时队中只剩下一个活结点12121212,结点12121212是叶结点,不可 能有子结点,所以终止检索,输出UUUU值和由结点12121212到根的路径。为了能知道该路 径上XXXX的取值情况,和例7.27.27.27.2一样,各个结点还需附上能反映xxxxiiii取值的信息。 在用FIFOFIFOFIFOFIFO分枝----限界算法处理背包问题时,由于结点的生成和确定其是否变为 E-E-E-E-结点是逐级进行的,因此无需对每个结点专设一个LevelLevelLevelLevel信息段,而只要用标 志““““####””””来标出活结点队中哪些结点属于同一级即可。于是状态空间树中每个结点 可用CUCUCUCU、PEPEPEPE、TAGTAGTAGTAG、UBUBUBUB和ParentParentParentParent这五个信息段构成。过程NNodeNNodeNNodeNNode((((算法7.9)7.9)7.9)7.9)取 一个可用结点并给此结点各信息段置值,然后将其加入活结点队。和LCKnapLCKnapLCKnapLCKnap一 样,通过适当修改该问题的 FIFOFIFOFIFOFIFO分枝----限界算法可将其变换成一个处理极大化 问题的算法,过程FIFOKnapFIFOKnapFIFOKnapFIFOKnap描述了经过修改后的算法。 算法7.97.97.97.9 生成一个新结点 void void void void NNode(parNNode(parNNode(parNNode(par,t capt capt capt cap,profprofprofprof,ubububub) {) {) {) { // // // //生成一个新结点IIII并将它加入活结点队 GetNode(iGetNode(iGetNode(iGetNode(i)))); Parent[i]=parParent[i]=parParent[i]=parParent[i]=par;TAG[i]=tTAG[i]=tTAG[i]=tTAG[i]=t; CU[i]=capCU[i]=capCU[i]=capCU[i]=cap;PE[i]=PE[i]=PE[i]=PE[i]=profprofprofprof;UB[E]=UB[E]=UB[E]=UB[E]=ubububub; AddQ(XAddQ(XAddQ(XAddQ(X)))); }//}//}//}//NnodeNnodeNnodeNnode 在算法FIFOKnapFIFOKnapFIFOKnapFIFOKnap中,LLLL表示最优解值的下界。由于只有生成了n+ln+ln+ln+l级的结点才 可能到达解结点,因此可以不用LCKnapLCKnapLCKnapLCKnap中的εεεε。第3333~6666行对可用结点表、根结 点EEEE、下界LLLL和活结点队置初值。活结点队最初有根结点EEEE和级结束标志’’’’####’’’’。iiii是 级计数器,它的初始值是1111。在算法执行期间iiii的值总是对应于当前E-E-E-E-结点的级数。 在第7777~26262626行whilewhilewhilewhile循环的每一次选代中,取出iiii级上所有的活结点,它们是由第 8888~23232323行的循环从队中逐个被取出的。一旦取出级结束标志,则从第11111111行跳出循 环;否则仅在UB(E)UB(E)UB(E)UB(E)≥LLLL时扩展EEEE,在第13131313~21212121行生成E-E-E-E-结点的左、右子结点, 这部分的代码与过程LCKnapLCKnapLCKnapLCKnap相应部分的代码类似。当控制从whilewhilewhilewhile循环转出 时,活结点队上所剩下的结点都是n+1n+1n+1n+1级上的结点。其中具有最大PEPEPEPE值的结点 是最优解对应的答案结点,它可通过逐个检查这些剩下的活结点来找到。过程 Finish(Finish(Finish(Finish(算法7.7)7.7)7.7)7.7)打印最优解的值和为了得到这个值应装入背包的那些物品。 算法7.107.107.107.10 背包问题的FIFOFIFOFIFOFIFO分枝----限界算法 void void void void FIFOKnap(pFIFOKnap(pFIFOKnap(pFIFOKnap(p,wwww,MMMM,n) {//n) {//n) {//n) {//功能和假设均与 LCKnapLCKnapLCKnapLCKnap相同 1 float p[n]1 float p[n]1 float p[n]1 float p[n],w[n]w[n]w[n]w[n],MMMM,LLLL,LBBLBBLBBLBB,UBBUBBUBBUBB,EEEE,profprofprofprof,capcapcapcap; 2 2 2 2 intintintint ansansansans ,XXXX,nnnn; 3 Init3 Init3 Init3 Init;i=li=li=li=l; 4 4 4 4 LUBound(pLUBound(pLUBound(pLUBound(p,wwww,MMMM,0000,nnnn,llll,LLLL,UBB)UBB)UBB)UBB); 5 NNode(05 NNode(05 NNode(05 NNode(0,0000,MMMM,0000,UBB)UBB)UBB)UBB); ////////根结点 6 6 6 6 AddQAddQAddQAddQ((((‘‘‘‘#’’’’)))); ////////级标志 7 while(i<=n) { //7 while(i<=n) { //7 while(i<=n) { //7 while(i<=n) { //对于iiii级上的所有活结点 8 while(1) 8 while(1) 8 while(1) 8 while(1) 9 9 9 9 DeleteQ(EDeleteQ(EDeleteQ(EDeleteQ(E)))); 10 case 10 case 10 case 10 case 11 11 11 11 :E=E=E=E=’’’’#’’’’:exitexitexitexit; //i//i//i//i级结束,转到24242424行 12 12 12 12 :UB[E]>=LUB[E]>=LUB[E]>=LUB[E]>=L: //E//E//E//E是活结点 13 cap=CU[E]13 cap=CU[E]13 cap=CU[E]13 cap=CU[E];profprofprofprof=PE[E]=PE[E]=PE[E]=PE[E]; 14 if(cap>=w[i]) {//14 if(cap>=w[i]) {//14 if(cap>=w[i]) {//14 if(cap>=w[i]) {//可行左子结点 15 Nnode(E,1,cap-w[i]15 Nnode(E,1,cap-w[i]15 Nnode(E,1,cap-w[i]15 Nnode(E,1,cap-w[i],prof+P[iprof+P[iprof+P[iprof+P[i]]]],UF[E])UF[E])UF[E])UF[E]); 16 }//if 16 }//if 16 }//if 16 }//if 17 17 17 17 LUBound(pLUBound(pLUBound(pLUBound(p,wwww,capcapcapcap,profprofprofprof,nnnn,i+1i+1i+1i+1,LBBLBBLBBLBB,UBB)UBB)UBB)UBB); 18 if(UBB>=L) { //18 if(UBB>=L) { //18 if(UBB>=L) { //18 if(UBB>=L) { //右子结点是活结点 19 19 19 19 NNode(ENNode(ENNode(ENNode(E,0000,capcapcapcap,profprofprofprof,UBB)UBB)UBB)UBB); 20 L=max(L20 L=max(L20 L=max(L20 L=max(L,LBB)LBB)LBB)LBB); 21 }//if 21 }//if 21 }//if 21 }//if 22 }//case 22 }//case 22 }//case 22 }//case 23 } 23 } 23 } 23 } 24 24 24 24 AddQAddQAddQAddQ((((‘‘‘‘#’’’’) //) //) //) //级的末端 25 i=i+125 i=i+125 i=i+125 i=i+1; 26 } 26 } 26 } 26 } 27 27 27 27 ansansansans=PE[X]=L=PE[X]=L=PE[X]=L=PE[X]=L的活结点XXXX; 28 Finish(i28 Finish(i28 Finish(i28 Finish(i,ansansansans,n)n)n)n); 29 }//29 }//29 }//29 }//FIFOKnapFIFOKnapFIFOKnapFIFOKnap 7.3 7.3 7.3 7.3 货郎担问题货郎担问题货郎担问题货郎担问题 前面介绍了货郎担问题的一个动态规划算法,它的计算复杂度为ОООО(n(n(n(n22222222nnnn))))。本 节讨论货郎担问题的分枝----限界算法,它的最坏情况时间虽然也为ОООО(n(n(n(n22222222nnnn)))),但对 于许多具体实例而言,却比动态规划算法所用的时间要少得多。 设G=(V,E)G=(V,E)G=(V,E)G=(V,E)是代表货郎担问题的某个实例的有向图,|V|=n|V|=n|V|=n|V|=n,ccccijijijij表示边的成 本。若∉∉∉∉EEEE,则有ccccijijijij====∞。为不失一般性,假定每一次周游均从结点1111开始并 在结点1111结束,于是解空间SSSS可 表示成:S={l,:S={l,:S={l,:S={l,π,l|,l|,l|,l|π是{2,3,{2,3,{2,3,{2,3,…………,n},n},n},n}的一 种排列}}}},|S|=(n-1)|S|=(n-1)|S|=(n-1)|S|=(n-1)!。为减小SSSS的大 小,可将SSSS限制为:只有在0000≤jjjj≤n-ln-ln-ln-l, >>>∈EEEE且iiii0000=i=i=i=innnn=l=l=l=l的情况下, (l,i(l,i(l,i(l,i1111,i,i,i,i2222,,,,…………,i,i,i,in-ln-ln-ln-l))))∈SSSS。可以将这样的SSSS构 造成一棵类似于n-n-n-n-皇后问题的状态 空间树((((见图6.2)6.2)6.2)6.2)。 图7.127.127.127.12给出了|V|=4|V|=4|V|=4|V|=4的一个完全图 的一种状态空间树。树中每个叶结 点LLLL是一个解结点,它代表由根到 LLLL路径所确定的一次周游。 结点14141414表示iiii0000=1,i=1,i=1,i=1,i1111=3,i=3,i=3,i=3,i2222=4,i=4,i=4,i=4,i3333=2=2=2=2和iiii4444=1=1=1=1 的一次周游。 为了用LCLCLCLC分枝----限界法检索货郎担问题的状态空间树,需要定义成本函数 c(c(c(c(····)))),成本估计函数cccc’’’’((((····))))和上界函数u(u(u(u(····)))),使它们对于每个结点XXXX,有 cccc’’’’(X)(X)(X)(X)≤c(X)c(X)c(X)c(X)≤u(X)u(X)u(X)u(X)。c(c(c(c(····))))可以定义为: 由根到XXXX的路径确定的周游路线成本 XXXX是叶结点 子树XXXX中最小成本叶结点的成本 XXXX不是叶结点 在c(c(c(c(····))))作了以上定义的情况下,对于每个结点XXXX均满足cccc’’’’(X)(X)(X)(X)≤c(X)c(X)c(X)c(X)的函数cccc’’’’((((····)))) 可简单地定义成:cccc’’’’(X)(X)(X)(X)是由根到结点XXXX那条路径确定的((((部分))))周游路线的成本。 例如在图7.127.127.127.12的结点6666处所确定的部分周游路线为iiii0000=1,i=1,i=1,i=1,i1111=2,i=2,i=2,i=2,i2222=4=4=4=4。它包含边 和<2,4><2,4><2,4><2,4>。不过一般并不采用以上定义的cccc’’’’((((····)))),而是采用一个更好的cccc’’’’((((····))))。在这 个cccc’’’’((((····))))的定义中使用了图GGGG的归约成本矩阵。下面先给出归约矩阵的定义。如果 矩阵的一行((((列))))至少包含一个零且其余元素均非负,则此行((((列))))称为已归约行 ((((列))))。所有行和列均为已归约行和列的矩阵称为归纳矩阵。可以通过对一行 ((((列)))) 中每个元素都减去同一个常数t(t(t(t(称为约数))))将该行((((列))))变成已归约行((((列))))。逐行逐 列施行归约就可得到原矩阵的归约矩阵。假设第 iiii行的约数为 ttttiiii,第 jjjj列的约数 n n n n n n n n 为rrrrjjjj,1111≤i,ji,ji,ji,j≤nnnn,那么,各行、列的约数之和L=L=L=L=∑ttttiiii++++∑rrrrjjjj称为矩阵约数。作为一 i=1 j=1 i=1 j=1 i=1 j=1 i=1 j=1 个例子,考虑有五个结点的图GGGG,它的成本矩阵CCCC为: : : : C(X)=C(X)=C(X)=C(X)= 因为对GGGG的每次周游只含有由i(1i(1i(1i(1≤iiii≤5)5)5)5)出发的五条边中的一条边,同样也只含有进入j(1j(1j(1j(1≤jjjj≤5)5)5)5)的五条边<1,j><1,j><1,j><1,j>, <2,j><2,j><2,j><2,j>,<3,j><3,j><3,j><3,j>,<4,j><4,j><4,j><4,j>,<5,j><5,j><5,j><5,j>中的一条边,所以在此成本矩阵中,若对iiii行((((或jjjj 列))))施行归约,即将此行((((列))))的每个元素减去该行((((列))))的最小元素t,t,t,t,则此次周游成 本减少tttt。这表明原矩阵中各条周游路线的成本分别是与其归约成本矩阵相应的 周游路线成本与矩阵约数之和,因此矩阵约数LLLL显然是此问题的最小周游成本的 一个下界值,于是可以将它取作状态空间树根结点的cccc’’’’值。对矩阵CCCC的l,2,3, 4,5l,2,3, 4,5l,2,3, 4,5l,2,3, 4,5 行和1,31,31,31,3列施行归约得CCCC的归约成本矩阵CCCC||||,矩阵约数L=25L=25L=25L=25。因此图GGGG的周游路 线成本最少是25252525。 为了定义函数cccc’’’’((((····)))),在货郎担问题的状态空间树中对每个结点都附以一个归 约成本矩阵。设AAAA是结点RRRR的归约成本矩阵,SSSS是RRRR的子结点且树边对应这 条周游路线中的边。在SSSS是非叶结点的情况下,SSSS的归约成本矩阵可按以下 步骤求得:① 为保证这条周游路线采用边而不采用其它由iiii出发或者进人jjjj 的边,将AAAA中iiii行和jjjj列的元素置为∞; ② 为防止采用边((((因为在已选定的路线上加入边之后若再采用边就 会构成一个环从而得不到这条周游路线)))),将A(j,1)A(j,1)A(j,1)A(j,1)置为∞;③对于那些不全为 ∞的行和列施行归约则得到SSSS的归约成本矩阵,令其为BBBB,矩阵约数为rrrr。非叶结 点SSSS的cccc’’’’值可定义为: cccc’’’’(S(S(S(S)=c)=c)=c)=c’’’’(R) + A(i(R) + A(i(R) + A(i(R) + A(i,j) + r (7.4)j) + r (7.4)j) + r (7.4)j) + r (7.4) 如果SSSS是叶结点,由于一个叶结点 确定一条唯一的周游路线,因此可用这 条周游路线的成本作为SSSS的cccc’’’’值,即 cccc’’’’(S)=c(S)(S)=c(S)(S)=c(S)(S)=c(S)。 至于上界函数u(u(u(u(····))))可将其定义为, 对于树中每个结点RRRR,u(R)=u(R)=u(R)=u(R)=∞∞∞∞。 现在用LCLCLCLC分枝----限界算法LCBBLCBBLCBBLCBB求 解(7.2)(7.2)(7.2)(7.2)式货郎担问题实例的最小成本周 游路线。LCBBLCBBLCBBLCBB使用了上面定义的cccc’’’’((((····)))) 和u(u(u(u(····))))。图7.137.137.137.13给出了LCBBLCBBLCBBLCBB所产生的那 一部分状态空间树,结点外的数是该结 点的cccc’’’’值。根结点1111是第一个E-E-E-E-结点, 它的归纳成本矩阵为(7.3)(7.3)(7.3)(7.3)式的矩阵CCCC||||, 此时U=U=U=U=∞∞∞∞。扩展结点1111,依次生成结点 2,3,42,3,42,3,42,3,4和5555。它们对应的归约成本矩阵为:::: 以结点3333为例,它的归约成本矩阵由以下运算得到:先将矩阵CCCC||||的1111行和3333列所 有元素置成∞;再将CCCC||||(3,1)(3,1)(3,1)(3,1)置成∞;然后归约第1111列,将该列的每个元素减去11111111 即得。由(7.4)(7.4)(7.4)(7.4)式得:cccc’’’’(3)=25+17((3)=25+17((3)=25+17((3)=25+17(即CCCC||||(1(1(1(1,3)3)3)3)的值)+11=53)+11=53)+11=53)+11=53。结点2,42,42,42,4和5555的归约成 本矩阵和cccc’’’’值也可类似得到。UUUU的值不变。结点4444变成下一个E-E-E-E-结点,它的子结 点结点6,76,76,76,7和8888被依次生成,与它们对应的归约成本矩阵为: 此时的活结点有2,3,5,6,72,3,5,6,72,3,5,6,72,3,5,6,7和8888,其中cccc’’’’(6)(6)(6)(6)最小,所以结点6666成为下一个E-E-E-E-结点。 扩展结点6666,生成结点9999和10101010,它们的归约成本矩阵为: 结点10101010是下一个E-E-E-E-结点,由它生成结点11111111。结点11111111是一个答案结点,它对应 的周游路线的成本是cccc’’’’(11)=28(11)=28(11)=28(11)=28,UUUU被修改成28282828。下一个E-E-E-E-结点应是结点5555,由于 cccc’’’’(5)=31(5)=31(5)=31(5)=31>28282828,故算法LCBBLCBBLCBBLCBB结束并得出最小成本周游路线是1,4,2,5,3,11,4,2,5,3,11,4,2,5,3,11,4,2,5,3,1。 如果把一条周游路线看成是nnnn条边的集合,则可以得到解的另一种表示形式。 设图G=(V,E)G=(V,E)G=(V,E)G=(V,E)有eeee条边,于是一条周游路线均含有这eeee条边中的nnnn条不同的边。由 此可以将货郎担问题的状态空间树构造成一棵二元树,树中结点的左分枝表示 周游路线中包含一条指定的边,右分枝表示不包含这条边。例如图7.14(b)7.14(b)7.14(b)7.14(b)和(c)(c)(c)(c) 就表示图7.14(a)7.14(a)7.14(a)7.14(a)所示的那个三结点图中的两棵可能的状态空间树的前三级。就 一般情况而言,一个给定的问题可能有多棵不同的状态空间树,它们的差别在 于对图中边的取舍的决策次序不同。图7.14(b)7.14(b)7.14(b)7.14(b)所示的首先确定边<1,3><1,3><1,3><1,3>的取舍, 图7.14(c)7.14(c)7.14(c)7.14(c)所示的则首先确定边<1,2><1,2><1,2><1,2>的取舍。应采用哪种状态空间树进行检索随 货郎担问题的具体实例而定,因此所考虑的是动态状态空间树。 为了根据问题的具体实例构造出便于检索的二元状态空间树,应确定图中边 的取舍次序。如果选取边,则这条边将解空间划分成两个子集合,即在将 要构造出的状态空间树中,根的左子树表示含有边的所有周游,根的右子 树表示不含边的所有周游。此时,如果左子树中包含一条最小成本周游路 线测只需再选取n-ln-ln-ln-l条边;如果所有的最小成本周游路线都在右子树中,则还需 对边作nnnn次选择。由此可知在左子树中找最优解比在右子树中找最优解容易,所 以我们希望选取一条最有可能在最小成本周游路线中的边作为这条““““分割”””” 边。一般所采用的选择规则是:选取一条使其右子树具有最大cccc’’’’值的边。使用这 种选择规则可以尽快得到那些cccc’’’’值大于最小周游成本的右子树。还有一些其它的 选择规则,例如选取使左、右子树cccc’’’’值相差最大的边等等。本节只使用前一种选 择方法。 在二元状态空间树中,根结点的归约成本矩阵由该实例的成本矩阵归约而 得,根的cccc’’’’值是矩阵约数。如果非叶结点SSSS是结点RRRR的左子结点则SSSS的归约成本矩 阵的求取与前面所述步骤相同,它的cccc’’’’值仍由(7.4)(7.4)(7.4)(7.4)式获得。如果非叶结点SSSS是结 点RRRR的右子结点,由于RRRR的右分枝代表不包含边的周游,因此应将RRRR的归约 成本矩阵AAAA中的元素A(i,j)A(i,j)A(i,j)A(i,j)置成∞后,再归约此矩阵不全为∞的行和列((((实际上只 需重新归约第iiii行和第jjjj列)))),即得SSSS的归约成本矩阵BBBB和矩阵约数 △=min=min=min=min{A(i,k)A(i,k)A(i,k)A(i,k)}+ min+ min+ min+ min{A(k,j)A(k,j)A(k,j)A(k,j)}。 kkkk≠jjjjkkkk≠iiii 在计算cccc’’’’(S)(S)(S)(S)时,由于周游路线不包含边,所以 A(i,j)A(i,j)A(i,j)A(i,j)不必加入,即 cccc’’’’(S)=c(S)=c(S)=c(S)=c’’’’(R)+(R)+(R)+(R)+△ (7.5) (7.5) (7.5) (7.5) 如果SSSS是叶结点,与前面一样有cccc’’’’(S)=c(S)(S)=c(S)(S)=c(S)(S)=c(S)。如果选取的边在RRRR的归约矩阵 AAAA中对应的元素 A(i,j)A(i,j)A(i,j)A(i,j)为正数,,,,则该边显然不可能使 RRRR右子树的cccc’’’’值为最大,因为 cccc’’’’(S)=c(S)=c(S)=c(S)=c’’’’(R),(R),(R),(R),所以为了使RRRR右子树的cccc’’’’值最大,应从RRRR的归约成本矩阵元素为0000的对 应边中选取有最大△值的边。 仍以(7.2)(7.2)(7.2)(7.2)式的货郎担问题为例,用算法LCBBLCBBLCBBLCBB在动态二元树上进行检索。开 始根结点1111是E-E-E-E-结点((((见图7.15)7.15)7.15)7.15),cccc’’’’(1)=25(1)=25(1)=25(1)=25,归约矩阵CCCC||||中零元素对应的边为 <1,4><1,4><1,4><1,4>,<2,5><2,5><2,5><2,5>,<3,1><3,1><3,1><3,1>,<3,4><3,4><3,4><3,4>,<4,5><4,5><4,5><4,5>,<5,2><5,2><5,2><5,2>和<5,3><5,3><5,3><5,3>,各边的△值分别为 1,2,11,0,3,31,2,11,0,3,31,2,11,0,3,31,2,11,0,3,3和11111111,因此选取边(3,1)(3,1)(3,1)(3,1)或(5,3)(5,3)(5,3)(5,3)作为““““分割””””边可以使结点 1111右分枝的cccc’’’’ 值最大。假定LCBBLCBBLCBBLCBB选取<3<3<3<3,1>1>1>1>。结点1111生成结点2222和3333,其中cccc’’’’(2)=25(2)=25(2)=25(2)=25, cccc’’’’(3)=36(3)=36(3)=36(3)=36,与它们对应的归约成本矩阵为: 结点2222成为下一个E-E-E-E-结点,边<1,4><1,4><1,4><1,4>,<2,5><2,5><2,5><2,5>,<3,5><3,5><3,5><3,5>,<5,2><5,2><5,2><5,2>和<5,3><5,3><5,3><5,3>的△分别是 3,2,3,33,2,3,33,2,3,33,2,3,3和11111111,选取边<5,3><5,3><5,3><5,3>为““““分割””””边,生成结点 4444和 5555,cccc’’’’(4)=28(4)=28(4)=28(4)=28,cccc’’’’(5)=36(5)=36(5)=36(5)=36,与 它们对应的归约成本矩阵为: 结点 4444成为下一个E-E-E-E-结点,边<1,4><1,4><1,4><1,4>,<2,5><2,5><2,5><2,5>,<4,2><4,2><4,2><4,2>和<4,5><4,5><4,5><4,5>的△分别是 9,2,79,2,79,2,79,2,7和 0000,选取边<1,4><1,4><1,4><1,4>为下一条““““分割””””边,生成结点6666和7777,cccc’’’’(6)=28,c(6)=28,c(6)=28,c(6)=28,c’’’’(7)=37(7)=37(7)=37(7)=37,与它们 对应的归约成本矩阵为: 结点6666是下一个E-E-E-E-结点,此时需求的五条边已求出了三条,即 {<3,1>,<5,3>,<1,4><3,1>,<5,3>,<1,4><3,1>,<5,3>,<1,4><3,1>,<5,3>,<1,4>},再求两条边就可得到一条周游路线,由矩阵(e)(e)(e)(e)可知此时 只剩下两条边{<2,5><2,5><2,5><2,5>,<4,2><4,2><4,2><4,2>},故它们就是这条周游路线所需要的边。至此 LCBBLCBBLCBBLCBB求出了一条成本为28282828的周游路线5,3,1,4,2,55,3,1,4,2,55,3,1,4,2,55,3,1,4,2,5。UUUU被修改成28282828,下一个应成为 E-E-E-E-结点的结点是3333,由于cccc’’’’(3)=36>U(3)=36>U(3)=36>U(3)=36>U,故LCBBLCBBLCBBLCBB结束。 此例对LCBBLCBBLCBBLCBB作了一点修改,即在““““靠近””””解结点时作的处理与““““没靠近””””时的不 同。如在结点6666处,由于距离解结点只有两级,此时就不再采用分枝----限界方法求 结点6666的子结点和孙子结点的cccc’’’’值,而是对结点6666为根的子树中结点逐个检索来找 出答案结点。这种对多结点子树采用分枝----限界法而对结点数少的子树采用完全 检索的处理方法可使算法效率提高一些。这种处理方法对于图7.137.137.137.13同样适用。 关于货郎担问题还可用另外一些分枝____限界方法求解,有兴趣的读者可参阅 E.HorowitzE.HorowitzE.HorowitzE.Horowitz和 S.SahnlS.SahnlS.SahnlS.Sahnl所著的““““Fundamentals of Computer AlgorithmsFundamentals of Computer AlgorithmsFundamentals of Computer AlgorithmsFundamentals of Computer Algorithms””””(1978(1978(1978(1978年)))) 以及S.E.Good-manS.E.Good-manS.E.Good-manS.E.Good-man和S.T.HedetnlemlS.T.HedetnlemlS.T.HedetnlemlS.T.Hedetnleml所著的““““Introduction to the Design and Introduction to the Design and Introduction to the Design and Introduction to the Design and Analysis of AlgorithmsAnalysis of AlgorithmsAnalysis of AlgorithmsAnalysis of Algorithms””””(1977(1977(1977(1977年))))。 作业 1. 1. 1. 1. 证明定理7.17.17.17.1。 2. 2. 2. 2. 写一个用LIFOLIFOLIFOLIFO分枝----限界方法检索某最小成本答案结点的程序概要LIFOBBLIFOBBLIFOBBLIFOBB。 3. 3. 3. 3. 给定一个带有限期的作业排序问题:n=5n=5n=5n=5,(p(p(p(p1111,p,p,p,p2222,,,,…………,p,p,p,p5555)=(6,3,4,8,5))=(6,3,4,8,5))=(6,3,4,8,5))=(6,3,4,8,5),(t(t(t(t1111,t,t,t,t2222, , , , …………, , , , tttt5555))))。(2,l,2,l,1)(2,l,2,l,1)(2,l,2,l,1)(2,l,2,l,1),(d(d(d(d1111,d,d,d,d2222,,,,…………,d,d,d,d5555)=(3,1,4,2,4))=(3,1,4,2,4))=(3,1,4,2,4))=(3,1,4,2,4)。采用7.17.17.17.1节关于作业排序问题的cccc’’’’((((····))))和u(u(u(u(····)))) 的定义,画出FIFOBBFIFOBBFIFOBBFIFOBB,LIFOBBLIFOBBLIFOBBLIFOBB和 LCBBLCBBLCBBLCBB对上述问题所生成的、大小可变的元 组表示的部分状态空间树,求出对应于最优解的罚款值。 4. 4. 4. 4. 使用大小固定的元组表示写一个求解带限期作业排序问题的完整的LCLCLCLC分枝----限 界算法。 5. 5. 5. 5. 证明定理7.47.47.47.4。 6. 6. 6. 6. 证明定理7.67.67.67.6。 7. 7. 7. 7. 在大小可变的元组表示下解算例7.27.27.27.2。 8. 8. 8. 8. 在大小可变的元组表示下解算例7.37.37.37.3。 9. 9. 9. 9. 画出LCKnapLCKnapLCKnapLCKnap对下列背包问题实例所生成的部分状态空间树: ① n=5 n=5 n=5 n=5,(p(p(p(p1111,p,p,p,p2222,,,,…………,p,p,p,p5555)=(10,15,6,8,4))=(10,15,6,8,4))=(10,15,6,8,4))=(10,15,6,8,4),(w(w(w(w1111,w,w,w,w2222,,,,…………,w,w,w,w5555)=(4,6,3,4,2))=(4,6,3,4,2))=(4,6,3,4,2))=(4,6,3,4,2),M=12M=12M=12M=12。 ② n=5 n=5 n=5 n=5,(p(p(p(p1111,p,p,p,p2222,,,,…………,p,p,p,p5555)=(w)=(w)=(w)=(w1111,w,w,w,w2222,,,,…………,w,w,w,w5555)=(4,4,5,8,9))=(4,4,5,8,9))=(4,4,5,8,9))=(4,4,5,8,9),M=15M=15M=15M=15。 10. 10. 10. 10. 用LCLCLCLC分枝----限界法在动态状态空间树上做第9999题。使用大小固定元组表示。 11. 11. 11. 11. 使用大小固定的元组表示下的动态状态空间树,写出背包问题的LCLCLCLC分枝----限 界算法。 12. 12. 12. 12. 已知一货郎担问题的实例由下面成本矩阵所定义: ① 求它的归约成本矩阵。 ② 用与图7.127.127.127.12类似的状态空间树结构和7.37.37.37.3节定义的cccc’’’’((((····))))去获取LCBBLCBBLCBBLCBB所生成的 部分状态空间树。标出每个结点的cccc’’’’值并写出其对应的归约矩阵。 ③ 用7.37.37.37.3节介绍的动态状态空间树方法做第②题。 13. 使用下面的货郎担成本矩阵作第12题: 14. 使用像图7.12那样的静态状态空间树和归约成本矩阵方法写出实现货郎担 问题的LC分枝-限界算法的有效程序。 15. 使用动态状态空间树和归约成本矩阵方法写出实现货郎担问题的LC分枝-限 界算法的有效程序。 16. 对于任何货郎担问题实例,使用静态树的LC分枝-限界算法所生成的结点是 否比使用动态树的LC分枝-限界算法生成的结点少?证明所下的结论。 算法设计与分析练习题 1. 仅使用Ο、Ω、Θ和o的定义,证明下列各式成立。 1) 5n2 – 6n = Θ(n2) 2) n!= Ο(nn) 3) 2n22n + nlogn =Θ(n22n) 4) i2 = Θ(n3) 5) i3 = Θ(n4) 6) + 6 * 2n =Θ 7) n3 + 106n2 =Θ(n3) 8) 6n3/(logn + 1) =Ο(n3) 9) n1.001 + nlogn =Θ(n1.001) 10)nk+ε+ nklogn =Θ(nk+ε),k≥0,ε>0 2. 采用定理 2.2、2.4 和2.6,证明第 1题的所有式子成立。 3. 证明以下等式不成立。 1) 10n2+9=Ο(n) 2) n2logn=Θ(n2) 3) n2/logn =Θ(n2) 4) n32n+6n23n=Ο(n32n) 4. 证明当且仅当 lim f(n)/g(n)=0 时,f(n)=o(g(n))。 5. 下面哪些规则是正确的?为什么? 1) {f(n)=Ο(F(n)),g(n)=Ο(G(n))} → f(n)/g(n)=Ο(F(n)/G(n)) 2) {f(n)=Ο(F(n)),g(n)=Ο(G(n))} → f(n)/g(n)=Ω(F(n)/G(n)) 3) {f(n)=Ο(F(n)),g(n)=Ο(G(n))} → f(n)/g(n)=Θ(F(n)/G(n)) 4) {f(n)=Ω(F(n)),g(n)=Ω(G(n))} → f(n)/g(n)=Ω(F(n)/G(n)) 5) {f(n)=Ω(F(n)),g(n)=Ω(G(n))} → f(n)/g(n)=Ο(F(n)/G(n)) 6) {f(n)=Ω(F(n)),g(n)=Ω(G(n))} → f(n)/g(n)=Θ(F(n)/G(n)) n→∞ (n )2n n2n n i=0∑ n i=0∑ 7) {f(n)=Θ(F(n)),g(n)=Θ(G(n))} → f(n)/g(n)=Θ(F(n)/G(n)) 8) {f(n)=Θ(F(n)),g(n)=Θ(G(n))} → f(n)/g(n)=Ω(F(n)/G(n)) 9) {f(n)=Θ(F(n)),g(n)=Θ(G(n))} → f(n)/g(n)=Ο(F(n)/G(n)) 6. 计算以下函数的渐进复杂性,设计一个频率表加以说明。 1).SelectionSort(a[],int n) 语句 s/e 频率 总步数 Void SelectionSort(elemTyoe a[],int n) { //及时终止的选择排序 bool Sorted = false; for(int size = n;!sorted&&(size>1);size--){ int pos = 0; sorted = true; for(int i=1;i=0 && ta[i+1]) { Swap(a[i],a[i+1]); swapped = true; //发生了交换 }; return swapped; } void BubbleSort(elemTyoe a[],int n) { //及时终止的冒泡排序算法 for(int i = n; i>1 && Bubble(a,i);i--); } 总计 4) 判断下列算法的渐进时间复杂度 Status CharLoopCheck() { InitStack(S);InitQueue(Q); C=getchar(); //读取一个字符。或者从文件上读入;或者从键盘上读入。 While(c!=’@’){ Push(S,c); EnQueue(Q,c); C=getchar(); };//while while(!empty(s)) { pop(s,x); Dequeue(Q,y); if(x!=y) return ‘Error’; };while if(!empty(s) || !empty(Q)) {return ‘Error’;} else return ‘ok’ }// CharLoopCheck 7. 某算法的计算时间可用下面的递推关系式描述。试采用迭代的方式求解 该关系式,并用大写О表示解(要求给出详细的推导过程)。 a n=1,a为常数 T(n)= 2T(n/2) + c*n n>1,c为常数 8. 折半查找过程的判定树上,定义根结点到每个内部结点(查找成功的结点) 的路径长度之和为内部路径长度,记为 I;定义根结点到每个外部结点(查找 不成功的结点)的路径长度之和外部路径长度,记为 E。 试证明:具有 n个内部结点的这样的判定树,满足 E = I + 2n 。 9.已知 f(n)=О(log2n),求证:f(n)=О(logkn),k>1。 10. 根据大写О、Ω、Θ、小写 o的定义,计算给定函数的渐进表示。 请大家熟练掌握大写О、Ω、Θ、小写 o定义,并加以灵活应用。 例如, 则,根据定义有: f(n)= О(?),f(n)= Ω(?)。 11. 阅读下面的算法,并回答给定的问题。 注意,行号是为了说明问题而设置,它不属于算法本身。 void ABC(A,n) { //A(0:n)是一个一维数组,共有 n个可比较值大小的元素; //且A(0)未存放元素;变量 x是与 A(i)同类型的数据元素。 1 int i, j; 2 for(j=2;j<=n;j++) { 3 i=j-l; 4 A[0]=A[j]; 5 while(A[0]=w[1]) { p = 1;q = 0;} 8 else { p = 0;q = 1;} 9 s = 2; 10 }; 11 for(i=s;iw[i+1]) {if(w[i]>w[q]) q=i;if(w[i+1]w[q]) q=i+1;if(w[i]0,且 a[i-1]=b[j-1]; ③ c[i][j]=max{c[i][j-1],c[i-1][j]},如果 i,j>0,且 a[i-1]≠b[j-1]。 依据上述思路的算法如下: # include # include # define n 100 //假设串的最大长度为 100 char a[n],b[n],str[n]; int lcs_len(char *a,char *b,int c[n][n]) { int m=strlen(a),n=strlen(b),i,j; for(i=0;i<=m;i++) c[i][0]=0; for(j=1;j<=n;j++) A;//最长公共子序列的长度初始化 for(i=1;i<=m;i++) for(j=1;j<=n;j++) if(a[i-1]==b[j-l]) c[i][j] = c[i-1][j-1]+l; else if(c[i-1][j] >= c[i][j-1]) c[i][j] = B; else c[i][j] = c[i][j-1]; return C;//返回最长公共子序列的长度 } char *build-lcs(char s[],char *a,char *b) { int k,i=strlen(a),j=strlen(b),c[n][n]; k = lcs_len(a,b,D); s[k] = ‘\0’; while(k>0) if(c[i][j]==c[i-1][j]) i--; else if(c[i][j]==c[i][j-1]) E; else { s[--k] = a[i-1]; i--;j--; }; return s ; } void main() { printf(“Enter two string(<%d)!\n”, n); scanf(“%s%s”,a,b); printf(“LCS=%s\n”,build_lcs(str,a,b)); 16. 试证明,对于函数 f(n)和g(n),若lim g(n)/f(n)存在,则 f(n)=Ω(g(n))当 且仅当存在确定的常数 c,有 lim g(n)/f(n)≤c。 17. 证明:如果一个算法在平均情况下的计算时间复杂度为θ(g(n)),则该算 法在最坏情况下所需的时间是Ω(g(n))。 18.证明:n!=О(nn) 19. 用贪婪法解装箱问题。 装箱问题可简述如下:设有编号为 1,…,n的n种物品,体积分别为 v1,v2,…,vn。将这 n种物品装到容量都为 V的若干箱子里。约定这 n种物 品的体积均不超过 V,即对于 1≤i≤n,有0 #include typedef struct ele {//物品结构信息 int vno ;//物品号 struct ele *link;//另一物品的指针 }ele; typedef struct hnode { int remainder;//箱子尚剩空间 ele *head;//箱内物品链的头结点指针 struct hnode *next //箱子链的后继箱子指针 }hnode; void main() { int n,i,box_count,box_volume,*a; hnode *box_h,*box_t,*j; ele *p,*q; printf(“输入箱子容积\n”);scanf(“%d”,&box_volume); printf(“输入物品种数\n”);scanf(“%d”,&n); a=(int )malloc(sizeof(int)*n);//存储物品体积信息的数组 printf(“请按体积从大到小顺序输入各物品的体积:”); for(i=1;i<=n;i++) scanf(“%d”,a[i]); box_h = box_t = null;//预置已用箱子链为空。 //box_h 指向已用箱子链的第一个结点, //box_t 则指向已用箱子链的最后一个结点。 box_count = 0;//预置已用箱子计数器 box_count 为0 for(i=1;i<=n;i++) {//物品 i按以下步骤装箱。 //从第一只箱子开始顺序寻找能放入物品 i的箱子 j p=(ele *)malloc(sizeof(ele));//生成一个物品结点存放物品 i p→vno=i; for(j=box_h;j!=null;j=j→next) //j 是已经放了某些物品的箱子号。 if( ① ) break;//找还可装物品 i的箱子 j if(j==null) {//所有已用的箱子中都不能放物品 i j=( hnode *)malloc(sizeof(hnode));//生成一个新的箱子结点 j j→remainder= ② ;//修改箱子 j的剩余容量 j→head=null;j→next = null; if(box_h==null) box_h = box_t = j;//尚没有已经使用的箱子 else ③ ;//将箱子 j加入到已用箱子链中 box_count++; } else ④ ;//将物品 i放入箱子 j for(q=j→head;q!=null && q→link!=null;q=q→link); if(q==null) {//新启用的箱子 p→link=j→head;j→head=p;}//p 结点是箱子结点 j中的第一个物品结点 else {p→link=null;q→link=p;}//将p结点插入到箱子结点 j的物品链中的最后 }//for //输出使用的箱子数及每个箱子中的全部物品与剩余容量 printf(“共使用%d只箱子。”,box_count); printf(“各箱子装物品情况如下:”); for(j=box_h,i=1;j!=null;j=j→next,i++) {//第i只箱子情况 printf(“第%2d 只箱子,还剩余容积%4d,所装物品有:\n”,i,j→remainder); for(p=j→head;p!=null;p=p→link) printf(“%4d”,p→vno);//输出物品的编号 printf(“\n”); } } 所确定的数据结构如下: remainder head next…box_h remainder head next remainder head next vno link vno link … vno link box_t vno link vno link … vno link 20.用回溯法求 8-皇后问题的算法描述如下: { n=8; m=0;//从空配置开始 good=1;//空配置中皇后不相互捕捉 do{ //循环找解 if(good) if(m==n) {//找到了一个解 输出解; 回溯,改变上一个 m的选值,形成下一个候选解; } else 扩展当前候选解至下一列,修改 m的值; else 回溯,改变上一个 m的选值,形成下一个候选解; good = 检查当前候选解的合理性; }while(m!=0); } 为了程序实现方便,约定第 i行上的皇后的编号为 i(1≤i≤8)。可以引入一个一维数组 col[]作为该问题的数据结构。col[j]表示在棋盘第 j列、col[j]行上有一个皇后,该皇后也 确定了两条斜线上有皇后。为了使程序在找到全部解后能够回溯到最初位置,设定 col[0]=0。当回溯到第 0列时,说明程序已经求得全部解或无解,结束程序的执行。为了 使程序在检查皇后配置的合理性方面简易方便,引入以下三个工作数组: 数组 a[],a[k]表示第 k行上还没有皇后; 数组b[],b[t]表示第 k条右高左低的斜线上没有皇后,k=行号+列号,且同一右高左 低的斜线上的方格,它们的行号与列号之和均相同; 数组c[],c[t]表示第 k条左高右低的斜线上没有皇后,k=n+列号-行号,且同一左高 右低的斜线上的方格,它们的行号与列号之差均相同。 初始时,所有的行和斜线上均没有皇后,从第 1列的第 1行开始配置第 1个皇后。假 设已经在第 m列、col[m]行上放置了一个合理的皇后,即在数组 a[],b[],c[]中为第 m 列、col[m]行的位置设定有皇后标志。下一步准备考察在第 m+1 列放置皇后。当从第 m 列回溯到 m-1 列(为该列重新配置皇后)时,应清除数组 a[],b[],c[]中已经设置的关于第 m-1 列、第 col[m-1]行有皇后的标志。一个皇后在 m列,col[m]行的方格内的配置是合理 的,等价于数组 a[],b[],c[]中的对应分量都为 1。详细程序如下,请在划线的空白处填 入适当的语句以完成该程序。 #include #include #define maxn 8 int n,m,good; int col[maxn+1],a[maxn+1]; int b[2*maxn+1],c[2*maxn+1]; void main() { int j;char awn; int n=8; for(j=0;j<=n;j++) a[j]=1;//初始化数组 a for(j=0;j<=2*n;j++) b[j]=c[j]=1;//初始化数组 b和c m=l; col[1]=1; ok=1; col[0]=0; while(m!=0) {//循环找解 if(good) if( ① ){//找到一个解 printf(“列\t 行”); for(j=1; j<=n; j++) printf(“%3d\t%d\n”,j,col[j]); printf(“Enter a character(Q/q for exit)!\n”); scanf(“%c”,&awn); if(awn==’Q’||awn==’q’) exit(0); while(col[m]==n) {//当最后一行已经合理地配置了皇后时, //改变上 1列的配置,求更多的解 m--;//回溯 ② ;//清除关于第 m列,第 col[m]行的有皇后标志 }//while col[m]++;//调整第 m列的皇后配置,在当前行的下 1行上配置皇后 } else {//在第 m列,col[m]行位置设定有皇后标志 a[ col[m] ] = b[ m+col[m] ] = c [n+m-col[m] ] = 0; col[++m]=1;}//尝试第 m+1 列,从第 1行开始配置 else {//配置没有成功,回溯调整 while(col[m]==n)){//回溯 m--; ② ;//清除关于第 m列,第 col[m]行的有皇后标志。 } col[m]++;//调整第 m列的皇后配置,在当前行的下 1行上配置皇后 } good = a[ col[m]]&& ③ && ④ ; }//while } 21.找出从自然数 1,2,…,n中任取 r个数的所有组合。 例如 n=5,r=3,所有组合为: 1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5 采用回溯法找问题的解,将找到的组合以从小到大顺序存于 a[0], a[1],…,a[r-1]中,组合的元素满足以下性质: (1)a[i+1]>a[i],即后一个数字比前数字一个大; (2)a[i]-i<=n-r+1。 按回溯算法思想,找解过程可以叙述如下:首先放弃组合数个数为 r的条件,候选组 合从只有一个数字 1开始。因该候选解满足除问题规模之外的全部条件,扩大其规模, 并使其满足上述条件(1),候选组合改为 1,2。继续这一过程,得到候选组合 1,2,3。 该候选解满足包括问题规模在内的全部要求,因而是一个解。在该解基础上,选下一个 候选解,因 a[2]上的 3调整为 4,以及以后再调整为 5,都满足问题的全部要求,得到解 1,2,4和1,2,5。由于对 5不能再作调整,就要从 a[2]回溯至 a[1],这时的 a[1]=2,可以调整为 3,并向前试探,得到解 1,3,4。重复上述向前试探和向后回溯, 直至要从 a[0]再回溯时,说明已找完问题的全部解。按上述思想写成程序如下: #include #define MAXN 100 int a[MAXN]; void comb_back(int m,int r) { int i,j; i=0;a[i]=1; do{ if( ① ){//还可以向前试探 if(i==r-l) {//已找到一个组合 fot(j=0;j int deltai[]=(2,1,-1,-2,-2,-l,1,2); int deltaj[]=(1,2,2,l,-1,-2,-2,-1); int board[8][8]; int exitn( int i,int j,int s,int a[] ){ //求(i,j)的出口数,和各出口号存于 a[]中,s是顺序选择着法的开始序号 int i1,j1,k,count; for(count=k=0 ;k<8;k++) { i1 = i + deftai[(s+k) % 8]; j1 = j + deftaj[(s+k) % 8]; if(i1>=0 && i1<8 && j1>=0 && j1<8 && board[i1][j1]==0) a[count++]=(s+k) % 8; }//for return count; }//for int next(int i,int j,int s)//选下一出口,S是顺序选择着法的开始序号 { int m,k,kk,min,a[8],b[8],temp; m = exitn(i,j,s,a);//确定(i,j)的出口个数 if(m==0) return -1;//没有出口 for(min=9,k=0;k #define MAXN 64 int a[MAXN+1][MAXN]; void main() { int twom1,twom,i,j,m,k; printf(“指定 n(=2 的k次幂)位选手,请输入 k。\n”,&k); scanf(“%d”,&k); a[1][1]=2;//预设两位选手的比赛日程。 A[2][1]=1; m = 1;twom1 = l; while(m void reverse(char s[]) { void reverser(char s[],int i,int len); reverser(s, ① ,strlen(s)); } void reverser(char s[],int i,int len) { int c,j; j = ② ; if (i
还剩429页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

zwyw610

贡献于2013-09-03

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf