计算机算法基础 318页

  • 3.86 MB
  • 2022-08-30 发布

计算机算法基础

  • 318页
  • 当前文档由用户上传发布,收益归属用户
  1. 1、本文档由用户上传,淘文库整理发布,可阅读全部内容。
  2. 2、本文档内容版权归属内容提供方,所产生的收益全部归内容提供方所有。如果您对本文有版权争议,请立即联系网站客服。
  3. 3、本文档由用户上传,本站不保证质量和数量令人满意,可能有诸多瑕疵,付费之前,请仔细阅读内容确认后进行付费下载。
  4. 网站客服QQ:403074932
普通高等教育“十五”国家级规划教材计算机算法基础(第三版)余祥宣崔国华邹海明华中科技大学出版社\n内容简介本书是教育部普通高等教育“十五”国家级规划教材。计算机算法是计算机科学和计算机应用的核心。无论是计算机系统、系统软件的设计,还是为解决计算机的各种应用课题做的设计都可归结为算法的设计。本书围绕算法设计的基本方法,对计算机领域中许多常用的非数值算法作了精辟的描述,并分析了这些算法所需的时间和空间。全书共分11章,第1章系统地介绍了计算机算法所涉及的数学知识,第2章至第9章介绍了递归算法、分治法、贪心法、动态规划、基本检索与周游方法、回溯法以及分枝-限界法等基本设计方法,第10章对当今计算机科学的前沿课题———P=?NP问题的有关知识作了初步介绍,第11章则对日益兴起的并行算法的基本设计方法作了介绍。本书可作为高等院校与计算机有关的各专业的教学用书,也可作为从事计算机科学、工程和应用的工作人员的自学教材和参考书。书名:计算机算法基础(第三版)作者:余祥宣出版社:华中理工大学出版社出版日期:2000ISBN:7-5609-2126-4/TP301.6定价:19.80,)\n序凡是学习了一种程序设计语言(不论是初级的还是高级的)课程并能编写一些实用程序的读者,也许都有这样一种体会,学会编程容易,但要想编出好程序难,因而很想系统地学习设计算法的知识。另外,一些著名计算机科学家在有关计算机科学教育的论述中认为,计算机科学是一种创造性思维活动的科学,其教育必须面向设计。计算机算法设计与分析正是面向设计的、处于核心地位的教育课程。基于上述的认识,自20世纪80年代初,作者对计算机科学专业的学生一直坚持开设算法设计与分析课程。在这门课程的教学过程中,我们查阅了国外流行的数种教材,发现多数教材在面向设计方面不是重视不够就是处理不甚恰当,只有E.Horowitz和S.Sahni合著的“FundamentalsofComputerAlgorithms”一书比较集中地反映了以上观点。不过要将该书作为一个学期的教材则嫌内容太多。为了解决此门课程教学的急需,在选用此书作为主要素材并参考、吸取了其它书籍的某些长处的基础上,根据计算机各专业学生目前需要形成的知识结构和在教学实践中的体会,于1985年编写了这本《计算机算法基础》,希望能对从事计算机算法教学和想设计出良好算法的人有所裨益。本书出版以来,受到许多学校的普遍欢迎,并于1992年荣获机电部电子教材类一等奖,2000年被列为教育部普通高等教育“十五”国家级规划教材。计算机科学技术发展很快,特别是近几年来并行处理技术渐趋成熟,高性能并行计算机已投入使用。因此,算法课程除了讲授串行算法的设计方法外,必须增加并行算法的有关内容,以便将学生培养成为面向21世纪的计算机高级技术人才。鉴于上述原因,于1998年对本书进行了修订再版。再版时,删去了某些过时的内容,增加了新的内容,如第11章并行算法。计算机算法设计与分析和数学密切相关,为使读者在设计和分析算法方面具有坚实的基础,本次修订把与算法密切相关的数学知识归结成一章。由于递归是算法设计与分析的重要方法,在本次再版中增加了递归算法的内容,全面地介绍此类算法相关的知识。全书共分11章。在第1章中介绍了有关的数学概念,同时在计数方法、母函数以及级数求和等方面做了系统的介绍;在第2章中介绍了算法的基本概念,并对计算复杂度、算法的描述工具和本书用到的基本数据结构作了简要的阐述;第3章围绕递归算法的实现机制、递归算法设计以及算法复杂度分析等内容,对递归算法做了全面的介绍;然后,针对设计算法时常用的一些基本设计策略组织了第4章至第9章的内容。其中每一章都先介绍一种设计算法的基本方法,然后从解决计算机科学和应用中出现的实际问题入手,由简到繁地描述几个经典的精巧算法,同时对每个算法所需的时间和空间作出数量级的分析,使读者既能学到一些常用的精巧算法,又能通过对这些设计策略的反复应用,牢固掌握这些基本设计方法,收到融会贯通之效。细心的读者从目录中就可立即发现,一个问题往往可以用多种设计·Ⅰ·\n策略求解。要指出的是,随着本书内容的逐步展开,读者将会体会到综合应用多种设计策略可以更有效地解决问题。第10章对当今计算机科学的前沿课题———P=?NP问题的有关知识作了初步介绍,目的是希望读者在学习了前9章内容之后,在理论上能提高到一个新的高度,激发某些读者对此课题的研究兴趣。第11章讨论了各种通用并行计算模型上的并行算法设计和分析方法。它以并行计算模型为线索,讲述了并行算法的基础知识及其基本设计技术,着重介绍了各种常用的和典型的并行算法。本书的内容是自成体系的,凡具有大学二年级数学基础和有使用过一种高级程序设计语言编程序经验的人,都能学懂本书的内容。对于已学过数据结构课程的读者,可以跳过2.4节,而对于没学过数据结构课程的读者,则必须认真学习并掌握此节的全部内容。各章后面都附有一定数量的习题,其中有些题目最好是在写出算法后,用一种语言写成程序并到机器上去运行,以检验所设计的算法的有效性。讲授这门课程一般70学时左右即可完成。根据数年来对大学本科生、研究生讲授这门课程的经验,建议对本科生讲授第1章至第9章以及第11章的全部或大部分内容,对第10章只作简要的介绍;研究生则需认真学习第10章,其余章节的学习与本科生的相同。这门课程既可采用课堂讲授方式,也可采用讲授、自学和讨论相结合的方式。本书第2章以及第4章到第9章由余祥宣编写,第10章由邹海明编写,第1章、第3章和第11章由崔国华编写。值本书再版之际,谨向在数年前就向我们建议增加并行算法的中国科学技术大学唐策善教授致以诚挚的感谢。编者2005年10月·Ⅱ·\n目录第1章数学预备知识⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(1)1.1集合⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(1)1.1.1集合之间的关系⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(1)1.1.2幂集⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(2)1.1.3集合的运算⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(2)1.2计数方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(3)1.2.1加法法则及乘法法则⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(3)1.2.2一一对应⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(3)1.2.3排列⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(4)1.2.4组合⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(6)1.3母函数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(9)1.3.1母函数的性质及应用⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(9)1.3.2指数型母函数⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(14)1.4级数求和⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(16)1.4.1由组合的实际意义产生的计数公式及级数求和公式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(16)1.4.2其它的一些常用求和公式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(18)习题一⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(19)第2章导引与基本数据结构⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(21)2.1算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(21)2.1.1算法的重要特性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(21)2.1.2算法学习的基本内容⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(22)2.2分析算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(23)2.2.1计算时间的渐近表示⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(24)2.2.2常用的整数求和公式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(27)2.2.3作时空性能分布图⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(27)2.3用SPARKS语言写算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(28)2.4基本数据结构⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(33)2.4.1栈和队列⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(34)2.4.2树⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(37)2.4.3集合的树表示和不相交集合的合并———树结构应用实例⋯⋯⋯⋯⋯⋯⋯⋯⋯(42)2.4.4图⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(48)习题二⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(50)\n2计算机算法基础第3章递归算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(52)3.1递归算法的实现机制⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(52)3.1.1子程序的内部实现原理⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(52)3.1.2递归过程的内部实现原理⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(54)3.2递归转非递归⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(54)3.3递归算法设计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(57)3.4递归关系式的计算⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(62)3.4.1递归算法的时间复杂度分析⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(62)3.4.2k阶线性齐次递归关系式的解法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(64)3.4.3线性常系数非齐次递归关系式的解法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(68)习题三⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(70)第4章分治法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(71)4.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(71)4.2二分检索⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(72)4.2.1二分检索算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(73)4.2.2以比较为基础检索的时间下界⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(77)4.3找最大和最小元素⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(78)4.4归并分类⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(81)4.4.1基本方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(81)4.4.2改进的归并分类算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(84)4.4.3以比较为基础分类的时间下界⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(86)4.5快速分类⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(87)4.5.1快速分类算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(87)4.5.2快速分类分析⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(89)4.6选择问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(91)4.6.1选择问题算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(91)4.6.2最坏情况时间是O(n)的选择算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(94)4.6.3SELECT2的实现⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(96)4.7斯特拉森矩阵乘法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(97)习题四⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(99)第5章贪心方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(101)5.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(101)5.2背包问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(102)5.3带有限期的作业排序⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(104)5.3.1带有限期的作业排序算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(104)5.3.2一种更快的作业排序算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(107)5.4最优归并模式⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(109)5.5最小生成树⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(112)\n目录35.6单源点最短路径⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(118)习题五⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(121)第6章动态规划⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(124)6.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(124)6.2多段图⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(126)6.3每对结点之间的最短路径⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(130)6.4最优二分检索树⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(132)6.50/1背包问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(137)6.5.10/1背包问题的实例分析⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(137)6.5.2DKP的实现⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(140)6.5.3过程DKNAP的分析⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(142)6.6可靠性设计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(143)6.7货郎担问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(146)6.8流水线调度问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(148)习题六⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(151)第7章基本检索与周游方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(153)7.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(153)7.1.1二元树周游⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(153)7.1.2树周游⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(161)7.1.3图的检索和周游⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(162)7.2代码最优化⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(166)7.3双连通分图和深度优先检索⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(176)7.4与/或图⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(180)7.5对策树⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(184)习题七⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(190)第8章回溯法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(193)8.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(193)8.1.1回溯的一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(193)8.1.2效率估计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(200)8.28-皇后问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(202)8.3子集和数问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(204)8.4图的着色⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(206)8.5哈密顿环⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(208)8.6背包问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(210)习题八⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(215)第9章分枝-限界法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(217)9.1一般方法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(217)9.1.1LC-检索⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(218)\n4计算机算法基础9.1.215-谜问题———一个例子⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(219)9.1.3LC-检索的抽象化控制⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(222)9.1.4LC-检索的特性⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(223)9.1.5分枝-限界算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(225)9.1.6效率分析⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(229)9.20/1背包问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(230)9.2.1LC分枝-限界求解⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(230)9.2.2FIFO分枝-限界求解⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(234)9.3货郎担问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(236)习题九⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(242)第10章NP-难度和NP-完全的问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(243)10.1基本概念⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(243)10.1.1不确定的算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(243)10.1.2NP-难度和NP-完全类⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(249)10.2COOK定理⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(250)10.3NP-难度的图问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(255)10.3.1集团判定问题(CDP)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(256)10.3.2结点覆盖的判定问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(256)10.3.3着色数判定问题(CN)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(257)10.3.4有向哈密顿环(DHC)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(258)10.3.5货郎担判定问题(TSP)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(260)10.3.6与/或图的判定问题(AOG)⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(260)10.4NP-难度的调度问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(262)10.4.1相同处理器调度⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(262)10.4.2流水线调度⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(263)10.4.3作业加工调度⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(264)10.5NP-难度的代码生成问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(265)10.5.1有公共子表达式的代码生成⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(265)10.5.2并行赋值指令的实现⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(268)10.6若干简化了的NP-难度问题⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(270)习题十⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(272)第11章并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(274)11.1并行计算机与并行计算模型⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(274)11.2并行算法的基本概念⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(277)11.2.1并行算法的复杂性度量⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(277)11.2.2并行算法的性能评价⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(278)11.2.3并行算法的设计⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(279)11.2.4并行算法的描述⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(279)\n目录511.3SIMD共享存储模型上的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(280)11.3.1广播算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(280)11.3.2求和算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(281)11.3.3并行归并分类算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(282)11.3.4求图的连通分支的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(284)11.4SIMD互连网络模型上的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(286)11.4.1超立方模型上的求和算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(286)11.4.2一维线性模型上的并行排序算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(288)11.4.3树形模型上求最小值算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(289)11.4.4二维网孔模型上的连通分支算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(290)11.5MIMD共享存储模型上的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(293)11.5.1并行求和算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(293)11.5.2矩阵乘法的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(293)11.5.3枚举分类算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(295)11.5.4二次取中的并行选择算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(295)11.5.5并行快速排序算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(297)11.5.6求最小元的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(299)11.5.7求单源点最短路径的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(300)11.6MIMD异步通信模型上的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(301)11.6.1选择问题的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(302)11.6.2求极值问题的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(303)11.6.3网络生成树的并行算法⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(306)习题十一⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(308)参考文献⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯(309)\n第1章数学预备知识数学是算法设计与算法分析的重要基础,本章将介绍其后各章所涉及的数学概念和数学方法。在后面的各章节中,读者若遇到不熟悉的数学概念和数学方法,则可以学习或查阅本章的相关部分。1.1集合数学意义上的集合概念在计算机科学上有着广泛的用途。定义1.1在研究某一类对象时,可把这类对象的整体称为集合,组成一个集合的对象称为该集合的元素。设A是一个集合,a是集合A中的元素,可表示为a∈A,读作a属于A。如果a不是集合A的元素,则表示为a|A,读作a不属于A。例如,26个小写英文字母,可组成一个字母集合A,每个小写字母皆属于A,可写为a∈A,b∈A,⋯,z∈A。所有阿拉伯数字都不属于A,则有2|A,8|A等。有限个元素x1,x2,⋯,xn组成的集合,称为有限集合。无限个元素组成的集合,称为无限集合。例如,整数构成的集合是一个无限集合。把不含元素的集合,称为空集,记为ꯁ。集合的表示法,有列举法和描述法两种。列举法是把集合的元素一一列举出来。例如,26个小写英文字母组成的集合A,可写成1A={a,b,c,⋯,z};阿拉伯数字组成的集合D,可写成D={0,1,2,⋯,9}以及集合C={a,23a,a,⋯}等。描述法描述出集合中元素所符合的规则。例如,N={n|n是自然数},表明N是自然数的集合。A={x|x∈Z且0≤x≤5}其中,Z是整数集,则A={0,1,2,3,4,5}。1.1.1集合之间的关系(1)设两个集合A,B包含的元素完全相同,则称集合A和B相等,表示为A=B。例如,若集合A={a,b,c},集合B={b,a,c},则有A=B。应指出,一个集合中元素排列的顺序是无关紧要的。对有限集合A,其中不同元素的个数称为集合的基数,表示为#A或|A|。例如,B={a,b,c,4,8},其基数#B=5。(2)设两个集合A,B,当A的元素都是B的元素时,称A包含于B,或称A是B的子\n2计算机算法基础集,表示为AB。当AB且A≠B时,称A是B的真子集,表示为AB。如果所研究的集合皆为某个集合的子集,则称该集合为全集,记为E。(3)由(1)和(2)可知,对于任意两个集合A,B,A=B的充要条件是AB且BA。1.1.2幂集A设A是一个集合,则A的所有子集组成的集合称为A的幂集,表示为2或ρ(A)。例如:设A={a,b,c},则有ρ(A)={ꯁ,{a},{b},{c},{a,b},{b,c},{a,c},{a,b,c}}设A是有限集,#A=n,则ρ(A)的元素个数为01nnCn+Cn+⋯+Cn=2应指出,空集ꯁ是任何集合的一个子集。1.1.3集合的运算(1)设有两个集合A,B,则由A和B的所有共同元素构成的集合,称为A和B的交集,表示为A∩B。例如,A={a,b,c},B={c,d,e,f},则A∩B={c}。(2)设有两个集合A,B,则所有属于A或属于B的元素组成的集合,称为A和B的并集,表示为A∪B。例如,A={a,b},B={7,8},则A∪B={a,b,7,8}。(3)设有两个集合A,B,则所有属于A而不属于B的一切元素组成的集合,称为B对A的补集,表示为A-B。例如,A={a,b,c,d},B={c,d,e},则A-B={a,b},B-A={e}。设有全集E和集合A,则称E-A是集合A的补集,表示为A。(4)设有两个集合A,B,则所有序偶(a,b)组成的集合,称为A,B的笛卡儿乘积,表示为A×B。A×B={(a,b)|a∈A且b∈B}例如,A={a,b,c},B={0,1},则A×B={(a,0),(a,1),(b,0),(b,1),(c,0),(c,1)}序偶函数的元素排列是有顺序的,不能任意颠倒,(a,b)和(b,a)是不相同的两个序偶,因此,两个序偶相等,应该是对应元素相同。例如,对于(a,b)=(c,d),应有a=c和b=d。对任意集合A,B,C有如下运算律:(1)A∪A=A,A∩A=A;(2)A∪B=B∪A,A∩B=B∩A;(3)(A∪B)∪C=A∪(B∪C),(A∩B)∩C=A∩(B∩C);(4)A∪(B∩C)=(A∪B)∩(A∪C),A∩(B∪C)=(A∩B)∪(A∩C);(5)A∪(A∩B)=A,A∩(A∪B)=A;(6)A∪A=E,A∩A=ꯁ;(7)A∪B=A∩B,A∩B=A∪B;\n第1章数学预备知识3(8)E∪A=E,E∩A=A;(9)A∪ꯁ=A,A∩ꯁ=ꯁ。1.2计数方法1.2.1加法法则及乘法法则在研究计数时经常要用到两个最基本的法则,一个是加法法则,另一个是乘法法则。1.加法法则若具有性质A的事件有m个,具有性质B的事件有n个,则当A和B是无关的二类时,具有性质A或性质B的事件有m+n个。2.乘法法则若具有性质A的事件有m个,具有性质B的事件有n个,则当A和B相互独立时,具有性质A与性质B的事件有m·n个。例1.1设A到B有3条不同的路径,B到C也有3条不同的路径,求从A经过B到C的路径数。解如图1.1所示,假定前面3条由A到B的路径分别为a,b,c,后面B到C的路径分别为1,2,3,则A到C(经过B)的路径为a1,a2,a3;b1,b2,b3;c1,c2,c3所以从A经过B到C共有3×3=9条不同的路径。图1.1A经过B到C的路径例1.2求布尔函数f(x1,x2,⋯,xn)的数目。解根据乘法法则,长度为n的(0,1)符号串x1x2⋯xn,xi=0,1,i=1,2,⋯,n,共有n2×2×⋯×2=2个,记这些符号串分别为an1,a2,⋯,a2。若以n=2为例,则a1=00,a2=n个401,a3=10,a4=11。f(aj)可取值0或1,j=1,2,3,4,则n=2的布尔函数数目应为2=16。n24同理,n个变元的布尔函数数目为2。当n=4时,2=16,故4个布尔变量x1,x2,x3,16x4的布尔函数数目为2=65536。324例1.3n=7×11×13,求除尽n的数的个数。aaa解由于除尽n的数可写成71×112×133,0≤a1≤3,0≤a2≤2,0≤a3≤4,故除尽n的数的个数为4×3×5=60。1.2.2一一对应一一对应是在计数过程中经常用到的方法之一。若问题A和问题B一一对应,A不容\n4计算机算法基础易求解,而B较容易求解,则可以通过对B的计数,反过来得到A的解。例1.4在集合A={ai|ai为整数,1≤i≤100}中,问求最大元素的过程中需要做多少次比较?解考虑到每做一次比较就淘汰一个数,则从100个数中找出其中最大的数M,必须淘汰99个数,故须做99次比较。n-2例1.5证明:n个有标号1,2,⋯,n的顶点的树的数目等于n。证明对这个结论有许多不同的证明方法,下面采用一一对应的办法来证明。假定T是其中一棵树,树叶中标号最小者设为a1,a1的邻接点为b1,消去点a1和边(a1,b1),点b1便成为余下的树的树叶。在余下的树中继续寻找标号最小的树叶,设为a2,a2以边(a2,b2)与b2相连接,再消去a2及边(a2,b2),继续以上的步骤n-2次,直到最后剩下一条边为止。于是,一棵树对应一个序列b1,b2,⋯,bn-2b1,b2,⋯,bn-2是1到n中的数,且并不是一定不相同。反之,任给一个序列b1,b2,⋯,bn-2(1)其中,1≤bi≤n,i=1,2,⋯,n-2,由此便可找到与之对应的树T,方法如下:从序列1,2,⋯,n(2)中找到第1个不出现在b1,b2,⋯,bn-2中的数,这个数显然就是a1,同时找出树T的边(a1,b1),从序列(1)和序列(2)中分别消去b1和a1。在余下的序列中继续以上的步骤n-2次,直到序列(1)为空为止。这时,将序列(2)剩下的两个数设为ak和bk,则边(ak,bk)也是树T的最后一条边。于是,便得到树T。这就证明了n个标号顶点的树和n-2个数b1,b2,⋯,bn-2n-2一一对应,其中1≤bi≤n,i=1,2,⋯,n-2。根据乘法法则,序列(1)的数目为n个。结论得证。1.2.3排列设A={a1,a2,⋯,an}是n个不同元素的集合,r满足0≤r≤n,任取A中r个元素按顺序排成一列,称为从A中取r个的一个排列。例如,A={a,b,c,d},r=3,从A中取3个元素排列的全体如下:abc,acb,bac,bca,cab,cba,abd,aab,bad,bda,dab,dba,acd,adc,cad,cda,dac,dca总数为24个。n令Pr表示从n个元素中取r个元素排列的全体数目,也用P(n,r)来表示。从n个元素中取r个元素排列,可以看作是与下面的问题一一对应。图1.2所示为有编号的r个盒子,设A为有标志1,2,⋯,n的n个球,从A中取出一个球放在第1个盒子中,从余下的n-1个球中取另一个放在第2个盒子中,以此类推,最后,从n-r+1个余下的球中取一个放到第r个盒子中。由此可得从A中取r个盒子的一个排列。\n第1章数学预备知识5图1.2r个盒子第1个盒子有n种出现的可能,第2个盒子有n-1种出现的可能,⋯⋯第r个盒子有n-r+1种出现的可能,根据乘法法则:nPr=n(n-1)(n-2)⋯(n-r+1)由于n!=(n-1)(n-2)⋯2×1,所以有nPr=n!/(n-r)!为了方便起见,令0!=1,所以nnP0=1,Pn=n!例1.6由5种颜色的星状物、20种不同的花排成如下的图案:两边是星状物,中间是3朵花。问共有多少种这样的图案?解5种颜色的星状物取两个排列的排列数为5P2=5×4=2020种不同的花,取3种排列的排列数为20P3=20×19×18=6840根据乘法法则,共有图案数为20×6840=136800例1.7A单位有7位代表,B单位有3位代表,排成一列合影,要求B单位3个人排在一起。问有多少种不同的排列方案?解B单位3个人的某一排列为1个元素参加单位A进行排列,可得方案数为(7+1)!=8!但B单位3个人共有3!种排列。根据乘法法则,总排列方案数为8!×3!例1.8求20000到70000间的偶数中由不同数字组成的5位数的个数。解假定所求的数为abcde这5位数,其中2≤a≤6,e∈{0,2,4,6,8}。10-28(1)若a∈{2,4,6},则e有4种选择,bcd有P3=P3种选择。根据乘法法则,有3×48×P3=4032种可能。88(2)若a∈{3,5},则e有5种选择。bcd依然有P3种选择。根据乘法法则,有2×5×P3=3360种可能。根据加法法则,总个数为4032+3360=7392。例1.9随机地选择n(n<365)个人,求其中至少有两人生日相同的概率。解n个人生日不同的排列数为365×364×⋯×3×2×1P(365,n)=(365-n)!nn个人生日的序列可能有365种,故n个人在一起生日不同的概率为n365×364×⋯×(365-n+1)P(365,n)/365=n365\n6计算机算法基础n个人中有相同生日的概率为365×364×⋯×(365-n+1)1-n365若从n个元素中取r个元素沿一圆周排列,则称为周圆排列,或简称为圆排列。n从n个元素中取r个元素沿一圆周排列的排列数用Qr表示,或表示为Q(n,r)。圆排列与排列的不同之处在于圆排列头尾相邻,比如,4个元素a,b,c,d可以有4种不同的排列:abcd,dabc,cdab,bcda但在圆排列中则是一回事,属于同一个圆排列。nn从n个元素中取r个元素作圆排列的排列数Qr与Pr的关系是nnQr=Pr/r这个道理很明显,将取r个元素作排列的结果与圆排列比较,每个排列重复了r次。同理,nn!Qn==(n-1)!n例1.105颗红色珠子、3颗蓝色珠子装在圆板的四周,试问:有多少种方案?若蓝色珠子不相邻则又有多少种排列方案?蓝色珠子在一起又如何?8解若不加限制,则有Q8=7!种排列方案。若要求蓝色珠子在一起,即把它们看作是一个元素进行圆排列,则为6Q6=5!最后讨论蓝色珠子不相邻的情况。5颗红色珠子的圆排列有4!种。第1个蓝色珠子有5种选择;第2个蓝色珠子为了避免与第1个蓝色珠子相邻,只能有4种选择;第3个蓝色珠子只有3种选择,故有4!×5×4×3=1440例1.115对夫妇出席一次宴会,围一圆桌坐下,试问:有几种不同的方案?若要求每对夫妻相邻又有多少种方案?解若5对夫妻10个人围圆桌而坐(没有限制条件),则此问题解为10Q10=9!=362880若加上限制条件———夫妻相邻而坐,但可以交换座位,则可将其看成是5个元素的圆排列,排列数为4!。根据乘法法则可得其总方案数为52×4!=32×24=7681.2.4组合若从n个元素中取出r个而不考虑它的顺序,则称之为从n中取r的组合。其数目记为nnC(n,r),Cr或。r组合问题可以看作是:球有标志1,2,⋯,n,盒子则没有区别,从n个球中取r个放到r个盒子里,每个盒子一个,便得到n取r的组合。\n第1章数学预备知识7若在每一种组合结果的基础上对盒子进行排列,便得到n取r的排列,则有P(n,r)=C(n,r)·r!nn或Pr=Cr·r!nn!Cr=(n-r)!r!例1.12甲和乙两单位共有11个成员,其中甲单位7人,乙单位4人,拟从中组成一个5人小组:(1)要求必须包含乙单位2人;(2)要求至少包含乙单位2人;(3)要求乙单位某一人与甲单位特定一人不能同时在一个小组。试分别求各有多少种方案。474!7!解(1)n1==×=210232!2!3!4!(2)若乙单位2人,甲单位3人,共210种方案;若乙单位3人,甲单位2人,则方案数为474!7!=×=84323!2!5!47最后乙单位4人,甲单位1人,共=7种方案。41根据加法法则,总方案数为210+84+7=301(3)将甲单位某人设为A,不与乙单位的B分在一组,这是指从不附加其他限制条件下的组合中,去掉A和B在一起的情况。至于A和B在一起的组合,可以考虑先将A与B排除在外,余下9人中取3人组合,然后将A与B加上去,所以A和B不在一起的组合数为119-=462-84=37853例1.13假定有a1,a2,a3,a4,a5,a6,a7,a8八位成员,两两配对分成4组,试求方案N。解方法一a1选择其同伴有7种可能,余下6人中一人选择其同伴只有5种可能,余下4人,其中一人选择同伴有3种选择可能,所以共有方案数为N=7×5×3=105方法二将8个成员进行全排列,共有8!种可能。若将它分成4组{12},{34},{56},{78}其中,若1和2,3和4,5和6,7和8互换,对于选择同伴没有影响,则全排列中选择同伴有4重复,其重复数为2。同时,4组之间的排列也不影响同伴关系,故全排列中对同伴关系也有重复,则其重复数为4!。故得8!8×7×6×5N=4==1054!21686方法三将8人分成4组,第1组有种选择;余下6人,第2组有种选择;同理,22\n8计算机算法基础4第3组有种选择。根据乘法法则,共有286428×7×6×5×4×3N=4!=3=10522222×4!式中,除以4!是因为其中4!组与顺序无关。在组合的定义中若增加一些限制条件,则会产生一些新的计数概念和方法,最常见的有允许重复的组合和不相邻的组合。1.允许重复的组合允许重复的组合指的是从A={1,2,⋯,n}中取r个元素{a1,a2,⋯,ar},ai∈A,i=1,2,⋯,r,而且允许ai=aj,i≠j。例如,A={1,2,3},若取2个作允许重复的组合,则除了不重复的组合{1,2}{1,3}{2,3}之外,还有{1,1}{2,2}{3,3}。定理1.1在n个不同的元素中取r个进行组合,若允许重复,则组合数为C(n+r-1,r)。证明只要证明允许重复的组合与从n+r-1个不同的元素中取r个作不重复的组合一一对应,定理就获得了证明。先证明从n中取r个可重复组合和从n+r-1中取r个的不允许重复的组合一一对应。不失一般性,假定有n个不同元素分别为1,2,⋯,n,从中取r个作允许重复的组合a1,a2,⋯,ar,由于存在重复,故a1≤a2≤⋯≤ar从(a1,a2,⋯,ar)引出(a1,a2+1,⋯,ak+k-1,⋯,ar+r-1),使每一允许重复的组合(a1,a2,⋯,ar)对应于一个不允许重复的组合(a1,a2+1,⋯,ak+k-1,⋯,ar+r-1),令后者为(b1,b2,⋯,br),即bk=ak+(k-1),k=1,2,⋯,r,其中b11i=1,2,⋯,r-11.3母函数母函数是计数的重要工具,它在第3章的解递归关系式中也有重要的应用。1.3.1母函数的性质及应用定义1.2设a0,a1,a2,⋯,an,⋯是一数列,定义它的母函数为幂级数12nf(x)=a0+a1x+a2x+⋯+anx+⋯虽然上面的幂级数有收敛问题,但在这里暂不考虑收敛性。我们的目的是将任意一个012n数列用另一种形式表示,即将数列想象为幂级数形式。这里将x=1,x,x,⋯,x,⋯解释为代数符号或者是区分序列中不同项a0,a1,a2,⋯,an,⋯的形式记号。例如,令m是正整数,则二项式系数序列mmmm,⋯,012m\n10计算机算法基础mmm2mm的母函数fm(x)=+x+x+⋯+x。根据二项式定理,fm(x)=(1+012mmx)。更一般地,令a是实数,则二项式系数序列aaaa,,,⋯,,⋯012naaa2an的母函数fa(x)=+x+x+⋯+x+⋯012na根据广义二项式定理fa(x)=(1+x)1.母函数的基本公式下面给出在一定条件下母函数与母函数之间存在的一些重要关系。假定序列{ai}的母函数为A(x);序列{bi}的母函数为B(x),则有0kn。因此,只要有人能将现有指数时间算法中的任何一个算法化简为多项式时间算法,就取得了一个伟大的成就。图2.1和表2.1显示了当常数为1时的6种典型计算时间函数的增长情况。从中可以看出,O(logn)、O(n)和O(nlogn)比另外3种时间函数的增长率慢得多。由这些结果可以看出,当数据集的规模(即n的取值)很大时,在现代计算机上运行具有比O(nlogn)复杂度还高的算法往往是很困难的。尤其是指数时间算法,它只有在n值取得非常小时才实用。因此,在顺序处理机上扩大所处理问题的规模,最有效的途径是降低算法计算复杂度的图2.1一般计算时间函数的曲线数量级,而不是提高计算机的速度。表2.1计算时间函数值2nlognnnlognnn2010112122484248166416382464512256416642564096655365321601024327684294967296符号O作为算法性能描述的工具,它表示计算时间的上界函数。为了进一步刻画算法的性能特性,有时也希望确定时间的下界函数,为此,引进另一个数学符号。定义2.2如果存在两个正常数c和n0,对于所有的n>n0,有|f(n)|≥c|g(n)|则记为f(n)=Ω(g(n))。在某些情况下,某算法的计算时间既有f(n)=Ω(g(n))又有f(n)=O(g(n)),即g(n)既是f(n)的上界又是它的下界。为简便起见,引进另一个数学符号来表示这种情况。定义2.3如果存在正常数c1,c2和n0,对于所有的n>n0,有c1|g(n)|≤|f(n)|≤c2|g(n)|则记为f(n)=Θ(g(n))。一个算法的f(n)=Θ(g(n))意味着该算法在最好和最坏情况下的计算时间就一个常因子范围内而言是相同的。这几种数学符号要经常使用,希望大家在此就能明确它们各自的含义。\n第2章导引与基本数据结构27上面仅对算法的计算时间特性作了较详细的介绍,算法计算空间的分析也可作完全类似的讨论,在此从略。2.2.2常用的整数求和公式在算法分析中,在确定语句的频率时,经常会遇到以下形式的表达式:∑f(i)(2.1)g(n)≤i≤h(n)其中,f(i)是一个带有理数系数且以i为变量的多项式。这个表达式最常用到的是以下几种形式:2∑1∑i∑i(2.2)1≤i≤n1≤i≤n1≤i≤n由于它们都是有限求和,因此可列出它们的求和公式。由此可以容易地看出,第一个多项式的和数为n。为以后使用方便,下面直接写出其余多项式的求和公式:2∑i=n(n+1)/2=Θ(n)(2.3)1≤i≤n23∑i=n(n+1)(2n+1)/6=Θ(n)(2.4)1≤i≤nk+1kknnk+1通式是∑i=++低次项=Θ(n)(2.5)1≤i≤nk+122.2.3作时空性能分布图事后测试是在对算法进行设计、确认、事前分析、编码和调试之后要做的工作,以确定程序所耗费的精确时间和空间,即作时空性能分布图。由于事后测试与所用计算机密切相关,故在此只对这一阶段所要进行的基本工作和若干注意之点概略地作一些介绍。以作时间分布图为例,要精确地确定算法的计算时间,首先必须在所用计算机上配置一台能读出时间的时钟;还必须了解该时钟的精确程度以及计算机所用操作系统的工作方式。这是因为前者随所用计算机的不同而有相当大的差异,如果在一台时钟精确度不高的计算机上运行需时很少(譬如说比时钟的误差值还小)的程序,那么,所得的计时图只不过是一些“噪声”,其时间分布性能完全会被淹没在这些噪声之中。如果后者是以多道程序或分时方式工作的操作系统,则在取得算法工作的可靠时间上会出现困难,尤其对于那些计时中包含了换出磁盘上的用户程序要用的那部分时间的操作系统而言。由于时间随当前记入系统的用户数而变化,因此无法确定算法本身所花去的时间。为解决因时钟误差而引起的噪声问题,下面推荐两种可供选用的方法:一是增加输入规模,直至得到算法所需的可靠的时间总量;二是取足够大的r,将此算法反复执行r次,然后用r去除总的时间。在解决了计时方面的具体技术问题之后,就可考虑如何作出时间性能分布图。对于事前分析为Θ(g(n))时间的算法,应选择按输入不断增大其规模的数据集,再用这些数据集在计算机上运行程序,从而得到使用这些数据集情况下算法所耗费的时间,并画出这一数量级的时间曲线。如果这曲线与事前分析所得曲线形状基本吻合,则印证了早先分析的结论。而对于事前分析为O(g(n))时间的算法,则应在各种规模的范围内分别按最好、最坏和平均\n28计算机算法基础情况的那些数据集独立运行程序,作出各种情况的时间曲线,并由这些曲线来分析最优的有效逻辑位置。另外,如果为了解决某一个问题,分别设计了几种具有同一数量级的不同算法,或者为加快某种算法的速度,在同一数量级情况下作了一些改进,那么,只要在输入相同数据集的情况下作出它们的时间分布图就可比较出哪一个算法的速度更快些。2.3用SPARKS语言写算法为了便于表达算法所具有的特性,最好能用程序设计语言将算法写出来。选用语言最起码的一条要求是,由该语言所写出的每一个合法的句子都必须具有唯一的含义。程序就是用程序设计语言所表示的算法。本书中所出现的过程、子程序这样一些术语,有时也作为程序的同义词。选用何种语言来写算法呢?由于关心的是算法本身的基本思想和基本步骤,同时希望选用的语言简明、够用,写出的算法便于阅读并能容易地用人工或机器翻译成其它实际使用的程序设计语言,因此,这里选用的是SPARKS语言。它与ALGOL语言和PASCAL语言的形式很接近,凡是掌握了一门高级程序设计语言的人都能很快看懂并掌握SPARKS语言。SPARKS语言的基本数据类型是整型、实型、布尔型和字符型。变量只能存放单一类型的值,可以用下述形式来说明其类型:integerx,ybooleana,bcharc,d在SPARKS中,有特殊含义的标识符将作为保留字来考虑,用黑体字表示。给变量命名的规则是,以字母起头,不允许使用特殊字符,且不要太长;不允许与任何保留字重复。一行可以有数条语句,但要用分号隔开。完成对变量赋值的是赋值语句:〈变量〉←〈表达式〉左箭头表示把右边的值赋给左边的变量。有两个布尔值truefalse为产生这两个布尔值,设置了逻辑运算符andornot和关系运算符<≤=≠≥>SPARKS使用带有任意整数下界和上界的多维数组。例如,一个n维整型数组可用以下形式说明:integerA(l1:u1,⋯,ln:un),其下界是li,上界是ui,1≤i≤n,li和ui都是整数或整型变量。如果某一维的下界li为1,则在数组说明中的那个li可以不写出。例如,integerA(5,7∶20)与integerA(1∶5,7∶20)等效。为了保持SPARKS语法的简明性,只使用数组作为基本结构单元来构造所有数据对象,而没有引进记录等结构类型。条件语句具有以下形式:\n第2章导引与基本数据结构29ifcondthenS1或ifcondthenS1endifelseS2endif其中,cond是一个布尔表达式,S1,S2是任意组SPARKS语句。endif表示条件语句的结束。条件语句的流程图由图2.2给出。图2.2if语句假定布尔表达式按“短路”方式求值:对给出的布尔表达式(cond1orcond2),若cond1为真,则不对cond2求值;而对给出的布尔表达式(cond1andcond2),若cond1为假,则不对cond2求值。SPARKS中的另一种语句是case语句(情况语句),此语句可以很容易地把数个选择对象区别开,而无需使用多重if—then—else语句。它有如下形式:case∶cond1∶S1∶cond2∶S2⋯∶condn∶Sn∶else∶Sn+1endcase其中,Si是SPARKS语句组,1≤i≤n+1;else子句并不是必需的。该语句的语义由下面的流程图(图2.3)所描述。图2.3case语句为便于写算法,SPARKS提供了几种可实现迭代的循环语句。第一种循环语句是while语句:whileconddoSrepeat其中,cond和S均与前面的意思相同。该语句的含义由图2.4给出。第二种循环语句是loop—until—repeat语句:\n30计算机算法基础loopSuntilcondrepeat它有如下含义(见图2.5)。loop—until—repeat语句与while语句相比,它保证了至少要执行一次S语句。图2.4while语句图2.5loop—until—repeat语句第三种循环语句是for循环语句:forvble←starttofinishbyincrementdoSrepeatvble是一个变量,start,finish和increment是算术表达式。一个整型或实型变量或者一个常数都是算术表达式的简单形式。子句“byincrement”不是必需的,当其没出现时就自动取+1。此语句的含义可以用SPARKS写成:vble←startfin←finishincr←incrementwhile(vble-fin)*incr≤0doSvble←vble+incrrepeat注意:这些表达式只计算一次,并且将其值作为变量vble,fin和incr(其中两个是新引进的变量)的值存入。这3个变量的类型与箭头右边表达式的类型应一致。S代表SPARKS的语句序列,它不改变vble的值。最后一种循环语句是loop—until—repeat语句的简化形式:loopSrepeat它的含义见图2.6。从形式上看,这语句描述了一个无限循环!然而,假定这语句和S中的某种检测条件一起使用,那么,这个检测条件将导致一个出口。退出这样的循环的一种方法是在S中使用图2.6loop—repeat语句gotolabel语句,将控制转移到“label”。任何语句都可以附以标号,其方法是在那条语句的前面放置一个标识符和一个冒号。尽管通常并不需要goto语句,然而,当要将递归程序转换成迭代形式时,goto语句则是有用的。goto的一种受限制的形式是\n第2章导引与基本数据结构31exit它的作用是将控制转移到含有exit的最内层循环语句后面的第一条语句。这循环语句可以是一条while语句,也可以是一条loop—until—repeat或者for语句。exit能有条件地或无条件地使用。例如loopS1ifcondthenexitendifS2repeat其执行情况见图2.7。一个完整的SPARKS程序是一个或多个过程的集合,第一个过程作为主程序。执行从主程序开始。对于任一SPARKS过程,譬如过程A,当到达end或return语句时,控制返回到调用过程A的那图2.7带有exit的loop—repeat个SPARKS过程。如果过程A是主程序,控制则返回到操作系统。单个的SPARKS过程有以下形式:procedureNAME(〈参数表〉)〈说明部分〉SendNAME一个SPARKS过程可以是一个纯过程(又称为子例行程序),也可以是一个函数。在这两种情况下都要对过程命名,并且把所用的形参作为一个表放在过程名后面的圆括号中。实参与形参的结合是由访问调用规则控制的,其意思是,在运行时把实参的地址送到被调用的过程中去代换与其对应的形参。对于那些是常数或表达式的实参,则假定它们存放在内部生成字中,因此将它们的地址送到被调用过程。在函数中,返回值由放在紧接return的一对括号中的值来表示。例如,return(〈表达式〉)其中,表达式的值作为函数的值来传送。对于过程而言,end的执行意味着执行一条没有值与其相联系的return语句。为了停止程序的执行,可以使用stop语句。一个过程包括3种类型的变量:局部变量、全程变量和形参。局部变量(localvariabl)是在当前过程中说明的变量。全程变量(globalvariabl)是在已包含当前过程的过程中说明为局部变量的变量。形参(formalparameter)是参数表中的一个标识符,由于它实际上永远不会含有值,因此它已不是一个变量,在运行时它由调用语句中对应位置的实参所代换。对上述这3类变量所具有的特性是否均要给予详细的说明呢?由于需要的是能简明描述算法的基本思想与步骤的语言,故对于与此无根本损害而在计算机上付诸实现时又不可缺少的一些语法成分,则可以在写算法时采取较为“灵活”的方式。SPARKS就具有这样的特点,它允许编制算法的人对变量说明的详略与否根据具体情况灵活处理,只要能清楚地反\n32计算机算法基础映出变量的前后关系就行。下面用一个例子来加以说明。考虑求n(n>0)个数的最大值的SPARKS过程MAX。算法2.1求n项的最大元素procedureMAX(A,n,j)∥置j,使A(j)是A(1∶n)中的最大元素,n>0∥xmax←A(1);j←1fori←2tondoifA(i)>xmaxthenxmax←A(i);j←i;endifrepeatendMAX容易看出,在执行了过程MAX以后,对j换名的实参的值是最大元素在A(1∶n)中的位置。算法中除变量xmax以外,其它变量中哪些是局部变量或是形参一眼就可看出。将xmax看成是全程变量是说得通的。假定A(1∶n)是实数数组,那么,这个过程的完整说明就可表示如下:procedureMAX(A,n,j)globalrealxmax;parametersintegerj,n;realA(1∶n)localintegeri;正如上面说到的那样,除xmax外,其余变量的前后关系是很明显的,因此可以约定,在以SPARKS描述的算法中,除非另加说明,否则所用到的变量不是局部变量就是形参。实施不太严格说明的另一优点是提供了一个称为同质异相(polymorphism)的一般化类型。以过程MAX为例,由于真正关心的是算法模型的处理,因此要是不给出A(1∶n)是整型、实型或字符型的说明,即作出同质异相的处理可能使算法模型更具有一般性。这反而是大多数程序设计语言要求必须对A的数据类型进行说明所不可能有的长处,使用这些程序语言,对于同一个算法模型,不得不写出3个独立的过程。因此,说明的这种短缺更符合要求。于是,对过程MAX只需作如下说明:procedureMAX(A,n,j)globalxmaxintegeri,j,n过程中使用变量的分类方法,除了将变量分成局部变量、全程变量和参变量的分类方法以外,还可以有别的分类方法。为了帮助读者理解过程中各种变量的作用,下面再介绍另一种分类方法。需要指出的是,下述内容并不要求在用SPARKS写的算法中出现,而是在写算法过程中对所使用的变量应有的一种认识。这种分类法只与参变量和全程变量有关。从一个变量是否把值带入或带出过程这一角度出发,也可以把过程中的变量(指参变量和全程变量)分为3类:一类变量是只将一个值带入过程,其值在过程的整个执行期间保持不变,将其称为in型变量;另一类变量是在过程入口处设定义,当过程结束时给此变量赋予一个值并由其带出,这类变量称为out型变量;第三类变量是将一个值带入,并且将一个(可能变化了的)值带出,这类变量叫做inout型变量。如果将凡是改变其参变量或全程变量的过程叫做具有边界效应(sideeffect),那么上述\n第2章导引与基本数据结构33分类法对于理解这一概念就很有帮助。由于纯过程没有函数值返回,且它的返回值是通过改变参变量或(和)全程变量的值来实现的,因此纯过程至少应有一个out或inout变量,即纯过程完全是通过边界效应来起作用的。函数过程也可以有边界效应,但为了避免函数的副作用,在使用SPARKS写算法时,约定只准写没有边界效应的函数过程或者纯过程。那么,在什么情况下将一个算法写成函数过程或者写成纯过程呢?这与该算法在调用它的过程中的使用情况有关。如果该算法的返回值在调用它的过程的某表达式中使用,且只使用一次,则最好将其写成函数过程。例如,如果需要写一个确定两棵树是否相等的过程,就应作一个布尔函数,比方说EQUAL(S,T),它或者返回一个真值或者返回一个假值,那么在程序中就可出现对EQUAL调用的下述形式:ifEQUAL(S,T)then...又如,要作一个计算最大公因数的函数并在赋值语句中使用它:z←x*y/gcd(x,y)要是gcd(x,y)使用的次数在一次以上,则可以将它的值赋给一个变量(t←gcd(x,y))或者将它写成一个具有边界效应的纯过程gcd(a,b,c),然后使用语句callgcd(x,y,t)来调用它。除了上述情况外,一般都把一个算法写成纯过程。还要指出的是,过程对其它过程的调用是在执行了某种任务后,返回到原调用过程中的下一条语句。如果一个过程包含对自身的调用,就叫做直接递归(directrecursion)。如果一个过程调用另一过程,而这另一过程又调用原来的过程,就称为间接递归(indirectrecur-sion)。这两种递归形式在SPARKS中都允许使用。对于输入、输出,SPARKS没给出有关格式的任何细节,因为它们对拟定算法模型是非必要的,所以输入、输出只采用两个过程:read(〈参数表〉);print(〈参数表〉)首尾均带有双斜线的注解可以放在一个程序中的任何地方,例如,∥这是一条注解∥至此,SPARKS语言基本定义完毕,就一个程序语言而论,它还是不完整的。例如,混合算术规则、I/O格式规则等都没提到,甚至连完整的字符集也没给出。不过这些问题对我们来说都不重要,故无需被这些问题所搅扰。最后,当用自然语言或常见的数学表示能较好地描述算法模型时,也允许这样做。带有自然语言或常见数学表示的SPARKS称为拟SPARKS。2.4基本数据结构要设计一个有效的算法,就必须选择或设计适合该问题的数据结构,使得算法采用这种数据结构时能对数据施行有效的运算,因此构造数据结构是改进算法的基本方法之一。一个熟练的计算机工作者必定掌握了若干种已被证明和分析过的数据结构,当他为解决某个问题而设计算法时,这些数据结构就会在他头脑中不断浮现,并从中作出选择。本节介绍了本书中最频繁使用的一些数据结构,事实上,它们在各种算法中也经常被使用。对于专门学\n34计算机算法基础习过数据结构课程的读者,学习本节内容,一方面可复习一下用SPARKS描述的数据结构内容,另一方面或许既可弥补以前学习的不足又可对算法设计和分析取得初步的感性认识。如果还没学过数据结构,那么就请认真学习并掌握本节的全部内容。2.4.1栈和队列在计算机程序中经常用到的一种最简单的数据组织形式就是有序表(又叫线性表)。它由某个集合中的n个元素组成,通常写成(a1,a2,⋯,an),这里n≥0,当n=0时是一个空表。栈(stack)和队列(queue)是两种特殊的有序表。对栈来说,所有的插入和删除都在称为栈顶(top)的表的一端进行。而队列的所有插入只能在称为尾部(rear)的一端进行,所有的删除则只能在称为前部(front)的另一端进行。栈的运算意味着如果依次将元素A,B,C,D,E插入栈,那么从栈中移出的第一个元素必定是E,即最后进入栈的元素将首先被移出,因此栈又被称为后进先出(LIFO)表。队列的运算要求第一个插入队中的元素也第一个被移出,因此队列是一个先进先出(FIFO)表。栈和队列的一个例子见图2.8,在该例中,栈和队都包含同样的5个元素并且以相同的次序插入。栈和队列的存储结构一般使用以下两种表示方法:一种是用一个一维数组表示,另一种则是使图2.8栈和队列的例用链接表表示。现分述如下。如果用一个一维数组STACK(1∶n)来表示栈,则其中的n就是允许存入栈中元素的最大数目。于是,栈中的第一个元素(又称为存入栈底的元素)将被存放在STACK(1),第二个元素存放在STACK(2),第i个元素则存放在STACK(i)。此时,还需要一个变量top作为栈顶指针,它指向该栈的顶部元素。为了测试栈是否为空,使用“iftop=0”即可。若非空,则栈顶元素在STACK(top)。插入和删除元素是栈的两个实质性运算,可用SPARKS描述如下。算法2.2栈运算procedureADD(item,STACK,n,top)∥将item插入规模最大为n的栈STACK;top是STACK中当前的元素数目∥iftop≥nthencallSTACKFULLendiftop←top+1STACK(top)←itemendADD(a)插入一个元素procedureDELETE(item,STACK,top)∥移出STACK顶部元素,除非STACK为空,否则就将该元素存入item∥iftop≤0thencallSTACKEMPTYendifitem←STACK(top)top←top-1\n第2章导引与基本数据结构35endDELETE(b)删去一个元素每执行一次ADD或DELETE过程都要花去一常量的时间,而且与栈中已存元素的个数无关。STACKFULL和STACKEMPTY是两个未加详细说明的过程,这是因为它们与具体的应用有关。通常在栈满的情况下就要发出需分配更多存储单元的信号并且从过程STACKFULL返回;而栈空则往往是一种有意义的情况。链接表是一些结点的集合,每个结点则由若干个信息段组成,在这些信息段中可以有一个信息段用来存放数据对象(即前面所说的元素),其余的信息段用来存放数据对象之间的顺序的链接信息。用来表示栈的链接表是一种单向链接表(又称线性链表),表中的每个结点有两个分别叫做DATA和LINK的信息段。DATA信息段用来存放数据对象,LINK信息段则指向在它之前刚存入的数据对象的那个结点。由于所有结点的地址都大于零,而栈底元素之前再没有元素,因此这个结点的LINK信息段存放一个零。例如,依次把元素A、B、C、D、E插入链式栈(见图2.9)。图2.9含有5个元素的链式栈这个链式栈中还用了一个变量STACK,它指示栈顶的那个结点(即插入的最后一项)。置STACK=0,表示栈为空。使用链式栈也能很容易地完成插入和删除元素的运算。插入一个元素用下面4条语句就可完成:callGETNODE(T)DATA(T)←itemLINK(T)←STACKSTACK←T过程GETNODE的功能是给变量T分配一个可用结点,如果不再有可用结点则作相应处理,例如,终止这个程序。删除元素可以像下述的那样处理:ifSTACK=0thencallSTACKEMPTYendifitem←DATA(STACK)T←STACKSTACK←LINK(STACK)callRETNODE(T)如果栈为空,则调用过程STACKEMPTY。反之,将栈顶元素存入item,保留栈顶指针,STACK转而指向下一个结点,用过程RETNODE把刚释放的原栈顶结点存入可用结点表,准备以后提供给GETNODE使用。栈的链接表示虽然比它的顺序数组表示占用较多的空间,但由于链式栈不需要一片连续单元,且多种类似的链式结构和一些别的结构可共用自由空间中的相同地方,因此使用链\n36计算机算法基础接具有更大的灵活性。还要指出的是,插入和删除的计算时间与任何一种存储结构表示的栈的大小无关,且总是一个常量。当队列用数组Q(0∶n-1)表示时,可以把它当成一个环形来看待。插入元素是按顺时针方向将rear移到下一个空位置来实现的。当rear=n-1且0位置为空时,下一个元素就存入Q(0)。front总是指着队中第一个元素按逆时针方向转的前一个位置。只有队列为空时,才有front=rear,开始时front=rear=0。图2.10中描述了一个n>4的数组中含有J1~J44个元素的环形队列的两种可能存放方式。图2.10含有元素J1,J2,J3,J4且容量为n的环形队列对于用数组结构表示的队列,元素的插入和删除可用SPARKS描述成如下过程。算法2.3队列运算procedureADDQ(item,Q,n,front,rear)∥将item插入到存放在Q(0∶n-1)的环形队列中∥∥rear指向队列的最后一项,front指向队列中∥∥第一项按逆时针方向转到前一个位置∥rear←(rear+1)modn∥模运算求余数,使rear顺时针前进一个位置∥iffront=rearthencallQUEUEFULLendifQ(rear)←item∥将新项插入队列∥endADDQ(a)插入一个元素procedureDELETEQ(item,Q,n,front,rear)∥将队Q(0∶n)的第一个元素移出并将此元素存入item∥iffront=rearthencallQUEUEEMPTYendiffront←(front+1)modn∥顺时针向前移动∥item←Q(front)∥将队中前部元素置入item∥endDELETEQ(b)删去一个元素容易看出,在把数组看成环形的情况下,这两个算法的计算时间都是O(1)。特别要指出的是,这两个算法在队列满和队列空的测试条件上表面看来似乎是相同的,都是判断是否front=rear,但由于这两种情况下队列所呈现的状态不同,因此在front=rear时队列是满还是空完全可以区别。在ADDQ情况下,即在执行了rear←(rear+1)modn语句之后,打算\n第2章导引与基本数据结构37插入一个元素时,front=rear才调用QUEUEFULL。此时,由于队列中的第一个元素不在Q(front),而在由这点顺时针前进的第一个位置上,因此,实际上还有一个空位置Q(rear),即Q(front)。于是,ADDQ在还剩一个空位置时通过调用QUEUEFULL发出队列满的信号,这与DELETE情况下,队列空的状态完全不同。这还表明,使用这种处理方法后,队列在任何时刻至多只能有n-1个元素而不是n个元素。虽然也有将n个位置都用上的算法,譬如设置一个作为识别标志的变量tag,在队列空时置tag=0,但这样做会使算法时间增长,而在任何包含队列的算法中ADDQ和DELETEQ都要多次使用,因此往往宁愿少用一个队列位置来节省计算时间。QUEUEFULL和QUEUEEMPTY也是两个依赖于具体应用而没详加说明的过程。队列的链接表示与栈的相应表示类似,每个结点也是由DATA和LINK两个信息段组成,前者用来保存数据对象,后者保存其链接信息。只是它使用了两个指针变量front和rear来指示前部和尾部的位置。元素在尾部插入而在前部删除。一旦front=0就发出队列空的信息。图2.11示出了具有4个元素A、B、C、D且按此顺序进入的链式队列。在队列的链表结构上作插入和删除运算获取可用结点和释放无用结点的工作也与GETNODE和RETNODE过程类似,至于链接队列的插入和删除过程的描述则留作一道习题。图2.11具有4个元素的链接队列2.4.2树定义2.4树(tree)是一个或多个结点的有限集合,它使得:①有一个特别指定的称作根(root)的结点;②剩下的结点被分成m≥0个不相交的集合T1,⋯,Tm,这些集合的每一个都是一棵树,并称T1,⋯,Tm为这个根的子树(subtree)。为讨论方便起见,引进了许多经常使用的术语。现以图2.12所示的树为例来说明。这棵树有13个结点,每个结点的数据项是一个字母。这棵树的根为A(有时也说成结点A),它采用了一般将根画在顶部的图形表示方法。一个结点的子树数目称为该结点的度(degree)。A的度是3,C的度是1,F的度是0。度为0的结点称作叶子(leaf)或终端结点(terminalnode),度不为0的结点则叫做非终端结点。图2.12中K,L,F,G,M,I,J都是叶子,其余的结点则是非终端结点。结点X的子树的根是X的儿子(children),X则是它儿子们的父亲(parent)。因此,D的儿子是H,I,J;D的父亲是A。同一父亲的儿子之间称为兄弟(sibling)。例如,H,I,J是兄弟。根据需要,还可随时增加一些类似的词汇,例如,可以说D是M的祖父等。一棵树的度是这棵树中结点度的最大值。图2.12中的树的度为3。结点的级(level)(又叫层)是这样确定的,设根是1级,若某结点在p级,则它的儿子在p+1级。一棵树中结点的最大级数定义为该树的高度(height)或深度(depth)。\n38计算机算法基础图2.12一棵树森林(forest)是m≥0个不相交树的集合。森林与树的概念非常接近,如果去掉树的根,就得到一个森林。例如,如果去掉图2.12中的树的根A,就得到有三棵树的森林。怎样在计算机的存储器中表示一棵树呢?由树的构造方式很自然地会想到采用链接表存储结构,使链接表中的结点与树中的结点相对应。但是,由于树中各个结点的度可能很不一致,这就导致链接表各个结点所具有的信息段数目不相同。可以想象,使用这样的数据表示写算法是比较繁杂的。为了克服这一弱点,应使链接表结点信息段数归一。图2.13提供了一种使链接表结点的信息段数固定的结构表示法,它给出了图2.12中那棵树的表结构。图2.13中,每个结点有3个信息段:TAG,DATA和LINK。信息段TAG的作用是设置标志。当TAG=0时,DATA和LINK的使用与以前所述相同;当TAG=1时,DATA信息段存放的不再是数据对象而是一个链接信息,即在此情况下DATA和LINK都存放表指针。这样,树就可以如下表示:表中的第一个结点中存放根并链接一些结点,这些结点指示着一些子表并且含有这根的所有子树。图2.13图2.12中树的表结构1.二元树二元树是使用得非常频繁的很重要的树结构。它具有以下特性:任何一个结点至多只能有两个儿子,即不存在度大于2的结点;另外,二元树的子树是有次序之分的,即分为左子树和右子树,而树的子树实际上无次序之分;再者,二元树允许有0个结点,而一棵树至少要有一个结点。将以上特性归纳起来,可对二元树定义如下。定义2.5二元树(binarytree)是结点的有限集合,它或者为空,或者由一个根和两棵树(称之为左子树、右子树)的不相交的二元树所组成。作为例子,图2.14显示了两棵特殊的二元树。图2.14(a)所示的是一棵斜树(skewedtree),它向左斜,还有一棵与之对应的右斜树。图2.14(b)所示的树称为完全二元树(com-pletebinarytree)。这类树稍后定义。但要指出的是,由这棵树可以看出,它所有的叶结点\n第2章导引与基本数据结构39都在两个相邻的级上。另外要说明的是,树中引进的所有术语对二元树都适用。图2.14两棵特殊的二元树图2.15深度为4的满二元树i-1引理2.1一棵二元树第i级的最大结点数是2。深度为k的二元树的最大结点数为2k-1,k>0。证明留给读者。k深度为k且有2-1个结点的二元树叫做深度为k的满(full)二元树。图2.15给出了一棵深度为4的满二元树。对于满二元树有一种非常自然且有用的顺序表示,即给满二元树的结点从根开始从左到右逐级顺序编上1,2,3,4,⋯的号码(见图2.16)。一棵有n个结点深度为k的二元树,当它的结点相当于深度为k的满二元树中编号为1到n的结点时,则称此二元树是完全的。由此定义可直接推出:完全二元树的叶子至多出现在相邻的两级上。利用以上的顺序编号法可以把完全树的结点紧凑地存放在一个一维数组TREE中,即把编号为i的结点存放在TREE(i)中而无需任何链接信息。下面的引理采用了这种编号方图2.16图2.14中两棵二元树的顺序表示式,因此能容易地确定任一结点i的父亲、左儿子和右儿子的位置。引理2.2一棵有n个结点的完全二元树,如果它的结点按上述方法顺序编号,则对于编号为i(1≤i≤n)的结点,有*(1)若i≠1,则PARENT(i)在i/2;若i=1,则i就是根并且i没有父亲。(2)若2i≤n,则LCHILD(i)在2i;若2i>n,则i没有左儿子。(3)若2i+1≤n,则RCHILD(i)在2i+1;若2i+1>n,则i没有右儿子。证明略。对于完全二元树来说,这种顺序表示法从空间使用效率上看是很理想的,因为它没有浪费一点空间。但如果将这种表示法延伸到表示任何二元树,则在多数情况下要浪费不少空*如果x是任意实数,则定义x=小于或等于x的最大整数,称x为x的下限。同样,定义x=大于或等于x的最小整数,称x为x的上限。\n40计算机算法基础间。例如,对图2.14(a)所示的左斜树使用顺序表示,只用了这数组空间的1/3还不到。在k最坏情况下,对于深度为k的右斜树,它虽然需要2-1个位置的数组,但只有k个位置被用到。即使对于完全二元树来说,这种顺序表示也有不足之处。例如,若要作插入或删除结点运算,则需要移动许多潜在的结点,并因此还引起剩余结点级数的改变。使用链接表示可以轻易地克服顺序表示的不足。在一般情况下链表的每个结点有3个信息段,它们是LCHILD,DATA和RCHILD。如果需要确定结点的父亲,则可以增加一个PARENT信息段。图2.17给出了图2.14中二元树的链接表示。图2.17图2.14中二元树的链接表示在实际生活中,有很多问题若用二元树构造其数据,则往往使所设计的算法较之使用别的数据结构简单、有效。所使用的数据放在结点的DATA信息段,这些数据通常是一些数或是可以比较其大小的数据对象(例如,字符串就可按字母顺序比较大小)。下面就在数据可比较大小的假设下介绍两种特殊的二元树,一种是堆的二元树,另一种是二分检索树。这是两种很有用的且在本书中要用到的结构。定义2.6堆(heap)是一棵完全二元树,它的每个结点的值至少和该结点的儿子们(如果存在的话)的值一样大。堆是一种数据结构,它通常用来解决既要把元素插入集合,又要从集合中快速找出最大(或最小)元素的这样一类问题。如果所有元素各不相同,则由堆的定义立即可知最大元素在堆的根上。也可以按每个结点的值小于或等于它儿子的值来定义堆,在这种定义下根就含有最小元素。这样定义的堆称为min-堆,而由定义2.6所定义的堆叫做max-堆。图2.18给出了一个由数据(35,40,45,50,80,90)所组成的堆。定义2.7二分检索树(binarysearchtree)T是一棵二元树,它或者为空,或者其每个结点含有一个可比较大小的数据元素,并且:图2.18由集合(35,40,45,50,80,90)组成的堆(1)T的左子树的所有元素比根结点T中的元素小;\n第2章导引与基本数据结构41(2)右子树的所有元素比根结点T中的元素大;(3)T的左子树和右子树也是二分检索树。注意:二分检索树要求树中所有结点中的元素是互异的。在编译程序中,把名字表构造成二分检索树,要确定某标识符是否在名字表中出现时,这种结构的检索算法就比很多别的算法快,为简单起见,可以造一张只含有13个SPARKS保留字的表,并把它们存入字符数组NAME(1∶13)中。NAME:(1)(2)(3)(4)(5)(6)(7)casedoelseendendcaseendififNAME:(8)(9)(10)(11)(12)(13)loopprocedurerepeatreturnthenwhile图2.19所示的是一棵含有NAME中数据元素的二分检索树。图2.19一棵二分检索树当用链接表来表示二分检索树时,当然表2.2图2.19的数组表示希望结点的大小固定,但允许保留字的长短LCHILDDATARCHILD不一。为了解决这一矛盾,在实际处理时保D(1)273留字并不进入结点的DATA信息段,而是存D(2)435D(3)6107入该保留字所在数组元素的下标。LCHILDD(4)018和RCHILD信息段中则保存该保留字的左、D(5)9510右儿子(它们也是保留字)在树中的位置。表D(6)08112.2给出了在计算机中用数组D(1∶13)存放D(7)121213D(8)020这棵二分检索树的实际表示,其中每个数组D(9)040元素D(i)含有DATA、LCHILD和RCHILDD(10)060三个信息段。例如,在D(3)的DATA段中D(11)09010是一个下标值,它指示repeat存放在数组D(12)0110D(13)0130NAME中的位置,而LCHILD和RCHILD中的6和7也是下标值,但它们指示repeat的左儿子loop和右儿子then在树中(即数组D中)的位置。由二元树可很自然地推广到k(k≥2)元树。在k元树中一个结点至多有k个儿子,并且这些儿子是有序的。二元树的顺序表示和结点大小固定的链接表示都可推广到多元树。\n42计算机算法基础2.树转换成二元树由于二元树结构的处理比较简单,因此,常常把其它树转换成二元树来处理,这样,可使需要的空间减小、算法简化。其转换方法如下:设有一棵树T(它的根为T1),人为安排它的子树有序且设为T11,T12,⋯,T1k。用T1做二元树的根,T11做T1的左子树,然后T1i做T1,i-1的右子树,2≤i≤k。看起来就像图2.20所示的那样。图2.20将一棵树转换成二元树(a)一般情况;(b)一个例子2.4.3集合的树表示和不相交集合的合并———树结构应用实例假定有一个全集合U,它由n个元素所组成,那么从U中可以构造出很多个集合。对于这些集合经常进行的是求取它们的交集和并集的运算。而如果这些集合是一些不相交的集合,即这些集合中的任意两个集合都没有公共元素,那么对它们大量施行的两种基本运算是合并某些集合和查找某个元素在哪一个集合之中。例如,有一个n=10的全集合U={1,2,3,4,5,6,7,8,9,10},由它可构造出集合S1={1,7,8,9},S2={2,5,10}和S3={3,4,6}。它们是3个不相交集合。如果将S1和S2合并成一个新的集合,就执行合并运算,S1∪S2={1,7,8,9,2,5,10}。如果要查找元素4包含在哪个集合之中,经过一番查找运算就可知道4在集合S3中。为了使上述运算有效地执行,就需要将集合中的元素构造成一定的结构形式,设计出在这些结构下执行上述运算的算法,分析这些算法的计算复杂度,并从中选出计算时间短、算法简便的数据结构。通常,用一个位向量SET(1∶n)来表示集合,且若U的第i个元素在这集合中,则置SET(i)=1,否则置零。这种表示的优点在于可以迅速地确定某个元素是否属于这个集合,而且能够很方便地利用计算机上的“逻辑加”和“逻辑乘”指令来实现两个集合的并和交之类的运算。但这种表示有很大的局限性,它只有在n很小(例如,n小于等于一个机器字长)时,运算才特别有效。当n很大(例如,大于一个机器字长),而每个集合的大小相对于n来说又很小时,这种表示将使得并或交运算的执行时间不是与两个集合中的元素数目成正比,而是与n成正比,因此,这种表示是无效的。集合的另一种表示方法是用集合的元素表来表示每一个集合,由于元素表中的元素是\n第2章导引与基本数据结构43无序的,因此要执行并或交运算,花费的时间与参加运算的两个集合长度的乘积成正比,而确定某元素属于哪一个集合的运算时间则与这些集合长度的和成正比。即使事先将参加运算的集合中的元素作某种排序,执行并或交运算的时间还是与参加运算的集合长度之和成正比。集合还可以用树结构来表示。由下面的讨论可以看到,用树来表示集合,对于不相交集合,能方便地进行集合的并和确定某元素属于哪个集合这两种基本运算(使得为这两种运算所拟定的算法能有效地实现)。前面例子中的3个不相交集合S1,S2,S3可以用图2.21所示的3棵树来表示。图2.21用树表示不相交集合集合在用树表示的情况下,求两个不相交集合的并集,一种最直接的方法就是使一棵树变成另一棵树的子树。于是,S1∪S2就有图2.22所示的形式之一。图2.22S1∪S2的两种树表示在这种树表示中,值得特别指出的是结点按父亲关系相连接的情况,若树用链接表来表示,则链接表中的每个结点需要设置PARENT信息段,而无需设置LCHILD和RCHILD信息段。为讨论方便起见,参加运算集合中的元素都用存放这些元素数组的下标来代替,并使这些下标与表示链接表的数组的下标相对应,在这种对应关系下,链接表的数组元素的下标就代表集合中的相应元素,从而可以取消链接表中结点的DATA信息段。这样一来,链接表的结点只含有一个信息段,即PARENT信息段。因此,不妨把表示链接表的数组就记为PARENT(1∶n)。PARENT(i)中存放着元素i在树中的父结点的指针。这样,根结点的PARENT信息段的内容就为零,而根结点就是PARENT数组中存放这个零值的数组元素的下标。用这个下标作为集合的名字,可以使讨论进一步简化。这样一来,两个不相交集合的合并,实质上就是把一棵树中根的PARENT信息段置成另一棵树的根;而生成的新树的根就代表这两个集合合并之后的并集。至于找元素i所属集合的运算则成了确定包含元素i的树的根。由以上讨论可知,为这两种运算初步设计的算法就是过程U和F。算法2.4简单的合并与查找运算procedureU(i,j)∥根为i和j的两个不相交集合用它们的并来取代∥integeri,jPARENT(i)←jendU\n44计算机算法基础procedureF(i)∥找包含元素i的树的根∥integeri,jj←iwhilePARENT(j)>0do∥若此结点是根,则PARENT(j)=0∥j←PARENT(j)repeatreturn(j)endF这两个算法虽然相当简单,但性能却不理想。例如,假设有n个元素1,2,⋯,n,开始它们的每一个分别在只有它自己的一个集合中,即Si={i},1≤i≤n,那么最初的结构是由这n个结点的森林和PARENT(i)=0,1≤i≤n所组成,如果要依次作下面一系列的合并和查找运算:U(1,2),F(1),U(2,3),F(1),U(3,4)F(1),U(4,5),⋯,F(1),U(n-1,n)则产生图2.23所示的退化树。由于每执行一次U算法所花的时间为常数,因此进行这n-1次合并的计算时间为O(n)。而算法F的执行时间与此元素在树中所处的级数成正比,即,如果元素在树中的第i级,则F的计算时间就为2O(i),因此,例中的n-2次查找所用的时间为O(n)。易于看出,这个例子给出了一系列合并、查找情况下算法U和F的最坏情况。产图2.23最坏情况树生这种最坏情况的症结何在呢?问题显然不在F而在U,即多次的合并运算构造出一棵串行的退化树。为避免构造出退化树,在对树i和树j作合并运算时使用一条加权规则,即,如果树i的结点数少于树j的结点数,则使j成为i的父亲,反之,则使i成为j的父亲。要使用这一规则就需要知道每一棵树有多少结点,实现这一点的一种简单方法是,在每棵树的根结点增设一个COUNT计数信息段。如果i是根结点,则COUNT(i)=树i的结点数。为了保证结点大小固定,实际上不用在根结点增设COUNT信息段,而是将结点计数以负数形式保存在根结点的PARENT信息段中。由于除根结点外的所有其它结点的PARENT是正数,所以,这样处理等价于用一个符号位把计数器与链接指针区别开来,而不会导致混乱。算法2.5使用加权规则的合并算法procedureUNION(i,j)∥使用加权规则合并根为i和j的两个集合,i≠j∥∥PARENT(i)=-COUNT(i),PARENT(j)=-COUNT(j)∥integeri,j,xx←PARENT(i)+PARENT(j)ifPARENT(i)>PARENT(j)thenPARENT(i)←j∥i的结点少∥PARENT(j)←xelsePARENT(j)←i∥j的结点少∥PARENT(i)←x\n第2章导引与基本数据结构45endifendUNIONUNION这个算法的计算时间虽比U算法的时间增加了一些,但还是以常数限界的。如果将算法2.5中的一系列合并运算改为使用UNION,就得到图2.24中的树。图2.24利用加权规则得到的树现在,对于结点1作上述n-2次查找运算,由于任何结点的最大级数都不超过2,所以n-2次查找的时间是O(n)。但是,这并不是使用加权规则作任意n次合并和查找的最坏情况。下面的引理2.3给出了执行一次查找的最大时间。引理2.3设T是一棵由算法UNION所产生的有n个结点的树。在T中没有结点的级数会大于logn+1。证明对于n=1,引理显然为真。假设对于所有结点数为i的树,i≤n-1,引理为真。现证明对于i=n引理亦真。设T是由算法UNION所产生的一棵n个结点的树,现考虑最后一次执行的合并运算UNION(k,j)。设m是树j的结点数,n-m是树k的结点数。不失一般性,可以假设1≤m≤n/2,于是,T中任一结点的最大级数或者与k中的最大级数相同,或者比j的最大级数大1。若为前者,则T的最大级数≤log(n-m)+1≤logn+1。若为后者,则T的最大级数≤logm+2≤log(n/2)+2≤logn+1。证毕。下例说明引理2.3的界限不仅是一个上界,而且是最小上界,它对于某个合并序列是可以达到的。3例2.1在由初始结构PARENT(i)=-COUNT(i)=-1,1≤i≤2所开始的下述合并序列上,考虑算法的工作情况:UNION(1,2),UNION(3,4),UNION(5,6),UNION(7,8)UNION(1,3),UNION(5,7),UNION(1,5)所得到的树由图2.25示出。由这个例子可以很容易推广到一般情况而得到具有logm+1级的m个结点的树。作为引理2.3的一个结果,对于算法UNION所产生的n个结点的树,执行一次查找的最长时间至多是O(logn)。如果要处理的合并和查找序列含有n次合并和m次查找,则最坏情况时间就变成了O(mlogn)。令人振奋的是还可作进一步的改进。这种改进是在查找运算中使用一条称之为压缩的规则。压缩规则:如果j是由i到它的根的路径上的一个结点,则置PARENT(j)←root(i)。使用压缩规则的查找算法描述如下。\n46计算机算法基础图2.25使用加权规则的一棵最坏情况树算法2.6使用压缩规则的查找算法procedureFIND(i)∥查找含有元素i的树根,使用压缩规则去压缩由i到根j的所有结点∥j←iwhilePARENT(j)>0do∥找根∥j←PARENT(j)repeatk←iwhilek≠jdo∥压缩由i到根j的结点∥t←PARENT(k)PARENT(k)←jk←trepeatreturn(j)endFIND仔细考察此算法可以看出,对于单一的查找运算,FIND的计算时间比F增加了一倍。因此,在某些情况下(例如,在大量查找和少量合并运算情况下)使用FIND可使整个处理时间慢下来。但在最坏情况下,相对于只是使用加权规则的处理确实是一个很大的改进。例2.2考虑例2.1中的合并序列用UNION算法所生成的树。现要求作下列8次查找:FIND(8),FIND(8),FIND(8),FIND(8),FIND(8),FIND(8),FIND(8),FIND(8)如果使用过程FEND(8),就要求沿3个PARENT信息段而上;处理这8次查找共需要移动24次。而在使用FIND算法的情况下,第一个FIND(8)要沿3个PARENT信息段而上,然后重置这3个链接信息段;在剩下的7次查找中,每一次沿一个信息段而上且重置一次末级结点8的链接信息段,总共要移动20次。\n第2章导引与基本数据结构47在处理合并和查找序列时,UNION-FIND算法的最坏情况时间如何限界呢?这由引理2.4给出。在叙述这个引理之前,先引进一个增长非常慢的函数α(m,n),这个函数与阿克曼(Ackermann)函数A(p,q)的逆函数有关,函数α(m,n)的定义如下:α(m,n)=min{z≥1|A(z,4m/n)>logn}此处所使用的Ackermann函数是Ackermann函数的变型,其定义如下:2qp=00q=0且p≥1A(p,q)=2p≥1且q=1A(p-1,A(p,q-1))p≥1且q≥2函数A(p,q)是增长速度很高的一个函数,表2.3给出了该函数的部分值。表2.3A(p,q)的部分函数值Aq0123456789101112⋯p0024681012141618202224⋯345678910111210242222222222⋯222222222024222⋯⋯⋯⋯⋯⋯⋯⋯2222Y30242222}65536个2⋯⋯⋯⋯⋯⋯⋯⋯………………………………………事实上,可以证明如下结论成立:(1)A(p,q+1)>A(p,q)(2)A(p+1,q)>A(p,q)2Y2(3)A(3,4)=2}65536个2对α(m,n),假设m≠0,在lognbwhilei>1dothenifc>dy←F(x)thenife>fthenx←1i←i-2elsex←2repeatendifelsex←3endifelsex←4endif2.4写一个布尔函数,由该函数获取一个以0或1为元素的数组A(1∶n),n≥1,并要求确定每个连\n第2章导引与基本数据结构51续为1的序列的大小是否为偶数。分析所写的算法的计算时间。2.5如果习题2.4的函数不是以递归形式写出的话,就请再写出习题2.4的递归程序。2.6写出用链接表表示队列时插入和删去元素的算法ADDQ和DELETEQ。2.7在数组C(0∶n-1)中存放着一个以F和R作为前部和尾部的环形队列。(1)根据F,R和n列出求此环形队列中元素个数的关系式。(2)写出删去此队列中第k个元素的算法。(3)写出将元素Y插入紧接在k个元素之后的算法。(4)求所写的关于(2)、(3)算法的时间复杂度。2.8双端队列是一个可在其两端作插入和删去运算的线性表。如何在一个一维数组中表示这个队列?写出在该队列两端插入和删去元素的算法。2.9考虑一个假想的数据对象X2。X2是可在它的两端插入但只能在一端删去元素的线性表。对X2设计一种链接表表示,并写出插入和删去的算法。试说明所用的表示法的初始条件和边界条件。2.10证明引理2.1。2.11写出在二分检索树T上检索标识符X的算法。假定T中的每一个结点是3个信息段:LCHILR,DATA和RCHILD。求所写算法的计算时间是多少?2.12设计一种能将图存放在穿孔卡片上的合适表示。写出输入这个图并产生它的邻接矩阵的算法。2.13利用习题2.12的外部表示,写一个输入这个图并生成它的邻接表的算法。2.14对于有n个结点和e条边的无向图G,证明其所有结点度数之和为2e。2.15(1)设G是一个具有n个结点的无向连通图。证明G至少有n-1条边,并证明凡具有n-1条边的无向连通图是一棵树。(2)一个有n个结点的强连通图的最小边数是多少?它具有什么样的形状?证明你的答案。2.16对于一个有n个结点的无向图G,证明下列命题等价:(1)G是一棵树;(2)G是连通的,但若去掉任何一条边,则所得到的图是不连通的;(3)对于属于V(G)中的每对不同的结点u和v,恰好存在一条由u到v的简单路;(4)G不包含环且有n-1条边;(5)G是连通的且有n-1条边。\n第3章递归算法递归是程序设计中的一个非常重要的课题。用递归技术设计的算法简单明了,常为人们所采用。递归算法的设计与分析是算法设计与分析的一大类。3.1递归算法的实现机制递归算法(包括直接递归和间接递归子程序)都是通过自己调用自己,将求解问题转化成性质相同的子问题,最终达到求解的目的的。递归算法充分地利用了计算机系统内部机能,自动实现调用过程中对相关且必要的信息的保存与恢复,从而省略了求解过程中许多细节的描述。3.1.1子程序的内部实现原理要理解递归,首先应理解一般子程序的内部实现原理。1.子程序调用的一般形式子程序的调用一般有四种形式,分别如图3.1(a)~3.1(d)所示。图3.1子程序调用的几种形式(a)1次调用;(b)n次调用;(c)嵌套调用;(d)平行调用对于图(a),当主程序执行到callA时,系统自动地保存好1∶语句在指令区的地址(为了叙述方便,不妨视地址为1,下同),便于调用A结束后能从系统获得返回地址,按地址执行下一条指令。对于图(b),主程序中有多次对同一子程序A的调用。在第i次重复调用子程序A之前,系统自动地保存好地址i,便于第i次调用A结束后能顺利地按地址i返回。这种情况与图(a)所示的不一样,保存的地址有多个,但在某一时刻最多只保存一个地址,一旦获得地址返回后,保留的地址i被释放。\n第3章递归算法53对于图(c)、(d),当主程序执行到callA(或callB)时,系统自动地保存好地址1,转入子程序A(或B),在第二次调用子程序callB(或callA)时,再保存好地址2,执行完子程序B(或A)之后,获得地址2返回,继续执行。当子程序A(或B)执行完之后,获得地址1并返回,接着执行到主程序完。图(c)、(d)两种情况与图(b)不同,在某一时刻可能保存多个地址,而且后保存的地址先释放。因此,对返回地址的管理,需要用栈方式实现。由此看出,系统在实现子程序的调用时,要用栈方式管理调用子程序时的返回地址。除了对地址的管理以外,系统要为支持程序的模块化而提供局部变量的概念及实现。在内部实现时,编译系统为每个将要执行的子程序的局部变量(包括形参)分配存储空间,并限定这些局部变量不能由该子程序以外的程序直接访问,子程序之间的数据传送通过参数或全局变量实现。这样就保证了局部变量及其特性的实现。将这些局部变量、返回地址一同放在栈顶,就能较好地实现这一要求。于是,被调过程对其局部变量的操作是对栈顶中相应变量的操作,这些局部变量随着被调过程的执行而存在于栈顶,当被调过程结束时,局部变量从栈顶撤出。2.值的回传在计算机的高级语言中,实参与形参的数据传送以两种方式实现,一是按值传送(比如PASCAL语言中的值参),二是按址传送(比如PASCAL语言中的变参)。由于形实结合的方式不同,在调用前后,值参对应的实参的值是不发生变化的,而变参所对应的实参的值将执行过程中对变参的修改进行回传。对于变参回传值,计算机内部实现有两种方法。(1)两次值传送方式。按指定类型为变参设置相应的存储空间,在执行调用时,将实参值传送给变参,在返回时将变参的值回传给实参。(2)地址传送方式。在内部将变参设置成一个地址,调用时首先执行地址传送,将实参的地址传送给变参,在子程序执行过程中,对变参的操作实际上变成对所对应的实参的操作。在下面讨论递归问题时,对变参的值的回传用第一种方式,即两次值传送方式。除了变参外,还有函数的值的回传。有两个原因使这种回传不能直接进行:一是因为要将仅能在被调用层使用的变量的值传送到调用层的变量,所以不能在调用层直接进行;二是由于各调用操作中实参的多样性,使得传送不能在被调用层直接进行。鉴于此,借用一个全局变量,通过栈实现回传,但是,这种方式会造成栈的结构上的不一致,以及调用操作的次序问题等不便之处。由于在某个时刻,最多只有一个返回操作,所以在后面讨论中,专设一个回传变量的全局变量,用于存放回传值。3.子程序调用的内部操作综上所述,子程序调用的内部实现为两个方面。(1)在执行调用时,系统至少执行的操作。①返回地址进栈,同时在栈顶为被调子程序的局部变量开辟空间;②为被调子程序准备数据:计算实参值,并赋给对应的栈顶的形参;③将指令流转入被调子程序的入口处。(2)在执行返回操作时,系统至少执行的操作。①若有变参或是函数,将其值保存到回传变量中;\n54计算机算法基础②从栈顶取出返回地址;③按返回地址返回;④若有变参或是函数,从回传变量中取出保存的值传送给相应的变量或位置上。3.1.2递归过程的内部实现原理一个递归过程的执行类似于多个子程序的嵌套调用,递归过程是自己调用自己本身代码。如果把每一次的递归调用视为调用自身代码的复制件,则递归实现过程基本上和一般子程序的实现相同。当然,在内部实现时,系统并不是去复制一份程序代码放到内存,而是采用代码共享的方式,其细节不必深究。于是,前面提出的调用前系统做的三件事,与调用结束时系统还要做的四件事,对于递归调用仍亦如此。3.2递归转非递归对于那些本来就要用递归设计求解的问题,在设计其算法时能不能既发挥递归表示直观及易于证明算法正确的特点,又克服由于使用递归而带来总开销增加的不足呢?为此,建议采用如下办法:在算法设计的初期阶段使用递归,一旦所设计的递归算法被证明为正确且确信是一个好算法时,就可以消去递归,把该算法翻译成与之等价的、只使用迭代的算法。这一翻译过程可使用一组简单的转换规则来完成,也可根据具体情况将所得到的迭代算法作进一步的改进,以提高迭代过程的效率。下面介绍的是将直接递归过程翻译成只使用迭代过程的一组规则。对于间接递归过程的处理只需把这组规则稍作修改即可。所谓翻译主要是,将递归过程中出现递归调用的地方,用等价的非递归代码来代替,并对return语句作适当处理。这组规则如下。(1)在过程的开始部分,插入说明为栈的代码并将其初始化为空。在一般情况下,这个栈用来存放参数、局部变量和函数的值,每次递归调用的返回地址也要存入栈。(2)将标号Li附于第一条可执行语句。然后,对于每一处递归调用都用两组执行下列规则的指令来代替。(3)将所有参数和局部变量的值存入栈。栈顶指针可作为一个全程变量来看待。(4)建立第i个新标号Li,并将i存入栈。这个标号的i值将用来计算返回地址。此标号放在规则(7)所描述的程序段中。(5)计算这次调用的各实参(可能是表达式)的值,并把这些值赋给相应的形参。(6)插入一条无条件转向语句,转向过程的开始部分。(7)如果此过程是函数,则对递归过程中含有此次函数调用的那条语句作如下处理:将该语句的此次函数调用部分用从栈顶取回该函数值的代码来代替,其余部分的代码按原描述方式照抄,并将规则(4)中建立的标号附于这条语句上。如果此过程不是函数,则将规则(4)中建立的标号附于规则(6)所产生的转移语句后面的那条语句。以上步骤是消去过程中各处的递归调用,下面对递归过程中出现的return语句进行处理(将纯过程结束处的end语句看成是一条没有值与其相联系的return语句)。在每个有return语句的地方,执行下述规则:\n第3章递归算法55(8)如果栈为空,则执行正常返回。(9)否则,将所有输出参数(即理解为out或inout型的参数)的当前值赋给栈顶上的那些对应的变量。(10)如果栈中有返回地址标号的下标,就插入一条此下标从栈中退出的代码,并把这个下标值赋给一个未使用的变量。(11)从栈中退出所有局部变量和参数的值并把它们赋给对应的变量。(12)如果这个过程是函数,则插入以下指令,这些指令用来计算紧接在return语句后面的表达式并将结果值存入栈顶。(13)用返回地址标号的下标实现对该标号的转向。在一般情况下,使用上述规则都可将一个直接递归过程正确地翻译成与之等价的只使用迭代的过程。它的效率通常比原递归模型要高,进一步简化这程序可使效率再次提高。下面举一个递归化迭代的例子,虽然例中的问题最好是使用迭代来求解,若用递归描述反而变得不很直观,但用它有助于读者对以上规则有一些感性认识。例3.1写一个求数组A(1∶n)中最大元素的过程。算法3.1递归求取最大值procedureMAX1(i)//这是一个函数过程,它返回使A(k)是A(1∶n)中最大元素的最大下标k//globalintegern,A(1∶n),j,k;integeriifiA(j)thenk←ielsek←jendifelsek←nendifreturn(k)endMAX1用几个简单的数据在这算法上实验一下立即就可理解这个递归模型。在运行时间上,由于过程调用和隐式栈管理方面的消费使我们自然考虑到消去递归。算法3.2与算法3.1等价的迭代算法procedureMAX2(i)localintegerj,k;globalintegern,A(1∶n);integeriintegerSTACK(1∶2×n);//规则(1)//top←0//规则(1)//L1:ifiA(j)thenk←Ielsek←jendifelsek←nendififtop=0thenreturn(k)//规则(8)//elseaddr←STACK(top);top←top-1//规则(10)//i←STACK(top);top←top-1//规则(11)//top←top+1;STACK(top)←k//规则(12)//ifaddr=2thengotoL2endif//规则(13)//endifendMAX2这个迭代过程可以通过分析它的运算方式来简化。由于过程只返回到一个地方,所以不必重复存放返回地址。另外,因为在任何时刻只有一个函数值,即当前最大值的下标,故可以把这个值不存入栈而是存放在一个单变量中。由于过程中只有一个参变量i,事实上,每进行一次新的递归,调用i的值就加1,即退出一层递归恢复原来那层递归的i值,只比要退出的这层递归中的i值少1,因此参变量i值也不需要使用栈。这样一来就可将栈完全取消。将i置n并用k存放当前最大值的下标还可取消由gotoL1所产生的循环。经过这一系列简化所导出的过程就是算法3.3。算法3.3算法3.2的改进模型procedureMAX3(A,n)integeri,k,n;i←k←nwhilei>1doi←i-1ifA(i)>A(k)thenk←kendifrepeatreturn(k)endMAX3消去递归的目的是为了产生效率更高的、在计算上等效的迭代程序。因为在某些场合下可能还有更简单的转换规则,所以不必在任何情况下死套前面所叙述的13条规则,而应具体情况具体分析。例如,若过程只有最后一条语句是递归调用,则可通过简单地计算过程调用中实参的值并转向过程开始部分来消去递归,而且不需要栈。下面给出这样一个例子。例3.2求整数a与b的最大公因数。算法3.4求最大公因数procedureGCD(a,b)//假设a>b≥0//ifb=0thenreturn(a)elsereturn(GCD(b,amodb))\n第3章递归算法57endifendGCD消去递归后得到下面程序。算法3.5与算法3.4等价的迭代算法procedureGCD1(a,b)L1:ifb=0thenreturn(a)elset←b;b←amodb;a←t;gotoL1endifendGCD1稍作整理可得算法3.6。算法3.6算法3.5的改进模型procedureGCD2(a,b)whileb≠0dot←b;b←amodb;a←trepeatreturn(a)endGCD2当然,如果所使用机器的有关编译程序能将递归过程编译成有效的代码,则完全不必将递归化为迭代。3.3递归算法设计哪些问题可以用递归求解,这是首先应该明白的。如果同时满足以下3个要求:(1)问题P的描述涉及规模,即P(size);(2)规模发生变化后,问题的性质不发生变化;(3)问题的解决有出口。则可用递归求解,其表现形式为procedureP(参数表);beginif递归出口then简单操作elsebegin简单操作;callP;简单操作end;endp;下面通过几个例子进行示范。例3.3[简单的0/1背包问题]设一背包可容物品的最大质量为m,现有n件物品,质量为m1,m2,⋯,mn,mi,均为正整数,要从n件物品中挑选若干件,使放入背包的质量之和正好为m。由于0/1背包问题中,对第i件物品要么取,要么舍,不许取一部分,因此,这个问题可能有解,也可能无解。于是,用布尔函数描述问题。\n58计算机算法基础本题满足递归求解问题的3个要求。在递归描述中,涉及的必要参数是:当前背包的剩余容量,当前可选物品的质量序列(序列是固定的),序列的最大下标决定了序列的一个子序列。因此,必要的参数是m和n。具体实现时,要找准递归的出口、递推的关系。这两部分是保证递归算法的正确性的必要条件。用knap(m,n)表示问题。(1)先取最后一件物品mn放入包中,若mn=m,则knap←true;(2)若mn0。如果还有可选物品,即n>1,就变为考虑knap(m-mn,n-1)是否可行的问题,也就是当选中的是mn时,看子问题knap(m-mn,n-1)是否有解。如果有解,则转化knap(m,n)knap(m-mn,n-1)转化否则knap(m,n)knap(m,n-1)即放弃mn,在m1,⋯,mn-1上考虑0/1背包问题。(3)若mn>m,则第n件物品不能装入包中,这时如果还剩有可选物品(即n>1),那么转化knap(m,n)knap(m,n-1)根据以上分析,有算法如下:functionknap(m,n)begincasem-mnof:=0:knap←true;:>0:beginifn>1thenifknap(m-mn,n-1)=truethenknap←true;elseknap←knap(m,n-1)elseknap←false;end;:<0:beginifn>1thenknap←knap(m,n-1)elseknap←false;end;endcase;endp;例3.4[n阶Hanoi塔问题]有n个盘子依其半径大小套在柱子A上,其中半径大的在底下。柱子B和C没套盘子,现要将A上的盘子换到C上,要求每次只能移一个,并且不\n第3章递归算法59容许将大盘子压在小盘子的上面。为了叙述方便,在移动盘子的过程中,3根柱子分别称为源柱、辅助柱、目标柱,初始时它们分别是X,Y,Z;对盘子从小到大编号1,2,3,⋯,n。显然,这是一个直接递归问题。下面通过不完全归纳法,找出递归出口和递归关系。1当n=1时,XZ。121当n=2时,XY,XZ,YZ。当n=3时,它和n=2时的递归关系是,将源柱为X、目标柱为Z的3阶Hanoi塔问题分解为源柱为X、目标柱为Y的2阶Hanoi塔问题;将源柱X上的3号盘子挂到目标柱Z上;源柱为Y、目标柱为Z的2阶Hanoi塔问题求解等3个子问题。由于2阶Hanoi塔问题已找到了求解过程,源柱X上的3号盘子直接挂到目标柱Z是可行的。因此,3阶和2阶Hanoi塔问题的递归关系已明确。事实上,2阶和1阶的Hanoi塔问题也存在这种递归关系。推而广之。如果用Hanoi(n,x,y,z)表示n阶Hanoi塔问题,则n阶Hanoi塔问题与n-1阶的Hanoi塔问题之间的递归关系也是由3个子问题合成的:(1)Hanoi(n-1,X,Z,Y);n(2)XZ;(3)Hanoi(n-1,Y,X,Z)。这是递归关系,出口是n-1时的情况。例3.5[棋子移动]有2n个棋子(n≥4)排成一行,白子用0代表,黑子用1代表,n=5的初始状态为00000111111__(右边至少有两个空位)移动规则是:每次必须同时移动相邻两个棋子,颜色不限,移动方向不限;每次移动必须跳过若干棋子,不能调换两个棋子的位置。要求最后成为0101010101下面用不完全归纳法找出口和递归关系:n=400001111__第一步000__11101(4,5)→(9,10)第二步0001011__1(8,9)→(4,5)第三步0__1011001(2,3)→(8,9)第四步010101__01(7,8)→(2,3)第五步__01010101(1,2)→(7,8)n=50000011111__第一步0000__111101(5,6)→(11,12)第二步00001111__01(9,10)→(5,6)这是前8枚棋子为n=4的情况,移法如n=4的第一步到第五步,用同样的方法可完成n=5时的移子。\n60计算机算法基础n=6000000111111__第一步00000__1111101(6,7)→(13,14)第二步0000011111__01(11,12)→(6,7)前10枚棋子为n=5的情况。由此归纳如下。n=4是递归的出口,在退出时做5个移动操作:move(4,5)→(9,10)move(8,9)→(4,5)move(2,3)→(8,9)move(7,8)→(2,3)move(1,2)→(7,8)如果2n个棋子的移动用chess(n)表示,则递归关系是move(n,n+1)→(2n+1,2n+2);move(2n-1,2n)→(n,n+1);callchess(n-1);于是,递归过程如下:procedurechess(n);beginifn=4thenbeginmove(4,5)→(9,10)move(8,9)→(4,5)move(2,3)→(8,9)move(7,8)→(2,3)move(1,2)→(7,8)endelsebeginmove(n,n+1)→(2n+1,2n+2);move(2n-1,2n)→(n,n+1);callchess(n-1);end;endp;例3.6求n个元素的全排列。分析:n=1输出a1;n=2输出a1a2;a2a1;n=3输出a1a2a3;a1a3a2;\n第3章递归算法61a2a1a3;a2a3a1;a3a2a1;a3a1a2;分析n=3,全部排列分成如下3类:(1)a1类:a1之后跟a2,a3的所有全排列;(2)a2类:a2之后跟a1,a3的所有全排列;(3)a3类:a3之后跟a2,a1的所有全排列。将(1)中a1,a2的互换位置,得到(2);将(1)中a1,a3的互换位置,得到(3)。它说明可以用循环的方式重复执行交换位置,后面跟随剩余序列的所有排列,对剩余序列再使用这个方法,这就是递归调用,当后跟的元素没有时就得到递归的边界。于是:procedurerange(a,k,n)//求a的第k到n个元素的全排列//beginifk=nthenprint(a)elsefori←ktondobegina[k]←→a[i];callrange(a,k+1,n)end;endp;在主程序中的调用是callrange(a,1,n)例3.7[自然数拆分]任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和,试求n的所有拆分(用不完全归纳法)。n=2可拆分成2=1+1n=3可拆分成3=1+2=1+1+1n=4可拆分成4=1+3=1+1+2=1+1+1+1=2+2⋯⋯n=7可拆分成7=1+6=1+1+5=1+1+1+4=1+1+1+1+3=1+1+1+1+1+2\n62计算机算法基础=1+1+1+1+1+1+1=1+1+1+2+2=1+1+2+3=1+2+4=1+2+2+2=1+3+3=2+5=2+2+3=3+4用数组a存储完成n的一种拆分。从上面不完全归纳法的分析可知当n=7时,按a[1]分类,有a[1]=1,a[1]=2,⋯,a[1]=n/2,共n/2大类拆分。在每一类拆分时,a[1]←i,a[2]←n-i,从k=2,继续拆分从a[k]开始,a[k]能否再拆分取决于a[k]/2是否大于等于a[k-1]。递归过程的参数t指向要拆分的数a[k],于是有算法proceduresplit(t:int);beginfork←1totdowrite(a[k]);j←t;L←a[j];fori←a[j-1]toL/2dobegina[j]←i;a[j+1]←L-i;callsplit(j+1);end;endp;proceduremain(n)beginfori←1ton/2dobegina[1]←i;a[2]←n-i;callsplit(2);end;endp;3.4递归关系式的计算3.4.1递归算法的时间复杂度分析根据递归算法的设计思想,可以通过建立算法时间复杂度的递归关系式,然后求得算法的时间复杂度。下面给出几个具体的例子。例3.8在前面求数组A[1∶n]的最大元的问题(见例3.1)中,这里采用另一种方法编写递归算法,并用非形式化的形式,即自然语言描述此算法。解递归算法如下:\n第3章递归算法63procedureM[j,j]//这是一个函数过程,它将A[i,j]中最大元赋予A[i,j]//(1)当j,i<2时,采用逐一比较的方法求出A[i:j]的最大元,并给A[i:j]赋相应值;//A[i:j]中元素个数≤2时//(2)当j-i≥2时,i+j①L←2②若M[i,L]≥M[L+1,j]则M[j,j]←M[i,L],否则M[j,j]←M[L+1,j]endM下面对此算法进行时间复杂度分析。设算法的时间复杂度为T(n),其中n=j-i+1,即为A[i,j]中元素个数,则C1n≤2T(n)=n2T+C2n>22n2n所以T(n)=2T+C2=2T2+(2+1)C222lognn2logn-1=22logn+(1+2+2+⋯+22)C222=nC1+(n-1)C2=Θ(n)例3.9一个楼有n个台阶,有一个人上楼有时一次跨一个台阶,有时一次跨两个台阶,编写一个算法,计算此人有几种不同的上楼方法,并分析算法的时间复杂度。解这里设计一个递归算法。procedureH(n)//H(n)是个函数过程,算法将方法数赋予H(n)//(1)当n=1时,H(1)←1(2)当n=2时,H(2)←1(3)当n>2时,H(n)←H(n-1)+H(n-2)//H(n)等于第一次跨一个台阶和第一次跨两个台阶的方法数的和//endH算法时间复杂度分析:设时间复杂度为T(n),则Cn≤2T(n)=T(n-1)+T(n-2)n>22n-1n所以T(n)≤2T(n-1)≤2T(n-2)≤⋯≤2T(1)=O(2)递归算法的特点是思路清晰,算法的描述简洁且易理解,递归算法大多可用数学的形式表示,即表示成一个递归关系式,例如,上例中算法可表示成1n=1H(n)=2n=2H(n-1)+H(n-2)n>2当递归关系式比较复杂时,若直接按递归关系式实现算法,则算法的时间复杂度往往是非多项式时间,这是递归算法的一个不足。一种有效的解决方法是利用数学的方法解递归关系式,将结果或者中间结果编程实现,这样就可以大大地降低时间复杂度。下面将讨论递\n64计算机算法基础归关系式的解法,重点讨论线性常系数递归关系式的解法。3.4.2k阶线性齐次递归关系式的解法定义3.1递归关系式c1an-1+c2an-2+⋯+ckan-k+d(n)n≥k≥1an=ai=bi0≤i≤k-1其中,ck≠0,c1,c2,⋯,ck,b0,b1,⋯,bk-1是给定的常数,称为k阶线性常系数递归关系式。当d(n)=0时,称此递归关系式为齐次的。1.一阶线性齐次递归关系式对于一阶线性齐次递归关系式an=c1an-1c1≠0a0=b0n-1可采用遂步推的方法求得an=C1b。2.二阶线性齐次递归关系式对于二阶线性齐次递归关系式an+ban-1+can-2=0c≠0a0=c0,a1=c122令母函数G(x)=a0+a1x+a2x+⋯,则(1+bx+cx)G(x)计算如下:2G(x)=a0+a1x+a2x+⋯2bxG(x)=ba0x+ba1x+⋯22+)cxG(x)=ca0x+⋯22n(1+bx+cx)G(x)=a0+(a1+ba0)x+(a2+ba1+ca0)x+⋯+(an+ban-1+can-2)x+⋯因为an+ban-1+can-2=02所以(1-bx+cx)G(x)=a0+(a1+ba0)xa0+(a1+ba0)xG(x)=21+bx+cx22和an+ban-1+can-2=0对应的分母1+bx+cx用D(x)表示,即D(x)=1+bx+cx。与22D(x)对应的K(x)=x+bx+c,称为与递归关系对应的特征多项式,K(x)=0,即x+bx+c=0称为特征方程,它的根为2-b±b-4acr1,2=2称为特征根,则2D(x)=1+bx+cx=(1-r1x)(1-r2x)针对r1,r2为实根还是复根,r1是否等于r2?分别讨论如下。(1)当r1与r2为互不相同的实数时,因为∞a0+(a1+ba0)xABnnnG(x)==+=∑[Ar1+Br2]x(1-r1x)(1-r2x)1-r1x1-r2xn=0\n第3章递归算法65所以,当n=0时,A+B=a0;当n=1时,Ar1+Br2=a1。其中,a0111a0r2-a1a1-a0r2a1-a0r1A===B=a1r2r1r2r2-r1r1-r2r2-r1nna1-a0r2na1-a0r1nan=Ar1+Br2=(r1)+(r2)r1-r2r2-r1(2)当r1≠r2时,但r1和r2是一对共轭复根时,设θi-iθr1=ρe,r2=ρenninθn(r1)=ρe=ρ(cosnθ+isinnθ)nn-inθn(r2)=ρe=ρ(cosnθ-isinnθ)nnnnan=A1(r1)+A2(r2)=A1ρ(cosnθ+isinnθ)+A2ρ(cosnθ-isinnθ)nn=(A1+A2)ρcosnθ+(A1-iA2)ρsinnθ由于A1和A2是待定常数,令k1=A1+A2,k2=A1-iA2;k1和k2也是待定常数,故annn=k1ρcosnθ+k2ρsinnθ。2b(3)当r1=r2时,即b=4c,令r=r1=r2=。2a0+(a1+ba0)xABG=2=+2(1-rx)1-rx(1-rx)1222因为2=(1+rx+rx+⋯)/(1-x)=1+2x+3x+⋯(1-rx)∞∞∞hhhhhh所以G=A∑rx+B∑(h+1)rx=∑[A+B(h+1)]rxh=0h=0h=0nnnan=[A+B(n+1)]r=[(A+B)+Bn]r=[h+kn]rbr=2当n=0时,有a0=h,当n=1时,有a1=(h+k)r,则a1k=-a0r所以对于二重根r,a1nan=a0+-a0nrr例3.10解递归关系式an-an-1-12an-2=0a0=3a1=262解由特征方程x-x-12=0,求得特征根为r1=-3,r2=4,所以a1-a0r2na1-a0r12nnan=(r1)+(r2)=5×4-2×(-3)r1-r2r2-r1an=an-1-an-2例3.11解递归关系式a1=1,a2=0解递归关系的特征方程为\n66计算机算法基础2x-x+1=01+-313πππ±ix==±i=cos±isin=e322233nn所以有an=A1cosπ+A2sinπ33⋯⋯13a2=-A1+A2=02213a1=A1+A2=1221因为A1=1,A2=33nπ1nπ所以an=cos+sin333例3.12解递归关系式an+4an-1+4an-2=0a0=1,a1=4解特征方程22x+4x+4=(x-2)2D(x)=(1-2x),r=2nan=(h+kn)2a0=h=1,a1=(1+k)2=4,k=1n则an=(1+n)23.k阶线性齐次递归关系式对于任意阶线性齐次递归关系式an+c1an-1+c2an-2+⋯+ckan-k=0al=dl,0≤l≤k-1,k≥1,dl是已知的常数,1≤k令a0,a1,a2,⋯序列的母函数为2A(x)=a0+a1x+a2x+⋯2kD(x)=a0+c1x+c2x+⋯+ckx2kD(x)A(x)=(1+c1x+c2x+⋯+ckx)(a0+a1x+⋯)2=a0+(a1+c1a0)x+(a2+c1a1+c2a0)x3+(a3+c1a2+c2a1+c3a0)x+⋯k-1+(ak-1+c1ak-2+c2ak-3+⋯+ck-1a0)x+⋯m+(am+c1am-1+c2am-2+⋯+ckam-k)x+⋯由于m≥k,有am+c1am-1+c2am-2+⋯+ckam-k=0,则2k-1D(x)=a0+(a1+c1a0)x+(a2+c1a1+c2a0)x+⋯+(ak-1+c1ak-2+⋯+ck-1a0)x2k-1令pk-1(x)=a0+(a1+c1a0)x+(a2+c1a1+c2a0)x+⋯+(ak-1+c1ak-2+⋯+ck-1a0)x\n第3章递归算法67pk-1(x)所以A(x)=D(x)设特征方程kk-1k-2K(x)≡x+c1x+c2x+⋯+ck=0有s个不同的根r1,r2,⋯,rs,其中hi是ri的重根数,k≤i≤s,所以h1+h2+⋯+hs=k,所以shi(i)pk-1(x)AjA(x)=hhh,根据部分分数法有A(x)=∑∑j,(1-r1x)1(1-r2x)2⋯(1-rs)si=1j=1(1-rix)再利用广义二项式定理,有(1)(1)(1)h-1na1n=(b0+b1n+⋯+bh-1n)r11(2)(2)(2)h-1n+(b0+b1n+⋯+bh-1n2)r2+⋯2(s)(s)(s)h-1n+(b0+b1n+⋯+bh-1ns)rss(i)(i)其中,bj是待定常数,i=1,2,⋯,s,j=0,1,⋯,hj-1。bj可通过解k个线性方程al=dl,0≤l≤k-1来确定。例3.13解递归关系式an-9an-1+26an-2-24an-3=0a0=6,a1=17,a2=5332解递归关系的特征方程为K(x)=x-9x+26x-24=0,因为322x-9x+26x-24=(x-2)(x-7x+12)=(x-2)(x-3)(x-4)nnn所以an=A12+A23+A34再根据初始条件n=0,1,2,分别得A1+A2+A3=62A1+3A2+4A3=174A1+9A2+16A3=5311161116D=234=2A1=1734==322491653916同理得A2=1,A3=2nnn所以an=3×2+3+2×4例3.14解递归关系式an+an-1-11an-2-13an-3+26an-4+20an-5-24an-6=0a0=7,a1=4,a2=37,a3=32,a4=163,a5=64665432解因为K(x)=x+x-11x-13x+26x+20x-24=0324=2×3×132所以K(x)=(x+2)(x-3)(x-1)32D(x)=(1-2x)(1-3x)(1-x)n2nnnan=c1n+c2+c33+c4n(-2)+c5n(-2)+c6(-2)\n68计算机算法基础根据初始条件a0=7,a1=4可得c2+c3+⋯+c6=7c1+c2+3c3-2c4-2c5-2c6=42c1+c2+9c3+16c4+8c5+4c6=373c1+c2+27c3-72c4-24c5-8c6=324c1+c2+81c3+256c4+64c5+16c6=1635c1+c2+243c3-800c4-160c5-32c6=646c1=-1,c2=5,c3=2,c4=-1,c5=4,c6=0n2n即an=-n+5+2×3+(-n+4n)(-2)3.4.3线性常系数非齐次递归关系式的解法对于非齐次递归关系式,没有一个公式化的方法,我们仅就以下几种特殊类型进行讨论。类型一递归关系式为nan+c1an-1+c2an-2+⋯+ckan-k=rb(n)(3.1)al=dl(3.2)其中,k≥1,l=0,1,2,⋯,k-1,c1,c2,⋯,ck,r和dl是给定的常数,b(n)是关于n的一个q次多项式。此类型的解题步骤是kk-1(1)确定r是与式(3.1)对应的特征方程,x+c1x+⋯+ck=0的几重根,根的重数记为m,若r不是特征方程的根,则记m=0。nmm+1m+gi(2)记αn=r(k0n+k1n+⋯+kgn),将αn作为an代入式(3.1),通过比较n项的系数确定k0,k1,⋯,kg。(3)求式(3.1)对应的齐次递归关系式an+c1an-1+c2an-2+⋯+ckan-k=0的含有K个参数的解βn。(4)an=αn+βn,其中K个参数由初始条件式(3.2)确定,上述方法的正确性证明从略。n例3.15解递归关系式an+3an-1-10an-2=(-7)n解对应的特征方程2K(x)≡x+3x-10=(x+5)(x-2)=0有两个特征根:2和-5,7不是特征根,故其m=0。n令αn=(-7)(k0+k1n)代入递归关系式,得nn-1n-2n(-7)(k0+k1n)+3(-7)[k0+k1(n-1)]-10(-7)[k0+k1(n-2)]=(-7)nn-2等式两端除(-7),得2(-7)(k0+k1n)-21(k0-k1+k1n)-10(k0-2k1+k1n)=49n(49k1-21k1-10k1)n+(49k0-21k0-10k0+21k1+20k1)=49n18k1n+(18k0+41k1)=49n\n第3章递归算法69-4149-2009n-200949则k1=49/18,k0=×=,所以,αn=(-7)+n。故解为184832432418nnn492009an=k1(2)+k2(-5)+(-7)n-18324k1和k2是任意常数,由初始条件来确定。2例3.16解递归关系式an-3an-1+2an-2=6n,a0=6,a1=7解递归关系的特征方程为2x-3x+2=(x-1)(x-2)=022n23n右端项6n可以看做是6n(1),故m=1,q=2,αn=(k1n+k2n+k3n)1,代入递归关系式得2323(k1n+k2n+k3n)-3[k1(n-1)+k2(n-1)+k3(n-1)]232+2[k1(n-2)+k2(n-2)+k3(n-2)]=6n3232即[k3n+k2n+k1n]-3[k3n+(k2-3k3)n+(k1-2k2+3k3)n32+(-k1+k2-k3)]-2[k3n+(k2-6k3)n+(k1-4k2+12k3)n2-(2k1-4k2+8k3)]=6n32n(k3-3k3+2k3)+n(k2-3k2+2k2+9k3-12k3)+n(k1-3k1+6k22-9k3+2k1-8k2+24k3)+(3k1-3k2+3k3-4k1+8k2-16k3)=6n22则-3k3n+(-2k2+15k3)n+(-k1+5k2-13k3)=6n-3k3=6所以-2k2+15k3=0-k1+5k2-13k3=0故k3=-2,k2=-15,k1=-4923则有αn=-49n-15n-2nn23因为对应的齐次式的解βn=k1+k22,所以an=αn+βn=-49n-15n-2n+k1+3k22,其中k1和k2由初始条件来确定。类型二递归关系式为nhnan+c1an-1+c2an-1+⋯+ckan-k=r1b1(n)+r2b2(n)+⋯+rsbs(n)al=dl其中,k≥1,l=0,1,2,⋯,k-1,c1,c2,⋯,ck,r1,r2,⋯,rs及dl是给定的常数,bi(n)是关于n的qi次多项式,i=1,2,⋯,S。n此类型的解法是:按类型一的方法,分别求出an+c1an-1c2an-2+⋯+ckan-k=ribi(n)(i)(1)(2)(s)对应的an和βn,其中βn中含k个参数,然后令an=αn+αn+⋯+αn+βn,最后由初始条件确定an中的k个参数。nn例3.17解递归关系式an-an-1-6an-2=10.4-7.3解因为齐次式的特征方程为2x-x-6=(x-3)(x+2)=0利用类型一的方法求出\n70计算机算法基础(1)80n(2)21nnnαn=×4,αn=-×3×n,βn=k1(-2)+k2×335(1)(2)80n21nnn所以an=αn+αn+βn=×4-n×3+k1(-2)+k2×3。其中k1和k2可由初始条35件确定。习题三3.1求下列递归关系的一般解:n(1)an-4an-1=5n(2)an+6an-1=5×3n(3)an-4an-1=4n(4)an+6an-1=4(-6)nn(5)an-4an-1=2×5-3×4nn(6)an-4an-1=7×4-6×5n2(7)an+6an-1=(-6)(2n+3n)2n(8)an-4an-1=(n-n)432(9)an-an-1=4n-6n+4n-1nn(10)an-7an-1+12an-2=5×2-4×3nn(11)an+2an-1-8an-2=3(-4)-14×(3)n(12)an-6an-1+9an-2=3nn(13)an-7an-1+16an-2-12an-3=2+3nnn(14)an-2an-1=2+3+43.2解下列递归关系:(1)an=nan-1,a0=11(2)an-an-1=n,a0=721(3)an-an-1=n33.3解递归关系式:n(1)an=3an-1+3-1,a0=0(2)an-4an-1=4n,a0=03.410个数字(0到9)和4个四则运算符(+,-,×,÷)组成的14个元素。求由其中的n个元素的排列构成一算术表达式的个数。3.5n条直线将平面分成多少个域?假定无三线共点,且两两相交。3.6求n位二进制数中最后3位为010的数的个数。3.7求n位的二进制数中最后3位才第1次出现010的数的个数。\n第4章分治法4.1一般方法当要求解一个输入规模为n且取值又相当大的问题时,直接求解往往是非常困难的,有的甚至根本没法直接求出。正确的方法是,每当遇到这类问题时,首先应仔细分析问题本身所具有的特性,然后根据这些特性选择适当的设计策略来求解。在将这n个输入分成k个不同子集合的情况下,如果能得到k个不同的可独立求解的子问题,其中1ak,只有I3留待求解,在I1子问题中的j=0。在与ak作了比较之后,留待求解的问题(如果有的话)可以再一次使用分治方法来求解。如果求解的问题(或子问题)所选的下标k都是其中间元素的下标(例如,对于I,k=(n+1)/2),则所产生的算法就是通常所说的二分检索。4.2.1二分检索算法算法4.3用SPARKS语言描述了这个二分检索方法。过程BINSRCH有n+2个输入:A,n和x,一个输出j。只要还有待检查的元素,while循环就继续下去。case语句是对3种情况的选择,如果前两个条件不为真,则自动执行“else子句”。过程结束时,如果x不在表中,则j=0,否则A(j)=x。算法4.3二分检索procedureBINSRCH(A,n,x,j)∥给定一个按非降次序排列的元素数组A(1∶n),n≥1,判断x是否出现。若是,置j,使得x=A(j),若非,j=0∥integerlow,high,mid,j,n;low←1;high←nwhilelow≤highdomid←(low+high)/2case:xA(mid):low←mid+1:else:j←mid;returnendcaserepeatj←0endBINSRCH判断BINSRCH是否为一个算法,除了上段所述之外,还必须使x和A(mid)的比较有恰当的定义。如果A的元素是整数、实数或字符串,则这些比较运算都可用适当的指令正确完成。另外,还需判断BINSRCH是否终止。关于这一点留待证明算法正确性时回答。在对算法的正确性作出证明以前,为了增加对此算法的置信度,不妨用一个具体的例子来模拟算法的执行。例4.1假定在A(1∶9)中顺序存放着以下9个元素:-15,-6,0,7,9,23,54,82,101。要求检索下列x的值:101,-14和82是否在A中出现。这是两次成功和一次不成功的检索。在模拟算法执行时只需追踪变量low,high和mid,其追踪轨迹由表4.1列出。关于程序正确性的证明至今还是一个尚未解决的课题。在这里仅给出BINSRCH正确性的一种“非形式证明”。定理4.1过程BINSRCH(A,n,x,j)能正确地运行。证明假定x>A(mid)之类的比较运算能恰当地被执行,且过程中所有语句都能按所\n74计算机算法基础表4.1例4.1的实际运行轨迹x=101x=-14x=82lowhighmidlowhighmidlowhighmid19519519569714269789811199921找不到898找到找到要求的那样工作。最初,low=1,high=n,n≥0,且A(1)≤⋯≤A(n)。如果n=0,则不进入while循环,j被置成0,算法终止。否则,就会进入while循环去查找x是否是A中的元素,对于每一次循环,有可能被检查比较的元素是A(low),A(low+1),⋯,A(mid),⋯,A(high)。如果x=A(mid),则将mid的值送j,算法成功地终止。否则,若xA(mid),则通过low←mid+1把low增加到mid+1来缩小检索范围且不会影响检索结果。又因为low和high都是整型变量,按上述方式缩小检索区总可在有限步内使low变得比high大。如果出现这种情况,则说明x不在A中,退出循环,j被置0,算法终止。证毕。BINSRCH需要的空间是很容易确定的,它要用n个位置存放数组A,还要有存放变量low,high,mid,x和j的5个空间位置。因此,所需的空间位置是n+5。至于它的计算时间,则要分别考虑最好、平均和最坏3种情况。为了清楚起见,对于检索问题还需将最好情况区分为成功检索的最好情况和不成功检索的最好情况来加以分析。对于平均和最坏情况的分析也作类似的处理。显然,x只有在取A中任一元素的情况下,才会出现成功的检索,所以成功的检索一共有n种,而为了测试所有不成功的检索,只需将x取n+1个不同的值。因此,在算出BINSRCH在x这2n+1种取值情况下的执行时间之后,求取它在最坏、平均和最好情况的计算时间就不难了。在对算法的一般情况分析以前,不妨先对例4.1的实例作出分析,看看算法在频率计数上有些什么特性。从算法中可以看到,所有的运算基本上都是在进行比较和数据传送。前两条和最末一条是赋值语句,频率计数均为1。在while循环中,我们集中考虑x和A中的元素比较,而其它运算的频率计数显然与这些元素比较运算的频率计数具有相同的数量级。假定只需一次比较就可确定case语句控制是的3种情况的哪一种,进而找这9个元素中的每一个所需的元素比较次数:A(1)(2)(3)(4)(5)(6)(7)(8)(9)元素-15-6079235482101比较次数323413234要找到一个元素至少要进行1次比较,至多要进行4次比较。对找到的9项比较次数取平均值,即可得到每一次成功检索的平均比较次数为25/9≈2.77。不成功检索的终止方式取决于x的值,总共有9+1=10种可能的方式。如果xA(5),下一次则与其右结点7所示元素比较;若xA(mid)thenlow←mid+1elsej←—mid;returnendif(*)endif在这种情况下,有时要作两次元素比较。下面介绍一种二分检索的有趣变型BINSRCH1。在BINSRCH1的while循环中x和A(mid)之间只用作一次比较。算法4.4每次循环作一次比较的二分检索procedureBINSRCH1(A,n,x,j)∥除n>0外,其余说明与BINSRCH同∥integerlow,high,mid,j,n;low←1;high←n+1∥high总比可能的取值大1∥whilelowA(n)时),它的元素比较数就是BINSRCH1的两倍。然而,对于任何一种成功的检索(例如,当x=A(mid)时),BINSRCH1平均要比BINSRCH多作(logn)/2次比较。BINSRCH1的最好、平均和最坏情况时间对于成功和不成功的检索\n第4章分治法77都是Θ(logn)。此结论的证明留作习题。4.2.2以比较为基础检索的时间下界对于在n个已分类元素序列上(即已按非降次序或非增次序排列的n个元素序列),在检索某元素是否出现问题时,能否预计还存在有以元素比较为基础的另外的检索算法,它在最坏情况下比二分检索算法在计算时间上有更低的数量级呢?下面就来讨论这个问题。假设n个元素A(1∶n)有关系A(1)maxthenmax←A(i)endififA(i)max为假时,才有必要比较A(i)maxthenmax←A(i)elseifA(i)2k当n是2的幂时,即对于某个正整数k,n=2,有T(n)=2T(n/2)+2=2(2T(n/4)+2)+2=4T(n/4)+4+2⋯k-1i=2T(2)+∑21≤i≤k-1k-1k=2+2-2=3n/2-2注意:当n是2的幂时,3n/2-2是最好、平均及最坏情况的比较。此数和直接算法的比较数2n-2相比,它少了25%。可以证明,任何一种以元素比较为基础的找最大和最小元素的算法,其元素比较下界均为|3n/2|-2次。因此,过程MAXMIN在这种意义上是最优的。那么,这是否意味着此算法确实比较好呢?不一定。其原因有如下两个。一是MAXMIN要求的存储空间比直接算法多。给出n个元素就有logn+1级的递归,而每次递归调用需要保留到栈中的有i,j,fmax,fmin和返回地址五个值。虽然可用第1章递归化迭代的规则去掉递归,但所导出的迭代模型还需要一个其深度为logn数量级的栈。二是当元素A(i)和A(j)的比较时间与i和j的比较时间相差不大时,过程MAXMIN并不可取。为说明问题,假设元素比较与i和j间的比较时间相同,又设MAXMIN的频率k计数为C(n),n=2,k是某个正整数,并且对case语句的前两种情况用i≥j-1来代替i=j和i=j-1这两次比较,这样,只用对i和j-1作一次比较就足以实现被修改过的这条case语句。于是MAXMIN的频率计数2n=2C(n)=2C(n/2)+3n>2解此关系式可得C(n)=2C(n/2)+3=4C(n/4)+6+3⋯⋯k-1ikk-1=2C(2)+3∑2=2+3×2-30≤i≤k-2=5n/2-3。而STRAITMAXMIN的比较数是3(n-1)(包括实现for循环所要的比较)。尽管它比5n/2-3大些,但由于递归算法中i,j,fmax,fmin进出栈所带来的开销,因此MAXMIN在这种情况下反而比STRAITMAXMIN还要慢些。根据以上分析可以得出结论:如果A的元素间的比较远比整型变量的比较代价昂贵,则分治方法产生效率较高(实际上是最优)的算法;反之,就得到一个效率较低的程序。因此,\n第4章分治法81分治策略只能看成是一个较好的然而并不总是能成功的算法设计指导。4.4归并分类4.4.1基本方法给定一个含有n个元素(又叫关键字)的集合,如果要把它们按一定的次序分类(本节中自始至终假定按非降次序分类),最直接的方法就是插入法。对A(1∶n)中元素作插入分类的基本思想是:forj←2tondo将A(j)放到已分类集合A(1∶j-1)的正确位置上repeat从中可以看出,为了插入A(j),有可能移动A(1∶j-1)中的所有元素,因此可以预计该算法在时间特性上不会太好,算法具体描述如下:算法4.7插入分类procedureINSERTIONSORT(A,n)∥将A(1∶n)中的元素按非降次序分类,n≥1∥A(0)←-∞∥开始时生成一个虚拟值∥forj←2tondo∥A(1∶j-1)已分类∥item←A(j);i←j-1whileitemmidthenfork←jtohighdo∥处理剩余的元素∥B(i)←A(k);i←i+1repeatelsefork←htomiddoB(i)←A(k);i←i+1repeatendiffork←lowtohighdo∥将已归并的集合复制到A∥A(k)←B(k)repeatendMERGE在过程MERGESORT开始以前,这n个元素应放在A(1∶n)中。使用callMERGESORT(1,n)语句将使关键字重新排列成非降序列而存于A中。例4.3使用归并分类算法,将含有10个元素的数组A=(310,285,179,652,351,423,861,254,450,520)按非降次序分类。过程MERGESORT首先把A分成两个各有5个元素的子集合,然后把A(1∶5)分成大小为3和2的两个子集合,再把A(1∶3)分成大小为2和1的子集合,最后将A(1∶2)分成各含一个元素的两个子集合,至此就开始归并。此时的状态可排成下列形式:(310|285|179|652,351|423,861,254,450,520)\n第4章分治法83其中,直杠表示子集合的边界线。归并A(1)和A(2)得(285,310|179|652,351|423,861,254,450,520)再归并A(1∶2)和A(3)得(179,285,310|652,351|423,861,254,450,520)然后将A(4∶5)分成两个各含一个元素的子集合,再将两个子集合归并得(179,285,310|351,652|423,861,254,450,520)接着归并A(1∶3)和A(4∶5)得(179,285,310,351,652|423,861,254,450,520)此时算法就返回到MERGESORT首次递归调用的后继语句的开始处,即准备执行第二条递归调用语句。又通过反复地递归调用和归并,将A(6∶10)分好类,其结果如下:(179,285,310,351,652|254,423,450,520,861)这时就有了两个各含5个元素的已分好类的子集合。经过最后的归并得到分好类的结果:(179,254,285,310,351,423,450,520,652,861)图4.4所示的是在n=10的情况下,由MERGESORT所产生的对它自己进行一系列递归调用的树表示。每个结点中的一对值都是参变量low和high的值。值得注意的是,集合的分割一直进行到产生出只含单个元素的子集合为止。图4.5所示的则是一棵表示MERGESORT对过程MERGE调用的树。每个结点中的值依次是参变量low,mid和high的值。例如,含有值1,2,3的结点表示A(1∶2)和A(3)中元素的归并。图4.4用树表示MERGESORT(1,10)的调用图4.5用树表示对MERGE的调用如果归并运算的时间与n成正比,则归并分类的计算时间可用递归关系式描述如下:an=1,a是常数T(n)=2T(n/2)+cnn>1,c是常数k当n是2的幂即n=2时,可以通过逐次代入求出其解:T(n)=2(2T(n/4)+cn/2)+cn=4T(n/4)+2cn=4(2T(n/8)+cn/4)+2cn⋯k=2T(1)+kcn=an+cnlognkk+1k+1如果2A(j)的结果。在描述算法各种可能执行的比较树中,每个内结点用比较对“i∶j”来代表A(i)和A(j)的比较,当A(i)A(j)时进入右分枝。各外部结点表示此算法的终止。从根到外结点的每一条路径分别与一种唯一的排列相对应。由于n个关键字有n!种排列,而每种排列可以是某种特定输入下的分类结果,因此比较树必定至少有n!个外结点,每个外结点表示一种可能的分类序列。图4.6给出了对3个关键字分类的一棵二元比较树,其边上的不等式表示此条件成立时控制的转向。图4.6对3个关键字分类的比较树对于任何一个以比较为基础的算法,在描述其执行的那棵比较树中,由根到某外结点的\n第4章分治法87路径长度表示生成该外结点中那个分类序列所需要的比较次数。因此,这棵树中最长路径的长度(即此树的高度)就是该算法在最坏情况下所作的比较次数。从而,要求出所有以比较为基础的对n个关键字分类的算法最坏情况下界,只需求出这些算法对应的比较树的最小高度,设它为T(n)。如果一棵二元树的所有内结点的级数均小于或等于k,对k施行归纳k可以证得该树至多有2个外结点(比内结点数多1)。令T(n)=k,则T(n)n!≤2n/2而当n>1时有n!≥n(n-1)(n-2)⋯(|n/2|)≥(n/2)因此,对于n≥4有T(n)≥(n/2)log(n/2)≥(n/4)logn故以比较为基础的分类算法的最坏情况的时间下界为Ω(nlogn)。4.5快速分类4.5.1快速分类算法由著名的计算机科学家霍尔(C.A.R.Hoare)给出的快速分类算法也是根据分治策略设计的一种高效率的分类算法。它虽然也是把文件A(1∶n)分成两个子文件,但与归并分类算法有所不同:在被分成的两个子文件以后不再需要归并。于是,被分成的两个子文件至少必须满足一子文件中的所有元素都小于或等于另一子文件的任何一个元素。这是通过重新整理A(1∶n)中元素的排列顺序来达到的,其实现的基本思想如下:选取A的某个元素,譬如说t=A(s),然后将其它元素重新排列,使A(1∶n)中所有在t以前出现的元素都小于或等于t,而所有在t后面出现的元素都大于或等于t。文件的这种重新整理叫做划分(par-titioning),元素t称为划分元素(partitionelement)。因此,所谓快速分类就是通过反复对产生的文件进行划分来实现的。过程PARTITION完成对文件A(m∶p-1)的划分。在过程中,假定第一个元素A(m)是划分元素(这种假定是非本质的,仅仅是为了方便而已,后面将会看到把其它元素选成划分元素比选第一项要好些的情况)。A(p)不属于文件A(m∶p-1),且假定A(p)≥A(m),引进A(p)是为了在特殊情况下能控制程序顺利进行。因此,在对PARTITION初次调用,即m=1,p-1=n时,则必须将A(n+1)定义成大于或等于A(1∶n)的所有元素。子过程INTERCHANGE(x,y)执行以下赋值语句:temp←x;x←y;y←temp。算法4.12用A(m)划分集合A(m∶p-1)procedurePARTITION(m,p)∥在A(m),A(m+1),⋯,A(p-1)中的元素按如下方式重新排列:如果最初t=A(m),则在重排完成之后,对于m和p-1之间的某个q,有A(q)=t,并使得对于m≤k1S(n)=0n≤1\n第4章分治法91它比2logn小。如同4.4节所述,INSERTIONSORT在n小于16的情况下执行是相当快的,因此,QUICKSORT2可以在每当q-p<16时用INSERTIONSORT来加速。QUICKSORT和MERGESORT的平均情况时间都是O(nlogn),在平均情况下哪个更快一些呢?这里不给出其理论证明,而是给出一组在IBM370/158机上测试的数据来作近似的比较。这两个算法都是用PL/1编程的递归模型,对于QUICKSORT,PARTITION中划分元素采用了三者取中的规则(即划分元素是A(m),A(m+p-1)/2和A(p-1)中值居中者)。输入数据集由(0,10000)的随机整数组成。表4.3记录了以毫秒为单位的实际平均计算时间。研究这个表立即就可看出,对应于n的所有取值,QUICKSORT都比MERGESORT快。而且还可看到,每当n值增加500,QUICKSORT的平均计算时间大致增加250ms;MERGESORT则不规则一些,n每增加500,其平均计算时间大约增加350ms。表4.3分类算法的平均计算时间/msn10001500200025003000350040004500MERGESORT500750105014001650200022502650QUICKSORT40060085010501300155018002050n50005500600065007000750080008500MERGESORT29003450350038504250455049505200QUICKSORT230026502800300033503700390041004.6选择问题上述PARTITION算法也可用来求选择问题的有效解。在这一问题中,给出n个元素A(1∶n),要求确定第k小的元素,如果划分元素v测定在A(j)的位置上,则有j-1个元素小于或等于A(j),且有n-j个元素大于或等于A(j)。因此,若kj,则第k小元素是A(j+1∶n)中第(k-j)小元素。所导出的算法是过程SELECT(算法4.15)。此过程把第k小元素放在A(k),并划分剩余的元素,使得A(i)≤A(k),1≤i0,使得k1k-ikTA(n)≤cn+∑TA(n-i)+∑TA(i-1)n≥2n1≤in,因此SELECT2的复杂度不是O(n)。如果取r=9,则至少有2.5\n96计算机算法基础(n/9)个元素小于或等于v,且至少也有这么多个元素大于或等于v。因此,对于n≥90,|S|1和|R|都至多是n-2.5(n/9)+[2.5(n/9)]=n-1.25(n/9)≤31n/36+1.25≤63n/72。2从而可以得到下面的递归式:c1nn<90T(n)=T(n/9)+T(63n/72)+c1nn≥90其中,c1是一个适当的常数。可以施归纳法于n证明:对于n≥1,有T(n)≤72c1nSELECT2所需要的附加空间除了几个简单变量所需要的空间外,还需要作为递归栈使用的空间。由于步骤7的递归调用是这次执行SELECT2的最后一条语句,故容易将它消去递归。因此,仅步骤4的递归需要栈空间。递归的最大深度是logn,所以需要O(logn)的栈空间。4.6.3SELECT2的实现在用SPARKS写实现SELECT2的算法以前,需要解决两个问题:①怎样找大小为r的集合的中间值?②将步骤3所找到的n/r个中间值放在什么地方?由于所使用的r值都不大(例如r=5或9),因此,可用算法4.7的改进型INSERTIONSORT(A,i,j)对每组的r个元素实行有效的分类。分类后的在A(i,j)中间的那个元素就是这r个元素的中间值。把各组所找到的中间值存放在包含所有组全部元素的那个数组的前部,这对于处理步骤4是很方便的。例如,如果正在找A(m∶p)的第k小元素,就将这些中间值依次存放在A(m),A(m+1),A(m+2),⋯中。实现SELECT2的SPARKS描述是过程SEL(算法4.17)。在SEL中,通过第6行和21行的loop—repeat以及16行到21行的语句等价代换了步骤7的递归调用。INTERCHANGE(x,y)仅是交换x和y的值。算法4.17SELECT2的SPARKS描述lineprocedureSEL(A,m,p,k)∥返回一个i,使得i∈[m,p],且A(i)是A(m∶p)中第k小元素,r是一个全程变量,其取值为大于1的整数∥1globalr2integern,i,j3ifp-m+1≤rthencallINSERTIONSORT(A,m,p)4return(m+k-1)5endif6loop7n←p-m+1∥元素数∥8fori←1to|n/r|do∥计算中间值∥9callINSERTIONSORT(A,m+(i-1)*r,m+i*r-1)∥将中间值收集到A(m∶p)的前部∥10callINTERCHANGE(A(m+i-1),A(m+(i-1)*r+|r/2|-1))11repeat\n第4章分治法9712j←SEL(A,m,m+|n/r|-1,||n/r|/2|)∥mm∥13callINTERCHANGE(A(m),A(j))∥产生划分元素∥14j←p+115callPARTITION(m,j)16case17∶j-m+1=k∶return(j)18∶j-m+1>k∶p←j-119∶else∶k←k-(j-m+1);m←j+120endcase21repeat22endSEL4.7斯特拉森矩阵乘法二维数组无论在数值还是在非数值计算领域中都是一种相当基本而又极其重要的抽象数据结构,矩阵则是它的数学表示,因此,在研究矩阵的基本运算时,尽可能改进运算的效率无疑是件非常重要的工作。矩阵加和矩阵乘是两种最基本的矩阵运算。设A和B是两个n×n矩阵,这两个矩阵相加指的是它们对应元素相加作为其和矩阵的相应元素,因此它们的和矩阵仍是一个n×n2矩阵,记为C=A+B。其时间显然为Θ(n)。如果将矩阵A和B的乘积记为C=AB,那么C也是一个n×n矩阵,乘积C的第i行第j列的元素C(i,j)等于A的第i行和B的第j列对应元素乘积的和,可表示为C(i,j)=∑A(i,k)B(k,j)1≤i,j≤n(4.6)1≤k≤n2按上式计算C(i,j)需要做n次乘法和n-1次加法,而乘积矩阵C有n个元素,因此,由矩3阵乘定义直接产生的矩阵乘算法的时间为Θ(n)。人们长期以来对改进矩阵乘法的效率作过不少尝试,设计了一些改进算法,但在计算时3间上都仍旧囿界于n这一数量级。直到1969年斯特拉森(V.Strassen)利用分治策略并加上一些处理技巧设计出一种矩阵乘算法以后,才在计算时间数量级上取得突破。他所设计2.81的算法的计算时间是O(n)。此结果在第一次发表时曾震动了数学界。下面介绍斯特拉森矩阵算法的基本设计思想与主要处理技巧。为简单起见,假定n是2的幂,即存在一非负整数k,使得n=2。在n不是2的幂的情况下,则可对A和B增加适当的全零行和全零列,使其变成级是2的幂的方阵。按照分治设计策略,首先可以将A和B都分成4个(n/2)×(n/2)矩阵,于是A和B就可以看成是两个以(n/2)×(n/2)矩阵为元素的2×2矩阵。对这两个2×2矩阵施以通常的矩阵乘法运算(即通过(2.6)式计算乘积矩阵元素),可得A11A12B11B12C11C12=(4.7)A21A22B21B22C21C22其中,\n98计算机算法基础C11=A11B11+A12B21,C12=A11B12+A12B22,C21=A21B11+A22B21,C22=A21B12+A22B22(4.8)使用通常的矩阵乘法和加法,计算(n/2)×(n/2)矩阵C11,C12,C21和C22的各元素的值以及C=AB各乘积元素的值,可以直接证明C11C12C=AB=C21C22如果分块子矩阵的级(这里是n/2级方阵)大于2,则可以继续将这些子矩阵分成更小的方阵,直至每个子方阵只含一个元素,以至可以直接计算其乘积为止。这样的算法显然是由分治策略设计而得的。为了用式(4.8)计算AB,需要执行以(n/2)×(n/2)矩阵为元素的82次乘法和4次加法。由于每两个n/2级方阵相加可在对于某个常数c而言的cn时间内完成,如果所得到的分治算法的时间用T(n)表示,则可以得到下面的递归关系式:bn≤2T(n)=28T(n/2)+dnn>2其中,b和d是常数。3求解这个递归关系式得到T(n)=O(n),与通常的矩阵乘算法计算时间具有相同的数32量级。由于矩阵乘法比矩阵加法的花费要大(O(n)对O(n)),斯特拉森发现了在分治设计的基础上使用一种减少乘法次数而让加减法次数相应增加的处理方法来计算式(4.8)中的Cij。其处理方法是,先用7个乘法和10个加(减)法来算出下面7个(n/2)×(n/2)矩阵:P=(A11+A22)(B11+B22)Q=(A21+A22)B11R=A11(B12-B22)S=A22(B21-B11)(4.9)T=(A11+A12)B22U=(A21-A11)(B11+B12)V=(A12-A22)(B21+B22)然后用8个加(减)法算出这些Cij:C11=P+S-T+VC12=R+T(4.10)C21=Q+SC22=P+R-Q+U以上共用7次乘法和18次加(减)法。由T(n)所得出的递归关系式是bn≤2T(n)=(4.11)27T(n/2)+ann>2其中,a和b是常数。求解这个递归关系式,得22k-1kT(n)=an(1+7/4+(7/4)+⋯+(7/4))+7T(1)\n第4章分治法992lognlogn≤cn(7/4)+7c是一个常数log4+log7-log4log7=cn+nlog7log72.81=(c+1)n=O(n)≈O(n)在斯特拉森之后,有很多人继续设法改进他的结果,值得指出的是J.E.Hopcroft和L.R.Kerr已经证明了两个2×2矩阵相乘必须要用7次乘法,因此要进一步获得改进,则需考虑3×3或4×4等更高级数的分块子矩阵或者用完全不同的设计策略。最后,要提请读者注意的是,斯特拉森矩阵乘法目前还只具有理论意义,因为只有当n相当大时它才优于通常的矩阵乘法。经验表明,当n取为120时,斯特拉森矩阵乘法与通常的矩阵乘法在计算时间上仍无显著差别。尽管如此,它还是给出了有益的启示:即使是由定义出发所直接给出的明显算法并非总是最好的。斯特拉森矩阵乘法可能为获得更有效和在计算机上切实可行的算法奠定了基础。关于矩阵乘法的更深入的讨论,读者可参看《线性代数与多项式的快速算法》(游兆永编,上海科学技术出版社1980年出版)。习题四4.1利用2.5节的规则,将过程DANDC(算法4.1)转换成迭代形式DANDC2,使得由DANDC2通过化简能够得到过程DANDC1(算法4.2)。4.2在下列情况下求解2.1节的递归关系式:g(n)n足够小T(n)=2T(n/2)+f(n)否则当①g(n)=O(1)和f(n)=O(n);②g(n)=O(1)和f(n)=O(1)时。4.3根据4.2节开始所给出的二分检索策略,写一个二分检索的递归过程。4.4作一个“二分”检索算法,它将原集合分成1/3和2/3大小的两个子集合。将这个算法与算法4.3相比较。4.5作一个“三分”检索算法,首先检查n/3处的元素是否等于某个x的值,然后检查2n/3处的元素。这样,或者找到x,或者把集合缩小到原来的1/3。分析此算法在各种情况下的计算复杂度。4.6对于含有n个内部结点的二元树,证明E=I+2n其中,E、I分别为外部和内部路径长度。4.7证明BINSRCH1的最好、平均和最坏情况的计算时间对于成功和不成功的检索都是Θ(logn)。4.8将递归过程MAXMIN翻译成在计算上等价的非递归过程。4.9按以下处理思想写一个找最大最小元素的迭代程序并分析它的比较次数。它不是以分治策略为基础的,但可能比MAXMIN更有效。先比较相邻的两个元素,然后,将较大的元素与当前最大元素相比较,较小的元素与当前最小元素相比较。4.10过程MERGESORT的最坏情况时间是O(nlogn)。它的最好情况时间是什么?能说归并分类的时间是Θ(nlogn)吗?4.11写一个“由底向上”的归并分类算法,从而取消对栈空间的需要。4.12如果一个分类算法在结束时相同元素出现的顺序与集合没分类以前一样,则称此算法是稳定的。归并分类算法是稳定的算法吗?\n100计算机算法基础4.13QUICKSORT是一种不稳定的分类算法。但是,若把A(i)中的关键字变成A(i)*n+i-1,那么,所有的关键字都不相同了。在分类之后,如何将关键字恢复成原来的值呢?4.14讨论在过程PARTITION(即算法4.12)中,将语句ifiv,那么,在PARTITION中还要作哪些改变?写出作出修改后的划分集合的算法并将它与原PARTITION相比较。4.17比较MERGESORT1和QUICKSORT2这两个分类算法。设计对这两个算法的平均和最坏情况时间进行比较的数据集。4.18如何利用插入分类算法INSERTIONSORT来改进快速分类算法QUICKSORT2?写出这一改进算法。为此,算法4.7需作哪些修改?写出INSERTIONSORT的修改模型。4.19画出对4个元素分类的比较树。4.20(1)假设只有在A中元素各不相同时才使用算法SELECT2。问r取下列哪些值能保证算法在最坏情况下用O(n)时间就可执行完毕?证明你的结论:r=3,7,9,11。(2)如果将r选得较大(但,是适当的),那么,SELECT2的计算时间是增加还是减少?为什么?4.21在题4.20中,将r=3,7,9,11改为r=7,11,13,15并且在不限制A中元素各不相同的情况下,再完成题4.20。34.22用SPARKS语言写一个计算时间为O(n)的两个n×n矩阵相乘的算法,并确定作乘法和加法的精确次数。4.23通过手算证明由(4.9)和(4.10)式确实能得到C11,C12,C21和C22的正确值。4.24在n是3的幂的情况下,考虑n×n级矩阵的乘法。使用分治策略设计的算法可以减少3×3级2.81矩阵乘法通常所要作的27次乘法。对于3×3级矩阵乘法必须作多少次乘法才使得计算时间比O(n)小呢?对于4×4级矩阵乘法作同样的讨论。4.25斯特拉森算法的另一种形式是用下面的恒等式来计算式(4.8)中的Cij(这样处理共用了7次乘法和15次加法):S1=A21+A22M1=S2S6T1=M1+M2S2=S1-A11M2=A11B11T2=T1+M4S3=A11-A21M3=A12B21S4=A12-S2M4=S3S7S5=B12-B11M5=S1S5S6=B22-S5M6=S4B22S7=B22-B12M7=A22S8S8=S6-B21Cij是C11=M2+M3C12=T1+M5+M6C21=T2-M7C22=T2+M5证明由这些恒等式确实可计算出C11,C12,C21和C22的正确值。\n第5章贪心方法5.1一般方法在现实世界中,有这样一类问题:它有n个输入,而它的解就由这n个输入的某个子集组成,只是这个子集必须满足某些事先给定的条件。把那些必须满足的条件称为约束条件;而把满足约束条件的子集称为该问题的可行解。显然,满足约束条件的子集可能不止一个,因此,可行解一般来说是不唯一的。为了衡量可行解的优劣,事先也给出了一定的标准,这些标准一般以函数形式给出,这些函数称为目标函数。那些使目标函数取极值(极大值或极小值)的可行解,称为最优解。对这一类需求取最优解的问题,又可根据描述约束条件和目标函数的数学模型的特性或求解问题方法的不同进而细分为线性规划、整数规划、非线性规划、动态规划等问题。尽管各类规划问题都有一些相应的求解方法,但其中的某些问题,还可用一种更直接的方法来求解,这种方法就是贪心方法。贪心方法是一种改进了的分级处理方法。它首先根据题意,选取一种量度标准;然后按这种量度标准对这n个输入排序,并按序一次输入一个量。如果这个输入和当前已构成在这种量度意义下的部分最优解加在一起不能产生一个可行解,则不把此输入加到这部分解中。这种能够得到某种量度意义下的最优解的分级处理方法称为贪心方法。要注意的是,对于一个给定的问题,往往可能有好几种量度标准。初看起来,这些量度标准似乎都是可取的。但实际上,用其中的大多数量度标准作贪心处理所得到该量度意义下的最优解并不是问题的最优解,而是次优解。尤其值得指出的是,把目标函数作为量度标准所得到的解也不一定是问题的最优解。因此,选择能产生问题最优解的最优量度标准是使用贪心法设计求解的核心问题。在一般情况下,要选出最优量度标准并不是一件容易的事,不过,一旦能选择出某个问题的最优量度标准,那么用贪心方法求解这个问题则特别有效。贪心方法可以用下面的抽象化控制来描述。算法5.1贪心方法的抽象化控制procedureGREEDY(A,n)∥A(1∶n)包含n个输入∥solution←ꯁ∥将解向量solution初始化为空∥fori←1tondox←SELECT(A)ifFEASIBLE(solution,x)thensolution←UNION(solution,x)endifrepeat\n102计算机算法基础return(solution)endGREEDY函数SELECT的功能是按某种最优量度标准从A中选择一个输入,把它的值赋给x并从A中消去它。FEASIBLE是一个布尔函数,它判定x是否可以包含在解向量中。UNION将x与解向量结合并修改目标函数。过程GREEDY描述了用贪心策略设计算法的主要工作和基本控制路线。一旦给出一个特定的问题,就可将SELECT,FEASIBLE和UNION具体化并付诸实现。5.2背包问题本节介绍使用贪心设计策略来解决更复杂的问题———背包问题。已知有n种物品和一个可容纳M重量的背包,每种物品i的重量为wi。假定将物品i的一部分xi放入背包就会得到pixi的效益,这里,0≤xi≤1,pi>0。采用怎样的装包方法才会使装入背包物品的总效益最大呢?显然,由于背包容量是M,因此,要求所有选中要装入背包的物品总重量不得超过M。如果这n件物品的总重量不超过M,则把所有物品装入背包自然获得最大效益。如果这些物品重量的和大于M,则在这种情况下该如何装包呢?这是本节所要解决的问题。由以上叙述,可将这个问题形式描述如下:极大化∑pixi(5.1)1≤i≤n约束条件∑wixi≤M(5.2)1≤i≤n0≤xi≤1,pi>0,wi>0,1≤i≤n(5.3)其中,式(5.1)是目标函数,式(5.2)和式(5.3)是约束条件。满足约束条件的任一集合(x1,⋯,xn)是一个可行解,使目标函数取最大值的可行解是最优解。例5.1考虑下列情况下的背包问题:n=3,M=20,(p1,p2,p3)=(25,24,15),(w1,w2,w3)=(18,15,10)。其中的4个可行解是(x1,x2,x3)∑wixi∑pixi①(1/2,1/3,1/4)16.524.25②(1,2/15,0)2028.2③(0,2/3,1)2031④(0,1,1/2)2031.5在这4个可行解中,第④个解的效益值最大。下面将可看到,这个解是背包问题在这一情况下的最优解。为了获取背包问题的最优解,必须把物品放满背包。由于可以只放物品i的一部分到背包中去,因此这一要求是可以达到的。如果用贪心策略来求解背包问题,则正如5.1节中所说的一样,首先要选出最优的量度标准。不妨先取目标函数作为量度标准,即每装入一件物品就使背包获得最大可能的效益值增量。在这种量度标准下的贪心方法就是按效益值的非增次序将物品一件件放到背包中去。如果正在考虑中的物品放不进去,则可只取其一部分来装满背包。但是,这最后一次的放法可能不符合使背包每次获得最大效益增量的量度标\n第5章贪心方法103准,这可以换一种能获得最大增量的物品,将它(或它的一部分)放入背包,从而使最后一次装包也符合量度标准的要求。例如,假定还剩有两个单位的空间,而在背包外还有两种物品,这两种物品有(pi=4,wi=4)和(pj=3,wj=2),则使用j就比用i要好些。下面对例5.1的数据使用这种选择策略。物品1有最大的效益值(p1=25),因此首先将物品1放入背包,这时x1=1且获得25的效益。背包容量中只剩下两个单位空着。物品2有次大的效益值(p2=24),但w2=15,背包中装不下物品2,使用x2=2/15就正好装满背包。不难看出物品2的2/15比物品3的2/10效益值高。所以,此种选择策略得到②的解,总效益值是28.2。它是一个次优解。由此例可知,按物品效益值的非增次序装包不能得到最优解。为什么上述贪心策略不能获得最优解呢?原因在于背包可用容量消耗过快。由此,很自然地启发我们用容量作为量度,让背包容量尽可能慢地被消耗。这就要求按物品重量的非降次序来把物品放入背包。例5.1的解③就是使用这种贪心策略得到的,它仍是一个次优解。这种策略也只能得到次优解,其原因在于容量虽然慢慢地被消耗,但效益值没能迅速地增加。这又启发我们采用在效益值的增长速率和容量的消耗速率之间取得平衡的量度标准。即每一次装入的物品应使它占用的每一单位容量获得当前最大的单位效益。这就需使物品的装入次序按pi/wi比值的非增次序来考虑。在这种策略下的量度是已装入物品的累计效益值与所用容量之比。其量度标准是每次装入要使累计效益值与所用容量的比值有最多的增加或最少的减小(第二次和以后的装入可能使此比值减小)。将此贪心策略应用于例5.1的数据,得到解④。如果将物体事先按pi/wi的非增次序分好类,则过程GREEDY-KNAPSACK就得出这一策略下背包问题的解。如果将物品分类的时间不算在内,则此算法所用时间为O(n)。算法5.2背包问题的贪心算法procedureGREEDY-KNAPSACK(P,W,M,X,n)∥P(1∶n)和W(1∶n)分别含有按P(i)/W(i)≥P(i+1)/W(i+1)排序的n件物品的效益值和重量。M是背包的容量大小,而X(1∶n)是解向量∥realP(1∶n),W(1∶n),X(1∶n),M,cu;integeri,n;X←0∥将解向量初始化为零∥cu←M∥cu是背包剩余容量∥fori←1tondoifW(i)>cuthenexitendifX(i)←1cu←cu-W(i)repeatifi≤nthenX(i)←cu/W(i)endifendGREEDY-KNAPSACK值得指出的是,如果把物品事先按效益值的非增次序或重量的非降次序分好类,再使用算法5.2就可分别得到量度标准为最优(使每次效益增量最大或使容量消耗最慢)的\n104计算机算法基础解。由背包问题量度选取的研究可知,选取最优的量度标准实为用贪心方法求解问题的核心。下面证明用第三种策略的贪心算法所得的贪心解是一个最优解。基本思想是,把这贪心解与任一最优解相比较,如果这两个解不同,就去找开始不同的第一个xi,然后设法用贪心解的这个xi去代换最优解的那个xi,并证明最优解在分量代换前后的总效益无任何变化。反复进行这种代换,直到新产生的最优解与贪心解完全一样,从而证明了贪心解是最优解。这种证明最优解的方法在本书中经常使用,因此读者从现在起就应掌握它。定理5.1如果p1/w1≥p2/w2≥⋯≥pn/wn,则算法GREEDY-KNAPSACK对于给定的背包问题实例生成一个最优解。证明设X=(x1,⋯,xn)是GREEDY-KNAPSACK所生成的解。如果所有的xi等于1,显然这个解就是最优解。于是,设j是使xj≠1的最小下标。由算法可知,对于1≤i∑pixi。不失一般性,可以假定∑wiyi=M。设k是使得yk≠xk的最小下标。显然,这样的k必定存在。由上面的假设,可以推得ykj分别得证明:(1)若kxk,显然有∑wiyi>M,与Y是可行解矛盾。若yk=xk,与假设yk≠xk矛盾,故ykj,则∑wiyi>M,这是不可能的。现在,假定把yk增加到xk,那么必须从(yk+1,⋯,yn)中减去同样多的量,使得所用的总容量仍是M。这导致一个新的解Z=(z1,⋯,zn),其中,zi=xi,1≤i≤k,并且∑wi(yi-zi)k∑piyi,则Y不可能是最优解。如果这两个和数相等,同时Z=X,则X就是最优解;若Z≠X,则需重复上面的讨论,或者证明Y不是最优解,或者把Y转换成X,从而证明了X也是最优解。证毕。5.3带有限期的作业排序在这一节将应用贪心设计策略来解决操作系统中单机、无资源约束且每个作业可在等量的时间内完成的作业调度问题。即,假定只能在一台机器上处理n个作业,每个作业均可在单位时间内完成;又假定每个作业i都有一个截止期限di>0(它是整数),当且仅当作业i在它的期限截止以前被完成时,方可获得pj>0的效益。这个问题的一个可行解是这n个作业的一个子集合J,J中的每个作业都能在各自的截止期限之前完成。可行解的效益值是J中这些作业的效益之和,即∑p。具有最大效益值的可行解就是最优解。i∈J\n第5章贪心方法105例5.2设n=4,(p1,p2,p3,p4)=表5.1例5.2的可行解与效益值(100,10,15,20)和(d1,d2,d3,d4)=(2,1,可行解处理顺序效益值①(1)11002,1),则这个问题的可行解和它们的效益②(2)210值如表5.1所示。其中,解⑦是最优的,所③(3)315允许的处理次序是:先处理作业4,再处理④(4)420⑤(1,2)2,1110作业1。于是,在时间0开始处理作业4⑥(1,3)1,3或3,1115而在时间2完成对作业1的处理。⑦(1,4)4,1120⑧(2,3)2,3255.3.1带有限期的作业排序算法⑨(3,4)4,335为了拟定出一个最优解的算法,应制定如何选择下一个作业的量度标准,利用贪心策略,使得所选择的下一个作业在这种量度下达到最优。不妨首先把目标函数∑pi作为量度。i∈J使用这一量度,下一个要计入的作业将是在满足所产生的J是一个可行解的限制条件下让∑pi得到最大增加的作业。这就要求按pi的非增次序来考虑这些作业。利用例5.2中的数i∈J据来应用这一准则,开始时J=ꯁ,∑pi=0,由于作业1有最大效益且J={1}是一个可行解,i∈J于是把作业1计入J。下一步考虑作业4,J={1,4}也是可行解。然后,考虑作业3,因为{1,3,4}不是可行解,故作业3被舍弃。最后,考虑作业2,由于{1,2,4}也不可行,作业2被舍弃。最终所留下的是效益值为120的解J={1,4}。它是这个问题的最优解。现在,对上面所叙述的贪心方法以算法5.3的形式给出其粗略的描述。算法5.3作业排序算法的概略描述procedureGREEDY-JOB(D,J,n)∥作业按p1≥p2≥⋯≥pn的次序输入,它们的期限值D(i)≥1,1≤i≤n,n≥1。J是在它们的截止期限前完成的作业的集合∥1J←{1}2fori←2tondo3ifJ∪{i}的所有作业都能在它们的截止期限前完成thenJ←J∪{i}4endif5repeat6endGREEDY-JOB定理5.2对于作业排序问题用算法5.3所描述的贪心方法总是得到一个最优解。证明设(pi,di),1≤i≤n,是作业排序问题的任一实例;J是由贪心方法所选择的作业的集合;I是一个最优解的作业集合。可证明J和I具有相同的效益值,从而J也是最优的。假定I≠J,因为若I=J,则J即为最优解。容易看出,如果IJ,则I就不可能是最优的。由于贪心法的工作方式也排斥了JI,因此至少存在着这样的两个作业a和b,使得a∈J且a|I,b∈I且b|J。设a是使得a∈J且a|I的一个具有最高效益值的作业。由于贪心方法可以得出,对于在I中又不在J中的所有作业b,都有pa≥pb。这是因为若pb>pa,则贪心方法会先于作业a考虑作业b并且把b计入到J中去。现在,设SJ和SI分别是J和I的可行调度表。设i是既属于J又属于I的一个作业;又\n106计算机算法基础设i在SJ中在t到t+1时刻被调度,而在SI中则在t′到t′+1时刻被调度。如果ta。在σ′中将ra与rb相交换,因为dra≥drb,故作业可依新产生的排列σ"=s1s2⋯sk的次序处理而不违反任何一个期限。连续使用这一方法,就可将σ′转换成σ且不违反任何一个期限。定理得证。即使这些作业有不同的处理时间ti≥0,上述定理亦真。其证明留作习题。根据定理5.3,可将带限期的作业排序问题作如下处理:首先将作业1存入解数组J中,然后处理作业2到作业n。假设已处理了i-1个作业,其中有k个作业已计入J(1),J(2),⋯,J(k)之中,且有D(J(1))≤D(J(2))≤⋯≤D(J(k)),现在处理作业i。为了判断JU{i}是否可行,只需看能否找出按期限的非降次序插入作业i的适当位置,使得作业i在此处插入后有D(J(r))≥r,1≤r≤k+1。找作业i可能的插入位置可如下进行:将D(J(k)),D(J(k-1)),⋯,D(J(1))逐个与D(i)比较,直到找到位置q,它使得D(i)1,q<1≤k,则说明这k-q个作业均可延迟一个单位时间处理,即可将这些作业在J中均后移一个位置而不超过各自的期限值。在以上条件成立的情况下,只要D(i)>q,就可将作业i在位置q+1处插入,从而得到一个按期限的非降次序排列的含有k+1个作业的可行解。以上过程可反复进行到第n个作业处理完毕,所得到的贪心解由定理5.2可知就是一个最优解。这一处理过程可用算法5.3来描述。算法中\n第5章贪心方法107引进了一个虚构的作业0,它放在J(0),且D(J(0))=0。引入这一虚构作业是为了便于将作业插入位置1。算法5.4带有限期和效益的单位时间的作业排序贪心算法lineprocedureJS(D,J,n,k)∥D(1),⋯,D(n)是期限值,n≥1,作业已按p1≥p2≥⋯pa被排序。J(i)是最优解中的第i个作业,1≤i≤k。终止时,D(J(i))≤D(J(i+1)),1≤iD(i)andD(J(r))≠rdo7r←r-18repeat9ifD(J(r))≤D(i)andD(i)>rthen∥把i插入J∥10fori←ktor+1by-1do11J(i+1)←J(1)12repeat13J(r+1)←i;k←k+114endif15repeat16endJS对于JS,有两个赖之以测量其复杂度的参数,即作业数n和包含在解中的作业数s。第6~8行的循环至多迭代k次,每次迭代的时间为O(1)。若第9行的条件为真,则执行10~13行。这些要求O(k-r)时间去插入作业i。因此,对于4~15行的循环,其每次迭代的总时间是O(k)。该循环共迭代n-1次。如果s是k的终值,即s是最后所得解的作业数,则2算法JS所需要的总时间是O(sn)。由于s≤n,因此JS的最坏情况时间是O(n)。这种情2况在p1=d1=n-i+1,1≤i≤n时就会出现。进而可以证明最坏情况计算时间为Θ(n)。在计算空间方面,除了D所需要的空间外,为了存放解J,还需要Θ(s)的空间量。要指出的是,JS并不需要具体的效益值,只要知道pi≥pi+1,1≤i≤n即可。5.3.2一种更快的作业排序算法通过使用不相交集合的UNION与FIND算法(参见2.4.3小节)以及使用一个不同的2方法来确定部分解的可行性,可以把JS的计算时间由O(n)降低到数量级相当接近于O(n)。如果J是作业的可行子集,那么可以使用下述规则来确定这些作业中的每一个作业的处理时间:若还没给作业i分配处理时间,则分配给它时间片[α-1,α],其中α应尽量取大且时间片[α-1,α]是空的。此规则就是尽可能推迟对作业i的处理。于是,在将作业一个一个地装配到J中时,就不必为接纳新作业而去移动J中那些已分配了时间片的作业。如果正被考虑的新作业不存在像上面那样定义的α,这个作业就不能计入J。这样处理的正确性\n108计算机算法基础证明留作习题。例5.3设n=5,(p1,⋯,p5)=(20,15,10,5,1)和(d1,⋯,d5)=(2,2,1,3,3)。使用上述可行性规则,得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}。由于只有n个作业且每个作业花费一个单位时间,因此只需考虑这样一些时间片[i-1,i],1≤i≤b,其中b=min{n,max{dj}}。为简便起见,用i来表示时间片[i-1,i]。易于看出,这n个作业的期限值只能是{1,2,⋯,b}中的某些(或全部)元素。实现上述调度规则的一种方法是把这b个期限值分成一些集合。对于任一期限值i,设ni是使得nj≤i的最大整数且是空的时间片。为避免极端情况,引进一个虚构的期限值0和时间片[-1,0]。当且仅当nj=nj,期限值i和j在同一个集合中,即所要处理的作业的期限值如果是i或j,则当前可分配的最接近的时间片是ni。显然,若iCOST(k,j)18thenNEAR(k)←j19endif20repeat21repeat22ifmincost≥∞thenprint(′nospanningtree′)endif23endPRIM2过程PRIM所需要的时间是Θ(n),其中n是图G的结点数。具体分析如下:第3行花费Θ(e)(e=|E|)时间而第4行花费Θ(1)时间;第6~9行的循环花费Θ(n)时间;第12行和第16~20行的循环要求Θ(n)时间,故第11~21行循环的每一次迭代要花费Θ(n)时间。22所以,这个循环的总时间是Θ(n)。因此,过程PRIM有Θ(n)的时间复杂度。因为最小生成树包含了与每个结点v相关的一条最小成本边,所以还可以把这算法稍许加快一点。为此,假设T是图G=(V,E)的最小成本生成树。设v是T中的任一结点。又设(v,w)是所有与v相关的边中具有最小成本的一条边。假定(v,w)|E(T)并且对于所有的边(v,x)∈E(T),COST(v,w)L,要求选取一个能放在带上的程序的最大子集合Q。最大子集指的是在其中含有最多个数的程序。构造Q的一种贪心策略是按ai的非降次序把程序计入集合。(1)假设Pi被排成使a1≤a2≤⋯≤ai。使用上面的设计策略写一个SPARKS算法。要求输出一个数组S(1∶n),若Pi在Q中,则S(i)=1,否则S(i)=0。(2)证明这一策略总能找到最大子集Q,使得∑ai≤L。P∈Qi(3)设Q是使用上述贪心策略得到的子集合,带利用率∑ai/L可以小到什么程度?P∈Qi(4)假定现在要确定一个使带的利用率最高的程序子集合,可考虑按ai的非增次序计入程序这一贪心策略。只要Pi带上还有够用的空间,就应将它计入Q。假设程序已按使a1≥a2≥⋯≥an的次序排列。用SPARKS写一个与此策略对应的算法,并分析它的时、空复杂度。(5)证明(1)所用的设计策略不一定得到使∑ai/L取最大值的子集合。P∈Qi5.2(1)求以下情况背包问题的最优解:n=7,M=15,(p1,⋯,p7)=(10,5,15,7,6,18,3)和(w1,⋯,w7)=(2,3,5,7,1,4,1)。(2)将以上数据情况的背包问题记为I。设FG(I)是物品按pi的非增次序输入时由GREEDY-KNAPSACK所生成的解,FO(I)是一个最优解。问FO(I)/FG(I)是多少?(3)当物品按wi的非降次序输入时,重复(2)的讨论。\n122计算机算法基础5.3[0/1背包问题]如果将5.3节讨论的背包问题修改成n极大化∑pixi1n约束条件∑wixi≤M1xi=0或1,1≤i≤n这种背包问题称为0/1背包问题。它要求物品或者整件装入背包或者整件不装入。求解此问题的一种贪心策略是:按pi/wi的非增次序考虑这些物品,只要正被考虑的物品能装得进就将其装入背包。证明这种策略不一定得到最优解。5.4[集合覆盖]已知集合族S由m个集合S1,⋯,Sm组成。S的子集合T={T1,T2,⋯,Tk}也是一km个集合族,而且T中每一个Tj,1≤j≤k,等于S中的某个Si,1≤i≤m。如果∪Tj=∪Si,则称T是S的一个j=1i=1覆盖。T的大小|T|是T中集合的个数。S的最小覆盖是|T|取最小值的覆盖。考虑以下的贪心策略:通过迭代构造T;在第k次迭代时,T={T1,⋯,Tk-1};现在将S中的一个集合Si加到T,这个Si中含有还不km在T中最多的元素个数;当∪Tj=∪Si时停止。j=1i=1m(1)假设∪Si={1,2,⋯,n}且m0,要用的处理时间ti>0,限期di≥ti。5.9(1)对于5.3节的作业排序问题证明:当且仅当子集合J中的作业可以按下述规则处理时,J才表示一个可行解,即如果J中的作业i还没分配处理时间,则将它分配在时间片[α-1,α]处理,其中α是使得1≤r≤di的最大整数r,且时间片[α-1,α]是空的。(2)仿照例5.4的格式,在题5.8之(1)所提供的数据集上执行算法5.5。5.10(1)已知n-1个元素已按min-堆的结构形式存放在A(1),⋯,A(n-1)。现要将另一存放在A(n)的元素和A(1∶n-1)中元素一起构成一个具有n个元素的min-堆。对此写一个计算时间为O(logn)的算法。(2)在A(1∶n)中存放着一个min-堆,写一个从堆顶A(1)删去最小元素后将其余元素调整成min-堆的算法,要求这新的堆存放在A(1∶n-1)中,且算法时间为O(logn)。(3)利用(2)所写出的算法,写一个对n个元素按非增次序分类的堆分类算法。分析这个算法的计算复杂度。5.11(1)证明如果一棵树的所有内部结点的度都为k,则外部结点数n满足nmod(k-1)=1。(2)证明对于满足nmod(k-1)=1的正整数n,存在一棵具有n个外部结点的k元树T(在一棵k元树中,每个结点的度至多为k)。进而证明T中所有内部结点的度为k。5.12(1)证明如果nmod(k-1)=1,则在定理5.4后面所描述的贪心规则对于所有的(q1,q2,⋯,qn)生成一棵最优的k元归并树。(2)当(q1,q2,⋯,q11)=(3,7,8,9,15,16,18,20,23,25,28)时,画出使用这一规则所得到的最优三元归并树。5.13证明5.5节的Prim方法生成最小成本生成树。5.14在假定图用邻接表来表示的情况下重写Prim算法,并分析它的计算复杂度。5.15证明引理5.1。n-15.16通过考察n结点的完全图,证明在一个n结点图中可能有比2-2棵还多的生成树。5.17在图5.12的有向图中,利用算法SHORTEST-PATHS获取按长度非降次序排列的由结点1到其余各结点最短路径长度。图5.12有向图图5.13有向图5.18说明为什么将SHORTEST-PATHS应用于图5.13的有向图就不能正常地工作。结点v1和v7之间的最短路径是什么?5.19修改算法SHORTEST-PATHS,使得它在获取最短路径的同时还得到这些最短路径。请给出修改后的算法的计算时间。\n第6章动态规划6.1一般方法在实际生活中,有这么一类问题,它们的活动过程可以分为若干个阶段,而且在任一阶段后的行为都仅依赖于i阶段的过程状态,而与i阶段之前的过程如何达到这种状态的方式无关,这样的过程就构成一个多阶段决策过程。在20世纪50年代,贝尔曼(RichardBell-man)等人根据这类问题的多阶段决策的特性,提出了解决这类问题的“最优性原理”,从而创建了最优化问题的一种新的算法设计方法———动态规划。在多阶段决策过程的每一阶段,都可能有多种可供选择的决策,必须从中选取一种决策。一旦各个阶段的决策选定之后,就构成了解决这一问题的一个决策序列。决策序列不同,所导致的问题的结果也不同。动态规划的目标就是要在所有容许选择的决策序列中选取一个会获得问题最优解的决策序列,即最优决策序列。显然,用枚举的方法从所有可能的决策序列中选取最优决策序列是一种最笨拙的方法。贝尔曼认为,利用最优性原理(principleofoptimality)以及所获得的递推关系式去求取最优决策序列可以使枚举量急剧下降。这个原理指出,过程的最优决策序列具有如下性质:无论过程的初始状态和初始决策是什么,其余的决策都必须相对于初始决策所产生的状态构成一个最优决策序列。如果所求解问题的最优性原理成立,则说明用动态规划方法有可能解决该问题;而解决问题的关键在于获取各阶段间的递推关系式。例6.1[多段图问题]多段图G=(V,E)是一个有向图。它具有如下特性:图中的结点被划分成k≥2个不相交的集合Vi,1≤i≤k,其中V1和Vk分别只有一个结点s(源点)和t(汇点)。图中所有的边〈u,v〉均具有如下性质:若u∈Vi,则v∈Vi+1,1≤i∑piyi。因此,序列y1,z2,z3,⋯,zn是一个对问题KNAP2≤i≤n2≤i≤n2≤i≤n(1,n,M)具有更大效益值的序列。最优性原理成立。能用动态规划求解的问题的最优化决策序列可表示如下。设S0是问题的初始状态。假定需要作n次决策xi,1≤i≤n。设X1={r1,1,r1,2,⋯,r1,p}是x1的可能决策值的集合,而1S1,j是在选取决策值r1,j以后所产生的状态,1≤j1≤p1。又设Γ1,j是相应于状态S1,j的最优1111决策序列。那么,相应于S0的最优决策序列就是{r1,jΓ1,j|1≤j1≤p1}中最优的序列,记为11OPT{r1,jΓ1,j}=r1Γ1。如果已作了k-1次决策,1≤k-1k且i(n-1)*M,则表明G中由i到j没有有向路径。3过程ALL-PATHS所需的时间很容易确定。第9行迭代了n次,而且整个循环与矩阵3A中的数据无关,因此过程的计算时间是Θ(n)。至于求由i到j的最短路径所需增设的内容,则留作一道习题。6.4最优二分检索树2.4.2节给出了二分检索树的定义。根据定义,要求树中所有的结点是互异的。对于一个给定的标识符集合,可能有若干棵不同的二分检索树。图6.5给出了关于SPARKS保留字的一个子集的两棵二分检索树。图6.5两棵二分检索树为了确定标识符X是否在一棵二分检索树中出现,将X先与根比较,如果X比根中标识符小,则检索在左子树中继续;如果X等于根中标识符,则检索成功地终止;否则检索在右子树中继续下去。上述步骤可以形式化为过程SEARCH。算法6.4检索一棵二分检索树procedureSEARCH(T,X,i)∥为X检索二分检索树T,这棵树的每个结点有3个信息段:LCHILD,IDENT和RCHILD。如果X不在T中,则置i=0,否则将i置成使得IDENT(i)=X∥1i←T2whilei≠0do3case4:XIDENT(i):i←RCHILD(i)∥检索右子树∥7endcase8repeat9endSEARCH\n第6章动态规划133已知一个固定的标识符集合,希望产生一种构造二分检索树的方法。可以预料,同一个标识符集合有不同的二分检索树,而不同的二分检索树有不同的性能特征。图6.5(a)所示的树在最坏情况下找一个标识符需要进行4次比较,而图6.5(b)所示的那棵树只需要3次比较,在平均情况下,这两棵树各需要12/5和11/5次比较。这一计算结果是假定检索每一个标识符具有同等的概率并且在任何时候都不做不在T中的标识符的检索。在一般情况下,可以预计所要检索的那些不同的标识符具有不同的频率(或概率)。另外,也要做一些不成功的检索(即对不在这棵树中标识符的检索)。假定所给出的标识符集是{a1,a2,⋯,an},其中a1an的标识符X。易于看出,在同一类Ei中的所有标识符,其检索都在同一个外部结点处终止。而在不同的Ei中的标识符则在不同的外部结点处终止。如果Ei的那个失败的结点在l级,则只要对while循环作l-1次迭代。于是,这个结点的成本分担额是Q(i)*(level(Ei)-1)。上述讨论导出二分检索树的预期成本公式如下:∑P(i)*level(ai)+∑Q(i)*(level(Ei)-1)(6.9)1≤i≤n0≤i≤n\n134计算机算法基础定义标识符集{a1,a2,⋯,an}的最优二分检索树是一棵使式(6.9)取最小值的二分检索树。例6.9标识符集(a1,a2,a3)=(doifstop)可能的二分检索树如图6.7所示。图6.73种标识符的各种二分检索树在每个内、外结点具有相同概率P(i)=Q(i)=1/7的情况下,有cost(树a)=15/7cost(树b)=13/7cost(树c)=15/7cost(树d)=15/7cost(树e)=15/7正如料想的那样,树b是最优的。在P(1)=0.5,P(2)=0.1,P(3)=0.05,Q(0)=0.15,Q(1)=0.1,Q(2)=0.05和Q(3)=0.05的情况下,则有cost(树a)=2.65cost(树b)=1.9cost(树c)=1.5cost(树d)=2.15cost(树e)=1.6在以上情况下树c是最优的。为了把动态规划应用于得到一棵最优二分检索树的问题,需要把构造这样的一棵树看成是一系列决策的结果,而且要能列出求取最优决策序列的递推式。解决上述问题的一种可能方法是,对于这些ai,1≤i≤n,要决策出将其中的哪一个作为T的根结点。如果选择ak,那么,a1,a2,⋯,ak-1这些内部结点和E0,E1,⋯,Ek-1这些类的外部结点显然都将位于这个根的左子树L中,而其余的结点则将在右子树R中。定义COST(L)=∑P(i)*level(ai)+∑Q(i)*(level(Ei)-1)1≤i0,则有fi(x)=max{fi-1(x),fi-1(x-wi)+pi}(6.14)为了能由前向后递推而最后求解出fn(M),需从f0(x)开始。对于所有的x≥0,有f0(x)=0,当x<0时,有f0(x)=-∞。根据式(6.14),马上可求解出0≤xM的那些序偶(p,w)也清除掉,因为由它们不能导出满足约束条件的可行解。这样生成的S的所有序偶,是背包问题KNAP(1,i,x)在0≤x≤M各种取值下的最优in解(注意:S是由一些序偶构成的有序集合)。通过计算S可以找到KNAP(1,n,x),0≤x≤nM的所有解。KNAP(1,n,M)的最优解fn(M)由S的最后一对序偶的P值给出。\n140计算机算法基础n由于实际上只需要KNAP(1,n,M)的最优解,它是由S的最末序偶(p,w)给出的。而nn-1n-1S的这最末序偶或者是S的最末序偶,或者是(pj+pn,wj+wn),其中(pj,wj)∈S且wjn-1n是S中满足wj+wn≤M的最大值。因此,只需按上述方法求出S的最末序偶,无需计算n出S也一样满足求KNAP(1,n,M)最优解的要求。n如果已找出S的最末序偶(p1,w1),那么,使∑pixi=p1,∑wixi=w1的x1,⋯,xn的决in-1n-1策值可以通过检索这些S来确定。若(p1,w1)∈S,则置xn=0。若(p1,w1)|S,则n-1n-1(p1-pn,w1-wn)∈S,并且置xn=1。然后,再判断留在S中的序偶(p1,w1)或者(p1-n-2pn,w1-wn)是否属于S以确定xn-1的取值,等等。3例6.13由例6.12,在M=6的情况下,f3(6)的值由S中的序偶(6,6)给出。(6,6)|22S,因此应置x3=1。序偶(6,6)是由序偶(6-p3,6-w3)=(1,2)得到的,因此(1,2)∈S。10又,(1,2)∈S,于是应置x2=0。但(1,2)|S,从而得到x1=1。故最优解f3(6)=6的最优决策序列是(x1,x2,x3)=(1,0,1)。对于以上所述的内容可以用一种非形式的算法过程DKP来描述。为了实现DKP,需ii要给出表示序偶集S和S1的结构形式,而且要将MERGE-PURGE过程具体化,使它能按i-1in-11要求归并S和S1,且清除一些序偶。此外,还要给出一个沿着S,⋯,S回溯以确定xn,⋯,x1的0、1值的算法。算法6.6非形式化的背包算法lineprocedureDKP(p,w,n,M)01S←{(0,0)}2fori←1ton-1doii-13S1←{(p1,w1)|(p1-pi,w1-wi)∈Sandw1≤M}ii-1i4S←MERGE-PURGE(S,S1)5repeatn-16(px,wx)←S的最末序偶n-17(py,wy)←(p1+pn,w1+wn),其中,w1是S中使得w+wn≤M的所有序偶中取最大值n-11的w∥沿S,⋯,S回溯确定xn,xn-1,⋯,x1的取值∥n8ifpx>pythenxn←0∥px是S的最末序偶∥n9elsexn←1∥py是S的最末序偶∥10endif11回溯确定xn-1,⋯,x112endDKP6.5.2DKP的实现可以用两个一维数组P和W来存放所有的序偶(p,w)。p的值放在P中,w的值放在01n-1W中。序偶集S,S,⋯,S互相邻接地存放。各个集合可以用指针F(i)来指示,这里n-10≤i≤n。对于0≤iM。因此,S1的序偶都是1≤j≤u的序偶(P(j)+pi,W(j)+wi)。这些序偶在7~22行的循环中生成,相应的归并工作也在这里进行。每次迭代首先生成一对序偶i-1i(pp,ww),接着将S中所有还没有被清除和归并到S中且有WP(next-1)的情况下,把序偶(pp,ww)加入到S中,否则(pp,ww)被清除。第19~21行i-1iiii-1清除S中所有能在此时清除且没有并入S的序偶。在7~22行将S1归并到S以后,Si中可能剩下一些序偶需并入S,此工作在第23~26行完成。要指出的是,由于第19~21行的处理,这些剩下的序偶没有一个能被清除。过程PARTS(29行)实现过程DKP(算法6.6)的第6~11行,具体实现留作一道习题。算法6.70/1背包问题的算法lineprocedureDKNAP(p,w,n,M,m)realp(n),w(n),P(m),W(m),pp,ww,MintegerF(0;n),l,h,u,i,j,p,next01F(0)←1;P(1)←W(1)←0∥S∥02l←h←1∥S的首端与末端∥3F(1)←next←2∥P和W中第一个空位∥i4fori←1ton-1do∥生成S∥5k←l6u←在l≤r≤h中使得W(r)+wi≤M的最大的ri7forj←ltoudo∥生成S1及归并∥i8(pp,ww)←(P(j)+pi,W(j)+wi)∥S1中的下一个元素∥i-19whilek≤handW(k)P(next-1)then(P(next),W(next))←(pp,ww)17next←next+118endif19whilek≤handP(k)≤P(next-1)do∥清除∥20k←k+121repeat22repeati-1i∥将S中剩余的元素并入S∥23whilek≤hdo24(P(next),W(next))←(P(k),W(k))25next←next+1;k←k+126repeati+1∥对S置初值∥27l←h+1;h←next-1;F(i+1)←next28repeat29callPARTS30endDKNAP6.5.3过程DKNAP的分析iiii-1ii假设S的序偶数是|S|。在i>0的情况下,每个S由S和S1归并而成,并且|S1|i-1ii-1≤|S|,因此|S|≤2|S|。在最坏情况下没有序偶会被清除,所以iin∑|S|=∑2=2-10≤i≤n-10≤i≤n-1n即,DKNAP的空间复杂度为O(2)。i-1ii-101n-1由S生成S需要Θ(|S|)这么多时间,因此计算S,S,⋯,S总的时间是Θ(∑i-1iiin|S|)。由于|S|≤2,所以计算这些S总的时间是O(2)。i如果每件物品的重量wj和所产生的效益值pj都是整数,那么S中每个序偶(p,w)的iP和W也是整数,且有P≤∑pj,W≤M。又由于在任一S中,它的序偶有互异的P值,也1≤j≤i有互异的W值,因此有ii|S|≤1+∑pj和|S|≤1+min{∑wj,M}1≤j≤i1≤j≤i于是,在所有wj和pj为整数的情况下,DKNAP的时间和空间复杂度(除去PARTS的时n间)是O(min{2,n∑pi,nM})。1≤i≤n以上分析似乎表明,当n取值大时DKNAP的有效性是令人失望的,但这类问题在很多情况下都能在“适当的”时间内解出。这是因为在很多情况下P和W都是整数,而且M比ni2小得多。另外,支配规则在清除不应并入S的序偶上是很有效的。使用探试方法(heuristic)可以加速DKNAP的计算。设L是最优解的估计值,它使得\n第6章动态规划143ifn(M)≥L,又设PLEFT(i)=∑pj。如果序偶(p,w)∈S,且使得P+PLEFT(i)M。f6(165)可由S求出,它等于160+p6=163。i此例中L值一直没有改变。在一般情况下,如果计算某S会产生更好的估计值,L值将会改变。如果不使用启发性方法,由DKNAP就会产生如下结果:0S={0}1S={0,100}2S={0,50,100,150}3S={0,20,50,70,100,120,150}4S={0,10,20,30,50,60,70,80,100,110,120,130,150,160}5S={0,7,10,17,20,27,30,37,50,57,60,67,70,77,80,87,100,107,110,117,120,127,130,137,150,157,160}5f6(165)可用已知条件(p6,w6)=(3,3)从S求出。6.6可靠性设计乘积函数最优化问题,也可使用动态规划法来求解。本节讨论这类问题中的一个实例。假定要设计一个系统,这个系统由若干个以串联方式连接在一起的不同设备所组成(见图\n144计算机算法基础6.11)。设ri是设备Di的可靠性(即ri是设备Di正常运转的概率),则整个系统的可靠性就是∏ri。即便这些单个设备是非常可靠的(每个ri都非常接近于1),该系统的可靠性也不一定很高。例如,若n=10,ri=0.99,1≤i≤10,则∏ri=0.904。为了提高系统可靠性,最好是增加一些重复设备,并通过开关线路把数个同类设备并联在一起(见图6.12)。由开关线路来判明其中的设备的运行情况,并将能正常运行的某台投入使用。图6.11以串联方式连接的n台设备图6.12每级中以并联方式连接多台设备m如果第i级的设备Dii的台数为mi,那么这mi台设备同时出现故障的概率为(1-ri)。m从而第i级的可靠性就变成1-(1-ri)i。例如,假定ri=0.99,mi=2,于是这一级的可靠m性就是0.9999。不过在任何实际系统中,每一级的可靠性要比1-(1-rii)小一些,这是由于这些开关线路本身并不是完全可靠的,而且同一类设备的失误率也不可能是完全独立的(例如,由于设计不当所造成的失误)。基于以上分析,不妨假设第i级的可靠性由函数φ(mi)给定,1≤i≤n。并且可以看出,开始时φ(mi)随mi值的增大而增大,在到达mi的某个取值以后φ(mi)的值则可能下降。这个多级系统的可靠性是∏φ(mi)。诚然,增加重复的设备可提高系统的可靠性,但在实际工作中,设计一个系统都是在有成本约束的条件下实现的。因此,所谓可靠性设计最优化问题是在可容许最大成本c的约束下,如何使系统的可靠性达到最优的问题。假设cj是一台设备j的成本,由于系统中每种设备至少有一台,故设备j允许配置的台数至多为nuj=|(c+cj-∑ck)/cj|k=1如果用RELI(l,i,X)表示在可容许成本X约束下,对第l种到第i种设备的可靠性设计问题,它就可形式描述成极大化∏φ(mi)1≤j≤i约束条件∑cjmj≤X(6.16)1≤j≤ii其中,cj>0,1≤mj≤uj=|(X+cj-∑ck)/cj|且mj为整数,l≤j≤i。k=l于是,整个系统的可靠性设计问题由RELI(1,n,c)表示。它的一个最优解是对m1,⋯,mn的一系列决策的结果。每得到一个mi都要对其取值进行一次决策。设fi(x)是在容许成本值X约束下对前i种设备组成的子系统可靠性设计的最优值,即fi(x)=max∏j(mj),那么最优解的可靠性是fn(c)。所作的最后决策要求从{1,2,⋯,un}中选择一个元素作为mn。一旦选出了mn的值,则下阶段的决策就应使剩余资金c-cnmn按最优的方\n第6章动态规划145式使用。于是,有fn(c)=max{φ(mn)fn-1(c-cnmn)}(6.17)将(6.17)式推广到一般,对任一fi(x),i≥1,有fi(x)=max{φ(mi)fi-1(x-cimi)}(6.18)显然,当0≤x≤c时,对于所有的x,有f0(x)=1。使用类似于解0/1背包问题的方法可i以解出(6.18)式。设S由(f,x)形式的序偶所组成,其中f=fi(x)。由m1,⋯,mi的决策序列所得出的每一个不同的x都至多只有一个序偶。支配规则对这个问题也适用,即当且仅i当f1≥f2而x1≤x2时,(f1,x1)支配(f2,x2)。那些受支配的序偶可从S中舍去。例6.15设计一个由设备D1,D2,D3组成的三级系统。每台设备的成本分别为30元,15元和20元,可靠性分别是0.9,0.8和0.5,计划建立该系统的投资不得超过105元。假m定,若i级有mi台设备Di并联,则该级的可靠性φ(mi)=1-(1-ri)i。上述条件可以表示为:c=105;c1=30,c2=15,c3=20;r1=0.9,r2=0.8,r3=0.5。由此立即可得:u1=2,u2=3,u3=3。i用S表示m1,⋯,mi的各种决策序列产生的所有不受支配的序偶(f,x)之集合。这里0i-1f=fi(x)。整个工作从S={(1,0)}开始。假设已求出S。S的求取可按以下步骤进行,对i-1i于mi的所有可能值,依次求出当mi=j,1≤j≤ui时,由S可能得到的所有序偶的集合Sj,ii然后将这ui个Sj按支配规则归并即得S。于是,由11S1={(0.9,30)}S2={(0.99,60)}1得S={(0.9,30),(0.99,60)}222由S1={(0.72,45),(0.792,75)}S2={(0.864,60)}S3={(0.8928,75)}2得S={(0.72,45),(0.864,60),(0.8928,75)}2注意:S2中已删去了由(0.99,60)所得到的序偶(0.9504,90)。因为这只剩下15元,不足以让m3=1。说明:归并时由于(0.792,75)受(0.864,60)支配,故舍去。3由S1={(0.36,65),(0.432,80),(0.4464,95)}3S2={(0.54,85),(0.648,100)}3S3={(0.63,105)}3得S={(0.36,65),(0,432,80),(0.54,85),(0.648,100)}i最优设计有0.648的可靠性,需用资金100元。通过对这些S的回溯,求出m1=1,m2=2,m3=2。i对于可靠性设计问题,值得注意的是,正如上例中已作的处理那样,在S中不需要保留任何比c-∑cj还大的x值的序偶,这是因为任何序偶都不能超出完成这个系统所能负担1≤j≤ni的资金。此外,为了减少这些S的规模,可以像求解背包问题一样,将启发性方法引进求解可靠性问题的动态规划算法,即提供某种判定下界,如果(f,x)小于它,则将(f,x)从S中删去。\n146计算机算法基础6.7货郎担问题货郎担问题属于易于描述但难于解决的著名难题之一,至今世界上还有不少人在研究它。该问题的基本描述是:某售货员要到若干个村庄售货,各村庄之间的路程是已知的,为了提高效率,售货员决定从所在商店出发,到每个村庄售一次货然后返回商店,问他应选择一条什么路线才能使所走的总路程最短?此问题可描述如下:设G=(V,E)是一个具有边成本cij的有向图,cij的定义如下:对于所有的i和j,cij>0,若〈i,〉j|E,则cij=∞。令|V|=n,并假定n>1。G的一条周游路线是包含V中每个结点的一个有向环。周游路线的成本是此路线上所有边的成本和。货郎担问题(travelingsalespersonproblem)是求取具有最小成本的周游路线问题。有很多实际问题可归结为货郎担问题。例如,邮路问题就是一个货郎担问题。假定有一辆邮车要到n个不同的地点收集邮件,这种情况可以用n+1个结点的图来表示。一个结点表示此邮车出发并要返回的那个邮局,其余的n个结点表示要收集邮件的n个地点。由地点i到地点j的距离则由边〈i,〉j上所赋予的成本来表示。邮车所行经的路线是一条周游路线,希望求出具有最小长度的周游路线。第二个例子是在一条装配线上用一个机械手去紧固待装配部件上的螺帽问题。机械手由其初始位置(该位置在第一个要紧固的螺帽的上方)开始,依次移动到其余的每一个螺帽,最后返回到初始位置。机械手移动的路线就是以螺帽为结点的一个图中的一条周游路线。一条最小成本周游路线将使这机械手完成其工作所用的时间取最小值。注意:只有机械手移动的时间总量是可变化的。第三个例子是产品的生产安排问题。假设要在同一组机器上制造n种不同的产品,生产是周期性进行的,即在每一个生产周期这n种产品都要被制造。要生产这些产品有两种开销,一种是制造第i种产品时所耗费的资金(1≤i≤n),称为生产成本,另一种是这些机器由制造第i种产品变到制造第j种产品时所耗费的开支cij,称为转换成本。显然,生产成本与生产顺序无关。于是,我们希望找到一种制造这些产品的顺序,使得制造这n种产品的转换成本和为最小。由于生产是周期进行的,因此在开始下一周期生产时也要开支转换成本,它等于由最后一种产品变到制造第一种产品的转换成本。于是,可以把这个问题看成是一个具有n个结点,边成本为cij的图的货郎担问题。货郎担问题要从图G的所有周游路线中求取具有最小成本的周游路线,而由始点出发的周游路线一共有(n-1)!条,即等于除始结点外的n-1个结点的排列数,因此货郎担问题是一个排列问题。排列问题比子集合的选择问题(例如,0/1背包问题就是这类问题)通nn常要难于求解得多,这是因为n个物体有n!种排列,只有2个子集合(n!>O(2))。通过枚举(n-1)!条周游路线,从中找出一条具有最小成本的周游路线的算法,其计算时间显然为O(n!)。为了改善计算时间必须考虑新的设计策略来拟制更好的算法。动态规划就是待选择的设计策略之一。但货郎担问题是否能使用动态规划设计求解呢?下面就来讨论这个问题。不失一般性,假设周游路线是开始于结点1并终止于结点1的一条简单路径。每一条\n第6章动态规划147周游路线都由一条边〈1,k〉和一条由结点k到结点1的路径所组成,其中k∈V-{1};而这条由结点k到结点1的路径通过V-{1,k}的每个结点各一次。容易看出,如果这条周游路线是最优的,那么这条由k到1的路径必定是通过V-{1,k}中所有结点的由k到1的最短路径,因此最优性原理成立。设g(i,S)是由结点i开始,通过S中的所有结点,在结点1终止的一条最短路径长度。g(1,V-{1})是一条最优的周游路线长度。于是,可以得出g(1,V-{1})=min{c1k+g(k,V-{1,k})}(6.19)2≤k≤n将式(6.19)一般化可得g(i,S)=min{cij+g(j,S-{j})}(6.20)j∈S如果对于k的所有选择,知道了g(k,V-{1,k})的值,由式(6.19)就可求解出g(1,V-{1})。而这些g值则可通过式(6.20)得到。显然,g(i,ꯁ)=ci1,12时,得到OFT和POFT调度的一般问题是难于计算的问题,得到OMFT调度的一般问题也是难于计算的问题(见第10章)。本节只讨论当m=2时获取OFT调度这种特殊情况的算法。为方便起见,用aj表示t1i,bj表示t2i。在两台设备情况下容易证明:在两台设备上处理的任务若不按作业的排列次序处理,则在调度完成时间上不比按次序处理弱(注意:若m>2则不然)。因此,调度方案的好坏完全取决于这些作业在每台设备上被处理的排列次序。当然,每个任务都应在最早的可能时间开始执行。图6.15所示的调度就完全由作业的排列次序5,1,3,2,4图6.15一种调度所确定。为讨论简单起见,假定ai≠0,1≤i≤n。事实上,如果允许有ai=0的作业,那么最优调度可通过下法构造出来:首先对于所有ai≠0的作业求出一种最优调度的排列,然后把所有ai=0的作业以任意次序加到这一排列的前面。容易看出,最优调度的排列具有下述性质:在给出了这个排列的第一个作业后,剩下的排列相对于这两台设备在完成第一个作业时所处的状态而言是最优的。假设对作业1,2,⋯,k的一种调度排列为σ1,σ2,⋯,σk。对于这种调度,设h1和h2分别是在设备P1和P2上完成任务(T11,T12,⋯,T1k)和(T21,T22,⋯,T2k)的时间;又设t=h2-h1,那么,在对作业1,2,⋯,k作了一系列决策之后,这两台设备所处的状态可以完全由t来表征。它表示如果要在设备P1和P2上处理一些别的作业,则必须在这两台设备同时处理前k个作业的不同的任务后,设备P2还要用大小为t的时间段处理前k个作业中没处理完的任务,即在t这段时间及其以前,设备P2不能用来处理别的作业的任务。记这些别的作业组成的集合为S,假设g(S,t)是上述t下S的最优调度长度,于是作业集合{1,2,⋯,n}的最优长度是g({1,2,⋯,n},0)。由最优调度排列所具有的性质可得g({1,2,⋯,n},0)=min{ai+g({1,2,⋯,n}-{i},bi)}(6.23)1≤i≤n将式(6.23)推广到一般情况,对于任意的S和i可得式(6.24)。式中,要求g(ꯁ,t)=max{t,0}且ai≠0,1≤i≤n。g(S,t)=min{ai+g(S-{i},bi+max{t-ai,0})}(6.24)i∈S式(6.24)中max{t-ai,0}这一项由以下推导得出。由于任务T2在max{aj,t}这段时间及其以前不能用设备P2处理,因此,h2-h1=bj+max{ai,t}-ai=bi+max{t-ai,0}。本来,可以使用一种与求解式(6.20)类似的方法求解g(S,t),但它有这么多不同的S,\n150计算机算法基础而对这些S都要计算g(S,t),因此这样计算最优调度长度g({1,2,⋯,n},0)至少要花费nO(2)的时间。下面介绍另一种代数方法来求解式(6.24),可得到一组非常简单的规则,使用这组规则可以在O(nlogn)的时间内产生最优调度。考虑这些作业的一子集S的任意一种调度R。假设直到t这段时间P2都不能用来处理S中的作业。如果i和j是这调度中排在前面的两个作业,则由式(6.24)就得到g(S,t)=ai+g(S-{i},bi+max{t-ai,0})=ai+aj+g(S-{i,j},b+max{bi+max{t-ai,0}-aj,0})(6.25)令tij=bj+max{bi+max{t-ai,0}-aj,0}=bj+bi-aj+max{max{t-ai,0},aj-bi}=bj+bi-aj+max{t-ai,aj-bi,0}=bj+bi-aj-ai+max{t,ai+aj-bi,ai}(6.26)如果作业i和j在R中互易其位,那么完成时间g′(S,t)=ai+aj+g(S-{i,j},tji)其中,tji=bj+bi-aj-ai+max{t,ai+aj-bj,aj}。将g(S,t)与g′(S,t)相比较可以看出,如果下面的式(6.27)成立,则g(S,t)≤g′(S,t)。max{t,ai+aj-bi,ai}≤max{t,ai+aj-bj,ai}(6.27)为了使式(6.27)对于t的所有取值都成立,则需要max{ai+aj-bi,ai}≤max{ai+aj-bj,aj}即,ai+aj+max{-bi,-aj}≤ai+aj+max{-bj,-ai},或min{bi,aj}≥min{bj,ai}(6.28)由式(6.28)可以断定存在一种最优调度,在这调度中,对于每一邻近的作业对(i,j),有min{bi,aj}≥min(bj,ai)。不难证明,具有这一性质的所有调度都有相同的长度。由此足以产生对于每个邻近作业对都满足式(6.28)的任何调度。根据式(6.28)作以下的观察就能得到具有这一性质的调度。如果min{a1,a2,⋯,an,b1,b2,⋯,bn}是ai,那么作业i就应是最优调度中的第一个作业。如果min{a1,a2,⋯,an,b1,b2,⋯,bn}是bj,那么作业j就应是最优调度中的最后一个作业。这使我们能作出一个决策,以便决定n个作业中的一个作业应放的位置。然后又将式(6.28)用于其余的n-1个作业,再正确地决定另一作业的位置,等等。因此,由式(6.28)导出的调度规则如下。(1)把全部ai和bi分类成非降序列。(2)按照这一分类次序考察此序列:如果序列中下一个数是aj且作业j还没调度,那么在还没使用的最左位置调度作业j;如果下个数是bj且作业j还没调度,那么在还没使用的最右位置调度作业j;如果已经调度了作业j,则转到该序列的下一个数。要指出的是,上述规则也正确地把ai=0的那些作业放在适当位置上,因此对这样的作业不用分开考虑。例6.19设n=4,(a1,a2,a3,a4)=(3,4,8,10)和(b1,b2,b3,b4)=(6,2,9,15),对这些a和b分类后的序列是(b2,a1,a2,b1,a3,b3,a4,b4)=(2,3,4,6,8,9,10,15),设σ1,σ2,σ3,σ4是最优调度。由于最小数是b2,置σ4=2。下一个数是a1,置σ1=1。接着的最小数是a2,由于作业2已被调度,转向再下一个数b1。作业1已被调度,再转向下一个数a3,置σ2=3。最\n第6章动态规划151后剩σ3是空的,而作业4还没调度,从而σ3=4。习题六6.1(1)递推关系式(6.8)对图6.16成立吗?为什么?(2)递推关系式(6.8)为什么对于含有负长度环的图不能成立?6.2修改过程ALL-PATHS,使其输出每对结点(i,j)间的最短路径,这个新算法的时间和空间复杂度是多少?6.3对于标识符集(a1,a2,a3,a4)=(end,goto,print,stop),已知成功检索概率为P(1)=1/20,P(2)=1/5,P(3)=1/10,P(4)=1/20,不成功检索概率为Q(0)=1/5,Q(1)=1/10,Q(2)=1/5,Q(3)图6.167个结点的有向图=1/20,Q(4)=1/20。用算法OBST对其计算W(i,j),R(i,j)和C(i,j)(0≤i,j≤4)。26.4(1)证明算法OBST的计算时间是O(n)。(2)在已知根R(i,j)(0≤imthenprint(′stackoverflow′)8stop9endif10STACK(i)←P;P←LCHILD(P)11repeat12loop13callVISIT(P)∥P的左子树周游完∥14P←RCHILD(P)15ifP≠0thenexitendif∥周游右子树∥16ifi=0thenreturnendif17P←STACK(i);i←i-118repeat∥访问父结点∥\n第7章基本检索与周游方法15719repeatendINORDER1通过二元树T中的结点数n来分析INORDER1的计算时间。在第5~11行的while循环的每一次迭代中,把一个结点存入栈(第10行),存入栈中的每一个结点都要被访问(第13行)。由于没有结点被访问一次以上,因此,在此算法的整个执行过程中第5~11行的循环就不可能迭代n次以上。事实上,至多有n-1个结点可能存入栈,这是因为叶子结点都不存入栈(第5行)并且n≥1的每一棵树至少有一个叶子结点。所以第5~11行的总的时间是O(n)。在第12~18行的循环的每次迭代中,有一个结点被访问。由于T中的每一个结点实际上被访问一次,而且在这算法中任何别的地方都不对结点进行访问,因此该算法中的这一循环迭代n次。故这循环所需要的总的时间是Θ(n)。所以,INORDER1的时间复杂度是Θ(n)。就栈空间而论,只有那些具有非空左子树的结点可能存入栈。当T是一棵左斜二元树时,其最坏情况就会发生(见图7.3(b))。在一棵左斜二元树中,除了叶子结点外的每一个结点都有一棵非空左子树和一棵空右子树。在这种情况下,需要大小为n-1的栈,如果每一个结点都有一棵空左子树并且除了叶子结点之外的所有结点都有一棵非空的右子树就会发生最好的情况。这样的一棵二元树是右斜二元树(见图7.3图7.3斜二元树(a))。在这种情况下,没有结点进入栈。用T的深度(a)右斜树;(b)左斜树来表示所需的栈空间则更为有效。可以证明,若T的深度为d,则所需的栈空间为O(d)。由以上分析可知,所有的周游算法都必须访问每一个结点,因此计算时间应该至少是Θ(n)。对所使用的附加空间(即栈空间)作一些改进是唯一可探讨之处。下面介绍在Θ(n)时间和Θ(1)空间内周游二元树的一种方法。如果每个结点有一个PARENT信息段与它的父结点相连接,则周游可以在Θ(n)时间和Θ(1)空间内完成,对此的算法设计与分析留作习题。在这里,将在没有PARENT信息段的情况下去获取一个具有类似特性的算法。PARENT信息段的存在使我们能从任何一个结点P到达根结点。为了得到一个空间为Θ(1)的算法,可通过颠倒由根结点到当前正被检查的结点的链的方向来完成这一效能。譬如,若假定P指向树T中当前正在检查的那个结点,Q指向它的父亲,那么将保留一条由Q到根T的路径,这条路径叫做Q-T路径,它由T到Q路径上的所有结点链接到一起而组成。如果U、V和W是这条路径上的这么3个结点———U是V的父亲而V是W的父亲,那么在W是V的右儿子的情况下就将V通过它的RCHILD信息段链接到U。否则就通过它的LCHILD信息段把V链接到U。但是,使用颠倒结点链接方向的方法,在访问了P所指结点以后,要将Q所指结点与P所指结点再链接在一起就会出现如下问题:设P所指结点为X,Q所指结点为Y,如果已周游了以X为根的子树的所有结点,此时要是Y只有这一个儿子结点,就可以通过LCHILD(Y)或RCHILD(Y)是否为零来判断X是Y的右儿子还是左儿子。因此,在这种\n158计算机算法基础情况下,将X与Y重新链接起来没有什么问题。但是,如果Y有两个儿子,则此时的LCHILD(Y)和RCHILD(Y)一个是Q-T路径上的一个倒挂链,一个指向Y的另一个儿子,于是,判断X是左儿子还是右儿子就出现了困难。为了解决这一问题,根据中根次序周游的定义,可以在访问Y的时候,将Y存入一临时工作单元LR,这样在周游了以X为根的子树后,如果LR中的内容是Y,则说明X是Y的右儿子,反之,则是左儿子。但是,Q-T路径上有两个儿子的结点可能有多个,而在访问它们时又都应将它们保存起来,因此不可避免地要引进栈。为了节省空间,可利用已访问过的叶子结点的LCHILD和RCHILD信息段来建立一个链接表结构的栈,用LCHILD信息段存放一个有两个儿子的结点,用RCHILD信息段作为链接指针。算法7.5用以上方法对中根次序周游进行了具体描述。除上面引进的临时工作单元P、Q和LR外,还引进了临时工作单元AV、TOP和R。其中,AV用来指示当前可作栈使用的叶子结点,TOP和R的作用在算法中一眼就可看出。算法7.5在Θ(n)的时间和Θ(1)的空间内周游二元树的算法lineprocedureINORDER2(T)∥使用固定量的附加空间,二元树T作中根次序周游∥1ifT=0thenreturnendif∥二元树为空∥2TOP←LR←0;Q←P←T∥置初值∥3loop4loop∥尽可能地往下移∥5case6:LCHILD(P)=0andRCHILD(P)=0:∥不能往下移了∥7callVISIT(P);exit8:LCHILD(P)=0:∥移到RCHILD(P)∥9callVISIT(P)10R←RCHILD(P);RCHILD(P)←QQ←P;P←R11:else:∥移到LCHILD(P)∥12R←LCHILD(P);LCHILD(P)←Q;Q←P;P←R13endcase14repeat∥P是叶结点,向上移到其右子树还没检查的结点∥15AV←P∥作为栈使用的叶结点∥16loop∥由P向上移∥17case18:P=T:return∥回退到了根,返回∥19:LCHILD(Q)=0:∥Q由RCHILD链接∥20R←RCHILD(Q);RCHILD(Q)←P;P←Q;Q←R21:RCHILD(Q)=0:∥Q由LCHILD链接∥22R←LCHILD(Q);LCHILD(Q)←P;P←Q;Q←R;\n第7章基本检索与周游方法159callVISIT(P)23:else:∥检查P是否为Q的右儿子∥24ifQ=LRthen∥P是Q的右儿子∥25R←TOP;LR←LCHILD(R)∥修改LR∥26TOP←RCHILD(R)∥退栈∥27LCHILD(R)←RCHILD(R)←0∥释放作栈用的叶结点∥28R←RCHILD(Q);RCHILD(Q)←P;P←QQ←R29else∥P是Q的左儿子∥30callVISIT(Q)31LCHILD(AV)←LR;RCHILD(AV)←TOP32TOP←AV;LR←Q33R←LCHILD(Q);LCHILD(Q)←P∥恢复到P的链接∥34P←RCHILD(Q);RCHILD(Q)←R;exit∥移到右边∥35endif36endcase37repeat38repeat39endINORDER2为便于理解INORDER2,图7.4描述了用INORDER2周游图7.2所示树的基本过程。2行:置初值,Q-T路径为空。11~13行:P移到B,Q-T路径只含根结点A,LCHILD(A)=A表示Q-T路径末端。11~13行:P移到D,结点B由LCHILD链接到Q-T8~10行:访问D,P移到B的右子树(结点G),D由RCHILD链接到Q-T图7.4用Θ(1)的附加空间周游图7.2的二元树\n160计算机算法基础6~7行:再不能下移,访问G。21~22行:继续回退,Q指向A,P指向B并恢复15行:叶结点G将作栈使用,存入AV。LCHILD(B)指向D。19~20行:沿Q-T路径回退,Q指向B,P指向D并恢复由于从B的左子树返回(注意:这是根据RCHILD(B)=RCHILD(D)指向G。0),故访问B且回退到A。图示同(c)图示同(d)(e)(f)23,24,29~35行:由于Q(即A)的左、右儿子均不为0,且LR≠Q,说明P是Q的左儿子,故访问A。然后将LR中的值进栈(即存入LCHILD(G)),原栈顶地址置于RCHILD(G),现栈顶地址变为结点G的地址。再将A存入LR。由A的RCHILD链接到Q-T,并恢复LCHILD(A)指向B,P移到C。11~13行:P移到E,结点C由LCHILD链接到Q-T。6~7行:再不能下移,访问E。15行:叶结点E将作栈使用,存入AV。23,24,29~35行:说明与(g)类似。访问C。6~7行:访问F23~28行:继续回退,说明与(j)类似。15行:F存入AV23~28行:沿Q-T路径回退,由于Q=LR,说明F是C的18行:由于P=T,表示已回退到根,返回。右儿子。修改LR。退栈,释放作栈用的叶结点E。P指向C,Q指向A,恢复RCHILD(C)=F。图示同(g)图示同(a)(j)(k)续图7.4用Θ(1)的附加空间周游图7.2的二元树现在,分析算法INORDER2需要的计算时间和辅助空间。设二元树的结点数为n,又设度为0,1和2的结点数分别为n0,n1和n2,于是n=n0+n1+n2。显然,P指向一个0度结点的次数为1,这种情况出现在第4~14行的循环中P下移到达这个0度结点之时。而P将两次指向只有一个儿子的结点,一次是在下移的时候,另一次则在从它的儿子向上移动的\n第7章基本检索与周游方法161时候(第16~37行)。对于有两个儿子的结点,P也将两次指向该结点,一次在下移时(第4~14行),另一次是在从它的右儿子处上移时(第16~37行),而当从它的左儿子处上移时(第29行),虽然此时访问该结点,但P被修改成指向该结点的右儿子。由此P值改变的总次数是n0+2n1+2n2。在第4~14行的循环的每次迭代中,如果P不是叶子,则P的值就改变;如果P是叶子,则转出这个循环且在第16~37行的循环中改变P的值,即P是叶子时既进入第4~14行的循环,又进入第16~37行的循环。另外,第16~37行的循环的每一次迭代都改变P的值。所以,第4~14行和第16~37行这两个循环的迭代总数为2n0+2n1+2n2。而这两个循环中的任何一次迭代所需的时间是Θ(1),故第3~38行循环的总时间是Θ(2n0+2n1+2n2)=Θ(n)。第1行和第2行都只花Θ(1)的时间,所以总共需要的时间是Θ(n)。由于算法中只有一些像P、Q、AV、LR、TOP和R之类的简单变量,因此所需的辅助空间为Θ(1)。值得指出的是,在所用辅助空间上,INORDER2虽比INORDER1节省,但这种节省是以增加计算时间为代价的,因此,只有在INORDER1所要求的辅助空间O(d)无法满足的情况下才考虑使用INORDER2。7.1.2树周游可以用类似于定义二元树的周游方法对树的周游进行定义。由于树的子树一般无顺序可言,而对二元树的各种周游算法又是建立在它的子树有严格顺序(左、右子树)这一基础之上的,因此,为便于对树周游作出定义,不妨假定树的子树也有某种顺序。这样一来,说一个结点的第一、第二、第三棵子树等就有了意义。由树和森林之间的关系知道,一棵树恰好是具有一棵树的森林,而一个森林可由去掉一棵树的根而得到,因此,利用森林的周游来递归定义树周游是很方便的。树周游一般也有3种方法,分别叫做树先根次序周游,树中根次序周游和树后根次序周游。设F是一个森林,这3种周游方法可描述如下。1.树先根次序周游(F)(1)若F为空,则返回;(2)访问F的第一棵树的根;(3)按树先根次序周游F的第一棵树的子树;(4)按树先根次序周游F其余的树。2.树中根次序周游(F)(1)若F为空,则返回;(2)按树中根次序周游F的第一棵树的子树;(3)访问F的第一棵树的根;(4)按树中根次序周游F其余的树。3.树后根次序周游(F)(1)若F为空,则返回;(2)按树后根次序周游F的第一棵树的子树;(3)按树后根次序周游F其余的树;(4)访问F的第一棵树的根。由于树在一般情况下都是用其相应的二元树来表示,因此,这里就不再给出树周游的详\n162计算机算法基础细算法。在后几节中,可以看到树后根次序周游的一些例子。假定T是由森林F转换成的二元树,由转换方法可知,对T的先根次序和中根次序周游与F上的这些周游有一个自然的对应,即T的先根次序周游相当于按树先根次序周游访问F的结点,T的中根次序周游相当于按树中根次序周游访问F的结点。但对T的后根次序周游则没有类似的自然对应。7.1.3图的检索和周游与图有关的一个基本问题是路径问题。就其最简单的形式而论,它要求确定在一个给定的图G=(V,E)中是否存在一条起自结点v而终于结点u的路径。一种更一般的形式则是确定与某已知起始结点v∈V有路相通的所有结点。后面这个问题可以从结点v开始,通过有次序地检索这个图G上由v可以到达的结点而得到解决,对此问题介绍下面两种检索方法。1.宽度优先检索和周游在宽度优先检索中,从结点v开始,并给v标上已到达(即访问)的标记(此时,称结点v还没检测。当一个算法访问了邻接于某结点的所有结点时,就说该结点已由此算法检测了)。下一步,访问邻接于v的所有未被访问的结点,这些结点是新的没检测的结点。而结点v现在已检测过了。由于这些新近已访问的结点还没有检测,于是将它们放置到未检测结点表的末端。这表上的第一个结点是下一个要检测的结点。这个未检测结点表起一个队列的作用并且可以用任何一种标准的队列表示形式来表示它。过程BFS描述了这一检索的细节。这个过程还用了两个算法DELETEQ(v,Q)和ADDQ(v,Q)。算法DELETEQ(v,Q)从队列Q删除一个结点并带着这个被删除的结点返回。算法ADDQ(v,Q)将结点v加到队列Q的尾部。在图7.5(a)所示的无向图上检验这个算法。假定这个图用邻接表(图7.5(b))来表示,这些结点就按1,2,3,4,5,6,7,8这样的次序被访问。而对图7.5(c)所示的有向图,其起始结点为1的宽度优先检索只访问结点1,2,3。由结点1不能到达结点4。图7.5例图和邻接表(a)无向图G;(b)G的邻接表;(c)有向图算法7.6宽度优先检索算法lineprocedureBFS(v)∥宽度优先检索G,它在结点v开始执行。所有已访问结点都标上VISITED(i)=1。图G\n第7章基本检索与周游方法163和数组VISITED是全程量。VISITED开始时都已置成0∥1VISITED(v)←1;u←v2将Q初始化为空∥Q是未检测结点的队列∥3loop4for邻接于u的所有结点wdo5ifVISITED(w)=0thencallADDQ(w,Q)∥w未检测∥6VISITED(w)←17endif8repeat9ifQ为空thenreturnendif∥不再有还未检测的结点∥10callDELETEQ(u,Q)∥从队中取一个未检测结点∥11repeat12endBFS定理7.2算法BFS访问由v可到达的所有结点。证明设G=(V,E)是一个(有向或无向)图,且v∈V。通过施归纳法于由v到所有可达结点w∈V的最短路径的长度来对这个定理进行证明。用d(v,w)来表示由v到某一可达结点w的最短路径的长度(即边数)。显然,d(v,w)≤1的所有结点w都要被访问。现在假定所有d(v,w)≤r的结点都要被访问,进而证明d(v,w)=r+1的所有结点都要被访问。设w是V中一个d(v,w)=r+1的结点,又设u是一个在v到w的最短路径上紧挨w的前一结点,于是d(v,u)=r,因此u通过BFS被访问。假定u≠v且r≥1。于是,u在临访问之前被放到未检测结点队列Q上,而这个算法直到Q变成空时才会终止,因此,u必定在某个时刻从Q中移出,而且所有邻接于u的未访问结点在第4~8行的循环中被访问,所以w也被访问。证毕。定理7.3设t(n,e)和S(n,e)是算法BFS在任一具有n个结点和e条边的图G上所花的最大时间和最大附加空间。若G由邻接表表示,则t(n,e)=Θ(n+e)和S(n,e)=2Θ(n)。若G由邻接矩阵表示,则t(n,e)=Θ(n)和S(n,e)=Θ(n)。证明只有在第5行的结点才被加到队列。仅当结点w有VISITED(w)=0时,它才可以加到队列上。在将w加到队列之后,紧接着就把VISITED(w)置成1(第6行)。因此,每个结点都至多只有一次被放在队列上。结点v不会放在队列上,所以至多做n-1次加入队列的动作,需要的队列空间至多是n-1。其余的变量所用的空间为O(1)。所以S(n,e)=O(n)。如果G是一个具有v与其余的n-1个结点相连接的图,那么邻接于v的全部n-1个结点都将在同一时刻被放在队列上。此外,数组VISITED需要Θ(n)的空间。因此,S(n,e)=Θ(n)。这结果与使用的是邻接矩阵还是邻接表无关。如果使用邻接表,那么对邻接于u的所有结点可以在时间d(u)内作出判断。若G是无向图,则d(u)是u的度数;若G是有向图,则d(u)是u的出度。因此,当结点u被检测时,第4~8行循环的时间是Θ(d(u))。因为G中的每一个结点至多可以被检测一次,所以第3~11行的循环的总时间是O(Σd(u))=O(e)。VISITED(i)应初始化为0,1≤i≤n。这要花费O(n)的时间。从而总的时间是O(n+e)。如果使用邻接矩阵,那么判断所有邻接于u2的结点要花Θ(n)的时间,因此总的时间就变成为O(n)。如果G是一个由v可到达所有结\n164计算机算法基础2点的图,那么就要检测所有的结点,因此总的时间至少分别是O(n+e)和O(n)。故使用邻2接表时t(n,e)=Θ(n+e),使用邻接矩阵时t(n,e)=Θ(n)。证毕。如果在一个连通无向图G上使用BFS,则G中的所有结点都要被访问,因此该图被周游。但是,若G是非连通的,则G中至少有一个结点没被访问。通过每一次用一个新的未访问的起始结点来反复调用BFS,就可以作出对这个图的一次完全周游。所导出的这个周游算法叫做宽度优先周游算法(BFT),定理7.3的证明也可以用来证明在一个n个结点e条边的图上,若使用邻接表,BFT所要求的时间和附加空间分别是Θ(n+e)和Θ(n)。若使2用邻接矩阵,则分别囿界于Θ(n)和Θ(n)。算法7.7宽度优先图周游算法procedureBFT(G,n)∥G的宽度优先周游∥declareVISITED(n)fori←1tondo∥将所有结点标记为未访问∥VISITED(i)←0repeatfori←1tondo∥反复调用BFS∥ifVISITED(i)=0thencallBFS(i)endifrepeatendBFT如果G是一个连通无向图,则在第一次调用BFS时就会访问G的所有结点。如果G是非连通的,则至少需要调用BFS两次,因此,BFS可以用来判断G是否连通。而且在BFT对BFS的一次调用时,新近访问的所有结点表明这些结点在G的一个连通分图之中,所以一个图的连通分图可以用BFT获得。为此,BFS可以作如下的修改:将新近访问的所有结点都放在一个表中,于是由这个表中的结点所构成的子图和这些结点的邻接表合在一起就构成一个连通分图。因此,若使用邻接表,宽度优先周游算法将在Θ(n+e)的时间内得到这*些连通分图。BFT也可用来得到一个无向图G的自反传递闭包矩阵。设A是这个矩阵,*当且仅当i=j或者i≠j而i和j在同一个连通分图中时,A(i,j)=1。为了判断在i≠j的情*况下A(i,j)是1还是0,可以构造一个数组CONNEC(1∶n),数组元素CONNEC(i)表示*结点i所在的那个连通分图的标记数,当CONNEC(i)=CONNEC(j)时,A(i,j)=1,反之为0。这个CONNEC数组可以在O(n)时间内构造出来。因此,有n个结点e条边的无向图2G的自反传递闭包矩阵,不管是使用邻接表还是使用邻接矩阵,都可以在Θ(n)的时间和*Θ(n)空间内算出(此空间计算不包括A本身所需要的空间)。作为宽度优先检索的最后一个应用,考虑获取无向图G的生成树问题。当且仅当G连通时,它有一棵生成树。从而BFS易于判断生成树的存在。此外,考虑在算法BFS的第4~8行中为到达那些未访问结点w所使用的边(u,w)的集合。这些边称为前向边(forwardedges)。设T表示前向边的集合。我们断言,如果G是连通的,则T是G的一棵生成树。对于图7.5(a)所示的图,边集T将是G中除了(5,8),(6,8),(7,8)以外的所有的边的集合(图7.6(a))。使用宽度优先检索所得到的生成树称为宽度优先生成树(breadthfirstspan-ningtree)。\n第7章基本检索与周游方法165图7.6图7.5(a)中的图的BFS和DFS生成树定理7.4修改算法BFS,在第1行和第6行分别增加语句T←ꯁ和T←T∪{(u,w)}。**修改后的算法叫做算法BFS。如果在v是连通无向图中任一结点的情况下调用BFS,那么在算法终止时,T中的边组成G的一棵生成树。证明若G是n个结点的连通图,则这n个结点都要被访问。而且除了起始结点v外,所有其它的结点都要放到队列上一次(第5行),从而T将正好包含n-1条边。而且这些边都是各不相同的。因此,T中的这n-1条边将定义一个关于这n个结点的无向图。由于这个图包含由起始结点v到所有其它结点的路径(因此在每个结点对之间存在一条路径),所以这个图是连通的。用归纳法容易证明对于有n个结点且正好有n-1条边的连通图是一棵树。因此T是G的一棵生成树。证毕。宽度优先检索有广泛的应用,其中解决最优化问题的一种重要方法:分枝-限界法(第七章)就是在宽度优先检索基础上建立起来的。2.深度优先检索和周游一个图的深度优先检索与宽度优先检索的差别在于一有新的结点到达就中止对原来结点v的检测,且同时开始对新结点u的检测。在此新结点被检测后,再恢复对v的检测。当所有可达结点全部被检测完毕时,就结束这一检索过程。这一检索过程最好用递归描述成算法5.8那样的形式。算法7.8图的深度优先检索lineprocedureDFS(v)∥已知一个n结点的无向(或有向)图G=(V,E)以及初值已置为零的数组VISITED(1∶n)。这个算法访问由v可以到达的所有结点。G和VISITED是全程量∥1VISITED(v)←12for邻接于v的每个结点wdo3ifVISITED(w)=0thencallDFS(w)endif4repeat5endDFS图7.6(a)起始于结点1并且使用图7.5(b)的邻接表,对于这个图的深度优先检索导致按1,2,4,8,5,6,3,7的次序去访问这些结点。对于DFS的非递归算法要使用栈来保留已部分检测的所有结点。容易证明DFS访问由v可达的所有结点。如果t(n,e)和S(n,e)表示DFS对一n个结点e条边的图所花的最大时间和最大附加空间,那么S(n,e)=Θ(n),在2使用邻接表的情况下,t(n,e)=Θ(n+e),而在使用邻接矩阵的情况下,t(n,e)=Θ(n)。一个图的深度优先周游是通过每次用一个新的未访问的起始结点来反复调用DFS实\n166计算机算法基础现的。这个DFT算法与BFT的区别仅在于用对DFS(i)的调用去代替对BFS(i)的调用。和在BFT的情况下一样,使用DFT可以得到一个图的那些连通分图。同样,也可以用DFT求出一个无向图的自反传递闭包矩阵。只要对DFS作如下修改:把T←ꯁ和T←T∪{(v,w)}分别加到第1行和第3行then的子句,那么,DFS终止时,在无向图G是连通的条件下,T中的这些边就定义了G的一棵生成树。用这种方法所得到的生成树叫深度优先生成树(depthfirstspanningtree)。图7.5(a)所得到的生成树包含除(2,5),(8,7)和(1,3)以外所有的边(见图7.6(b))。因此,DFS和BFS对迄今所讨论的检索问题是等效的。由以上讨论可知,BFS和DFS是两种根本不同的检索方法。在BFS中,一个结点在任何其它结点开始检测之前要完全被检测,这下一个要检测的结点是剩下未检测的第一个结点。习题中还讨论了一种检索方法(D-search),它与BFS的区别仅在于下一个要检测的结点是最新到达的未检测结点。在DFS中,一旦到达一个新的未检测结点,则中止原来那个结点的检测,并立即开始这个新结点的检测。显然DFS和D-search的实现都需要一个栈,但这两种检索方法是不同的。本节中所介绍的检索方法可用于各种各样的问题。7.2代码最优化编译程序的作用是,把按某种源语言写成的程序翻译成一个等效的汇编语言程序或机器语言程序。考察这么一个问题,把按某种语言(例如PASCAL)写成的算术表达式翻译成汇编语言代码。这种翻译显然依赖于正使用的那种特定的汇编语言(因此,依赖于所用的机器)。开始,假定有一台非常简单的机器模型,把这模型叫做模型机A。这台机器只有一个称为累加器的寄存器。所有的算术运算都必须在这寄存器中进行。如果○·代表像+、-、*、/这样的一个双目运算符,则○·的左运算量必须在这累加器中。为简单起见,将讨论只局限于这4个运算符。这一讨论易于推广到其它的运算符。相应的汇编语言指令是LOADX⋯将内存单元X的内容装入累加器;STOREX⋯将累加器的内容存入内存X单元;OPX⋯OP可以是ADD,SUB,MPY或DIV。指令OPX用累加器的内容作为左运算量,X存储单元的内容作为右运算量进行操作符OP的计算。作为一个例子,考虑算术表达式:(a+b)/(c+d)。这表达式的两个可能的汇编语言模式在表7.2中给出。T1和T2是内存中的临时存储单元。在这两种情况下,结果都留在累加器中。代码段(a)比代码段(b)长两条指令。如果每条指令用的时间量相同,则代码段(b)将比代码段(a)所用的时间少25%。对于表达式(a+b)/(c+d)和给定的机器A,代码段(b)显然是最优的。定义7.1表达式E翻译成某给定机器的机器语言或汇编语言是最优的,当且仅当这一翻译有最少的指令条数。下面再来看3个例子。考虑表达式a+b*c。表7.3显示了两种可能的翻译。代码段(b)虽不符合+的左运算量应放在累加器、右运算量应放在内存储器中的要求,但由于x+y=y+x,因此代码段(b)与(a)是等效的。\n第7章基本检索与周游方法167表7.2(a+b)/(c+d)两种可能的代码段表7.3a+b*c的代码段LOADaLOADcLOADbLOADbADDbADDdMPYcMPYcSTORET1STORET1LOADcLOADaSTORET1ADDaADDdADDbLOADaSTORET2DIVT1LOADT1ADDT1DIVT2(a)(b)(a)(b)定义7.2双目运算符○·在定义域D中是可交换的,当且仅当对于D中所有的a和b,有a○·b=b○·a。+和*运算符对于整数和实数是可交换的,而-和/运算则不是。利用某些运算符的可交换性可能得到较短的代码段。下面考虑表达式a*b+c*d。表7.4显示了两种可能的代码段。代码段(b)实际上计算(a+c)*b,它和a*b+c*b相等。表7.4a*b+c*b的代码段表7.5a*(b*c)+d*cLOADbLOADaLOADcLOADaMPYcMPYbMPYbADDcSTORET1ADDdLOADaSTORET1MPYbMPYT1STORET1LOADaLOADdMPYbMPYcSTORET2ADDT1LOADT1ADDT2(a)(b)(a)(b)定义7.3双目运算符○*相对于双目运算符ꯝ在定义域D中是左分配的,当且仅当对于D中的每一个a,b,c,有a○*(bꯝc)=(a○*b)ꯝ(a○*c)。○*相对于ꯝ是右分配的,当且仅当对于D中的每一个a,b,c,有(aꯝb)○*c=(a○*c)ꯝ(b○*c)。在实数范围内,*相对于+和-是左、右分配的,这是因为a*(b+c)=(a*b)+(a*c),a*(b-c)=(a*b)-(a*c),(a+b)*c=(a*c)+(b*c)和(a-b)*c=(a*c)-(b*c)。/相对于+和-不是左分配的,因为a/(b+c)≠(a/b)+(a/c)。但是在实数范围内,/是右分配的。不过,在整数范围内/相对于+和-不是右分配的。例如,(2+3)/5=1,而在整数算术中,由于2/5=0,3/5=0,得(2/5)+(3/5)=0。最后一个例子,考虑表达式a*(b*c)+d*c。表7.5描述了两种可能的代码段。表7.5(b)的代码段使用了(a*b)*c=a*(b*c)。定义7.4双目运算符○·在定义域D中是可结合的,当且仅当对于D中的所有的a,b,c有a○·(b○·c)=(a○·b)○·c。\n168计算机算法基础对于整数范围和实数范围,*运算是可结合的,/运算是不可结合的。利用运算符的可结合、分配和交换的性质,可能作出较短的代码段。注意,虽然在实数范围内,有(a+c)*b=(a*b)+(c*b),但是表7.4(a)和(b)的这两个代码段可能算出不同的答案。这种情况的出现是由于计算机有限长的算术运算在计算中会产生一些误差的缘故。在下面的讨论中将略去这一因素,并且假定在可应用结合律、交换律和分配律的时候都可以对它们自由地使用。对于一个给定的表达式可能给出不同的代码段,下面讨论怎样获取最优代码段的问题。开始,将讨论局限于这台简单的机器A。然后,再考察更一般的机器模型。运算符出现在它们的运算量之间的表达式的形式称为中缀形式。这是通常写算术表达式的方法。为了产生最优代码段,用二元树来表示算术表达式是方便的。二元树的每一个非叶子结点表示一个运算符。非叶子结点称为内部结点。某内部结点P的左子树表示由P所代表的运算符的左运算量的二元树形式。而右子树则表示其右运算量的二元树形式。一个叶子结点或者表示一个变量或者表示一个常数。图7.7给出了一些表达式的二元树形式。这些表示算术表达式的二元树称为表达式树。图7.7某些中缀表达式的二元树形式为了从表达式树得到产生最优代码段的算法,开始,假定所有的运算符既不可交换,也不可分配或结合,也不允许使用代数变换去化简表达式。例如,尽管a+b-a-b有值为0,但在上述假定下,最优代码段将是LOADa;ADDb;SUBa;SUBb。我们也不涉及对公共子表达式的处理,所有的子表达式都假定为互不相关的。因此,a*b*(a*b-d)的最优代码段与a*b*(c*e-d)的最优代码段相同。在这些假定下,易于看出若一个表达式有n个运算符,则它的代码段恰好有n条ADD,SUB,MPY,DIV这种类型的指令。将这类指令称为运算符指令。只有累加器的装入和存放指令条数会变化。例如,表7.2(a)和(b)的代码段都有3条运算符指令。代码段(a)有3条装入和两条存放指令,而代码段(b)只有两条装入和一条存放指令。易于证明在任何没有冗余指令的代码段中,除了第一条以外的每条装入指令都必须紧接在一条存放指令之后。因此,装入指令数总比存放指令数多1。所以只要产\n第7章基本检索与周游方法169生使装入指令数或存放指令数为最小值的代码段就是最优的代码。设P是任一表达式树的一个内结点;又设L和R分别是P的左、右子树;再设○·是在结点P的运算符。根据对运算符所作出的假定,计算L○·R的唯一方法是先独立地计算L和R,然后计算L○·R,L和R的代码段也应是最优的。假定已给出L和R的最优代码段CL和CP,那么计算L○·R就有表7.6所列出的那几种可能性。表中“,条件”这一列揭示了L和R的各种可能性以及在L和R不是叶子结点的情况下计算L和R的次序。在写出的代码中,○·a表示一条运算指令。如果○·是+,则指的是ADDa。表7.6揭示出,在产生L○·R的代码段时,只有L和R都是内部结点才有选择的机会。当L和R中的任何一个为叶子结点时,则(在①,②和③种条件下的)代码段是唯一的(禁止引进无用的代码)。当L和R都是内部结点时,条件⑤的代码段比条件④的要少些,所以应被采用。由此产生以下的观察结果,如果R是内部结点,在最优化码段中CR在CL的前面,否则,CL就在CR的前面。表7.6计算L○·R的各种可能性条件相应的代码段①L和R都是叶子,变量分别是a和bLOADa;○·b②L是具有变量a的叶子,R不是叶子CR;STORET1;LOADa;○·T1③R是具有变量a的叶子,L不是叶子CL;○·a④L和R都不是叶子,L在R之前计算CL;STORET1;CR;STORET2;LOADT1;○·T2⑤L和R都不是叶子,R在L之前计算CR;STORET1;CL;○·T1以上论述的与用分治方法或动态规划法是类似的。利用分治方法,可以先获得L和R的最优代码段,然后以某种方法将这些最优代码段结合起来而得出L○·R的最优代码段。用动态规划法可以将代码段看成是一系列决策的结果。其每一步都作出对某一个子表达式接着编码的决策。只有在已经产生了L和R的代码段后才可以对表达式L○·R编码。表7.6导致代码段的递归生成过程为CODE1(算法7.9)。这个算法使用过程TEMP(i)和RETEMP(i)。TEMP(i)得到临时存储器的一个存储单元,而RETEMP(i)则释放临时存储单元i。假定表达式树有一个由T所指示的根结点,并且每一个结点都有3个信息段LCHILD,RCHILD和DATA。内部结点的DATA信息段是运算符。叶子结点的此信息段是运算量地址。另外,该算法还假定T≠0。注意,这算法实质上执行对二元树T的周游,但是这一周游方法与7.1.1节中所讨论的3种方法都不相同的是,只有内部结点被访问。当访问一个结点时,就生成该结点的代码段。对一个结点的访问只有在它的两棵子树的代码段生成以后才能进行。这类似于后根次序周游。但是,在算法CODE1中,一棵非平凡的右子树在其相应的左子树之前被周游(一棵平凡的子树是只有一个根结点的子树)。如果把临时存储器当成一个栈来处理,而TEMP和RETEMP分别对应于从这个栈删去和插入的操作,则表7.7可显示由CODE1对图7.7中的某些例子所生成的代码段。根据前面的讨论,可以得出由CODE1所生成的代码段相对于机器A是最优的。在研究机器A的推广形式时,将给出较严密的证明。定理7.5用CODE1所产生的代码段能正确计算由表达式树T所表示的算术表达式的值。该定理的证明(对T的深度施行简单的归纳),留作习题。\n170计算机算法基础算法7.9生成代码段的算法procedureCODE1(T)∥假设T≠0,T是一棵表达式树,生成T的代码段∥ifT是叶子thenprint(″LOAD″,DATA(T))returnendifF←0∥如果RCHILD(T)不是叶子,则将F置成1∥ifRCHILD(T)不是叶子thencallCODE1(RCHILD(T))∥生成CR∥callTEMP(i)print(″STORE″,i)F←1endifcallCODE1(LCHILD(T))∥生成CL∥ifF=1thenprint(DATA(T),i)callRETEMP(i)elseprint(DATA(T),DATA(RCHILD(T)))endifendCODE1如果允许使用运算符的可交换性,CODE1在机器A上就不能产生出最优代码段。为了看清这一点,考察图7.7和表7.7的(b):当+可交换时,最优代码段是LOADb,MPYc,表7.7由CODE1对图7.7的一些例子所生成的代码段LOADaLOADbLOADaADDbMPYcADDbSTORET1MPYcLOADaADDT1(a)(b)(c)LOADcLOADeADDdADDfSTORET1STORET1LOADbLOADdADDT1MPYT1STORET1STORET1LOADaLOADbADDT1ADDcSTORET2LOADaDIVT2ADDT1(g)(h)\n第7章基本检索与周游方法171ADDa。如果○·是可交换的,表7.6中的可能性就要增加。为了在可交换运算符的情况下能产生最优代码段,需要修改CODE1。所需要的修改留下作为习题。现在,将机器A推广到另一种机器B。B有N≥1个可以执行算术运算的寄存器。对于B,有4种类型的机器指令:指令功能(1)LOADM,R将内存单元M的内容装入寄存器R,1≤R≤N。(2)STORER,M将寄存器R的内容存到内存单元M,1≤R≤N。(3)OPR1,M,R2执行(R1)OP(M)的计算,结果放在R2寄存器中。OP是任何一种双目运算符(例如,+,-,*,/),R1和R2是寄存器,M是一个内存单元。R1可等于R2。(4)OPR1,R2,R3与(3)类似。R1,R2和R3是寄存器。这些寄存器的某两个或全体可以相同。比较A和B这两种机器模型,可以看出,当N=1时模型B的(1),(2),(3)型指令与模型A相应的指令是相同的;(4)型指令只允许在没有附加存储存取装置的情况下执行像a+a,a-a,a*a和a/a这样一些普通的运算。这不会改变在A和B上产生最优代码段指令的条数。因此,当N=1时模型A在某种意义上与模型B是一样的。就模型B而言,一个给定的表达式E的最优代码段可以因N值的不同而不同。表7.8列出了图7.7所示的表达式(f)在N=1和N=2两种情况下的最优代码段。要指出的是,当N=1时,必须生成一条存放指令,而当N=2时,则不需要生成存放指令。寄存器标记成R1和R2。T1是存储器中的临时存储单元。并且注意,LOAD的条数不再需要正好比STORE的条数多1。因此,为了最优化而只考虑LOAD指令的条数或者只考虑STORE指令的条数就不够了。而是要使它们的和取最小值。为了简化这一讨论,首先假定没有运算符是可结合、可交换或可分配的。并且假定一个运算符的左、右两个运算量即使是相同的子表达式也必须分别进行计算,这一限制被扩大到表达式之类情况,例如a○·a,也要求对左、右运算量都作一次内存访问。表7.8N=1和N=2的最优代码段LOADc,R1LOADc,R1MPYR1,d,R1MPYR1,d,R1STORER1,T1LOADa,R2LOADa,R1ADDR2,b,R2ADDR1,b,R1DIVR2,R1,R1DIVR1,T1,R1N=1N=2图7.8计算最小的寄存器数给定一个表达式E,可能要问的第一个问题是:不用任何STORE指令可以算出E的值吗?第二个问题是:不用任何STORE指令而计算E的值所需要寄存器的最小数量是多少?在作了上面的那些假定的条件下来回答这些问题,并且还假定E的值将保留在这N个寄存器的一个之中。设E用一棵表达式树T来表示,若T只有一个结点,则这个结点必定是一个叶子,显然,所需要的全部工作就是把相应变量的值或常数装入到一个寄存器中。这种情况只需要一个寄存器,如图7.8(a)所示。若表达式E只有一个运算符,则表达式就是a○·b\n172计算机算法基础这种形式。这种情况也只需要一个寄存器R1来装a,然后可以使用○·R1,b,R1指令(见图7.8(b))。当出现一个以上运算符时,则有图7.8(c)所示的情况。设l1和l2分别是对根运算符的左运算量(L)和右运算量(R)进行单独计算所需要寄存器的最小数。设l是计算L○·R所需要寄存器的最小数。在作了上述这些假定的情况下就应单独计算L和R的值,因此,l≥max{l1,l2}。如果l1>l2,可以先用l1个寄存器计算L,并将L的值保存在一个指定的寄存器中,然后可用其余的l1-1≥l2个寄存器来计算R。最后,用一条(4)型指令就可计算出L○·R。因此,当l1>l2时,l=l1。类似地,当l1N时,代码段必须包含某些STORE型的指令,最优代码段将使得(1)和(2)型指令数之和最小。定理7.6的证明提供了一个代码段生成算法(算法7.10)。后面将证明CODE2在规定的这些假设条件下生成最优代码段。现在先来理解这个算法。图7.9结点的MR值(即结点上方的数)算法假定表达式树T中的每个结点有4个信息段:LCHILD,RCHILD,DATA和MR。MR的值已像定理7.6定义的那样被算出。CODE2用了两个与CODE1中相同的子程序TEMP和RETEMP。为了生成表达式树T的代码段,CODE2以callCODE2(T,1)的形式\n第7章基本检索与周游方法173被调用。寄存器总数N是一个全程变量。假定T≠0,即表达式不是空的。对于CODE2(T,i)的调用,只使用寄存器Ri,⋯,RN来生成表达式T的代码段。结果保留在Ri中。在对CODE2初次调用的情况下,若T是一个叶子,则只产生一条装入指令。若T是一个内结点,则进入case语句(6~24行)。L和R分别指向T的左、右儿子。设T的DATA信息段中的运算符为○·,若R是一个叶子,则MR(R)=0。因此,L○·R的最优代码段是L的最优代码段加上这条○·运算指令。这在7~9行生成。而对L是一个叶子的情况,只有在第8行和第18行递归调用CODE2时,才可能出现。当在这两处L是叶子时,只要在此处再产生一条装入指令。在初次调用CODE2时,如果MR(T)>N,则在此表达式树中必定至少有一个内结点,它的左、右儿子所需的最小寄存器数MR(L)≥N且MR(R)≥N。由定理7.6可知,在这种情况下至少要产生一条存放指令。L○·R的最优代码段是R的最优代码段后跟随一条存放R的结果的指令再加上L的最优代码段,这在第10~15行生成。要指出的是,在MR(L)≥N且MR(R)≥N的情况下,第10行和第13行的两次递归调用都允许CODE2使用寄存器Ri,⋯,RN。对递归深度施以简单的归纳可知,总有i=1。不管对CODE2是初次调用还是递归调用,如果其实在参数T的MR(L)和MR(R)至少有一个小于N且MR(R)≠0,则在第16~23行处理。若MR(L)DFN(u)≥L(u)。在这两种情况下L(u)都能得到正确的修正。对于ART的初次调用是callART(1,0)。在调用ART之前DFN初始化为0。算法7.11计算DFN和L的算法lineprocedureART(u,v)∥u是深度优先检索的开始结点。在深度优先生成树中,u若有父亲,那么v就是它的父亲。假设数组DFN是全程量,并将其初始化为0。num是全程变量,被初始化为1。n是G的结点数∥globalDFN(n),L(n),num,n1DFN(u)←num;L(u)←num;num←num+12for每个邻接于u的结点wdo3ifDFN(w)=0thencallART(w,u)∥还没访问w∥4L(u)←min(L(u),L(w))5elseifw≠vthenL(u)←min(L(u),DFN(w))6endif7endif8repeat9endART如果连通图G有n个结点e条边,且G由邻接表表示,那么ART的计算时间为O(n+e)。因此,L(1∶n)可在时间O(n+e)内算出。一旦算出L(1∶n),G的关节点就能在O(n)时间内识别出来。因此,识别关节点的总时间不超过O(n+e)。如何判断G的双连通分图呢?要是在第3行调用ART之后有L(w)≥L(u),就可断定u或者是根,或者是关节点。不管u是否为根,也不管u有一个或是多个儿子,将边(u,w)和对ART的这次调用期间遇到的所有树边和逆边加在一起(除了包含在子树w中其它双连通分图的边以外),构成一个双连通分图(它的形式证明将在定理7.10的证明中给出)。因此,为了得到双连通分图,对ART需作以下修改。(1)引进一个用来存放边的全程栈S。(2)在2到3行间增加一行:2.1ifv≠wandDFN(w)DFN(u)时,(u,w)早已存在栈中。(3)在3到4行间增加下列行:3.1ifL(w)≥DFN(u)thenprint(′newbiconnectedcomponent′)3.2loop3.3从栈S的顶部删去一条边3.4设这条边是(x,y)3.5print(′(′,x,′,′,y,′)′)3.6until((x,y)=(u,w)or(x,y)=(w,u))repeat3.7endif可以证明算法ART在增加了这些内容之后,其计算时间仍然是O(n+e)。下面来证明这算法的正确性。定理7.10当连通图G至少有两个结点时,增加了2.1和3.1~3.7行的算法,ART能正确地生成G的双连通分图。证明当G只有一个结点时,它没有边,因此这过程不产生输出。G的双连通分图就是这单个结点。对此情况可作单独处理。当G的结点数n≥2时,算法能正确运行,这可通过施归纳于G的双连通分图数来证明。如果G只有一个双连通分图,即G是双连通图,显然,它的深度优先生成树的根u只有一个儿子w,而且w是3.1行中使得L(w)≥DFN(u)的唯一结点。到w被检测完时,G中所有的边已作为一个双连通分图输出。现假定该算法对至多有m个双连通分图的所有连通图G都能正确执行。下面证明对于有m+1个双连通分图的所有连通图这个算法也能正确执行。考虑3.1行中第一次出现L(w)≥DFN(u)的情况。此时还没有任何边被输出,因此G中与w子孙相关联的所有边都在栈中,且在边(u,w)的上面。由于u的子孙都不是关节点而u是一个关节点,因此S栈中(u,w)上面的边集和边(u,w)一起构成一个双连通分图。一旦将这些边从栈S中删除并输出,此算法基本上相当于在一个从G中删去这个双连通分图后所剩下的图G′上运行。算法在G和G′上运行的差别仅在于以下一点,即,在完成对结点u的检测期间,可能要考虑刚输出的分图中的那些边(u,r)。然而,对于这些边都有DFN(r)≠0和DFN(r)>DFN(u)≥L(u),因此,这些边只会使第2~8行的循环作些无意义的迭代而并不会影响这个算法。容易证明G′至少有两个结点。又由于G′正好有m个双连通分图,因此,由归纳假设可以得出这m个双连通分图能正确地生成。证毕。要特别指出的是,上面描述的算法要在生成树满足以下条件的环境中工作,相对于这棵生成树,所给定的图没有交叉边。而相对于宽度优先生成树,一些图可能有交叉边,因此算法ART对BFS不适用。7.4与/或图很多复杂问题很难或没法直接求解,但可以分解成一系列(类型不同)的子问题,而这些子问题又可反复细分成一些更小的子问题,一直到分成一些可普通求解的、相当基本的问题\n第7章基本检索与周游方法181为止。然后,由这些分解成的子问题的全部或部分解再导出原问题的解。这种将一个问题分解成若干个子问题,又由子问题的解导出原问题解的方法称为问题化简。问题化简已在工业调度分析、定理证明等方面得到应用。把复杂问题分解成一系列子问题的过程可以用如下结构的有向图来表示:在有向图中,结点代表问题,一个结点的子孙代表与其相联系的子问题。为了暗示父结点的解可由哪些子问题联合导出,则用一条弧将那些能联合导出其解的子结点连接在一起。例如,图7.16(a)表示问题A可以通过求解子问题B和C来解出,或者可由单个求解子问题D或E来解出。边〈A,B〉和〈A,C〉则用一条弧连在一起。为了使图中的每个结点含义单一化,即它的解或者需要求解它所有的子孙得到,或者求解它的一个子孙就可得到,通过引进像图7.16(b)那样的虚结点可达到此目的。这前一类结点称为与结点,后一类结点称为或结点。由与结点出发的所有边用一条穿过它们的弧相连结。图7.16(b)的A和A″是或结点,A′是与结点。没有子孙的结点是终结点,它代表基本问题并标记上可解或不可解。可解的终结点用方框表示。图7.16表示问题的图下面来看一个例子,某人一星期洗一次衣服,所要做的事有:收集脏衣服、洗衣服、把衣服弄干、熨平、叠好并归堆。其中,某些事可采用不同的方法,如洗衣服一项可以是手洗也可以是机器洗。对于这个问题可以构造出图7.17所示的那样的一棵与/或树。图中,手洗的那个结点没有子孙也不是方形结点,它表示此人不采用手洗的方法。当然,洗衣服问题是个非常简单的问题,而实际应用中很多问题远非如此简单,因此使用问题化简就有了现实的意义。图7.17洗衣服问题对应的与/或图图7.18两个不是树的与/或图在对问题化简时,如果两个子问题在分解成的更小子问题中有一个公共的更小子问题,而这个更小的子问题只需求解一次,则在该问题的与/或图上可用一个结点来表示这个更小的公共子问题。图7.18显示了两个出现这种情况的与/或图。这样的与/或图就不再是树了,而且可能出现像图7.18(b)所示的有向环。要指出的是,有向环的出现并不意味着该问\n182计算机算法基础题不可解。事实上,图7.18(b)所示的问题A可通过求解基本问题G,H和I来导出其解。即通过求解G,H和I导出D和E的解,因此也就能导出B和C的解。下面再引进一个概念:解图是由与/或图中一些可解结点组成的子图,它表示对问题求解。图7.18所示的两个图的解图由粗线条示出。下面,只考虑问题的分解过程可以用与/或树来表示的情况,在这种情况下,如何根据问题的与/或树来判断该问题是否可解呢?这只需对这棵与/或树作后根次序周游就可得出答案。算法7.12对这一判断过程作了具体的描述。在算法执行过程中,一旦发现某与结点的一个儿子结点不可解(第6行),或者发现某或结点的一个儿子结点可解(第11行),就立即终止该算法,这可减少算法的工作量且对结果无任何影响。算法7.12判断与/或树是否可解算法lineprocedureSOLVE(T)∥T是一棵其根为T的与/或树,T≠0。如果问题可解则返回1,否则返回0∥1case2:T是终结点:ifT可解thenreturn(1)3elsereturn(0)4endif5:T是与结点:forT的每个儿子Sdo6ifSOLVE(S)=0thenreturn(0)7endif8repeat9return(1)10:else:forT的每个儿子Sdo∥或结点∥11ifSOLVE(S)=1thenreturn(1)endif12repeat13return(0)14endcase15endSOLVE对于一个给定的复杂问题,不仅需要知道此问题是否可解,而且希望知道如果问题可解,那么此问题的解是由哪些基本问题、沿着什么样的途径所导出的,即希望求出问题的解树。由于解树是与/或树的全体或一部分,因此求解树的算法可在生成与/或树算法的基础上加上一些对结点可解性的判断和删除措施而获得。因为与/或树结点的生成取决于问题的分解方法,假定问题的分解方法可用函数F来表示,所以对于一个已经生成的结点,可用函数F去生成它的所有儿子。而生成结点的次序既可按宽度优先也可按深度优先的次序来生成。因此,解树的结点也需用函数F并可按宽度优先或深度优先的次序来生成。不过要指出的是,一棵与/或树可能有无穷的深度,在使用解树的深度优先生成算法的情况下,即使已知解树存在,算法也可能导致所有生成的结点都在一条由根出发的无穷深度的路径上,从而根本就不能确定出一棵解树,这一点可通过对生成深度作出某种限制获得解决。譬如生成的深度只准达到某个d,凡在深度d处的非终止结点都标记为不可解。这样只要有一处的深度不大于d,就可保证生成一棵解树。宽度生成算法没有这样的缺点。因为每个结点都只有有限个儿子,所以与解树相对应的与/或树中任何一级都不可能有无穷多个儿子。于是,\n第7章基本检索与周游方法183只要存在解树,宽度优先生成算法就一定可以将其找出,而且找出的还是一棵具有最小深度的解树。不过,如果与/或树中由根出发的所有路径都有无穷的深度,宽度优先生成算法也会出现不终止的情况。这可通过限制所希望得到的解树的深度获得解决。过程BFGEN是一个解树的宽度优先生成算法。如果解树存在,则算法生成与/或树的一棵宽度优先解树,而与/或树则是在结点T开始,应用儿子生成函数所得到的。BFGEN使用了一个与SOLVE类似的子算法ASOLVE(T)。该子算法对部分生成的与/或树T作一次后根次序周游,并且将结点标上可解、不可解或可能可解的标记。由于T不是一棵完整的与/或树,因此它有3类叶子结点:第一类结点是非终止叶子结点。由于非终止叶子结点还没检测,因此对其可解性暂时没法判定,故将其标记为可能可解。其它两类叶子结点是完整与/或树的叶子,故根据叶子结点所代表问题的可解性标上可解或不可解。如果一个非叶子结点是与结点,则只要它有一个儿子不可解它就不可解。而对于一个非叶子结点的或结点,若它至少有一个儿子可解,则该结点就是可解的。所求得的任何不可解的结点都可从T中删去(第7行)。对于任何不可解结点P的子孙,也没有必要检测,因为,即使某些子孙可解,P也不能解出,所以第9行从队列中删去P的所有还没检测的子孙。如果已求出某结点可解,则没有必要进一步去检测那些还没检测的子孙,这一工作也在第9行完成。容易证明,如果存在一棵对应于(T,F)的解树,那么BFGEN就一定会找到这棵树。注意:如果找到了这样的一棵树,那么T就指向它的根并且这棵树可能有某些为了求解整个问题并不需要求出的可能结点,对T另外作一次扫描可以消去这些多余的结点。算法7.13宽度优先生成解树lineprocedureBFGEN(T,F)∥F生成T中的儿子结点;T是根结点。终止时,如果存在解树,则T是这解树的根∥1将队列Q初始化为空;V←T2loop3用F生成V的那些儿子∥检测V∥4ifV没有儿子then标记V为不可解else①将V的所有不是叶子结点的儿子放入队列Q,将那些叶子结点分别标上可解或不可解②把V的所有儿子加入树T5endif6callASOLVE(T)7从树T删去所有标记为不可解的结点8if根结点T标记为可解thenreturn(T)endif9从队列Q中删去以下的所有结点:它们在T中曾有一个祖先被标记为不可解或者在T中有一个标记为可解的祖先10ifQ为空thenprint(′nosolution′);stopendif11删去队列Q的第一个元素;设此结点是V12repeat13endBFGEN\n184计算机算法基础7.5对策树本节讨论树在博弈游戏中的应用。在一盘棋赛中,对弈各方都要根据当前的局势,分析和预见以后可能出现的局面,决定自己要采取的各种对策,以争取获得最好的结果。博弈是一种竞争,而竞争现象广泛存在于社会活动的许多方面,因此本节的内容可以很自然地引申并应用于含有竞争现象的政治、经济、军事、外交等各个领域。首先,来看一个非常简单的拾火柴棍游戏。假定盘上放有n支火柴,由弈者A和B两个人参加比赛。比赛规则是:两名弈者轮流从盘中取走火柴,每次从盘中取走1,2或3支火柴均为合法着。否则,为非法着;拿走盘中最后一支火柴的弈者则负了这一局,当然另一名弈者则胜这一局。任何时刻盘中剩下的火柴数都表示此时刻的棋局。拾火柴棍游戏在任一时刻的状态则由此时的棋局和轮到走下一着的弈者一起所决定。终局是表示胜局、负局或和局情况的棋局。其它棋局都是非终止棋局。在拾火柴棍游戏中只有一种终局形式,即盘中没火柴棍了。因此不是A胜就是B胜,不可能出现和局。在以下条件成立时,棋局序列C1,C2,⋯,Cm称为有效(棋局)序列。(1)C1是开始棋局。(2)Ci(0B,否则它就不能影响V′(X)。故B是GC(X)值应有的下界。把这个下界加入到算法VEB就得到算法AB。所添加的参数LB是X值应有的下界。算法7.16纵深α-β截断算法procedureAB(X,l,LB,D)∥LB是V′(X)的一个下界。其余注释与VEB同∥ifX是终结点orl=0thenreturn(e(X))endifans←LB∥V′(X)的当前下界∥fori←1toddoifans≥Dthenreturn(ans)endifans←max(ans,-AB(Ci,l-1,-D,-ans))repeatreturn(ans)endAB不难证明初次调用AB(Y,l,-∞,∞)与调用VB(Y,l)得到的结果是相同的。图7.21(b)显示了一棵假想的对策树,在这棵树中,使用算法AB比使用算法VEB产生更大的截断。先在图7.21说明下界的对策树图7.21(b)所示的这棵树上执行VEB。假定最初的调用为VEB(P1,l,∞),其中l是这棵树的深度。在检查了P1的左子树之后,P1的B值被置成10,并且生成P3,P4,P5和P6这些结点。此后,V′(P6)确定为9,进而P5的B值变成-9。使用这一算法继续算出结点P7的值。然而,在使用AB的情况下,由于P1的B值是10,P4的下界也就是10,因此P4实际的B值变为10。因为结点P7的值无论是什么都无关紧要,所以结点P7不生成,于是V′(P5)≥-9且不可能使V′(P4)达到它的下界。在分析过程AB时,确定一棵树中会生成哪一部分结点是极其困难的。而对于VEB的分析,至今也只对某些类的对策树作出了证明。有兴趣的读者可以参阅D.Knuth于1975年发表在《ArtificialIntelligence》第6期的论文“:Ananalysisofalpha-betacutoffs”。\n190计算机算法基础习题七本章习题中的二元树,除非特别声明,其结点都由3个信息段表示,即有LCHILD,DATA和RCHILD。7.1写一个统计二元树T的叶结点数的算法并分析它的计算时间。7.2使用7.1.1节所讨论的3种周游方法之一,写一个求二元树的镜像树的算法SWAPTREE(T)。图7.22给出了一棵二元树T和它的镜像树的例子。7.3(1)证明一棵二元树可由它的中根顺序和后根顺序所唯一定义。(2)证明一棵二元树可由它的中根顺序和先根顺序所唯一定义。(3)证明一棵二元树不能由它的先根顺序和后根顺序所唯一定义。图7.22二元树T和它的镜像树7.4已知一棵二元树的中根序列为I,后根序列为P,写一个构造该二元树的算法。可直接使用子过程GETNODE去获取一个新结点。其算法的计算复杂度是什么?7.5给出一个例子,使用例中的数据运行你在习题7.4作出的算法。7.6如果二元树T有n个结点,证明定理7.1对算法INORDER1成立。7.7写一个对二元树T作先根次序周游的非递归算法(可以使用栈),并分析其时、空复杂度。7.8写一个对二元树T作后根次序周游的非递归算法(可以使用栈),并分析其时、空复杂度。7.9如果n结点二元树T的每个结点有4个信息段:LCHILD,DATA,PARENT,RCHILD。要求以下写的算法所用附加空间都不超过O(1),时间都不超过O(n)。并证明其确实达到了这些要求。(1)写对T的中根次序周游算法。(2)写对T的先根次序周游算法。(3)写对T的后根次序周游算法。7.10写一个时间为Θ(n),附加空间为Θ(1)的二元树中根次序周游算法。树中的每个结点除了信息段:LCHILD,DATA和RCHILD外还有一个位信息段TAG。(提示:使用INORDER2链倒挂的思想,但不用LR的方法。用TAG位区别向左还是向右子树移动)7.11证明按树先根次序周游一棵树与按先根次序周游此树对应的二元树所给出的结果相同(即按相同的次序访问这些结点)。7.12证明按树中根次序周游一棵树与按中根次序周游此树对应的二元树所给出的结果相同(即按相同的次序访问这些结点)。7.13证明如果按树后根次序周游一棵树,那么访问这树中结点的次序与按后根次序周游对应二元树的访问这些结点的次序可能不同。7.14设树T的度为k,而且结点P有k个儿子信息段CHILD(P,i),1≤i≤k。写出下列算法并分析它们的时、空复杂度。(1)写一个树中根次序周游的非递归算法TI(T,k)。(2)写一个树先根次序周游的非递归算法TPRE(T,k)。(3)写一个树后根次序周游的非递归算法TPOST(T,k)。7.15证明对于任一无向图G=(V,E),v∈V。对BFS(v)的一次调用就会访问含结点v的连通分图的全部结点。\n第7章基本检索与周游方法1917.16重写BFS和BFT,使它能打印出无向图G的所有连通分图。假定G是按邻接表方式输入的,每个结点i的邻接表有头结点HEAD(i)。7.17利用BFS的思想写一个找包含已知结点v的最短(有向)环算法。证明你的算法能找出最短环,分析算法的时、空复杂度。7.18证明DFS访问G中由v可到达的所有结点。7.19证明定理7.3中给出的时、空限界对DFS也成立。7.20对图的另一种检索方法是D-search。此方法与BFS的不同之处在于,下一个要检测的结点是最新加到未检测结点表的那个结点。因此这个表应作成一个栈而不是一个队。(1)写一个D-search算法。(2)证明由结点v开始的D-search访问v可到达的所有结点。(3)你的算法的时、空复杂度是什么?(4)修改你的算法,使它能对无向连通图产生一棵生成树。7.21判断n结点的无向图G是否有环?若有,就尽可能多地写出对它的算法,分析这些算法的时、空复杂度;从而对这些算法的有效性作出评价。7.22写一个计算以二元树T表示的算术表达式的算法。假定表达式只使用双目运算符+、-、*、/。此二元树中的每个结点有3个信息段:LCHILD,DATA和RCHILD。如果P是叶结点,则DATA(P)是P所代表的变量或常数在存储器中的地址。VAL(DATA(P))是此变量或常数的当前值。你的算法需要多少计算时间?7.23证明定理7.5。7.24在表达式包含某些可交换运算符的情况下完善表7.6。7.25修改算法CODE1,使其在表达式树T含有可交换运算符的情况下也能生成最优代码。证明你的算法确能达到这一要求。7.26在T含有可结合运算符情况下,做与题7.25相同的工作。7.27获取下述表达式的表达式树,并标出每个结点的MR值。然后在N=1和N=2的情况下,由CODE2生成它们的最优代码。假定所有的运算符都是不可交换和不可结合的。(1)(a+b)*(c+d*(e+f)/(g+h))(2)a*b*c/(e-f+g*(h-k)*(l+m))(3)a*(b-c)*(d+f)/(g*(h+j)-k*l)7.28参看定理7.6对MR(P)的定义,写一个对二元表达式树T的每个结点计算其MR(P)值的算法。假设每个结点P有4个信息段LCHILD、DATA、MR和RCHILD。7.29证明定理7.7。7.30证明定理7.8。7.31证明CODE2的时间复杂度是Θ(n),其中n是T的结点数。7.32如果MR(T)≤n,在不允许使用装入指令的情况下,证明CODE2用最少的寄存器生成代码。7.33证明引理7.1。7.34写一个算法FLIP(T),用它来交换表达式树T中那些表示可交换运算符结点的左、右子树,使生成的树对每个给定的寄存器数N,大、小结点数之和最小。FLIP的计算复杂度是多少?7.35将CODE2扩展到具有可结合运算符表达式的计算。7.36识别图7.23的关节点并画出它们的双连通分图。7.37如果G是一个无向连通图,证明G中任何一条边都不可能在两个不同的双连通分图中。7.38假设无向连通图G的双连通分图是Gi=(Vi,Ei),1≤i≤k。证明:\n192计算机算法基础图7.23两个连通图(1)如果i≠j,那么Vi∩Vj至多包含一个结点。(2)结点v是G的关节点,当且仅当对于某i≠j,{v}=Vi∩Vj。7.39设G是一个无向连通图,写一个算法以求出将G变成双连通图所需要增加的最小边数并要求输出这些边。分析你的算法需要的时间和空间。7.40证明如果T是无向连通图G的宽度优先生成树,那么,相对于T,G可能有交叉边。7.41假设u不是根,证明u是一个关节点,当且仅当对于u的某个儿子w,L(w)≥DFN(u)。7.42证明在算法ART加了2.1和3.1~3.7行后,如果v=w或者DFN(w)>DFN(u),那么边(u,w)或者已在栈S的顶部,或者已作为双连通图的一部分输出。7.43修改算法SOLVE,使它能识别T的一棵解子树。7.44写出算法BFGEN中使用的子算法ASOLVE。7.45写一个算法PRUNE,用它去消除由算法BFGEN生成的解树T中所有不需要求解的结点,即,使输出的树是一棵为了求解整个问题必须求解它的每个结点的解子树。7.46考虑下面这棵假想对策树(见图7.24):图7.24假想对策树(1)使用最大最小方法式(7.1)去获取根结点的值。(2)弈者A应采用什么棋着?(3)列出用算法VE计算这棵对策树结点的值时结点的计算顺序。(4)对树中的每个结点X,用式(7.2)计算V′(X)。(5)在取X=根,l=∞,LB=-∞,D=∞的情况下,用算法AB计算此树的根的值期间,这树的哪些结点没有计算?7.47证明对于那些由A走子的级上的每个结点用式(7.2)计算的V′(X)与用式(7.1)计算的V(X)有相同的值,而对于其它级上的结点,用式(7.1)计算的V(X)为用式(7.2)计算的V′(X)取负。7.48证明初次调用AB(X,l,-∞,∞)与初次调用VB(Y,l)所得的结果相同。\n第8章回溯法8.1一般方法在算法设计的基本方法中,回溯法是最一般的方法之一。在那些涉及寻找一组解的问题或者求满足某些约束条件的最优解的问题中,有许多可以用回溯法来求解。8.1.1回溯的一般方法为了应用回溯法,所要求的解必须能表示成一个n-元组(x1,⋯,xn),其中xi是取自某个有穷集Si。通常,所求解的问题需要求取一个使某一规范函数P(x1,⋯,xn)取极大值(或取极小值或满足该规范函数条件)的向量。有时还要找出满足规范函数P的所有向量。例如,将A(1∶n)中的整数分类就是可用一个n-元组表示其解的问题,其中xi是A中第i小元素的下标。规范函数P是不等式A(xi)≤A(xi+1),其中1≤i0doif还剩有没检验过的X(k)使得X(k)∈T(X(1),⋯,X(k-1))andB(X(1),⋯,X(k))=truethenif(X(1),⋯,X(k))是一条已抵达一答案结点的路径thenprint(X(1),⋯,X(k))endifk←k+1∥考虑下一个集合∥elsek←k-1∥回溯到先前的集合∥endifrepeatendBACKTRACK需要注意的是,集合T()将提供作为解向量的第一个分量X(1)的所有可能值,解向量则取使限界函数B1(X(1))为真的那些X(1)的值。还要注意这些元素是如何按深度优先方式生成的。随着k值的增加,解向量的各分量不断生成,直到找到一个解或者不再剩有没经检验的X(k)为止。当k值减少时,算法必须重新开始生成那些可能在以前剩下而没经检验的元素。因此,还需拟定一个子算法,使它按某种次序来生成这些元素X(k)。如果只想要一个解,则可在print后面设置一条return语句。算法8.2提供了回溯算法的一种递归表示。由于它基本上是一棵树的后根次序周游,因此按照这种方法描述回溯法是自然的。这个递归模型最初由callRBACKTRACK(1)所调用。算法8.2递归回溯算法procedureRBACKTRACK(k)∥此算法是对回溯法抽象地递归描述。进入算法时,解向量X(1∶n)的前k-1个分量X(1),⋯,X(k-1)已赋值∥globaln,X(1∶n)for满足下式的每个X(k)X(k)∈T(X(1),⋯,X(k-1))andB(X(1),⋯,X(k))=truedoif(X(1),⋯,X(k))是一条已抵达一答案结点的路径thenprint(X(1),⋯,X(k))endifcallRBACKTRACK(k+1)repeatendRBACKTRACK将解向量(x1,⋯,xn)作为一个全程数组X(1∶n)来处理。这个元组第k个位置上满足B的所有元素逐个被生成,并被连接到当前的向量(X(1),⋯,X(k-1)),每次X(k)都要附之以这样的一种检查,即判断一个解是否已被找到。因此,这个算法被递归调用。当退出for循环时,不再剩有X(k)的值,从而结束此层递归并继续上次没解决的调用。要指出的是,当k大于n时,T(X(1),⋯,X(k-1))返回一个空集,因此根本不进入for\n200计算机算法基础循环。还要指出的是,这个程序印出所有的解,而且组成解的元组的大小是可变的。如果只想要一个解,则可加上一个标志作为一个参数,以指明首次成功的情况。8.1.2效率估计上面所给出的两个回溯程序的效率主要取决于以下4种因素:①生成下一个X(k)的时间;②满足显式约束条件的X(k)的数目;③限界函数Bi的计算时间;④对于所有的i,满足Bi的X(k)的数目。如果这些限界函数大量地减少了所生成的结点数,则认为它们是好的。不过,一些好的限界函数往往需要较多的计算时间,而所希望的不只是减少所生成的结点而是要减少总的计算时间。因此,在选择限界函数时,通常在好坏与时间消费上采取折中的方案。有许多问题,其状态空间树的规模太大,要想生成其全部结点实际上是不允许的,因此应该使用限界函数并且希望在一段适当的时间内至少会找出一个解。不过对于许多问题(例如,n-皇后问题)至今还没听说有完善的限界方法。对于许多问题,可以按任意次序使用包含各个解分量xi可能取值的那些有穷集Si。为了提高有效检索的效率,一般可采用一种称之为重新排列的方法。其基本思想是,在其它因素相同的情况下,从具有最少元素的集合中作下一次选择。这种策略虽已证明对n-皇后问题无效,而且还可构造出一些证明用此方法无效的例子,但从信息论的观点看,从最小集合中作下一次选择,平均来说更为有效。在图8.7中,对同一个问题用两棵回溯检索树显示了这一方法的潜在能力。如果能去掉图8.7(a)中那棵树的第一级的一个结点,那么实际上就从所考虑的情况中去掉了12个元组。如果从图8.7(b)中那棵树第一级上去掉一个结点,则只删去8个元组。更完善的重新排列策略将在后面和动态状态空间树一起研究。图8.7重新排列如上所述,有4种因素决定回溯算法所需要的时间。一旦选定了一种状态空间树结构,这4种因素的前3种因素相对来说就与所要解决问题的实例无关,只有第4种因素,即所生成的结点数因问题的实例不同而异。对于某一实例,回溯算法可能只生成O(n)个结点,而对于另一不同的实例,由于它与原实例密切相关,故也可能生成这棵状态空间树的几乎全部n结点。如果解空间的结点数是2或n!,则回溯算法的最坏情况时间一般将分别是nO(p(n)2)或O(q(n)n!)。p(n)和q(n)都是n的多项式。尽管回溯法对同一问题的不同实例在计算时间上可能出现极大差异,但当n很大时,对于某些实例而言,回溯算法确实可在很短时间内求出其解,因此回溯法仍不失为一种有效的算法设计策略,只是在决定采用回溯算法正式计算某实例之前,应预先估算出回溯算法在此实例情况下的工作效能。\n第8章回溯法201用回溯算法去处理一棵树所要生成的结点数,可以用蒙特卡罗(MonteCarlo)方法估算出来。这种估计方法的一般思想是,在状态空间树中生成一条随机路径。设X是这条随机路径上的一个结点,而且X在状态空间树的第i级。在结点X处用限界函数确定没受限界的儿子结点的数目mi,在这mi个没受限儿子结点中随机地选择一个结点作为这条路径上的下一个结点。这条路径的生成在以下结点处结束,或者它是一个叶子结点,或者该结点的所有儿子结点都已被限界。用这些mi可以估算出这棵状态空间树中不受限界结点的总数m。此数在准备检索出所有答案结点时非常有用。在这种情况下,需要生成所有的不受限结点。当只想求一个解时,由于只需生成m个结点的很少一部分,回溯算法就可以得到一个解,因此m不是一个理想的估计值。由mi来估算m,需要假定这些限界函数是固定的,即在算法执行期间当其信息逐渐增加时限界函数不变,而且同一个函数正好用于这棵状态空间树同一级的所有结点。这一假定对于大多数回溯算法并不适用。在大多数情况下,随着检索的进行限界函数会变得更强一些,在这些情况中,对m的估计值将大于考虑了限界函数的变化后所能得到的值。沿用限界函数固定的假定,可以看到第2级没受限的结点数为m1。如果这棵检索树是同一级上结点有相同的度的树,那么就可预计到每一个2级结点平均有m2个没限界的儿子,从而得出在第3级上有m1m2个结点。第4级预计没受限的结点数是m1m2m3。一般,在i+1级上预计的结点数是m1m2⋯mi。于是,在求解给定问题的实例8.1中所要生成的不受限界结点的估计数m=1+m1+m1m2+m1m2m3+⋯。过程ESTIMATE是一个确定m值的算法。它从状态空间树的根出发选择一条随机路径。函数SIZE返回集合Tk的大小。函数CHOOSE从Tk中随机地挑选一个元素。m和r是产生不受限结点估计数所用的临时工作单元。算法8.3估计回溯法的效率procedureESTIMATE∥程序沿着状态空间树中一条随机路径产生这棵树中不受限界结点的估计数∥m←1;r←1;k←1loopTk←{X(k):X(k)∈T(X(1),⋯,X(k-1))andBk(X(1),⋯,X(k))}ifSIZE(Tk)=0thenexitendifr←r*SIZE(Tk)m←m+rX(k)←CHOOSE(Tk)k←k+1repeatreturn(m)endESTIMATE对算法ESTIMATE稍加修改就可得到更准确的结点估计数。这只需增加一个for循环语句,选取数条不同的随机路径(一般可取20条),在求出沿每条路径的估计值后取平均值即得。\n202计算机算法基础8.28-皇后问题8-皇后问题实际上很容易一般化为n-皇后问题,即要求找出在一个n×n棋盘上放置nn个皇后并使其不能互相攻击的所有方案。令(x1,⋯,xn)表示一个解,其中x是把第i个皇后放在第i行的列数。由于没有两个皇后可以放入同一列,因此这所有的xi将是截然不同的。那么,应如何去测试两个皇后是否在同一条斜角线上呢?如果设想棋盘的方格像二维数组A(1∶n,1∶n)的下标那样标记,那么可以看到,对于在同一条斜角线上的由左上方到右下方的每一个元素有相同的“行-列”值,同样,在同一条斜角线上的由右上方到左下方的每一个元素则有相同的“行+列”值。假设有两个皇后被放置在(i,j)和(k,l)位置上,那么根据以上所述,仅当i-j=k-l或i+j=k+l时,它们才在同一条斜角线上。将这两个等式分别变换成j-l=i-k与j-l=k-i因此,当且仅当|j-1|=|i-k|时,两个皇后在同一条斜角线上。过程PLACE(k)返回一个布尔值,当第k个皇后能放置于X(k)的当前值处时,这个返回值为真。这个过程测试两种情况,即X(k)是否不同于前面X(1),⋯,X(k-1)的值以及在同一条斜角线上是否根本就没有别的皇后。该过程的计算时间是O(k-1)。算法8.4可以放置一个新皇后吗procedurePLACE(k)∥如果一个皇后能放在第k行和X(k)列,则返回true;否则返回false。X是一个全程数组,进入此过程时已置了k个值。ABS(r)过程返回r的绝对值∥globalX(1∶k);integeri,ki←1whilei0do∥对所有的行执行以下语句∥X(k)←X(k)+1∥移到下一列∥whileX(k)≤nandnotPLACE(k)do∥此处能放这个皇后吗∥X(k)←X(k)+1repeatifX(k)≤n∥找到一个位置∥thenifk=n∥是一个完整的解吗∥thenprint(X)∥是,打印这个数组∥elsek←k+1;X(k)←0∥转向下一行∥endifelsek←k-1∥回溯∥endifrepeatendNQUEENS此时,读者可能对于过程NQUEENS怎么会优于硬性处理感奇怪。原因是这样的,如64果硬性要求一个8×8的棋盘安排出8块位置,就有种可能的方式,即要检查将近4.489×10个8-元组。然而,过程NQUEENS只允许把皇后放置在不同的行和列上,因此至多需要作8!次检查,即至多只检查40320个8-元组。可以用过程ESTIMATE来估算NQUEENS所生成的结点数。要指出的是,过程ESTIMATE所需要的假定也适用于NQUEENS,即,使用固定的限界函数且在检索进行时函数不改变。另外,在状态空间树的同一级的所有结点都有相同的度。图8.8显示了由过程ESTIMATE求结点估计数所用的5个8×8棋盘。如同所要求的那样,棋盘上每一个皇后的位置是随机选取的。对于每种选择方案,都记下了可以将一个皇后合法地放在各行中列的数目(即状态树的每一级没受限的结点数)。它们都列在每个棋盘下方的向量中。向量后面的数字表示过程ESTIMATE由这些量值所产生的值。这5次试验的平均值是1625。8-皇后状态空间树的结点总数是7j1+∑(∏(8-i))=69281j=0i=0因此,不受限结点的估计数大约只是8-皇后状态空间树的结点总数的2.34%。1111122222333334444455555666778(8,5,4,3,2)=1649(8,5,3,1,2,1)=769(8,6,4,3,2)=1977(8,6,4,2,1,1,1)=1401(8,5,3,2,2,1,1,1)=2329图8.88-皇后问题的5种方案及不受限结点的估计值\n204计算机算法基础8.3子集和数问题子集和数问题是假定有n个不同的正数(通常称为权),要求找出这些数中所有使得某和数为M的组合。例8.2和例8.4说明了如何用大小固定或变化的元组来表示这个问题。本节将利用大小固定的元组来研究一种回溯解法,在此情况下,解向量的元素X(i)取1或0值,它表示是否包含了权数W(i)。生成图8.4中任一结点的儿子是很容易的。对于i级上的一个结点,其左儿子对应于X(i)=1,右儿子对应于X(i)=0。对于限界函数的一种简单选择是,当且仅当kn∑W(i)X(i)+∑W(i)≥Mi=1i=k+1时,B(X(1),⋯,X(k))=true。显然,如果这个条件不满足,X(1),⋯,X(k)就不能导致一个答案结点。如果假定这些W(i)一开始就是按非降次序排列的,那么这些限界函数可以被强化。在这种情况下,如果k∑W(i)X(i)+W(k+1)>Mi=1则X(1),⋯,X(k)就不能导致一个答案结点。因此,将要使用的限界函数是Bk(X(1),⋯,X(k))=true当且仅当knk∑W(i)X(i)+∑W(i)≥M且∑W(i)X(i)+W(k+1)≤M(8.1)i=1i=k+1i=1由于在即将拟制的算法中不会使用Bn,因此不必担心这个函数中会出现W(n+1)。至此已说明了直接使用8.1节介绍的两种回溯方案中任何一种方案所需要的一切。为简单起见,将算法8.2修改成适应求子集和数的需要便得到递归算法SUMOFSUB。算法8.6子集和数问题的递归回溯算法procedureSUMOFSUB(s,k,r)∥找W(1∶n)中和数为M的所有子集。进入此过程时X(1),⋯,X(k-1)的值已确定。s=k-1nn∑W(j)X(j)且r=∑W(j)。这些W(j)按非降次序排列。假定W(1)≤M,∑W(i)≥M∥j=1j=ki=11globalintegerM,n;globalrealW(1∶n);globalbooleanX(1∶n)2realr,s;integerk,j∥生成左儿子。注意,由于Bk-1=true,因此s+W(k)≤M∥3X(k)←14ifs+W(k)=Mthen∥子集找到∥5print(X(j),j←1tok)∥此处由于W(j)>0,1≤j≤n,因此不存在递归调用∥6else7ifs+W(k)+W(k+1)≤Mthen∥Bk=true∥8callSUMOFSUB(s+W(k),k+1,r-W(k))9endif10endif∥生成右儿子和计算Bk的值∥\n第8章回溯法20511ifs+r-W(k)≥Mands+W(k+1)≤M∥Bk=true∥12thenX(k)←013callSUMOFSUB(s,k+1,r-W(k))14endif15endSUMOFSUBkn过程SUMOFSUB将∑W(i)X(i)和∑W(i)分别保存在变量s和r中以避免每次都要i=1i=k+1n计算这些值。此算法假定W(1)≤M和∑W(i)≥M。初次调用是callSUMOFSUB(0,1,i=1n∑W(i))。着重要指出的是,算法没有明显地使用测试条件k>n去终止递归。之所以不需i=1要这一测试条件,是因为在算法入口处s≠M且s+r≥M,因此r≠0,从而k也不可能大于n。在第7行由于s+W(k)0种颜色,在只准使用这m种颜色对G的结点着色的情况下,是否能使图中任何相邻的两个结点都具有不同的颜色呢?这个问题称为m-着色判定问题。在m着色最优化问题则是求可对图G着色的最小整数m。这个整数称为图G的色数。对图着色的研究是从m-可着色性问题的著名特例———4色问题开始的。这个问题要求证明平面或球面上的任何地图的所有区域都至多可用4种颜色来着色,并使任何两个有一段公共边界的相邻区域没有相同的颜色。这个问题可转换成对一平面图的4-着色判定问题(平面图是一个能画于平面上而边无任何交叉的图)。将地图的每个区域变成一个结点,若两个区域相邻,则相应的结点用一条边连接起来。图8.10显示了一幅有5个区域的地图以及与该地图对应的平面图。多年来,虽然已证明用5图8.10一幅地图和它的平面图表示种颜色足以对任一幅地图着色,但是一直找不到一定要求多于4种颜色的地图。直到1976年这个问题才由爱普尔(K.I.Apple),黑肯(W.Haken)和考西(J.Koch)利用电子计算机的帮助得以解决。他们证明了4种颜色足以对任何地图着色。在这一节,不是只考虑那些由地图产生出来的图,而是所有的图。讨论在至多使用m种颜色的情况下,可对一给定的图着色的所有不同方法。假定用图的邻接矩阵GRAPH(1∶n,1∶n)来表示一个图G,其中若(i,j)是G的一条边,则GRAPH(i,j)=true,否则GRAPH(i,j)=false。因为要拟制的算法只关心一条边是否存在,所以使用布尔值。颜色用整数1,2,⋯,m表示,解则用n-元组(X(1),⋯,X(n))来给出,其中X(i)是结点i的颜色。使用和算法8.2相同的递归回溯表示,得到算法MCOLORING。此算法使用的基本状态空间树是一棵度数为m,高为n+1的树。在i级上的每一个结点有m个儿子,它们与X(i)的m种可能的赋值相对应,1≤i≤n。在n+1级上的结点都是叶结点。图8.11给出了n=3且m=3时的状态空间树。图8.11当n=3,m=3时MCOLORING的状态空间树\n第8章回溯法207算法8.7找一个图的所有m-着色方案procedureMCOLORING(k)∥这是图着色的一个递归回溯算法。图G用它的布尔邻接矩阵GRAPH(1∶n,1∶n)表示∥∥它计算并打印出符合以下要求的全部解,把整数1,2,⋯,m分配给图中∥∥各个结点且使相邻近的结点的有不同的整数。k是下一个要着色结点的下标∥globalintegerm,n,X(1∶n)booleanGRAPH(1∶n,1∶n)integerkloop∥产生对X(k)所有的合法赋值∥callNEXTVALUE(k)∥将一种合法的颜色分配给X(k)∥ifX(k)=0thenexitendif∥没有可用的颜色了∥ifk=nthenprint(X)∥至多用了m种颜色分配给n个结点∥elsecallMCOLORING(k+1)∥所有m-着色方案均在此反复递归调用中产生∥endifrepeatendMCOLORING在最初调用callMCOLORING(1)之前,应对图的邻接矩阵置初值并对数组X置0值。在确定了X(1)到X(k-1)的颜色之后,过程NEXTVALUE从这m种颜色中挑选一种符合要求的颜色,并把它分配给X(k),若无可用的颜色,则返回X(k)=0。算法8.8生成下一种颜色procedureNEXTVALUE(k)∥进入此过程前X(1),⋯,X(k-1)已分得了区域[1,m]中的整数且相邻近的结点有不同的整数。本过程在区域[0,m]中给X(k)确定一个值:如果还剩下一些颜色,它们与结点k邻接的结点分配的颜色不同,就将其中最高标值的颜色分配给结点k;如果没剩下可用的颜色,则置X(k)为0∥globalintegerm,n,X(1∶n)booleanGRAPH(1∶n,1∶n)integerj,kloopX(k)←(X(k)+1)mod(m+1)∥试验下一个最高标值的颜色∥ifX(k)=0thenreturnendif∥全部颜色用完∥forj←1tondo∥检查此颜色是否与邻近结点的那些颜色不同∥ifGRAPH(k,j)and∥如果(k,j)是一条边∥X(k)=X(j)∥并且邻近的结点有相同的颜色∥thenexitendifrepeatifj=n+1thenreturnendif∥找到一种新颜色∥repeat∥否则试着找另一种颜色∥endNEXTVALUEn-1i算法8.7的计算时间上界可以由状态空间树的内部结点数∑m得到。在每个内部结i=0点处,为了确定它的儿子们所对应的合法着色,由NEXTVALUE所花费的时间是O(mn)。nin+1n因此,总的时间由∑mn=n(m-m)/(m-1)=O(nm)所限界。i=1\n208计算机算法基础图8.12显示了一个包含4个结点的简单图。下面是一棵由过程MCOLORING生成的树。到叶子结点的每一条路径表示一种至多使用3种颜色的着色法。图8.12一个4结点图和所有可能的3着色注意:正好用3种颜色的解只有12种。8.5哈密顿环设G=(V,E)是一个n结点的连通图。一个哈密顿环是一条沿着图G的n条边环行的路径,它访问每个结点一次并且返回到它的开始位置。换言之,如果一个哈密顿环在某个结点v1∈V处开始,且G中结点按照v1,v2,⋯,vn+1的次序被访问,则边(vi,vi+1),1≤i≤n,均在G中,且除了v1和vn+1是同一个结点外,其余的结点均各不相同。在图8.13中,图G1含有一个哈密顿环1,2,8,7,6,5,4,3,1。图G2不包含哈密顿环。似乎没有一种容易的方法能确定一个已知图是否包含哈密顿环。本节考察找一个图中所有哈密图8.13两个图,第一个包含一个哈密顿环顿环的回溯算法。这个图可以是有向图也可以是无向图。只有不同的环才会被输出。用向量(x1,⋯,xn)表示用回溯法求得的解,其中,xi是找到的环中第i个被访问的结点。如果已选定x1,⋯,xk-1,那么下一步要做的工作是如何找出可能作xk的结点集合。若k=1,则X(1)可以是这n个结点中的任一结点,但为了避免将同一个环重复打印n次,可事先指定X(1)=1。若1nthenfp←cp;fw←cw;k←n;X←Y∥修改解∥8elseY(k)←0∥超出M,物品K不适合∥9endif10whileBOUND(cp、cw,k,M)≤fpdo∥上面置了fp后,BOUND=fp∥11whilek≠0andY(k)≠1do12k←k-1∥找最后放入背包的物品∥13repeat14ifk=0thenreturnendif∥算法在此处结束∥15Y(k)←0;cw←cw-W(k);cp←cp-P(k)∥移掉第k项∥16repeat17k←k+118repeat19endBKNAP1n当fp≠-1时,X(i),1≤i≤n,是这样的一些元素,它们使得∑P(i)X(i)=fp。在4~6i=1行的while循环中,回溯算法作一连串到可行左儿子的移动。Y(i),1≤i≤k,是到当前结点k-1k-1的路径。cw=∑W(i)Y(i)且cp=∑P(i)Y(i)。在第7行,如果k>n,则必有cp>fp,因为i=1i=1若cp≤fp,则在上一次使用限界函数时(第10~16行)就会终止到此叶结点的路径。如果k≤n,则W(k)不适合,从而必须作一次到右儿子的移动。所以在第8行,Y(k)被置成0。如果在第10行中BOUND≤fp,由于现今的这条路径不能导出比迄今所找到的最好解还要好的解,因此该路径可终止。在第11~13行,沿着到最近结点的路径回溯,而由这最近结果可以作迄今尚未试验过的移动。如果不存在这样的结点,则算法在第14行终止。反之,Y(k),cw和cp则对应于一次右儿子移动作出相应的修改。计算这个新结点的界。继续倒转去处理第10~16行,一直到作出有可能得到一个大于fp值的解的右儿子结点为止,否则fp就是背包问题的最优效益值。注意,第10行的限界函数并不是固定的,这是因为当检索这棵树的更多结点时,fp就改变。因此限界函数动态地被强化。\n212计算机算法基础例8.7考虑以下情况的背包问题:P=(11,21,31,33,43,53,55,65),W=(1,11,21,23,33,43,45,55),M=110,n=8。图8.14显示了对向量Y作出各种选择的情况下所生成的树,这棵树的第i级对应于将1或0赋值给Y(i),表示或者含有重量W(i)或者拒绝接纳重量W(i)。一个结点内所载的两个数是重量(cw)和效益(cp),给出了该结点的下一级的两个赋值。注意,若结点不含有重量和效益值则表示此两类值与它们父亲的相同。每个右儿子以及根结点外面的数是对应于那个结点的上界。左儿子的上界与它父亲的相同。算法8.12的变量fp在结点A、B、C和D的每一处被修改。每次修改fp时也修改X。终止时,fp=159和X=(1,1,1,0,1,1,0,0)。9在状态空间树的2-1=511个结点中只生成了其中的35个结点。注意到由于所有的P(i)都是整数而且所有可行解的值也是整数,因此BOUND(p,w,k,M)是一个更好的限界函数,使用此限界函数结点E和F就不必再扩展,从而生成的结点数可减少到28。图8.14算法8.12所生成的树每次在第10行调用BOUND时,在过程BOUND中基本上重复了第4~6行的循环,因此可对算法BKNAP1作进一步的改进。为了取消BKNAP1第4~6行所做的工作,需要把BOUND改变成一个具有边界效应的函数。这两个新算法BOUND1和BKNAP2以算法8.13和8.14的形式写出。这两个算法与算法8.11和8.12中同名的变量含意完全一样。\n第8章回溯法213算法8.13有边界效应的限界函数procedureBOUND1(P,W,k,M,pp,ww,i)∥新近移到的左儿子所对应的效益为pp,重量为ww。i是上一个不适合的物品。如果所有物品都试验过了,则i的值是n+1∥globaln,P(1∶n),W(1∶n),Y(1∶n)integerk,i;realp,w,pp,ww,M,bpp←p;ww←wfori←k+1tondoifww+W(i)≤Mthenww←ww+W(i);pp←pp+P(i);Y(i)←1elsereturn(pp+(M-ww)*P(i)/W(i))endifrepeatreturn(pp)endBOUND1算法8.14改进的背包算法procedureBKNAP2(M,n,W,P,fw,fp,X)∥与BKNAP1同∥integern,k,Y(1∶n),i,j,X(1∶n)realW(1∶n),P(1∶n),M,fw,fp,pp,ww,cw,cpcw←cp←k←0;fp←-1loopwhileBOUND1(cp,cw,k,M,pp,ww,j)≤fpdowhilek≠0andY(k)≠1dok←k-1repeatifk=0thenreturnendifY(k)←0;cw←cw-W(k);cp←cp-P(k)repeatcp←pp;cw←ww;k←j∥等价于BKNAP1中4~6行的循环∥ifk>nthenfp←cp;fw←cw;k←n;X←YelseY(k)←0endifrepeatendBKNAP2到目前为止,所讨论的都是在静态状态空间树环境下工作的回溯算法,现在讨论如何利用动态状态空间树来设计背包问题的回溯算法。下面介绍的一种回溯算法的核心思想是以5.3节贪心算法所得的贪心解为基础来动态地划分解空间,并且力图去得到0/1背包问题的最优解。首先用约束条件0≤xi≤1来代换xi=0或1的整数约束条件,于是得到一个放宽了条件的背包问题:max∑pixi1≤i≤n约束条件∑wixi≤M,0≤xi≤1,1≤i≤n(8.3)1≤i≤n\n214计算机算法基础用贪心方法解式(8.3)这个背包问题,如果所得贪心解的所有xi都等于0或1,显然这个解也是相应0/1背包问题的最优解。如若不然,则必有某xi使得0POSITION(i)的数目。例如,对于图9.2(a)所示的状态,有LESS(1)=0,LESS(4)=1和LESS(12)=6。在初始状态下,如果空格在图9.2(c)的阴影位置中的某一格处,则令X=1;否则令X=0。于是有定理9.1。16定理9.1当且仅当∑LESS(i)+X是偶数时,图9.2(b)所示的目标状态可由此初始状i=1态到达。证明留作习题。定理9.1用来判定目标状态是否在这个初始状态的状态空间之中。若在,就可着手确定导致目标状态的一系列移动。为了实现这一检索,可以将此状态空间构造成一棵树。在这棵树中,每个结点的儿子表示由状态X通过一次合法的移动可到达的状态。不难看出,移动牌与移动空格实质上是等效的,而且在作实际移动时更为直观,因此以后都将父状态到子状态的一次转换看成是空格的一次合法移动。图9.3(a)中树的根结点表示15-谜问题一个实例的初始状态,该图给出了此实例所构造状态空间树的前三级和第四级的一部分。图中已对这棵树作了一些修剪,即如果结点P的儿子中有和P的父亲状态重复的,则将这一枝剪去。对此实例作FIFO检索表现为依图9.3(a)中结点编号的顺序来生成这些结点。可以看出第四级有一个答案结点,由于是宽度优先检索,因此它还是离根最近的答案结点。图9.3(b)给出了对此实例按深度优先生成其状态空间树结点的一部分,从图中一连串的棋盘格局可以看出,这种检索方法不管开始格局如何(即不管问题的具体实例),总是采取由根开始的那条最左路径,而在这条最左路径上的每一次移动不是离目标更近了而是更远了。这种检索是呆板而盲目的。尽管上面使用的宽度优先检索可以找到离根最近的答案结点,但从处理方式看也是不管开始格局如何总是按千篇一律的顺序移动,因此在这种意义下它也是呆板和盲目的。所希望的是,一种能按不同具体实例作不同处理的有一定“智能”的检索方法。这种检索方法需给状态空间树的每个结点X赋予一定的成本c(X)。如果具体实例有解,则将由根出发到最近目标结点路径上的每个结点赋以这条路径的长度作为它们的成本。于是,在图9.3(a)所示实例中,c(1)=c(4)=c(10)=c(23)=3,其余结点均赋以∞的成本。如果能做到这一点,在使用宽度优先检索的情况下必然会实现非常有效的检索。把根作为E-结点开始,在生成它的儿子结点时,可以将成本为∞的结点统统杀掉,只有与根具有相同成本值的儿子结点4成为活结点,而且它立即成为下一个E-结点。按这种检索策略继续处理很快就可到达目标结点23。但这是一种很不实际的策略,因为要想简单地给出能得到像上面那样成本值的函数c(·)是不可能的。∧∧切合实际的做法是给出一个便于计算成本估计值的函数c(X)=f(X)+g(X),其中f(X)∧是由根到结点X路径的长度,g(X)是以X为根的子树中由X到目标状态的一条最短路径∧长度的估计值。为此,这个g(X)至少应是能把状态X转换成目标状态所需的最小移动数。对它的一种可能的选择是∧g(X)=不在其目标位置的非空白牌数目\n第9章分枝-限界法221图9.315-谜问题的实例及深度优先检索(a)15-谜问题的一部分状态空间树;(b)一种深度优先检索的前十步∧这样定义的g(X)是符合以上要求的。不难看出,为达到目标状态所需要的移动数可能大于∧∧∧g(X)。例如对图9.4的问题状态,由于只有7号牌不在其目标位置上,因此g(X)=1(g(X)∧的计数排除了空白牌)。然而,为了达到目标状态所需要的移动数比g(X)多得多。由此可∧以看出,c(X)是c(X)的下界。∧使用c(X)图9.3(a)的LC-检索将结点1作为E-结点的开始。结点1在生成它的所有儿子结点2,3,4和5之后死去。变成E-结点的下一个结点是具有最1234∧∧∧∧∧小c(X)的活结点,c(2)=1+4,c(3)=1+4,c(4)=1+2和c(5)=1568+4,结点4成为E-结点。生成它的儿子结点,此时的活结点是2,3,9101112∧∧∧5,10,11和12。c(10)=2+1,c(11)=2+3,c(12)=2+3,具有最小1314157∧c值的活结点10成为下一个E-结点。接着生成结点22和23,结点图9.4问题状态23被判定是目标结点,此次检索结束。在这种情况下,LC-检索几乎\n222计算机算法基础和使用精确函数c(·)一样有效。由此可以看出,通过对c(·)的适当选择,LC-检索的选择性将远比已讨论过的其它检索方法强得多。9.1.3LC-检索的抽象化控制设T是一棵状态空间树,c(·)是T中结点的成本函数。如果X是T中的一个结点,则c(X)是其根为X的子树中任一答案结点的最小成本。从而,c(T)是T中最小成本答案结点的成本。如前所述,要找到一个如上定义且易于计算的函数c(·)通常是不可能的,为此使∧用一个对c(·)估值的启发性函数c(·)来代替。这个启发函数应是易于计算的并且一般有∧如下性质,如果X是一个答案结点或者是一个叶结点,则c(X)=c(X)。过程LC(算法9.1)∧用c去找寻一个答案结点。这个算法用了两个子算法LEAST(X)和ADD(X),它们分别将∧一个活结点从活结点表中删去或加入。LEAST(X)找一个具有最小的c值的活结点,从活结点表中删除这个结点,并将此结点放在变量X中返回。ADD(X)将新的活结点X加到活结点表。通常把这个活结点表作成一个min-堆来使用。过程LC输出找到的答案结点到根结点T的那条路径。如果使用PARENT信息段将活结点X与它的父亲相链接,这条路径就很容易输出了。算法9.1LC-检索∧lineprocedureLC(T,c)∥为找一个答案结点检索T∥0ifT是答案结点then输出T;returnendif1E←T∥E-结点∥2将活结点表初始化为空3loop4forE的每个儿子Xdo5ifX是答案结点then输出从X到T的那条路径6return7endif8callADD(X)∥X是新的活结点∥9PARENT(X)←E∥指示到根的路径∥10repeat11if不再有活结点thenprint(′noanswernode′)12stop13endif14callLEAST(E)15repeat16endLC下面证明算法LC的正确性。变量E总是指着当前的E-结点。由LC-检索的定义,根结点是第一个E-结点(第1行)。第2行将活结点表置初值。在执行LC的任何时刻,这个表含有除了E-结点以外的所有活结点,因此这个表最初为空(第2行)。第4~10行的for循环检查E-结点的所有儿子。如果有一个儿子是答案结点,则算法输出由X到T的那条路\n第9章分枝-限界法223径并且终止。如果E的某个儿子不是答案结点,则成为一个活结点,将它加到活结点表(第8行)中且将其PARENT信息段置E。当生成了E的全部儿子时,E变成死结点,控制到达第11行。这种情况只有在E的所有儿子都不是答案结点时才会发生,于是检索应更深入地继续进行。在没有活结点剩下的情况下,这整棵状态空间树就被检索完毕,且没有找到答案结点,算法在第12行结束。反之,则通过LEAST(X)按规定去正确地选择下一个E-结点,并从这里继续进行检索。根据以上讨论,显然LC只有在找到一个答案结点或者在生成并检索了这整棵状态空间树时才会终止。因此,只有在有限状态空间树下才能保证LC终止。对于无限状态空间∧树,在其至少有一个答案结点并假定对成本估计函数c(·)能作出“适当”的选择时也能保证算法LC终止。例如,对于如下的每一对结点X和Y,在X的级数“足够地”大于Y的级数∧∧时,有c(X)>c(Y),这样的成本估计函数就能对结点作适当的选择。对于没有答案结点的无限状态空间树,LC不会终止。因此,将检索局限在寻找估计成本不大于某个给定的限界C的答案结点则是可取的。实际上LC算法与状态空间树的宽度优先检索算法和D-检索算法基本相同。如果活结点表作为一个队列来实现,用LEASL(X)和ADD(X)算法从队列中删去或加入元素,则LC就转换成FIFO检索。如果活结点表作为一个栈来实现,用LEAST(X)和ADD(X)算法从栈中删去或加入元素,则LC就转换成LIFO检索。唯一的不同之处在于活结点表的构造上,即仅在于得到下一个E-结点所使用的选择规则不同。9.1.4LC-检索的特性在许多应用中,希望在所有的答案结点中找到一个最小成本的答案结点。LC是否一定找得到具有最小成本c(G)的答案结点G呢?回答是否定的。考虑图9.5所示的状态空间树,方形叶子结点是答案结点。每个结点内有两个数,上面的数是c的值,下∧∧面的数是估计值c。于是c(根结点)=10而c(根结点)=∧0。LC首先生成根的两个儿子,然后c()=2的那个结点成为E-结点。扩展这一结点就得到答案结点G,它有图9.5LC-检索∧c(G)=c(G)=20,算法终止。但是,具有最小成本的答案结点是c(G)=10的结点。LC没能达到这个最小成本答案结点的原因在于有两个这样的∧∧结点X和Y,当其c(X)>c(Y)时,c(X)c(G′)的答案结点G处终止,而G′是一个最小成本答案结点。令R是G的这样一个最近的祖先,它使得子树R含有一个最小成本答案结点(见图9.6)。假设R,\n224计算机算法基础α1,α2,⋯,αk,G′是由R到G′的路径,R,β1,β2,⋯,βj,G是由R到G的路径。由R的定义,α1≠β1,且子树β1不具有成本为c(G′)的答案结点。为使检索到达结点G,R必须在某一时刻变成E-结点。此时,它的儿子(包括α1和β1)成为活结点。由c(·)的定义,可以得出c(R)=c(α1)=c(α2)=⋯=c(G′)和c(β1),c(β2),⋯,c(G)>c(R)。∧∧∧∧于是,由c(·)的条件,可以得出c(α1),c(α2),⋯,c(αk)<∧c(β1),因此在αi,1≤i≤k,变成E-结点并到达G′以前β1不能变成E-结点。证毕。这个定理易于推广到其每个结点的度都是有限的无限状图9.6状态空间树态空间树的情况。但是要得到满足定理9.2的要求又易于计∧∧算的c(·)通常是不可能的。一般只可能找到一个易于计算且具有如下特性的c(·),对于∧每一个结点X,c(X)≤c(X)。在这种情况下,算法LC不一定能找到最小成本答案结点(见∧∧图9.5)。如果对于每一个结点X有c(X)≤c(X)且对于答案结点X有c(X)=c(X),只要对LC稍作修改就可得到一个在达到某个最小成本答案结点时终止的检索算法。在这个改进型的算法中,检索一直继续到一个答案结点变成E-结点为止。这个新算法是LC1(算法9.2)。算法9.2找最小成本答案结点的LC-检索∧lineprocedureLC1(T,c)∥为找出最小成本答案结点检索T∥1E←T∥第一个E-结点∥2置活结点表为空3loop4ifE是答案结点then输出从E到T的路径5return6endif7forE的每个儿子Xdo8callADD(X);PARENT(X)←E9repeat10if不再有活结点thenprint(′noanswernode′)11stop12endif13callLEAST(E)14repeat15endLC1∧定理9.3令c(·)是满足如下条件的函数,在状态空间树T中,对于每一个结点X,有∧∧c(X)≤c(X),而对于T中的每一个答案结点X,有c(X)=c(X)。如果算法在第5行终止,则所找到的答案结点是具有最小成本的答案结点。∧∧证明此时,E-结点E是答案结点,对于活结点表中的每一个结点L,c(E)≤c(L)。由∧∧假设c(E)=c(E)且对于每一个活结点L,c(L)≤c(L)。因此c(E)≤c(L),从而E是一个最小成本答案结点。证毕。\n第9章分枝-限界法2259.1.5分枝-限界算法检索状态空间树的各种分枝-限界方法都是在生成当前E-结点的所有儿子之后再将另一结点变成E-结点。假定每个答案结点X有一个与其相联系的c(X),并且假定会找到最小∧∧成本的答案结点。使用一个使得c(X)≤c(X)的成本估计函数c(·)来给出可由任一结点X得出的解的下界。采用下界函数使算法具有一定的智能,减少了盲目性,另外还可通过设置∧最小成本的上界使算法进一步加速。如果U是最小成本解的成本上界,则具有c(X)>U的∧所有活结点X可以被杀死,这是因为由X可以到达的所有答案结点有c(X)≥c(X)>U。在∧已经到达一个具有成本U的答案结点的情况下,那些有c(X)≥U的所有活结点都可以被杀死。U的初始值可以用某种启发性方法得到,也可置成∞。显然,只要U的初始值不小于最小成本答案结点的成本,上述杀死活结点的规则不会去杀死可以到达最小成本答案结点的活结点。每当找到一个新的答案结点就可以修改U的值。现在讨论如何根据上述思想来得到解最优化问题的分枝-限界算法。以下只考虑极小化问题,极大化问题可通过改变目标函数的符号很容易地转换成极小化问题。为了找到最优解需要将最优解的检索表示成对状态空间树答案结点的检索,这就要求定义的成本函数满足代表最优解的答案结点的c(X)是所有结点成本的最小值。最简单的方法是直接把目标函数作为成本函数c(·)。在这种定义下代表可行解的结点c(X)就是那个可行解的目标函数值;代表不可行解的结点有c(X)=∞;而代表部分解的结点c(X)是根为X的子树中最小成本结点的成本。由此可知在状态空间树中每个答案结点(当然它必定是解结点)都对应一个可行解,只有成本最小的答案结点才与最优解对应,因而,在这类问题的状态空间树中答案结点与解结点是不相区别的。另外,由于计算c(X)一般和求解最优化问题一样困难,∧∧因此,分枝-限界算法采用一个对于所有的X有c(X)≤c(X)的估值函数c(·)。要特别指出∧的是,在解最优化问题时所使用的c(·)不是用来估计到达一个答案结点在计算方面的难易程度,而是对目标函数进行估计。作为最优化问题的一个例子,考虑5.4节所引入的带限期的作业排序问题。将此问题一般化,允许作业有不同的处理时间。假定有n个作业和一台处理机,每个作业i与一个三元组(pi,di,ti)相联系,它要求ti个单位处理时间,如果在期限dj内没处理完则要招致pi的罚款。问题的目标是从这n个作业中选取一个子集合J,要求在J中的作业都能在相应的期限内完成且使不在J中的作业招致的罚款总额最小。这样的J就是最优解。考虑下面的实例:n=4;(p1,d1,t1)=(5,1,1);(p2,d2,t2)=(10,3,2);(p3,d3,t3)=(6,2,1);(p4,d4,t4)=(3,1,1)。此实例的解空间由作业指标集{1,2,3,4}的所有可能的子集合组成。使用子集和数问题(例8.2)两种表示的任何一种都可以将这解空间构造成一棵树。图9.7所示为元组大小可变的状态空间树,而图9.8所示为元组大小固定的状态空间树。在这两棵树中,方形结点代表不可行的子集合。图9.7中所有的圆形结点都是答案结点。结点9代表最优解并且是仅有的最小成本答案结点。对于这个结点,J={2,3}罚款值(即成本)为8。在图9.8中,只有圆形叶结点才是答案结点。结点25代表最优解,它也是仅有的最小成本答案结点。这个结点对应于J={2,3}和8这么大的罚款。图9.8中各答案结点的罚款数标在这些结点的下面。\n226计算机算法基础图9.7大小可变的元组表示的状态空间树对图9.7和图9.8这两种状态空间表示可将成本函数c(·)定义为,对于圆形结点X,c(X)是根为X的子树中结点的最小罚款;对于方形结点,c(X)=∞。在图9.7的树中,c(3)=8,c(2)=9,c(1)=8。在图9.8的树中,c(1)=8,c(2)=9,c(5)=13,c(6)=8。显然,c(1)是最优解J对应的罚款。图9.8大小固定的元组表示的状态空间树∧∧对于所有的X容易得出这样一个下界函数c(·),它使得c(X)≤c(X)。设SX是在结点∧∧X对J所选择的作业的子集,如果m=max{i|i∈SX},则c(X)=∑pi是使c(X)有c(X)≤iU,所以7被杀死;结点8是不可行结点,也被杀死。接着,结点3成为E-结点,∧生成其儿子结点9和10。u(9)=8,因此U变成8;c(10)=11>U,所以10被杀死。下一个E-结点是6,它的两个儿子均不可行。结点9只有一个儿子且不可行,因此9是最小成本答案结点,它的成本值为8。∧在实现FIFO分枝-限界算法时,每修改一次U,在活结点队中那些有c(X)>U或者在∧U是已找到的一个成本值情况下有c(X)≥U的结点应被杀死,但由于活结点按其生成次序放在活结点队中,因此,队中符合可杀条件的结点是随机分布的,所以每修改一次U就从活结点表中找出并杀死这些结点是很不经济的。一种比较经济的方法是直到可杀结点要变成E-结点时才将其杀掉。但不管采用哪种方法,都必须识别出这个修改了的U是一个已找到的解的成本还是一个不是解成本的单纯的上界(这个单纯上界U由以下任意一种情况得到,一是还没找到一个答案结点,U由处理一可行结点时修改而得;一是虽然找到一答案结点,但它的成本值大于它的上界值,说明这答案结点的子孙中还有成本更小的答案结点,U∧取这上界值)。这样就可以决定在c(X)=U的情况下是否杀死结点X,即若U为前者则杀死X,若为后者,那么X是有希望导致成本值等于U的解的结点,于是应将X变成E-结点。在算法实现时,可引进一个很小的正常数ε来进行这一识别。此ε要取得足够小,使得对于任意两个可行结点X和Y,如果u(X)U的结点是否能减少所生成的结点数?∧∧(3)假定有两个成本估计函数c1(·)和c2(·),对于状态空间树的每一个结点X,若有∧∧∧∧∧c1(X)≤c2(X)≤c(X),则称c2(·)比c1(·)好。是否用较好的成本估计函数c2(·)比用∧c1(·)生成的结点数要少呢?对于以上问题,读者可凭直觉立即得出一些“是”或“非”的答案,但由下述定理可以看出有的结论可能刚好与你的直觉相反。假定下面出现的分枝-限界算法均用来求最小成本答案结点,c(X)是X子树中最小成本答案结点的成本。定理9.4设U1和U2是状态空间树T中最小成本答案结点的两个初始上界且U1U的结点X而减少。∧证明因为c(X)>U,所以扩展X不可能使U值减小,故扩展X不会影响算法在这棵树上其余部分的运算。证毕。∧定理9.6在FIFO和LIFO分枝-限界算法中使用一个更好的成本估计函数c(·)不会增加其生成的结点数。证明留作习题。定理9.7在LC分枝-限界算法中使用一个更好的∧成本估计函数c(·)可能增加所生成的结点的个数。证明考虑图9.9所示的状态空间树,所有叶结点都是答案结点,叶结点下面的数是其成本值,从这些值中可以得出c(1)=c(3)=3,c(2)=4。结点1,2和3外∧c1∧∧面的数是对应的∧值,显然c2是比c1更好的成本估图9.9定理9.7的一个例子c2\n230计算机算法基础∧∧∧计函数。如果使用c2,由于c2(2)=c2(3),因此结点2会在结点3之前变成E-结点,于是所∧有9个结点都将被生成,而使用c1将不会生成结点4,5和6。证毕。9.20/1背包问题为了用上一节所讨论的分枝-限界方法来求解0/1背包问题,可用函数-∑pixi来代替目标函数∑pixi,从而将背包问题由一个极大化问题转换成一个极小化问题。显然,当且仅当-∑pixi取极小值时∑pixi取极大值。这个修改后的背包问题可描述如下:n极小化-∑pixii=1n约束条件∑wixi≤M(9.1)i=1xi=0或xi=1,1≤i≤n在8.6节曾介绍过0/1背包问题的两种状态空间树结构,这里只讨论在大小固定的元组表示下如何求解0/1背包问题,至于在元组大小可变时,如何求解背包问题,在此基础上是不难解决的。状态空间树中那些表示∑wixi≤M的装包方案的每一个叶结点是答案结1≤i≤n点,其它的叶结点均不可行。为了使最小成本答案结点与最优解相对应,需要对每一个答案结点X定义c(X)=-∑pixi;对不可行的叶结点则定义c(X)=∞;对于非叶结点则将c(X)1≤i≤n递归定义成min{c(LCHILD(X)),c(RCHILD(X))}。∧∧还需要两个函数c(·)和u(·),使它们对于每个结点X,有c(X)≤c(X)≤u(X)。这样的两个函数可由下法得到:设X是j级上的一个结点,1≤j≤n+1。在结点X处已对前j-1种物品装包,这j-1种物品的装入情况为xi,1≤iU=38,因此,它立即被∧杀死。现在活结点表中具有最小c值的结点是6和8,无图9.10例9.2的LC分枝-限界树∧论哪个结点变成下一个E-结点都有c(E)≥U,因此,在找到答案结点8的情况下终止检索,这时打印出值-38和路径8,7,4,2,1,算法结束。要指出的是,由路径8,7,4,2,1并不能弄清是由哪些物品装入背包才得到-∑pixi=U,即看不出这些xi的取值情况,因此在实现过程LCBB时应保留一些能反映xi取值情况的附加信息。一种解决办法是每一个结点增设一个位信息段TAG,由答案结点到根结点的这一系列TAG位给出这些xi的值。于是,对此问题将有TAG(2)=TAG(4)=TAG(6)=TAG(8)=1和TAG(3)=TAG(5)=TAG(7)=TAG(9)=0。路径8,7,4,2,1的一系列TAG是1011,因此x4=1,x3=0,x2=1,x1=1。为了用过程LCBB(算法9.4)求解背包问题,需要确定:①被检索的状态空间树中结点的结构;②如何生成一给定结点的儿子;③如何识别答案结点;④如何表示活结点表。所需的结点结构取决于开始时采用哪一种状态空间树表示,这里仍采用大小固定的元组表示。要生成并放在活结点表上的每一个结点应有6个信息段:PARENT、LEVEL、TAG、CU、PE和UB信息段。其中,PARENT信息段是结点X的父结点链接指针;LEVEL信息段标志出结点X在状态空间树中结点X的级数,在生成结点X的儿子时使用,通过置XLEVEL(X)=1表示生成X的左儿子,XLEVEL(X)=0表示生成X的右儿子;位信息段TAG正如例9.2所描述的那样,用来输出最优解的xi值;CU信息段用来保存在结点X处背包的剩余空间,该信息段在确定X左儿子的可行性时使用;PE信息段用来保存在结点X处已装入物∧品相应的效益值的和,即∑pixi,它在计算c(X)和u(X)时使用;UB信息段用来存放1≤iL(第24行)或者L=UBB-εLthenL←prof;ANS←E11endif12:else:∥E有两个儿子∥13ifcap≥W(i)then∥左儿子可行∥14callNEWNODE(E,i+1,1,cap-W(i),prof+P(1)UB(E))15endif∥看右儿子是否会活∥16callLUBOUND(P,W,cap,prof,N,i+1,LBB,UBB)17ifUBB>Lthen∥右儿子会活∥18callNEWNODE(E,i+1,0,cap,prof,UBB)19L←max(L,LBB-ε)20endif21endcase22if不再有活结点thenexitendif23callLARGEST(E)∥下一个E-结点是UB值最大的结点∥24untilUB(E)≤Lrepeat25callFINISH(L,ANS,N)26endLCKNAP9.2.2FIFO分枝-限界求解例9.3[FIFOBB]背包问题采用式(9.1)表示,考虑用FIFOBB(算法9.3)来求解例9.2的背包实例,其工作情况如下:最开始根结点(即图9.11中结点1)是E-结点,活结点队为空。由于结点1不是答案结点,因此U置初值为u(1)+ε=-32+ε。扩展结点1,生成它的两个儿子2和3并将它们依次加入活结点队,结点2成为下一个E-结点,生成它的儿子4和5并将它们加入队。结点了成为下一个E-结点,生成它的儿子6和7,结点6加入队,由于结∧点7的c(7)=-30>U,因此立即被杀死。下次扩展结点4,生成它的儿子8和9并将它们∧加入队,修改U=u(9)+ε=-38+ε。接着要成为E-结点的结点是5和6,由于它们的c值均大于U,因此都被杀死。结点8是下一个E-结点,生成结点10和11,结点10不可行,于是\n第9章分枝-限界法235∧被杀死;结点11的c(11)=-32>U,因此也被杀死。接着扩展结点9,当生成结点12时将∧U和ans的内容分别修改成-38和12,结点12加入活结点队,生成结点13,但c(13)>U,因此13立即被杀死。此时,队中只剩下一个活结点12,结点12是叶结点,不可能有儿子,所以终止检索,输出U值和由结点12到根的路径。为了能知道在这条路径上x的取值情况,和例9.2一样,各个结点还需附上能反映xi取值的信息。图9.11例9.3的FIFO分枝-限界树在用FIFO分枝-限界算法处理背包问题时,由于结点的生成和确定其是否变为E-结点是逐级进行的,因此无需对每个结点专设一个LEVEL信息段,而只要用标志“#”来标出活结点队中哪些结点属于同一级即可。于是,状态空间树中每个结点可用CU、PE、TAG、UB和PARENT这5个信息段构成。过程NNODE(算法9.9)取一个可用结点并给此结点各信息段置值,然后将其加入活结点队。和LCKNAP一样,通过适当修改该问题的FIFO分枝-限界算法可将其变换成一个处理极大化问题的算法,过程FIFOKNAP描述了经过修改后的算法。算法9.9生成一个新结点procedureNNODE(par,tcap,prof,ub)∥生成一个新结点I并将它加入活结点队∥callGETNODE(I)PARENT(I)←par;TAG(I)←tCU(I)←cap;PE(I)←prof;UB(E)←ubcallADDQ(X)endNNODE在算法FIFOKNAP中,L表示最优解值的下界。由于只有生成了N+1级的结点才可能到达解结点,因此可以不用LCKNAP中的ε。第3~6行对可用结点表、根结点E、下界L和活结点队置初值。活结点队最初有根结点E和级结束标志′#′。i是级计数器,它的初始值是1。在算法执行期间i的值总是对应于当前E-结点的级数。在第7~26行while循环的每一次迭代中,取出i级上所有的活结点,它们是由第8~23行的循环从队中逐个被取出的。一旦取出级结束标志,则从第11行跳出循环;否则仅在UB(E)≥L时扩展E,在第13~21行生成E-结点的左、右儿子,这部分的代码与过程LCKNAP相应部分的代码类似。当控制从while循环转出时,活结点队上所剩下的结点都是N+1级上的结点。其中,具有最大PE\n236计算机算法基础值的结点是最优解对应的答案结点,它可通过逐个检查这些剩下的活结点来找到。过程FINISH(算法9.7)打印最优解的值和为了得到这个值应装入背包的那些物品。算法9.10背包问题的FIFO分枝-限界算法procedureFIFOKNAP(P,W,M,N)∥功能和假设均与LCKNAP相同∥1realP(N),W(N),M,L,LBB,UBB,E,prof,cap2integerANS,X,N3callINIT;i←14callLUBOUND(P,W,M,0,N,1,L,UBB)5callNNODE(0,0,M,0,UBB)∥根结点∥6callADDQ(′#′)∥级标志∥7whilei≤Ndo∥对于i级上的所有活结点∥8loop9callDELETEQ(E)10case11:E=′#′:exit∥i级结束,转到24行∥12:UB(E)≥L:∥E是活结点∥13cap←CU(E);prof←PE(E)14ifcap≥W(i)then∥可行左儿子∥15callNNODE(E,1,cap-W(i),prof+P(i),UF(E))16endif17callLUBOUND(P,W,cap,prof,N,i+1,LBB,UBB)18ifUBB≥Lthen∥右儿子是活结点∥19callNNODE(E,0,cap,prof,UBB)20L←max(L,LBB)21endif22endcase23repeat24callADDQ(′#′)∥级的末端∥25i←i+126repeat27ANS←PE(X)=L的活结点X28callFINISH(L,ANS,N)29endFIFOKNAP9.3货郎担问题2n6.7节介绍了货郎担问题的一个动态规划算法,它的计算复杂度为O(n2)。本节讨论2n货郎担问题的分枝-限界算法,它的最坏情况时间虽然也为O(n2),但对于许多具体实例而言,却比动态规划算法所用的时间要少得多。设G=(V,E)是代表货郎担问题的某个实例的有向图,|V|=n,cij表示边〈i,j〉的成本。\n第9章分枝-限界法237若〈i,j〉|E,则有cij=∞。为不失一般性,假定每一次周游均从结点1开始并在结点1结束,于是解空间S可表示成:S={1,π,1|π是{2,3,⋯,n}的一种排列},|S|=(n-1)!。为减小S的大小,可将S限制为:只有在0≤j≤n-1,〈ij,ij+1〉∈E且i0=in=1的情况下,(1,i1,i2,⋯,in-1)∈S。可以将这样的S构造成一棵类似于n-皇后问题的状态空间树(见图8.2)。图9.12给出了|V|=4的一图9.12n=4,i0=i4=1的货郎担问题的个完全图的一种状态空间树。树中每个叶状态空间树结点L是一个解结点,它代表由根到L路径所确定的一次周游。结点14表示i0=1,i1=3,i2=4,i3=2和i4=1的一次周游。为了用LC分枝-限界法检索货郎担问题的状态空间树,需要定义成本函数c(·),成本∧∧估计函数c(·)和上界函数u(·),使它们对于每个结点X,有c(X)≤c(X)≤u(X)。c(·)可以定义为由根到X的路径确定的周游路线成本X是叶结点c(X)=子树X中最小成本叶结点的成本X不是叶结点∧∧在c(·)作了以上定义的情况下,对于每个结点X均满足c(X)≤c(X)的函数c(·)可∧简单地定义成:c(X)是由根到结点X那条路径确定的(部分)周游路线的成本。例如,在图9.12的结点6处所确定的部分周游路线为i0=1,i1=2,i2=4。它包含边〈1,2〉和〈2,4〉。不∧∧∧过一般并不采用以上定义的c(·),而是采用一个更好的c(·)。在这个c(·)的定义中使用了图G的归约成本矩阵。下面先给出归约矩阵的定义。如果矩阵的一行(列)至少包含一个零且其余元素均非负,则此行(列)称为已归约行(列)。所有行和列均为已归约行和列的矩阵称为归约矩阵。可以通过对一行(列)中每个元素都减去同一个常数t(称为约数)将该行(列)变成已归约行(列)。逐行逐列施行归约就可得到原矩阵的归约矩阵。假设第i行的约nn数为ti,第j列的约数为rj,1≤i,j≤n,那么各行、列的约数之和L=∑ti+∑rj称为矩阵约i=1j=1数。作为一个例子,考虑一个有5个结点的图G,它的成本矩阵C为∞2030101115∞1642C=35∞24(9.2)19618∞3164716∞因为对G的每次周游只含有由i(1≤i≤5)出发的5条边〈i,1〉,〈i,2〉,〈i,3〉,〈i,4〉,〈i,5〉中的一条边〈i,〉j,同样也只含有进入j(1≤j≤5)的5条边〈1,j〉,〈2,〉j,〈3,j〉,〈4,〉j,〈5,j〉中的一条边〈i,〉j,所以在此成本矩阵中,若对i行(或j列)施行归约,即将此行(列)的每个元素减去该行(列)的最小元素t,则此次周游成本减少t。这表明原矩阵中各条周游路线的成本分别\n238计算机算法基础是与其归约成本矩阵相应的周游路线成本与矩阵约数之和,因此矩阵约数L显然是此问题∧的最小周游成本的一个下界值,于是可以将它取作状态空间树根结点的c值。对矩阵C的1,2,3,4,5行和1,3列施行归约得C的归约成本矩阵C′,矩阵约数L=25。因此,图G的周游路线成本最少是25。∞10170112∞1120C′=03∞02(9.3)15312∞0110012∞∧为了定义函数c(·),在货郎担问题的状态空间树中对每个结点都附以一个归约成本矩阵。设A是结点R的归约成本矩阵,S是R的儿子且树边〈R,S〉对应这条周游路线中的边〈i,〉j。在S是非叶结点的情况下,S的归约成本矩阵可按以下步骤求得:①为保证这条周游路线采用边〈j,〉j而不采用其它由i出发或者进入j的边,将A中i行和j列的元素置为∞;②为防止采用边〈i,1〉(因为在已选定的路线上加入边〈i,j〉之后若再采用边〈j,1〉就会构成一个环从而得不到这条周游路线),将A(j,1)置为∞;③对于那些不全为∞的行和列施行归约则得到S的归约成本矩阵,令其为B,矩阵约数为r。非叶结点S∧的c值可定义为∧∧c(S)=c(R)+A(i,j)+r(9.4)如果S是叶结点,由于一个叶结点确定一条唯一的∧周游路线,因此可用这条周游路线的成本作为S的c∧值,即c(S)=c(S)。至于上界函数u(·)可将其定义为,对于树中每个结点R,u(R)=∞。现在用LC分枝-限界算法LCBB求解式(9.2)的货郎担问题实例的最小成本周游路线。LCBB使∧用了上面定义的c(·)和u(·)。图9.13给出了图9.13算法LCBB生成的状态空间树LCBB所产生的那一部分状态空间树,结点外的数∧是该结点的c值。根结点1是第一个E-结点,它的归纳成本矩阵为式(9.3)的矩阵C′,此时U=∞。扩展结点1,依次生成结点2,3,4和5。它们对应的归约成本矩阵为∞∞∞∞∞∞∞∞∞∞∞∞11201∞∞200∞∞02∞3∞0215∞12∞043∞∞011∞012∞00∞12∞(a)结点2(b)结点3\n第9章分枝-限界法239∞∞∞∞∞∞∞∞∞∞12∞11∞010∞90∞03∞∞203∞0∞∞312∞01209∞∞1100∞∞∞0012∞(c)结点4(d)结点5以结点3为例,它的归约成本矩阵由以下运算得到:先将矩阵C′的1行和3列所有元素置成∞;再将C′(3,1)置成∞;然后归约第1列,将该列的每个元素减去11即得。由式(9.4)得∧c(3)=25+17(即C′(1,3)的值)+11=53∧结点2,4和5的归约成本矩阵和c值也可类似得到。U的值不变。结点4变成下一个E-结点,它的儿子结点6,7和8被依次生成,与它们对应的归约成本矩阵为∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞11∞01∞∞∞01∞0∞∞0∞∞∞2∞1∞∞003∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞11∞0∞∞00∞∞∞∞00∞∞(e)结点6(f)结点7(g)结点8∧此时的活结点有2,3,5,6,7和8,其中c(6)最小,所以结点6成为下一个E-结点。扩展结点6,生成结点9和10,它们的归约成本矩阵为∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞∞00∞∞∞∞∞∞∞∞∞∞∞∞∞∞0∞∞∞∞∞∞0∞∞(h)结点9(i)结点10结点10是下一个E-结点,由它生成结点11。结点11是一个答案结点,它对应的周游路线∧∧的成本是c(11)=28,U被修改成28。下一个E-结点应是结点5,由于c(5)=31>28,故算法LCBB结束并得出最小成本周游路线是1,4,2,5,3,1。如果把一条周游路线看成是n条边的集合,则可以得到解的另一种表示形式。设图G=(V,E)有e条边,于是,一条周游路线均含有这e条边中的n条不同的边。由此可以将货郎担问题的状态空间树构造成一棵二元树,树中结点的左分枝表示周游路线中包含一条指定的边,右分枝表示不包含这条边。例如,图9.14(b)和(c)就表示图9.14(a)所示的那个三结点图中的两棵可能的状态空间树的前三级。就一般情况而言,一个给定的问题可能有多棵不同的状态空间树,它们的差别在于对图中边的取舍的决策次序不同。图9.14(b)所示的首先确定边〈1,3〉的取舍,图9.14(c)所示的则首先确定边〈1,2〉的取舍。应采用哪种状态空间树进行检索随货郎担问题的具体实例而定,因此所考虑的是动态状态空间树。为了根据问题的具体实例构造出便于检索的二元状态空间树,应确定图中边的取舍次\n240计算机算法基础图9.14一个例子(a)圆;(b)部分状态空间树;(c)部分状态空间树序。如果选取边〈i,〉j,则这条边将解空间划分成两个子集合,即在将要构造出的状态空间树中,根的左子树表示含有边〈i,j〉的所有周游,根的右子树表示不含边〈i,j〉的所有周游。此时,如果左子树中包含一条最小成本周游路线,则只需再选取n-1条边;如果所有的最小成本周游路线都在右子树中,则还需对边作n次选择。由此可知在左子树中找最优解比在右子树中找最优解容易,所以我们希望选取一条最有可能在最小成本周游路线中的边〈i,j〉作∧为这条“分割”边。一般所采用的选择规则是:选取一条使其右子树具有最大c值的边。使用∧这种选择规则可以尽快得到那些c值大于最小周游成本的右子树。还有一些其它的选择规∧则,例如选取使左、右子树c值相差最大的边等等。本节只使用前一种选择方法。∧在二元状态空间树中,根结点的归约成本矩阵由该实例的成本矩阵归约而得,根的c值是矩阵约数。如果非叶结点S是结点R的左儿子则S的归约成本矩阵的求取与前面所述步∧骤相同,它的c值仍由式(9.4)获得。如果非叶结点S是结点R的右儿子,由于R的右分枝代表不包含边〈i,〉j的周游,因此应将R的归约成本矩阵A中的元素A(i,j)置成∞后,再归约此矩阵不全为∞的行和列(实际上只需重新归约第i行和第j列),即得S的归约成本矩阵∧B和矩阵约数Δ=min{A(i,k)}+min{A(k,j)}。在计算c(S)时,由于周游路线不包含边k≠jk≠i〈i,〉j,所以A(i,j)不必加入,即∧∧c(S)=c(R)+Δ(9.5)∧如果S是叶结点,则与前面一样有c(S)=c(S)。如果选取的边〈i,〉j在R的归约矩阵A中对应的元素A(i,j)为正数,则∧∧该边显然不可能使R右子树的c值为最大,因为c(S)=∧∧c(R)。所以,为了使R右子树的c值最大,应从R的归约成本矩阵元素为0的对应边中选取有最大Δ值的边。仍以式(9.2)的货郎担问题为例,用算法LCBB在动态二元树上进行检索。开始根结点1是E-结点(见∧图9.15),c(1)=25,归约矩阵C′中零元素对应的边为〈1,4〉,〈2,5〉,〈3,1〉,〈3,4〉,〈4,5〉,〈5,2〉和〈5,3〉,各边的Δ值分别为1,2,11,0,3,3和11,因此选取边〈3,1〉或图9.15式(9.2)的状态空间树∧〈5,3〉作为“分割”边可以使结点1右分枝的c值最大。假\n第9章分枝-限界法241∧∧定LCBB选取〈3,1〉。结点1生成结点2和3,其中c(2)=25,c(3)=36,与它们对应的归约成本矩阵为∞10∞01∞101701∞∞11201∞1120∞∞∞∞∞∞3∞02∞312∞04312∞0∞0012∞00012∞(a)结点2(b)结点3结点2成为下一个E-结点,边〈1,4〉,〈2,5〉,〈3,5〉,〈5,2〉和〈5,3〉的Δ分别是3,2,3,3和∧∧11,选取边〈5,3〉为“分割”边,生成结点4和5,c(4)=28,c(5)=36,与它们对应的归约成本矩阵为∞7∞0∞∞10∞01∞∞∞20∞∞020∞∞∞∞∞∞∞∞∞∞∞0∞∞0∞31∞0∞∞∞∞∞∞0∞12∞(c)结点4(d)结点5结点4成为下一个E-结点,边〈1,4〉,〈2,5〉,〈4,2〉和〈4,5〉的Δ分别是9,2,7和0,选取边〈1,4〉∧∧为下一条“分割”边,生成结点6和7,c(6)=28,c(7)=37,与它们对应的归约成本矩阵为∞∞∞∞∞∞0∞∞∞∞∞∞∞0∞∞∞00∞∞∞∞∞∞∞∞∞∞∞0∞∞∞∞0∞∞0∞∞∞∞∞∞∞∞∞∞(e)结点6(f)结点7结点6是下一个E-结点,此时需求的5条边已求出了3条,即{〈3,1〉,〈5,3〉,〈1,4〉},再求两条边就可得到一条周游路线,由矩阵(e)可知此时只剩下两条边{〈2,5〉,〈4,2〉},故它们就是这条周游路线所需要的边。至此LCBB求出了一条成本为28的周游路线5,3,1,4,2,5。U∧被修改成28,下一个应成为E-结点的结点是3,由于c(3)=36>U,故LCBB结束。此例对LCBB作了一点修改,即在“靠近”解结点时作的处理与“没靠近”时的不同。如在结点6处,由于距离解结点只有两级,此时就不再采用分枝-限界方法求结点6的儿子和∧孙子结点的c值,而是对结点6为根的子树中结点逐个检索来找出答案结点。这种对多结点子树采用分枝-限界法而对结点数少的子树采用完全检索的处理方法可使算法效率提高一些。这种处理方法对于图9.13同样适用。关于货郎担问题还可用另外一些分枝-限界方法求解,有兴趣的读者可参阅E.Horowi-tz和S.Sahni所著的“FundamentalsofComputerAlgorithms”(1978年)以及S.E.Good-man和S.T.Hedetniemi所著的“IntroductiontotheDesignandAnalysisofAlgorithms”(1977年)。\n242计算机算法基础习题九9.1证明定理9.1。9.2写一个用LIFO分枝-限界方法检索一个最小成本答案结点的程序概要LIFOBB。9.3给定一个带有限期的作业排序问题:n=5,(p1,p2,⋯,p5)=(6,3,4,8,5),(t1,t2,⋯,t5)=(2,1,∧2,1,1),(d1,d2,⋯,d5)=(3,1,4,2,4)。采用9.1节关于作业排序问题的c(·)和u(·)的定义,画出FI-FOBB,LIFOBB和LCBB对上述问题所生成的、大小可变的元组表示的部分状态空间树,求出对应于最优解的罚款值。9.4使用大小固定的元组表示写一个求解带限期作业排序问题的完整的LC分枝-限界算法。9.5证明定理9.4。9.6证明定理9.6。9.7在大小可变的元组表示下解算例9.2。9.8在大小可变的元组表示下解算例9.3。9.9画出LCKNAP对下列背包问题实例所生成的部分状态空间树:(1)n=5,(p1,p2,⋯,p5)=(10,15,6,8,4),(w1,w2,⋯,w5)=(4,6,3,4,2),M=12;(2)n=5,(p1,p2,⋯,p5)=(w1,w2,⋯,w5)=(4,4,5,8,9),M=15。9.10用LC分枝-限界法在动态状态空间树上做9.9题。使用大小固定的元组表示。9.11使用大小固定的元组表示下的动态状态空间树,写出背包问题的LC分枝-限界算法。9.12已知一货郎担问题的实例由下面成本矩阵所定义:∞731283∞614958∞618935∞11181498∞(1)求它的归约成本矩阵。∧(2)用与图9.12类似的状态空间树结构和9.3节定义的c(·)去获取LCBB所生成的部分状态空间∧树。标出每个结点的c值并写出其对应的归约矩阵。(3)用9.3节介绍的动态状态空间树方法做第(2)题。9.13使用下面的货郎担成本矩阵做9.12题:∞1110968∞73484∞4811105∞56955∞9.14使用像图9.12那样的静态状态空间树和归约成本矩阵方法写出实现货郎担问题的LC分枝-限界算法的有效程序。9.15使用动态状态空间树和归约成本矩阵方法写出实现货郎担问题的LC分枝-限界算法的有效程序。9.16对于任何货郎担问题实例,使用静态树的LC分枝-限界算法所生成的结点是否比使用动态树的LC分枝-限界算法生成的结点少?证明所下的结论。\n第10章NP-难度和NP-完全的问题10.1基本概念本章的内容包括了在算法研究方面的最重要的基本理论。这些基本理论对于计算机科学家、电气工程师和从事运筹学等方面的工作者都是十分有用的。因此,凡是在这些领域里的工作者建议读本章的内容。不过,在阅读本章之前,读者应熟悉以下基本概念:一是算法的事先分析计算时间,它是在所给定的不同数据集下,通过研究算法中语句的执行频率而得到的;二是算法时间复杂度的数量级以及它们的渐近表示。如果一个算法在输入量为n的情况下计算时间为T(n),则记作T(n)=O(f(n)),它表示时间以函数f(n)为上界。T(n)=Ω(g(n))表示时间以函数g(n)为下界。这些概念在第2章都作过详细的阐述。另一个重要概念是关于两类问题的区别,其中第一类的求解只需多项式时间的算法,而第二类的求解则需要非多项式时间的算法(即g(n)大于任何多项式)。对于已遇到和作过研究的许多问题,可按求解它们的最好算法所用计算时间分为两类。第一类问题的求解只需低次多项式时间。例如,本书前面讲过的有序检索的计算时间为O(logn),分类为2.81O(nlogn),矩阵乘法为O(n)等。第二类问题则包括那些迄今已知的最好算法所需时间2n为非多项式时间的问题,例如货郎担问题和背包问题的时间复杂度分别为O(n2)和n/2O(2)。对于第二类问题,人们一直在寻求更有效的算法,但至今还没有谁开发出一个具有多项式时间复杂度的算法。指出这一点是十分重要的,因为算法的时间复杂度一旦大于多项式时间(典型的时间复杂度是指数时间),算法的执行时间就会随n的增大而急剧增加,以致即使是中等规模的问题也不能解出。本章所讨论的NP-完全性理论,对于第二类问题,既不能给出使其获得多项式时间的方法,也不说明这样的算法不存在。取而代之的是证明了许多尚不知其有多项式时间算法的问题在计算上是相关的。实际上,我们建立了分别叫做NP-难度的和NP-完全的两类问题。一个NP-完全的问题具有如下性质:它可以在多项式时间内求解,当且仅当所有其它的NP-完全问题也可在多项式时间内求解。假如有朝一日某个NP-难度的问题可以被一个多项式时间的算法求解,那么所有的NP-完全问题就都可以在多项式时间内求解。下面将会看到,一切NP-完全的问题都是NP-难度的问题,但一切NP-难度的问题并不都是NP-完全的。10.1.1不确定的算法到目前为止在已用过的算法中,每种运算的结果都是唯一确定的,这样的算法叫做确定的算法(deterministicalgorithm)。这种算法和在计算机上执行程序的方式是一致的。从理\n244计算机算法基础论的角度看,对于每种运算的结果“唯一确定”这一限制可以取消。即允许算法每种运算的结果不是唯一确定的,而是受限于某个特定的可能性集合。执行这些运算的机器可以根据稍后定义的终止条件选择可能性集合中的一个作为结果。这就引出了所谓不确定的算法(nondeterministicalgorithm)。为了详细说明这种算法,在SPARKS中引进一个新函数和两条新语句:(1)choice(S)┄┄任意选取集合S中的一个元素。(2)failure┄┄发出不成功完成的信号。(3)success┄┄发出成功完成的信号。赋值语句X←choice(1∶n)使X内的结果是区域[1,n]中的任一整数(没有规则限定这种选择是如何作出的)。failure和success的信号用来定义此算法的一种计算,这两条语句等价于stop语句,但不能起return语句的作用。每当有一组选择导致成功完成时,总能作出这样的一组选择并使算法成功地终止。当且仅当不存在任何一组选择会导致成功的信号,那么不确定的算法不成功地终止。choice,success和failure的计算时间取为O(1)。能按这种方式执行不确定算法的机器称为不确定机(nondeterministicmachine)。然而,这里所定义的不确定机实际上是不存在的,因此通过直觉可以感到这类问题不可能用“快速的”确定算法求解。例10.1考察给定元素集A(1∶n),n≥1中,检索元素x的检索问题。需确定下标j,使得A(j)=x,或者当x不在A中时有j=0。此问题的一个不确定算法为j←choice(1∶n)ifA(j)=xthenprint(j);successendifprint(′0′);failure由上述定义的不确定算法当且仅当不存在一个j使得A(j)=x时输出“0”。此算法有不确定的复杂度O(1)。注意:由于A是无序的,因此确定的检索算法的复杂度为Ω(n)。例10.2[分类]设A(i),1≤i≤n,是一个尚未分类的正整数集。不确定的算法NSORT(A,n)将这些数按非降次序分类并输出。为方便起见,采用一辅助数组B(1∶n)。第1行将B初始化为零。在第2~6行的循环中,每个A(i)的值都赋给B中的某个位置,第3行不确定地定出这个位置,第4行弄清B(j)是否还没用过。因此,B中整数的次序是A中初始次序的某种排列。第7~9行验证B是否已按非降次序分类。当且仅当整数以非降次序输出时,算法成功地完成。由于第3行对于这种输出次序总存在一组选择,因此算法NSORT是一个分类算法,它的复杂度为O(n)。回忆前面讲过的各种确定的分类算法可知它们的复杂度应为Ω(nlogn)。算法10.1不确定的分类算法procedureNSORT(A,n)∥对n个正整数分类∥integerA(n),B(n),n,i,j1B←0∥B初始化为零∥2fori←1tondo\n第10章NP-难度和NP-完全的问题2453i←choice(1∶n)4ifB(j)≠0thenfailureendif5B(j)←A(i)6repeat7fori←1ton-1do∥验证B的次序∥8ifB(i)>B(i+1)thenfailureendif9repeat10print(B)11success12endNSORT通过允许作不受限制的并行计算,可以对不确定的算法作出确定的解释。每当要作某种选择时,算法就好像给自己复制了若干副本,每种可能的选择有一个副本,于是许多副本同时被执行。第一个获得成功完成的副本,将引起其它所有副本的计算终止。如果一个副本获得不成功的完成则只该副本终止。前面说过success和failure信号相当于确定算法中的stop语句,但它们不能用来取代return语句。上述解释是为了便于读者理解不确定算法。注意:对一台不确定机来说,当算法每次作某种选择时,它实际上是什么副本都不作,只是在每次作某种选择时,具有从可选集合中选择出一个“正确的”元素的能力(如果这样的元素存在的话)。一个“正确的”元素是相对于导致一成功终止的最短选择序列而定义的。在不存在导致成功终止的选择序列的情况下,则假定算法是在一个单位时间内终止并且输出“计算不成功”。只要有成功终止的可能,一台不确定机就会以最短的选择序列导致成功的终止。因为这种不确定机本来就是虚构和假想的,所以没有必要去注意机器在每一步是如何作出正确选择的。完全有可能构造出一些这样的不确定算法,它们的多种不同的选择序列都会导致成功完成。例10.2的过程NSORT就是这样的一个算法。如果整数A(i)有许多是相同的,那么在一个分类序列中将出现许多不同的排列。如果不是按已分好类的次序输出这些A(i),而是输出所用的排列,那么这样的输出将不是唯一确定的。今后只关心那些产生唯一输出的不确定算法,特别是只研究那些不确定的判定算法(nondeterministicdecisionalgorithm)。这些算法只产生“0”或“1”作为输出,即作二值决策,前者当且仅当没有一种选择序列可导致一个成功完成,后者当且仅当一个成功完成被产生。输出语句隐含于success和failure之中。在判定算法中不允许有明显的输出语句。显然,早先对不确定计算的定义意味着一个判定算法的输出由输入参数和算法本身的规范唯一地确定。虽然上面所述的判定算法的概念看来似乎限制过严,但事实上许多最优化问题都可以改写成判定问题并使其具有如下性质:该判定问题可以在多项式时间内求解,当且仅当与它相应的最优化问题可以在多项式时间内求解。从另一方面说,如果判定问题不能在多项式时间内求解,那么与它相应的最优化问题也不能在多项式时间内求解。例10.3[最大集团]图G=(V,E)的最大完全子图叫作G的一个集团(clique)。集团的大小用所含的结点数来量度。最大集团问题即为确定G内最大集团的大小问题。与之对应的判定问题是,对于某个给定的k,确定G是否有一个大小至少为k的集团。令DCLIQ-UE(G,k)是此集团判定问题的一个确定的判定算法。假设G的结点数为n,G内最大集团\n246计算机算法基础的大小可在多次应用DCLIQUE(G,k)而求得。对于每个k,k=n,n-1,n-2,⋯直到DCLIQUE输出1为止,过程DCLIQUE都要被引用一次。如果DCLIQUE的时间复杂度为f(n),则最大集团的大小可在n*f(n)时间内求出。假如最大集团的大小可以在g(n)时间内确定,于是其判定问题也可在g(n)时间内解出。因此最大集团问题可在多项式时间内求解,当且仅当集团的判定问题可在多项式时间内求解。例10.4[0/1背包]背包的判定问题是确定对xi,1≤i≤n,是否存在一组0/1的赋值,使得∑pixi≥R和∑wixi≤M。R是一个给定的数,而这些pi及wi都是非负的数。显然,如果背包的判定问题不能在确定的多项式时间内求解,则它的最优化问题也同样不能在确定的多项式时间内求解。在进一步讨论之前,为了测量复杂度必须先统一参数n。假设n是算法的输入长度;还假定所有的输入都是整数。有理数输入可以视为一对整数。一般说输入量的长度是以该量的二进制表示度量的,即如果输入量是十进制的10,相应的二进制表示就是1010,因此它的长度就是4。对于正整数k,用二进制形式表示时的长度就是log2k+1。0的长度规定为1。算法输入的大小或长度n是指输入的各个数长度的总和。因此,用不同的进位制时的长度是不同的,以r为基的正整数k,其长度为logrk+1。在十进制中(r=10)数100的长度为log10100+1=3。因为logrk=log2k/log2r,于是以r(r>1)为基输入的长度为c(r)·n,其中n是以二进制表示时的长度,c(r)是对于给定r后的一个常数。当使用基r=1给定输入量时,则称此输入是一进制形式的。在一进制形式中,数5的输入形式是11111。于是正整数k的长度即为k。注意:一进制形式输入的长度与其相应的基为r(r>1)的输入的长度间具有指数关系。例10.5[最大集团]最大集团判定问题的输入可以看作是一个边的序列和一个整数k。E(G)内的每条边又是一对数值(i,j)。对于每条边(i,j),如果采用二进制表示,则其输入的大小为log2i+log2j+2。于是任一实例的输入大小为n=∑(log2i+log2j+2)+log2k+1(i,j)∈E(G)iMor∑(P(i)*X(i)),≤,=等)这几种形式之一时,M是什么?当i是一条(b)或(c)型的赋值语句时,则它必须选择正确的数组元素,考察一条(b)型指令:R(m)←X。此时公式M可以写成:M=W∧(∧Mj)1≤j≤u其中,u是R的维数。注意,由于对算法A的限制⑦,因此u≤P(n)。W断言1≤m≤u。对W的详细说明则留作习题,每个Mj断言,或者m≠j,或者m=j且只有R的第j个元素改变。假设X和M的值分别存放在字x和m中,并且R(1∶m)存放在字α,α+1,⋯,α+u-1中,Mj由下式给出:Mj=∨T(m,k,t-1)∨Z1≤k≤w\n第10章NP-难度和NP-完全的问题255其中,若j的二进制表示中的第k位为0,则T是B,否则T是B。Z定义为Z=∧((B(r,k,t-1)∧B(r,k,t))∨B(r,k,t-1)∧B(r,k,t)))1≤k≤w1≤r≠P(n)r≠α+j+1∧((B(α+j-1,k,t)∧B(x,k,t-1))∨(B(α+j-1,k,t)∧B(x,k,t-1)))1≤k≤w2w注意:M中的文字数目是O(P(n)),由于j是w位长,因此它只能表示小于2的数。w于是对于u≥2的情况需使用别的下标方案,一种简单的推广是允许多精度运算,下标变量j需要多少字就用多少字,所用字数取决于u,它至多需要log(P(n))个字,这会在Mj内引起2少许的变化,但M中文字的数目仍然保持在O(P(n))。由于程序在存取多精度下标j中单个的字时可以要求此程序模拟多精度运算,因此不需要明显地引入多精度运算。当i是一条(c)型指令时,M的形式与(b)型指令所得到的类似。下面讨论在如下情况时如何来构造M,这里i的形式为Y←choice(S),其中S既可是形如S={S1,S2,⋯,Sk}的集合,也可是r∶u这样的形式,假设Y由字y来提供。如果S是一个集合,则定义M=∨Mj1≤j≤kMj断言Y是Sj。通过选择Mj=a1∧a2∧⋯∧aw,这一点是很容易做到的,其中若Sj中的第l位是1则ai=B(y,l,t),若Sj的第l位是0则ai=B(y,l,t)。如果S的形式为r∶u,那么M是断言r≤Y≤u的公式(这一点留作习题)。在上述两种情况下Gj,t可以转换成CNF,而Gj,t的长度至多增加一个常量。(6)令i1,i2,⋯,ik为对应于A中成功语句的语句编号,则H由下式给出:H=S(i1,P(n))∨S(i2,P(n))∨⋯∨S(ik,P(n))易证,当且仅当在输入I的情况下算法A的计算成功地终止时,Q=C∧D∧E∧F∧G∧H是可满足的。此外,根据前面所述,Q可转换成CNF形式。公式C含有wP(n)个文字,D23含有l个文字,E含有O(lP(n))个文字,F含有O(lP(n))个文字,G含有O(lwP(n))个文3字,H至多含有l个文字。当lw为常数时,Q中出现的文字的总和为O(lwP(n))=32O(P(n))。由于Q中有O(wP(n)+lP(n))个不同的文字,因此每个文字可用2O(log(wP(n)+lP(n)))=O(logn)位写出来。因为P(n)至少为n,所以Q的长度为343O(P(n)logn)=O(n)。由A和I构造Q的时间也是O(P(n)logn)。上面的结构表明,NP中的每一个问题可约化为可满足性问题,也可约化为CNF-可满足性问题。因此若这两个问题的任何一个在P中,则NPP,从而P=NP。另外,可满足性是在NP中,因此由构造公式Q的CNF表明可满足性∝CNF-可满足性。将此和CNF-可满足性在NP中加在一起则意味着CNF-可满足性是NP-完全的。注意:由于可满足性∝可满足性且可满足性在NP之中,故可满足性也是NP-完全的。10.3NP-难度的图问题用来证明一个问题L2具有NP-难度的策略如下:(1)挑选一个已知其具有NP-难度的问题L1。(2)证明如何从L1的任一实例I(在多项式确定时间内)获得L2的一个实例I′,使得从\n256计算机算法基础I′的解能(在多项式确定时间内)确定L1实例I的解。(3)从(2)得出结论L1∝L2。(4)由(1),(3)及∝的传递性得出结论L2是NP-难度的。在下面的叙述中,对于头几个证明将完全按上述4步进行,以后的证明则只进行(1)和(2)两步,一个具有NP-难度的判定问题L2可以通过展示一个求解L2的具有多项式时间的不确定算法来证明它是NP-完全的。下面所涉及的NP-难度的判定问题都是NP-完全的。对其中一些问题构造多项式时间的不确定算法则留作习题。10.3.1集团判定问题(CDP)在10.1节已介绍过集团判定问题,在定理10.2中将证明CNF-可满足性∝CDP。利用可满足性∝CNF-可满足性及∝的可传递性,就可容易地建立关系:可满足性∝CDP,于是CDP是NP-难度的。由于CDP∈NP,所以CDP也是NP-完全的。定理10.2CNF-可满足性∝集团判定问题(CDP)。证明令F=∧Cj是一个CNF形式的命题公式,xj,1≤i≤n,是F中的变量。现证如1≤i≤k何由F构造图G=(V,E),使得当且仅当F是可满足的,则G就有一个大小至少为k的集团。如果F的长度为m,则在O(m)时间内可由F获得G。因此,若对于CDP有一个多项式时间算法,则使用这一构造法就能得到一个对于CNF-可满足性的多项式时间算法。对于任意的F,G=(V,E)被定义如下:V={〈σ,i〉|σ是子句Cj的一个文字};E={(〈σ,〉i,〈δ,〉j)|i≠j且σ≠δ}。例10.11给出了这种构造的一个样本。如果F是可满足的,则对于xi,1≤i≤n,存在一组真值指派,使得在这组指派下每个子句为真。因此,在这组指派下,在每个Ci中至少存在一个文字σ为真,令S={〈σ,i〉|σ在C中为真}是对于每个i恰好只含有一个〈σ,i〉的集合。S形成G内一个大小为k的集团。类似地,如果G有一个大小至少为k的集团K=(V′,E′),则令S={〈σ,〉i|〈σ,〉i∈V′}。显然,当G不再有大于k的集团时|S|=k。更进一步,若S′={σ|〈σ,i〉∈S,对于某些i},则由于G中〈δ,〉i和〈δ,〉j之间无边相连,故S′不能同时含有文字δ和它的补δ。于是,若xj∈S′就置xi=真,若xi∈S′就置x=假,而对不在S′中的变量则选取任意的真值就可使F中的所有子句得到满足。因此,当且仅当G有一个大小至少为k的集团时F是可满足的,证毕。例10.11考察F=(x1∨x2∨x3)∧(x1∨x2∨x3)。根据定理10.2的构造法生成的图如图10.1所示。这个图含有6个大小为2的集团。考虑具有结点{〈x1,1〉,〈x2,2〉}的那个集团,通过置x1=真和x2=真(即x2=假)使F被满足。x3既可置为真也可置为假而对F的可满足性无任何影响。10.3.2结点覆盖的判定问题对于图G=(V,E),集合SV,如果E中所有的边都至少有一个结点在S中,则称S是图G的一个结点覆盖,覆盖的大小|S|是S中的结点数。图10.1图和可满足性的一个样本\n第10章NP-难度和NP-完全的问题257例10.12考察图10.2中的图:S={2,4}是大小为2的一个结点覆盖,S={1,3,5}是大小为3的一个结点覆盖。结点覆盖判定问题(nodecoverdecisionprob-lem)简记为NCDP,它是对给定的图G和一个整数k,判定G是否有大小至多为k的结点覆盖。定理10.3集团判定问题(CDP)∝结点覆盖判定问题(NCDP)。证明令G=(V,E)和k定义一个CDP的实图10.2图与结点覆盖的一个样本例,假设|V|=n。下面来构造一个图G′,使得当且仅当G有一个大小至少为k的集团时G′有一个大小至多为n-k的结点覆盖。图G′给出如下:G′=(V,E),其中E={(u,v)|u∈V,v∈V且(u,v)瓝E}。现证当且仅当G有一个大小至少为k的集团时,G′有一个大小至多为n-k的结点覆盖。令K是G中任一大小为k的集团。由于不存在E中的边会连接K中的结点,因此剩在G中的n-k个结点必定覆盖E中所有的边,即G′有一个大小至多为n-k的结点覆盖。可以类似地证明,若S是G′的一个结点覆盖,则V-S必定形成G中的一个完全子图。由于G′可以在多项式时间内从G获得,因此,如果对NCDP有一个多项式时间的确定算法,则CDP可以在多项式确定时间内解出。证毕。注意:由于CNF-可满足性∝CDP,CDP∝NCDP且∝是传递的,因此可得NCDP是NP-难度的。10.3.3着色数判定问题(CN)图G=(V,E)的着色是对于所有的i∈V所定义的一个函数f:V→{1,2,⋯,k}。如果(u,v)∈E,则f(u)≠f(v)。着色数判定问题(chromaticnumberdecisionproblem)简记为CN,它是对于某个给定的k,确定是否能对G着色。例10.13图10.2所示的要成为二色图的可能方案是f(1)=f(3)=f(5)=1,而f(2)=f(4)=2。显然这个图不可能是1-着色的。为证明CN是NP-难度的要用到另一个NP-难度的问题SATY。SATY问题是带有以下限制的CNF-可满足性问题,它只允许CNF的命题公式中每个子句至多有3个文字。证明CNF-可满足性∝SATY则留下作为习题。定理10.4每个子句至多有3个文字的可满足性(SATY)∝着色数判定问题(CN)。证明令F是一个有r个子句且每个子句内至多有3个文字的CNF形式的命题公式。令xi,1≤i≤n,是F中的n个变量。可以假定n≥4(否则若n<4,那么可以通过对x1,x2和x3的八组可能的真值指派做彻底的试验来确定F是否可满足),当且仅当F是可满足的,则可在多项式时间内构造出一个n+1可着色的图G。图G=(V,E)定义为V={x1,x2,⋯,xn}∪{x1,x2,⋯,xn}∪{y1,y2,⋯,yn}∪{C1,C2,⋯,Cr}E={(xi,xi),1≤i≤n}∪{(yi,yj)|i≠j}∪{(yi,xj)|i≠j}∪{(yi,xj)|i≠j}∪{(xi,Cj)|xi瓝Cj}∪{(xj,Cj)|xi瓝Cj}\n258计算机算法基础为了弄清当且仅当F是可满足的,则G是n+1可着色的,首先要看到所有的yi形成一个有n个结点的完全子图,因此,每个y必须分配一种不同的颜色。不失一般性,可以假设G中yi的着色就用颜色i,因为yi也与除了xi和xi以外的所有xj和xj相连接,所以颜色i只能再分给xi和xi。然而(xi,xi)∈E,因此,这两个结点中有一个需要分配一种新的颜色n+1,不妨将分配新颜色n+1的那个结点称为“假”结点,其它结点称为“真”结点。用n+1种颜色对G着色的唯一方法是对于每一个i,将新颜色n+1着在{xi,xi}的一个之上,1≤i≤n。在这种情况下,其余的结点是否可不再用更多的颜色来着色呢?回答是肯定的,因为n≥4且每个子句至多有3个文字,所以每个Ci至少与一对结点xj和xi相连接。从而没有哪个Ci可以着以颜色n+1,也没有哪个Ci可以着以不在Ci中的xj或xj所着的颜色。上述两句话意味着仅能给Ci着的颜色是在子句Ci中那些被称为“真”结点的xj或xj所着的颜色。因此,当且仅当对应于每个Cj存在一个“真”结点,G是n+1可着色的。故当且仅当F是可满足的,G是n+1可着色的。证毕。10.3.4有向哈密顿环(DHC)有向图G=(V,E)中的一个哈密顿环是一个长度为n=|V|的有向环,它恰好经过每个结点一次,然后回到起始的结点,DHC问题是确定G是否有一个有向哈密顿环。例10.14图10.3中1,2,3,4,5,1是一个有向哈密顿环。若把边〈5,1〉从图中删去,则它就没有有向哈密顿环。定理10.5CNF-可满足性∝有向哈密顿环(DHC)。证明令F是CNF形式的命题公式。下面将设法构造出一有向图G,使得当且仅当G存在一向哈密顿环时,F可满足。由于这一构造可在F大小的多项式时间内完成,因此CNF-可满足性∝DHC。用一个例子来理解G的构造可带来极大的方便。所采用的例子是图10.3图和哈密顿环的样本F=C1∧C2∧C3∧C4。其中,C1=x1∨x2∨x4∨x5,C2=x1∨x2∨x3,C3=x1∨x3∨x5,C4=x1∨x2∨x3∨x4∨x5。假设F有r个子句C1,C2,⋯,Cr和n个变量x1,x2,⋯,xn。画一个有r行和2n列的数组,第i行表示子句Ci,每个变量xi由两个相邻的列表示,其中一列表示文字xi,另一列表示文字xi。图10.4列出了这一数组。当且仅当xi是Cj中的一个文字,就在列xi和行Cj处插入一个○*。同样,当且仅当xi是Cj中的一个文字,就在列xi和行Cj处插入一个○*。在xi和xi的每对列之间引入两个结点ui和vi,ui在列的顶端而vi在列的底部。对于每个i,从ui向上到vj,画两条由边组成的链,一条把列xi的所有○*连接起来,另一条则连接起xi的所有○*(参看图10.4)。然后再画边〈ui,vi+1〉,1≤i≤n。在每一行Ci的右端引入一个方框i,1≤i≤r,画边〈ur,1〉和〈r,v1〉,再画边〈i,i+1〉,1≤i2是很容易的。令ai,1≤i≤n是分划问题的一个实例。定义n个处理时间为ti=ai,1≤i≤n的作业。当且仅当对这些ai存在一个分划时,则对于这个作业集,在两台处理器上就有一个其完成时间至多为Σti/2的不抢先调度。证毕。定理10.11分划问题∝最小WMFT不抢先调度。证明这里仍只对m=2的情况加以证明,同样,将其扩展到m>2是很简单的事。令ai,1≤i≤n,定义分划问题的一个实例。对于n个作业且w1=ti=ai,1≤i≤n,构造一个双处理器问题。对于这个作业集,当且仅当所有的ai有一分划,那么就存在一种其加权平均完成1212时间至多为∑ai+(∑ai)的不抢先调度S。为了证实这一点,令在处理器P1上的作业24==的权和时间是(w1,t1),⋯,(wk,tk),而在P2上的作业则有(w1,t1),⋯,(w1,t1)。假定这就是作业分别在它们的处理器上处理时的次序。于是对这个调度S有:WMFT=w1t1+w2(t1+t2)+⋯+wk(t1+⋯+tk)=====+w1t1+w2(t1+t2)+⋯+wl(t1+⋯+tl)121212=∑wi+(∑wi)+(∑wi-∑wi)2221212由此可见,WMFT≥∑wi+(∑wi)。这个值当且仅当所有的wi(也就是所有的24ai)有一分划时才能获得。证毕。10.4.2流水线调度这里沿用6.8节所定义的术语。如果有n个要调度的作业,当m=2时其最小完成时间的调度可以在O(nlogn)时间内做到;但当m=3时,无论是抢先调度或是不抢先调度,要得到其最小完成时间的调度都是具有NP-难度的。对于不抢先调度,这一点是容易证明的。下面用抢先调度来证明这一结论。这证明对于不抢先情况也适用。定理10.12分划问题∝最小完成时间的抢先流水调度问题(m>2)。证明只对三处理器的情况作出证明。令A={a1,a2,⋯,an}定义分划问题的一个实例。构造如下的抢先调度实例FS,有n+2个作业和m=3台处理器,每个作业至多有2个非零的任务:t1,i=ai,t2,i=0,t3,i=ai1≤i≤nt1,n+1=T/2,t2,n+1=T,t3,n+1=0\n264计算机算法基础t1,n+2=0,t2,n+2=T,t3,n+2=T/2n其中,T=∑ai1现在来证明,当且仅当A有一个分划,上述流水线调度实例有一个完成时间至多为2T的抢先调度。(1)如果A有一个分划u,则存在一个完成时间为2T的不抢先调度。图10.9给出了一种这样的调度。图10.9一种可能的调度(2)如果A不存在任何分划,则FS的所有抢先调度的完成时间必定大于2T。这一点可用反证法加以证明。假设对于FS存在一种完成时间不大于2T的抢先调度,于是可得以下观察结论:①任务t1,n+1必须在时间T之前完成(因t2,n+1=T,而且在t1,n+1完成之前不能开始)。②任务t3,n+1不能在时间T之前开始,因为t2,n+2=T。结论①意味着,在处理器1上最先开始的T个单位时间只有T/2是空的。令V是在时间T以前在处理器1上所完成任务的下标的集合(不包括任务t1,n+1),由于A不存在任何分划,所以∑t1,iT/2i瓝V1≤i≤n没包括在V中的作业在时间T之前不能在处理器3上着手处理,这是因为直到时间T为止在处理器1上对它们的处理还没完成。这一点与结论②结合起来则意味着,在时间T,留待处理器3处理的工作的时间总量是:t3,n+2+∑t3,i>Ti瓝V1≤i≤n所以,调度长度必定大于2T。证毕。10.4.3作业加工调度与流水线调度一样,作业加工调度问题中也有m台不同的处理器。被调度的n个作业都要求完成若干个任务。作业Ji的第j个任务要求tk,i,j的时间,它将在处理器Pk上执行。任一作业Ji的各任务将依1,2,3,⋯的次序执行,在任务j-1(假若j>1)完成以前不能执行任务j。值得注意的是,对于一个作业而言,它可能有许多任务在同一台处理器上被执行。在不抢先的调度中,任务一旦开始处理就一直进行到完成为止而不得被中断。FT(S)和\n第10章NP-难度和NP-完全的问题265MFT(S)的定义可以很自然地推广到这个问题。即使m=2,得到最小完成时间的抢先调度或最小完成时间的不抢先调度方案都是NP-难度问题。对于不抢先的情况,利用分划来证明是很容易的。下面对抢先的情况给出证明。这个证明对不抢先情况也成立,不过对该情况不是一种最简单的证明而已。定理10.13分划问题∝最小完成时间的作业加工调度问题(m>1)。证明只对使用两台处理器的情况进行证明。令A={a1,a2,⋯,an}定义分划问题的一个实例。构造具有n+1个作业和m=2台处理器的作业加工问题的实例JS。作业1,⋯,nt1,i,1=t2,i,2=ai1≤i≤n作业n+1t2,n+1,1=t1,n+1,2=t2,n+1,3=t1,n+1,4=T/2n其中,T=∑ai。1现证明当且仅当A有一分划时,上述作业加工问题有一个完成时间至多为2T的抢先调度。(1)如果A有一分划u,则存在一完成时间为2T的调度(见图10.10)。图10.10另一种调度(2)如果A不存在任何分划,则对于JS的所有调度,其完成时间必定大于2T。为看出这一点,假设对于JS有一个完成时间至多为2T的调度。于是作业n+1必须像图10.10那样被调度。此外,在P1和P2上还不可能有空闲时间。令R是在[0,T/2]时间内调度到P1上的作业集合。令R′是R的一个子集,它包含了在这段时间内已在P1上完成了其第一个任务的所有作业。由于这些ai不存在分划,故∑ti,j,11)个处理器将它们合并成长度为p+q的分类序列。处理器个数k的取值不同可能产生复杂度不同的并行归类算法。这里仅就处理器个数k=pq的情况构造算法。算法的基本思想是,利用分治策略对序列进行分组,并且这种分组方式是动态地和递归地进行。归并过程可简述如下。首先,在两序列中选定一些特定元素,并加以标记,这些标记元素将两序列分成了若干组,接着将第一序列中诸标记元素与第二序列中的诸标记元素进行比较,以确定第一序列中的每一标记元素应插入第二序列中的哪一组中;然后将第一序列中的诸标记元素与第二序列中它所插入的那一组中的其余元素进行比较,以确定第一序列中的每一个标记元素应插入第二序列中的哪一个位置上才能使序列保持有序。插入后,撇开原第二序列中的特定元素不管,则第一序列中的特定元素将第二序列分成若干个组,并且两个序列中的组构成组对。注意,我们规定组中元素不包含特殊元素,因此对于每个组对均存在3种情况:①第一组为空;②第二组为空;③第一、二组都不为空(这时第一组和第二组中元素分别来自第一和第二序列)。称情况③中的组对为非空组对。下面的工作是针对每一组对将第一组中元素插入第二组中去。对于情况①,不需插;对于情况②,是直接搬移;对于剩下的非空组对(若有的话),它们各自构成了独立的归并问题,因此,可并行地对各非空组对递归调用原算法,如此下去,直到不存在非空组对为止。下面给出并行归并算法的非形式化描述:procedurePARA-MERGE(p,q)∥将两个长度分别为p、q的已分类序列A和B用p·q个处理器归并成一个有序∥序列,约定p≤q∥\n第11章并行算法283(1)将A、B序列中位置分别是ip和iq(i=1,2,⋯)的一些元素打上*号;(2)将A中每个带*号的元素与B中每个带*号的元素同时进行比较;(3)将A中带*号的元素与其所插入的B组中的每一个元素进行比较,并把它们插入B中;(4)若有情况②的组对,则将组对中第一组元素搬入B中;(5)若有情况③的组对,则按处理器的个数=pq的分配原则给每个子问题配置处理器,然后对各个子问题并行调用PARA-MERGE算法。endPARA-MERGE现在来分析一下算法中第(5)步递归归并时,原来的pq台处理器够不够用?当执行到第(5)步时,设序列A和B中各组中的元素个数分别为pi和qi,显然,序列A的各组元素个数之和∑pi≤p;序列B的各组元素之和∑qi≤q。根据柯栖不等式∑piqi≤∑pi∑qi所以,∑piqi≤∑piqi≤∑pi∑qi≤pq。由此可见,在对各组对的并行递归调用时,处理器是够用的。下面分析算法的运算时间:在SIMD-CREW模型上,因为序列A和B中要打*号的元素至多有p和q个,所以至多用p+q个处理器在常数时间内完成算法中的第(1)步;在算法的第(2)步中,至多比较pq次,因此,至多用pq个处理器可在常数时间内完成算法的第(2)步;同样可证明至多用pq个处理器在常数时间内完成算法的第(3)步和第(4)步。在算法的第(5)步中因为任意一个非空组对的第一组元素个数小于等于p-1≤q,若设PARA-MERGE算法的运算时间为T(p),则第(5)步的运算时间≤T(p)。因此,T(p)满足关系式C1p足够小T(p)≤T(p)+C2否则其中,C1、C2是常数。解此关系式,可得1/2T(p)≤T(p)+C21/4≤T(p)+2C2⋯≤T(2)+(loglogp)C2=O(loglogp)由此得到PARA-MERGE算法在SIMD-CREW模型上的运算时间为O(loglogp)。对于SIMD-EREW模型,由于算法的第(2)和第(3)步中存在读冲突,因此,需要调用广播算法,这时PARA-MERGE算法的运算还与q有关,若记为T(p,q),则其满足关系式C1logqp足够小T(p,q)≤T(p)+C2logq否则解此关系式,得到T(p,q)≤Clogq+logq·loglogp=O(logqloglogp)。因此,在SIMD-EREW模型上,PARA-MERGE算法的运算时间为O(logq·loglogp)。\n284计算机算法基础有了并行归并算法后,并行归并分类算法就可通过并行归并来构造。设序列的长度为kn=2,k≥1并行归并分类算法描述如下:00第一并行步进行(2,2)并行归并;11第二并行步进行(2,2)并行归并;⋯⋯k-1k-1第k并行步:进行(2,2)并行归并。下面分析此算法在SIMD-CREW模型上的运算时间和性能。iii设运算时间为sort(n),当i>1时(2,2)并行归并的运算时间为O(loglog2),所以kkisort(n)≤∑(Cloglog2)=C∑logi≤Ckloglogni=1i=1即sort(n)=O(lognloglogn)(11.4)设算法所需处理机个数为P(n),所以k-10k-210k-1P(n)=max(22,22,⋯,22)=O(n)(11.5)由于最佳的串行分类算法的运算时间为O(nlogn),因此,并行归并分类算法的加速和效率分别是Sp(n)=O(n/loglogn)(11.6)Ep(n)=O(1/loglogn)(11.7)对于n不是2的幂的情况,只需在序列中增加若干元素,这些元素都大于原序列中的每个元素,并且使得序列长度为2的幂。可以证明,经过这样的预处理后,结论式(11.4)、(11.5)、(11.6)和(11.7)都仍然正确。在SIMD-EREW模型上,并行归并分类算法的运算时间和性能评价留作习题,不在这里赘述。11.3.4求图的连通分支的并行算法无向图G(V,E)的一个连通分支是G的一个最大连通子图。在SIMD共享存储模型上,求图的连通分支的方法主要有3种:采用某种形式的搜索方法、利用图的邻接矩阵的传递闭包法和顶点倒塌法。这里只介绍传递闭包法,顶点倒塌法将在SIMD互连网络中介绍。定义11.1设无向图G(V,E)的邻接矩阵是A,若矩阵B的元素定义如下:1顶点i与顶点j之间有路径存在bij=0顶点i与顶点j之间没有路径存在则称B是A的自反传递闭包。邻接矩阵A与其自反传递闭包B存在下列关系:nB=(I+A)其中,I为单位矩阵;符号“+”定义为逻辑加;n为图结点个数。下面根据这一关系来设计求连通分支的并行算法。假定共享存储器中保存了图G(V,E)的邻接矩阵A,定义G中任一个分支号为此分支中结点号的最小值。算法首先计算出A的自反传递闭包B,然后计算出矩阵C,其中,∞bij=0C(i,j)=jbij=1\n第11章并行算法285最后由d(i)=min(c(i,1),c(i,2),⋯,c(i,n))计算出G中任意结点i(1≤i≤n)所在的分支号D(i)。其算法如下:procedureconnected-componentsG(V、E)(1)foreachpij:1≤i,j≤npardo∥初始化∥ifi=jthena(i,j)←a(i,j)VIendifb(i,j)←a(i,j)d(i)←iendfor(2)forl←1tologndo∥计算传递闭包B∥foreachpijk:1≤i,j,k≤npardob′(i,j,k)=b(i,k)∧b(k,j);nb(i,j)=∨b′(i,j,k)k=1endforrepeat(3)foreachpij:1≤i,j≤npardo∥计算C∥ifb(i,j)=1thenc(i,j)←jelsec(i,j)←∞endifendfor;(4)foreachpij:1≤i,j≤npardo∥求每个结点所在的连通分支标号∥d(i)←min{c(i,j)|1≤j≤n}endforendconnected-components242logn算法的第(2)步是依次并行求出(I+A),(I+A),⋯,(I+A),由于自反传递闭包nn+Tn具有性质:(I+A)=(I+A),其中T为任意正整数,因此第(2)步可求出(I+A)。在第n(2)步中,语句b(i,j)=∨b′(i,j,k)是用n个处理器求b′(i,j,1)∨b′(i,j,2)∨⋯∨b′(i,j,k=1n),因此,可用求和并行算法,用n-1个处理器在O(logn)时间内完成。在第(4)步中,语句d(i)←min{c(i,j)|1≤j≤n}也可利用求和算法,用n-1个处理器在O(logn)时间内完成。由于存在读冲突,因此,此算法是SIMD-CREW模型上的算法。定理11.1在SIMD-CREW模型上,计算无向图G(V,E),|V|=n的连通分支con-23nected-components算法需要O(logn)时间和O(n)个处理器。证明因为算法的第(1)步需O(1)时间和O(n)个处理器;第(2)步由logn次串行循3环组成,对于每一次串行循环操作需O(logn)时间和O(n)个处理器,所以第(2)步需232O(logn)时间和O(n)个处理器;第(3)步需O(1)时间和O(n)个处理器;第(4)步需223O(logn)时间和O(n)个处理器。因此,算法需O(logn)时间和O(n)个处理器。证毕。如果计算模型容许写冲突,那么对算法进行适当的修改可使运动时间降为O(logn)。2这项工作的关键是:在SIMD-CRCW模型上设计使用O(n)个处理器,能在O(1)时间求出n个元素中最大元素的并行算法。具体做法如下:\n286计算机算法基础2假定我们要找c1,c2,⋯,cn这几个元素的最大值,则第(1)步用n个处理器,在O(1)时间内求出tij(1≤i,j≤n),其中1ci≥cjtij=0否则在第一步中存在读冲突,这一步的工作使得矩阵T=[tij]n×n具有如下性质:ck是c1,c2,⋯,cn中的最大元素的充要条件是矩阵T的第k行中n个元素皆为1。证明由矩阵T中元素tij(1≤i,j≤n)的定义可直接推出上述性质。详证略。2第(2)步用O(n)个处理器,在O(1)时间内求矩阵T中n个元素皆为1的某一行,并将其行号赋予k,此步可由下列语句实现:foreachpij:1≤i,j≤npardoiftij=0thentij←0endifendfor;foreachpi:1≤i≤npardoiftij=1thenk←iendifendfor显然,第(2)步中存在写冲突,在SIMD-CRCW模型上通过上面两步可确定c1,c2,⋯cn中的最大元素是ck。242logn下面对connected-components算法中第(2)步进行修改。在矩阵B,B,B,⋯,B2l中,每个矩阵的矩阵元素都是0或1,因此B=[cij],其中cij=max{bi1∧b1j,bi2∧b2j,⋯,bin∧bnj},bij为矩阵B中的元素,1≤i,j≤n,1≤l≤logn。这样,connected-components算法的第(2)步可改写成:forl←1tolognforeachpijk:1≤i,j,k≤npardob′(i,j,k)=b(i,k)∧b(k,j)endforforeachi,j:1≤i,j≤npardob(i,j)=max{b′(i,j,1),b′(i,j,2),⋯,b′(i,j,n)}endforrepeat2由于求n个元素的最大值可用O(n)个处理器在O(1)时间实现,因此,修改后算法的4第(2)步的运算时间为O(logn),需O(n)个处理器。对于整个算法有下面结论。定理11.2在SIMD-CRCW模型上,求无向图G(V,E),|V|=n的所有连通分支的自4反传递闭包算法需O(logn)时间和O(n)个处理器。由上面的讨论不难证明本定理。详证略。11.4SIMD互连网络模型上的并行算法11.4.1超立方模型上的求和算法m假定在m-维超立方模型上,n=2,n个待加的数的集合表示为:A=(a0,a1,⋯,an-1);\n第11章并行算法287且对于所有的0≤i≤n-1,处理器Pi存有局部变量ai。下面在此模型上构造一个并行求和n-1算法,使得算法结束时a0就是总和∑ai。i=0对于处理器Pi,0≤i≤n-1,设其号码i的二进制表示是im-1im-2⋯i0其中,ij=0或1,0≤j≤m-1。这样,可按处理器号码i的第m-1位值im-1将n个处理器分成两类:第一类中im-1=0,第二类中im-1=1。利用超立方模型中结点相邻关系可建立二类处理器集合中元素的一一对应关系。根据这种对应关系,即可构造并行算法。算法的非形式化描述如下:第一并行步对于n个处理器,以im-1的值分类,第一类中的处理器取其在第二类中所对应的处理器中的局部变量,然后与其本身的局部变量相加作为它的局部变量;第二并行步对于n/2个处理器,(即上一步中第一类的处理器),以im-2的值分类,第一类中的处理器取其在第二类中所对应的处理器的局部变量,然后与其本身的局部变量相加作为它的局部变量;⋯⋯第m并行步对于2个处理器,(即上一步中第一类中的处理器),以i0的值分类,第一类中的处理器取其在第二类中所对应的处理器的局部变量,然后与其本身的局部变量相加作为它的局部变量。由于每一步并行操作后,第一类处理器以“+”的形式保存了n个局部变量a0,a1,⋯,an-1的信息,故当执行完第m步后第一类中仅有处理器P0,它的局部变量a0就是n个数的总和。算法的形式化描述如下:procedureSUMMATION(a0,a1,⋯,an-1)forj=(logn)-1to0dojd←2;foreachPi:0≤i≤d-1pardotiai+d;ai←ai+tiendforrepeatendSUMMATION说明:符号表示数据从其相邻的处理器的局存中传往一个活动的处理器中的局存中。由于算法的外循环执行logn次,而每次循环的时间均为常数,所以以上算法的运算时间为Θ(logn)。因为求和问题的最佳串行算法的运算时间为Θ(n),所以,SUMMATION算法的加速比为Sp(n)=Θ(n/logn)效率为Ep(n)=Sp(n)/n=Θ(1/logn)例11.1在超立方模型上求16个数的和,其过程如图11.9所示。\n288计算机算法基础图11.9超立方模型上16个数的求和11.4.2一维线性模型上的并行排序算法一维线性模型是SIMD互连网络模型中最简单和最基本的互连形式,系统中有n个处理器,编号从1到n,每个处理器Pi均与其左、右近邻Pi-1、Pi+1相连,(P1和Pn除外)。我们将在此模型上构造n个数的并行排序算法。假定待排序的n个数的输入序列S={x1,x2,⋯,xn},处理器Pi(1≤i≤n)存有输入数据xi,算法步骤如下。第一并行步所有奇数编号的处理器Pi接收来自Pi+1中的xi+1。如果xi>xi+1则Pi和Pi+1彼此交换其内容。第二并行步所有偶数编号的处理器Pi接收来自Pi+1中的xi+1。如果xi>xi+1则Pi和Pi+1彼此交换其内容。交替重复上述两并行步,经n/2次迭代后算法结束。算法的形式化描述如下:procedureOETS(x1,x2,⋯,xn)fork=1ton/2doforeachPi:i=1,3,⋯,2n/2-1pardoifxi>xi+1thenxi→xi+1endif\n第11章并行算法289endforforeachPi:i=2,4,⋯,2(n-1)/2pardoifxi>xi+1thenxi←→xi+1endifendforrepeatendOETS例11.2对输入序列S={7,6,5,4,3,2,1},上述算法的排序过程见图11.10。OETS算法的正确性由定理11.3给予保证。定理11.3OETS算法至多经过n步可完成排序。证明此定理可通过对n施行归纳法来证明。(1)归纳基础:当n≤3时,可用穷举法验证定理是对的。(2)归纳假定:假定排序m个数算法OETS至多需图11.10OETS算法排序示例要m步。(3)归纳步骤:试图证明排序m+1个数,算法至多需要m+1步。为此,可借助排序图(SortGraph)来研究算法OETS对S={x1,x2,⋯,xm+1}的操作情况。在此图中,追踪S中最大元素M的轨迹。根据M起始时是位于奇数编号和偶数编号的处理器中,可有两种不同情况,分别示于图11.11(a)和(b)中,但每种情况都可认为在k步内可完成排序m+1个数,同时M的轨迹将排序图分成A和B两部分。假定现在M不存在了,如图11.12(a)所示擦去M的轨迹,将A和B如同图11.12(b)那样合并之,这样从第2行向下就是一个完整的m个数的排序图。根据归纳假定可知:k-1=m,所以k=m+1。证毕。下面进行算法分析,很容易求出OETS算法的运算时间T(n)=O(n),进而推出算法的2成本c(n)=T(n)p(n)=O(n),加速Sp(n)=O(logn)。图11.11在排序图中最大元素的轨迹图11.12证明定理11.3的排序图11.4.3树形模型上求最小值算法d在求集合S={x1,x2,⋯,xn}的最小元素问题中,首先假定n=2(d>0),因此,树形模dd+1d+1型共有2个叶子,总共有2-1个结点,即有P(n)=2-1个处理器。其中每个叶处理\n290计算机算法基础器能存放一个数,非叶处理器能存放两个数并可决定何者为小。算法的基本思想是:首先将S中的n个元素加载到各叶处理器中,然后从d级开始(根为第一级)每个非叶处理器从左、右儿子中取出两个数进行比较,并保存较小者,最后使根结点保存集合S中的最小元素。其算法如下:procedureMIN(x1,⋯,x2d)dd+11foreachPi:2≤i≤2-1pardo读取xi-2d+1endfor2forj=dto1doj-1jforeachPi:2≤i≤2-1pardo从左、右儿子中取出两个数进行比较,并保存最小者;endforrepeatendMIN算法分析如下:很显然,算法的第1步运算时间为O(1);因为d=logn,所以算法的第2步的运算时间为O(logn),因此算法的运算时间T(n)=O(logn)。由于求极值的最佳串行算法的运算时间为O(n),所以MIN算法的加速Sp(n)=O(n/logn)。算法的成本c(n)=T(n)×P(n)=O(nlogn),显然该算法不是成本最佳的。11.4.4二维网孔模型上的连通分支算法这里采用顶点倒塌法在二维网孔模型上求无向图G(V,E)的连通分支。定义11.2图G的一个超顶点集A是G的一个连通子图,此集由A中最小标号结点标识,这个结点称为A的根。并且对A中任一结点i赋一个指针信息:D(i)=k,其中k为A的根的标号。顶点倒塌法的基本思想是:一开始G中每个结点都当成一个超顶点集,然后是超顶点集的合并、扩大过程,此过程由一系列循环完成。每次循环分为三步:第一步对于G中每个超顶点集i,给其每个结点ij赋个0值,使其满足当存在与ij相邻的结点且此结点不在集i中时,c(ij)=min{D(s)/s与ij相邻,且s不在超顶点集i中},否则c(ij)=∞;第二步对于G中每个超顶点集i,修改其根结点i的D值,使其满足min{c(t)|D(t)=i,c(t)≠i}存在与i集相邻的超顶点集D(i)=i否则到此步为止,每个超顶点集i的根结点的D指针或指向与集i相邻的标号最小的超顶点集,或指向其本身(此时i是G的一个连通分支);第三步将由第二步连接起来的所有超顶点集扩大成一个超顶点集。关于循环的次数,由定理11.4给出。定理11.4在无向图G(V,E),|V|=n中,用顶点倒塌法求其连通分支的循环过程至多须执行log2n次,就可保证每个超顶点集是G的一个连通分支。\n第11章并行算法291证明由顶点倒塌法可知,对于任意一个超顶点集i,当且仅当没有其它的超顶点集与它相邻时超顶点集i才不能扩大,因此当超顶点集i不能扩大时,它就是图G的一个连通分支。设执行完k次循环后,G中可扩大超顶点集的个数为N(k),因此,有nk=0N(k)=N(k-1)/2k>0当k=logn时,N(logn)≤N(logn-1)/22≤N(logn-2)/2⋯logn≤N(0)/2≤1因为可扩大的超顶点集的个数不会等于1,所以N(logn)=0,即至多进行logn次循环后,每个超顶点集是G的一个连通分支。在叙述连通分支算法之前,首先引入算法所须执行的几个子过程和函数:(1)子过程RAR(a(i),b(c(i)))的功能是将未屏蔽的处理器i中局部变量a(i)的值等于处理器c(i)中的局部变量b(c(i))的值;(2)子过程RAW(a(i),b(c(i)))的功能是将未屏蔽的处理器i中局部变量a(i)的值写到处理器c(i)中的局部变量b(c(i))中;函数BITS(i,j,k)的功能是返回一个数,此数由整数i(i>0)的二进制表示第j位到第k位组成(0位是最低位),如BITS(17,3,1)=0,BITS(10,3,2)=2BITS(16,4,4)=1,BITS(15,2,3)=3(3)子过程Reduce(D,n)的功能是,对于一串由根结点的D指针链接的超顶点集,将此串中每个超顶点集的所有结点的D指针赋一个值,此值是串中最后一个超顶点集的根的标号。子过程具体算法如下:ProcedureReduce(D,n)forb←1tologndoforeachi:1≤i≤npardoifBITS(D(i),logn-1,b)=BITS(i,logn-1,b)thenRAR(D(i),D(D(i)))endifendforrepeatendReduce下面给出在二维网孔模型上用顶点倒塌法求图的连通分支的并行算法。假定无向图kG(V,E)有n=2个结点,图G的度数为d,令adj(i,j)是结点i的邻接表(1≤i≤n,1≤j≤n)。如果结点i有di(di≤d)条关联边,那么对于di+1≤j≤d,有adj(i,j)←∞,对于1≤j≤d,adj(i,j)是与结点i相邻的某一结点标号,每个顶点的邻接表存放在对应编号处理器的局\n292计算机算法基础部存储器中。其算法如下:procedureCCOMA输入:每个处理器i存放着邻接表adj(i,j),1≤j≤d输出:每个处理器i有一局部变量D(i),它是结点i所在连通分支的标识,D(i)等于i所在连通分支的最小标号结点(1≤i≤n)。1foreachi:1≤i≤npardo∥初始化,每个结点是个超顶点集∥D(i)←iendfor2forb←0tologn-1do∥循环进行logn次顶点倒塌法∥2.1foreachi:1≤i≤npardo∥C指针初始化∥c(i)←∞endfor2.2forj←1toddo∥每个结点i找邻接结点的最小超顶点集,并将此集的标号赋予i结点的C指针∥foreachi:1≤i≤npardoRAR(temp(i),D(adj(i)));iftemp(i)=D(i)thentemp(i)←∞endif;c(i)←min{c(i),temp(i)}endforrepeat2.3foreachi:1≤i≤npardo∥用D指针将某些相邻超顶点集连成串∥RAW(c(i),D(D(i)));∥D(i)是根结点∥ifD(i)=∞thenD(i)←iendififD(i)>ithenRAR(D(i),D(D(i)))endifendfor;2.4Reduce(D,n)repeatendCCOMA对于上述算法作如下说明:(1)经过执行算法的第2.3步中的子过程RAW(c(i),D(D(i)))后,对于任意一个超顶点集i,其相邻的标号最小的超顶点集D(i)的标号D(i)不一定小于i,但D(i)的相邻的标号最小的超顶点集的标号D(d(i))一定小于或等于i。因此,经过执行算法的第2.3步后,对于由D指针链接的两个超顶点集i和D(i),有i≥D(i)。(2)在算法的第2.3步中执行子过程RAW(c(i),D(D(i)))时可能存在写冲突,解决冲突的策略是把最小的c(i)值写入D(D(i))中。在给出算法的复杂性之前,首先引进一个引理,在分析复杂性时要用到它。引理11.1在n个处理器组成的二维网孔上,实现过程RAR及过程RAW需O(n)时间。证明由11.1中给出的二维网孔阵列图(见图11.4),当二维网孔由n个处理器组成时,最坏情况下两结点信息交换步数为2(n-1),(即对顶拐角上两结点的信息交换),即为\n第11章并行算法293O(n)。同样可证明对于二维网孔连接的两个变种在最坏情况下同一行上两结点信息交换步数为n-2,所以在最坏情况下两结点信息交换的步数也为O(n),因此在二维网孔上实现过程RAR及过程RAW需O(n)时间。k定理11.5已知无向图G(V,E),其中|V|=2=n,G的度数为d,那么在n个处理器组成的二维网孔上求G(V,E)的所有连通分支的CCOMA算法需O(dnlogn)时间。证明因为算法中第2.2步需O(dn)时间,而第2.2步需执行logn次,故整个算法需O(dnlogn)时间。11.5MIMD共享存储模型上的并行算法11.5.1并行求和算法假定有p个处理器,其标记为P0,P1,⋯,Pp-1,全局存储器有变量a0,a1,⋯,an-1,它们包含有待加的各数的值,全局变量g存放最终结果。算法的思路是:对于每个处理器Pi(0≤i≤p),它们各自利用其局部变量li计算ai+ai+p+ai+2p+⋯+ai+k,p(n-p≤i+kip≤n),然j后,将求得的子和加到全局变量g中。显然,此算法存在存储冲突,解决的办法是当一个处理器访问全局变量g时对其加锁,访问完毕后立即开锁,其算法如下:procedureSummation-MIMDg←0;foreachPi:0≤ix(l)thenk←k+1elseifx(j)=x(l)andj>lthenk←k+1endifendift(k)←x(j)repeatrepeatendforendENUERSORT由于每个处理机至多确定数组X中的n/p个元素位置,而每确定一个元素的位置需O(n)时间,因此对X进行排序需n/p*O(n)时间。因为进程同步开销为O(p),因此整个2并行算法的时间开销t(n)=n/p*O(n)+O(p)=O(n/p)。由于最佳串行分类算法需O(nlogn)时间,因此此并行算法的加速比为O(plogn/n)。注意:ENUERSORT算法存在读冲突,因此它是MIMD-CREW模型上开发的算法。11.5.4二次取中的并行选择算法首先引进在MIMD共享存储模型上求m个数的部分和算法。算法输入为序列A={a1,a2,⋯,am},输出为T={t1,t2,⋯,tm},其中ti=a1+a2+⋯+ai,1≤i≤m。处理机个数为m。其算法如下:procedurePS-MIMD(A,T,m)forj=0tologm-1dojfori=2+1tompardoti←aijti←ti+ai-2jendforjfori=2+1tompardoai←tiendfor\n296计算机算法基础repeatendPS-MIMD引理11.2PS-SIMD算法的复杂度为O(mlogm)。j证明显然此算法求部分和操作需O(logm)时间。对于i的每个定值有m-2个处理logm-1jj器并行操作,且同步二次,开销为O(m-2)时间,因为∑(m-2)=O(mlogm)所以算法在j=0同步上的开销为O(mlogm),由此推出算法的时间复杂度为O(mlogm)。1/2假定输入系列S={x1,x2,⋯,xn},在MIMD共享存储器模型上利用N=n个处理器求S中第k(1≤k≤n)小元素的并行算法,其非形式化描述如下:1/2(1)将S分成N段,每段至多n个元素。每段指派一个处理器,各段同时并行求出各自的中值(使用串行二次取中算法);(2)递归调用并行选择算法求出中值的中值m;iii(3)以m为划分元,并行地对各段进行划分。记i(1≤i≤N)段分成S,U,和R三个子段,它们分别由段中小于、等于和大于m的元素组成;iiiiii(4)并行求出S(1≤i≤N)中元素个数ks,U中元素个数ku和R中元素个数kr;11212N12(5)利用PS-MIMD算法,并行求出ksks+ks⋯,ks+ks+⋯+ks,并分别赋予ts,ts,N⋯,ts;11212N12(6)利用PS-MIMD算法并行求出kuku+ku,⋯,+ku+ku+⋯+ku并分别赋予tu,tu,N⋯,tu;11212N12(7)利用PS-MIMD算法并行求出krkr+kr,⋯,kr+kr⋯+kr并分别赋予tr,tr,⋯,Ntr;iii(8)根据S(1≤i≤N)的末地址ts并行地将N个S子段中元素写入S的相应位置中,记这些元素组成子段S1;iii(9)根据R(1≤i≤N)的末地址tr并行地将N个R子段中元素写入S的相应位置中,记这些元素组成子段S2;NNN(10)利用ts和tu和tr,判断第k个元素是等于m还是在S1或S2中,对于后两种情况则可对S1或S2递归调用并行选择算法。下面对二次取中并行选择算法进行算法分析。引理11.3在二次取中并行选择算法中,大于划分元素m的元素个数至多为(3/4)n;小于划分元素的元素个数至多为(3/4)n。证明因为m1,m2,⋯,mN中至少有N/2个元素大于等于m,又因为在第i段中至少2有[N/2]个元素大于等于mi;因此S中至少有(N/4)个元素大于等于m。2由此推出S中至多有n-N/4个元素小于m,即至多有(3/4)n个元素小于划分元素。同理可证明,S中至多有(3/4)n个元素大于划分元素。定理11.7在MIMD共享存储模型上,二次取中的并行选择算法在最坏情况下需O(nlogn)时间。证明记算法的运算时间为T(n),不难验证算法中第(1)、(2)、(3)、(4)、(8)、(9)步操作时间为O(n)或T(n)。由于PS-MIMD算法的运算时间为O(mlogm)(其中m为加数\n第11章并行算法297个数),因此算法中第(5)、(6)、(7)步操作时间都为O(nlogn)。由引理11.3知,算法中第(10)步的操作时间为T((3/4)n)。下面计算算法的同步开销。算法中需要同步的是第(1)、(3)、(4)、(8)、(9)步,其时间开销都为O(n)。因此,算法的运算时间T(n)满足T(n)≤cnlogn+T(n)+T((3/4)n)(11.8)下面用数学归纳法证明T(n)≤40cnlogn。4当n≤5时,可选定适当大的c使不等式T(n)≤40cnlogn成立。设5≤n≤m-1时结论成立。当n=m时,由式(11.8)得T(m)≤cmlogm+T(m)+T(3/4m)。由归纳法的1/440c(m)logm33m假设得:T(m)≤cmlogm++40cmlog(3/4m)。因为40clog(3/4)m2444<0.87×40cmlogm,又可证:当m≥5时,140c(m)4logm/2≤0.1×40cmlogm所以,T(m)m,xi∈S},然后递归调用原算法对S1和S3中元素进行排序。不难证明改进后的快速分类算法在最坏、最好和平均情况下的运算时间都是O(nlogn)。假定处理器个数N=n,在对改进的串行快速排序并行化时,可利用前面介绍的二次取中并行选择算法确定划分元m。在对二个子排序问题的递归调用上有两种方案。第一种方案是二个递归调用并行执行。由于此方案在最坏情况下所需的处理器个数P(n)=n/2+n/2>n,所以不可行。第二种方案是二个递归调用串行执行。由于每次递归调用只需要一半的处理器进行工作,使得大量的处理器无事可做,从而直接影响了算法的并行度,所以第二种方案也不理想。为了提高算法的并行度,可按下述方式解决:3次调用二次取中并行选择算法。在S中选择3个元素m1,m2和m3,它们分别是序列S中第n/4小、第n/2小和第3n/4小元素。以它们为划分元素,将S分成S1、S2、S3、S4、U1、U2和U3共7个序列,其中,S1={xi|xim3,xi∈S}U1={xi|xi=m1,xi∈S};U2={xi|xi=m2,xi∈S};U3={xi|xi=m3,xi∈S}。然后并行地递归调用原算法对S1和S2同时进行排序,接\n298计算机算法基础着并行地递归调用原算法对S3和S4同时进行排序,从而完成对序列S的排序。由于此方式在最坏情况下所需的处理器个数P(n)=max{n、2n/4}=n,所以是可行的。按照上述方式,在MIMD共享存储模型上的并行快速排序算法描述如下:procedurePQS(S)输入:S={x1,x2,⋯,xn},|S|=n输出:非降有序序列处理器数:N=n(1)若|S|<3,则调用串行排序算法对S进行排序,算法结束。(2)调用二次取中并行选择算法,确定S中第n/4小元素m1。(3)调用二次取中并行选择算法,确定S中第n/2小元素m2。(4)调用二次取中并行选择算法,确定S中第3n/4小元素m3。(5)将S分成N段,每段指定一个处理器,以m1,m2和m3为划分元素,并行地对各段进行划iiiiiii分。记第i(1≤i≤N)段分成S1、S2、S3、S4、U1、U2和U37个子段,其中iiS1={xi|xim3,xi属于第i段};iiU1={xi|xi=m1,xi属于第i段};U2={xi|xi=m2,xi属于第i段};iU3={xi|xi=m3,xi属于第i段}。iiiiiii(6)并行计算出每段中7个子段的元素个数|S1|、|S2|、|S3|、|S4|、|U1|、|U2|和|U3|,1≤i≤N。11212N(7)利用PS-MIMD算法并行求出|S1|、|S1|+|S1|,⋯,|S1|+|S1|+⋯+|S1|。iiiiii(8)类似于(7),分别求出|S2|、|S3|、|S4|、|U1|、|U2|和|U3|,1≤i≤N的部分和。i(9)根据(7)的计算结果,并行地将N个S1(1≤i≤N)中元素写入S相应的位置中,记这些元素组成子段S1。iiiiii(10)类似于(9),根据(8)的计算结果,分别把U1、S2、U2、S3、U3和S4中元素并行写入S的相应位置中,记这些元素分别组成子段U1、S2、U2、S3、U3和S4。(11)对子段S1和S2并行调用PQS算法。(12)对子段S3和S4并行调用PQS算法。endPQS记PQS算法的运行时间为T(n),不难证明当|S|≥3时T(n)满足关系式:T(n)≤cnlogn+2T(1/4n)(11.10)由此关系式可推出下面结论。定理11.8在MIMD共享存储模型上,当输入规模为n,处理器个数N=n时,PQS2算法的运算时间为O(nlogn)。证明记算法的运算时间为T(n),因此,当n<3时定理显然成立。当n≥3时,不妨设kn为4的幂,设n=4,因此T(n)≤cnlogn+2T(1/4n)22≤cnlogn+cnlog(n/4)+2T(1/4n)⋯⋯logn≤cn(logn+log(n/4)+⋯+log4)+24T(1)\n第11章并行算法299=2cn(k+(k-1)+⋯+1)+nT(1)=O(nlogn)证毕。11.5.6求最小元的并行算法这里将在MIMD共享存储模型上利用p个处理器建立求序列I={A(1),A(2),⋯,A(n)}的最小元的并行算法。算法的基本思想是,当n>p时,将序列中的元素分成元素个数基本相同的p个组,从处理器Pi(1≤i≤p)求出第i组中的最小元素,这样把求n个元素的最小问题转化为求p个元素的最小问题。当n≤p时,进行logn次迭代,每次迭代时,将元素按中心点对称的方法两两分组,求出每组的最小元,使得经过一次迭代后元素个数减少一半,最终求出最小元素。其算法如下:procedureMIMDMIN(A(1),A(2),⋯,A(n),min)输入:I={A(1),A(2),⋯,A(n)}输出:I中最小元min处理器个数:pifn>pthenforeachPi:1≤i≤ppardoforj=i+ptonsteppdoifA(j)1doforeachPi:1≤i≤k/2pardoifA(k-i+1)|AL|+|AE|如果A=AE,则返回元素m,否则按下式计算k′值:k若A′=ALk′=k-|AL|-|AE|〗若A′=AG(4)递归调用本算法,以求出A′中第k′小元素。endSELECT下面把上述串行算法并行化,产生在MIMD-CL模型上选择问题的并行算法。t首先假定处理机个数p=2-1,处理机按树形结构连接。在执行算法之前,数组A中n个元素已平均分配到p个处理机的局部内存中,在处理机Pi{1≤i≤p}的局部存储器中,存储单元mi存放局部存储器所存放的数组A中元素的个数。并行算法的非形式化描述如下:procedureMIMD-CL-PS(1)根结点通知其余节点将其所保存的元素个数送往根;其余节点向根发送各自所保存的元素p个数;根结点计算|A|=∑|mi|,如果|A|=1,根结点通知该元素所在结点将此元素送往根i=1结点,算法结束;否则就执行以下各步。(2)随机地从|A|个元素中选出一个元素m作为划分元素并送往根结点。其过程是①根结点在区间[1,|A|]中随机选择一整数j。②按先根周游次序循环地进行以下操作:结点i判定mi-j是否大于等于0,若mi-j≥0,则结点i将其保存的第j个元素送往根作为划分元素;否则j:=j-mi并将j发送给下一个结点。③根结点将划分元素m发送给其它所有结点。(3)每个结点i(1≤i≤p)将其局部存储器中的元素按m划分成ALi,AEi和AGi三个子集合,它们分别包含小于、等于和大于m的那些元素,并将|AL|,|AE|和|AG|发送给根结点。(4)根结点计算出:ppp|AL|=∑|AL||AE|=∑|AEi||AG|=∑|AGi|i=1i=1i=1若|AL||AL|+|AE|,则k←k-|AL|-|AE|,并且根结点通知每个结点i保存集合AGi中元素,并且mi←|AGi|。(5)递归调用本算法。\n第11章并行算法303endMTMD-CL-PS定理11.10在串行SELECT算法中,至多进行了O(n)次递归调用,平均递归调用O(logn)次。证明在最坏情况下,每进行一次递归调用元素个数减少一个,因此,在SELECT算法中至多递归调用O(n)次。设在SELECT算法中平均调用T(n)次。因为经过一次划分选择后,下一次递归调用所涉及的元素个数可能为0,1,2,⋯,n-1个,并且各种情况出现的概率相同,所以,平均涉及(n-1)/2个元素。由此建立T(n)的递推关系式:1+T((n-1)/2)n>1T(n)=1n=1由此不难推出T(n)=O(logn)。证毕。并行MIMD-CL-PS算法是利用并行划分来加快划分速度的,而每次划分的结果与串行SELECT算法的划分结果是一致的。因此,MIMD-CL-PS算法至多递归调用O(n)次,平均递归调用O(logn)次,由此可推出下面结论:定理11.11MIMD-CL-PS算法的通信复杂度在最坏情况下为O(np),在平均情况下为O(plogn)。证明在算法的第(1)步骤中,非根结点向根结点发送各自所保存的元素个数的过程是:首先是由叶子结点向其父结点发送,当一个结点i收到其左、右儿子发送来的值后将此二值与mi相加然后再发送给它的父结点。因此第(1)步骤的信息交换数为O(p)。类似地可推出第(2)、第(3)、第(4)步骤的信息交换数都为O(p)。由于MIMD-CL-PS算法在最坏情况下要运行O(n)遍,在平均情况下要运行O(logn)遍,因此算法的通信复杂度在最坏情况下为O(pn),在平均情况下为O(plogn)。证毕。2定理11.12在最坏情况下MIMD-CL-PS算法的运算时间为O(n/p+nlogp)。证明不难验证,算法的最坏情况是:每递归调用一次,数组A中元素个数仅减少一个,并且每连续n/p次递归调用后,仅使一个处理机保存的元素个数为零。因此,在最坏情况下算法要运行O(n)遍,在每一遍中划分操作时间为O(n/p)。由此推出整个算法的操作时22间为O(n/p)。因为二元树的深度为O(logp),所以在最坏情况下算法的运算时间为O(n/p+nlogp)。证毕。11.6.2求极值问题的并行算法令A={a1,a2,⋯,an},求集合A中元素的极值分为求极大值和最小值。不失一般性,仅考虑求极大值。在MIMD异步通信模型上求极值的并行算法与网络的拓扑结构密切相关,这里首先介绍在树形网络结构上的求极大值并行算法。假定有p个处理器,它们以二叉树的方式彼此相连,处理器P1为树的根。集合A中n个元素被分成大小大致相等的p个集合:A1,A2,⋯,Ap并假定子集合Ai(1≤i≤p)中的元素已存入处理器Pi中的局部存储器中。算法的基本思想是,首先根结点P1通知其余结点对各自局部存储器中的元素求极大值,然后根结点P1求出A1中的极大值。当其余结点求出它们各自保存的元素的极大值后,由叶子结点开始向父结点送其极值,当父结点收到左、右儿子的极值后与其本身极值进行比较,取最大者为它的极值,并将此值发送给它们的父结点,当根结点收到左、右儿子的极值\n304计算机算法基础后,通过比较,就产生了集合A中的极值。其算法如下:procedureTCMAX根结点P1的算法:send(m)messagetoL(P1);∥向其左儿子发送消息m∥send(m)messagetoR(P1);∥向其右儿子发送消息m∥callMax(A1,t(1));∥求集合A1中的极大值并赋予t(1)∥uponreceiving(M)fromsondo:ifM>t(1)thent(1)←Mendifuponreceiving(M)fromsondo:ifM>t(1)thent(1)←Mendif非根结点Pi的算法:uponreceiving(m)fromfathendo:ifPi不是叶子thensend(m)messagetoL(Pi);send(m)messagetoR(Pi);endifCallMax(Ai,t(i));ifPi是叶子thensend(M)messagetofarther;∥向其父发极大值信息M(=t(i))∥elseuponreceiving(M)fromsondo:ifM>t(i)thent(i)←Mendif;uponreceiving(M)fromsondo;ifM>t(i)thent(i)←Mendif;send(M)messagetofather∥向其父发极大值信息M(=t(i))∥endif;TCMAXend对于TCMAX算法,必须假定处理器的个数为2的幂减1,因为,此假定保证了树中每个非叶子结点都有两个儿子。当处理器个数不满足上述假定时,须对算法作适当的修改。如何修改留作习题。下面对TCMAX算法进行复杂性分析。定理11.12在具有p个处理机器的树形网络结构上,对于集合A={a1,a2,⋯,an},求其元素极大值的TCMAX算法的通信复杂性为O(p),时间复杂性为O(n/p+logp)。证明在整个算法中,结点间进行通信的信息有两类,即通知儿子进行求极大值操作的信息m和传送子树中元素极大值的信息M。这两类信息的信息交换次数都等于树的边数,因此算法的通信复杂性为O(p)。对于每个处理器,它们求出各自局部存储器中元素极大值的操作时间为O(n/p)。因为通信树的深度为O(logp),因此不难推出通信时间为O(logp),最后得到算法的时间复杂性为O(n/p)+O(logp)=O(n/p+logp)。下面介绍在环形网络拓扑结构中求极大值的并行算法。假定在MIMD-CL计算模型\n第11章并行算法305中,p个处理器松散耦合成一个环,集合A={a1,a2,⋯,an}中的元素已分散存放在p个处理器的局部存储器中,各个处理器存储的元素个数大致相等,每个处理器只能和其近邻交换信息,没有中心控制器存在。在环形结构中求集合A中元素的极大值算法可分为两大类:在第一类算法中,每个处理器可以向其左、右近邻发送和接收信息,在第二类算法中,每个处理器只能向其左近邻发送信息和从其右近邻接收信息。这里,我们仅介绍第二类算法。[12]下面给出的RLMAX算法是基于Chang和Roberts提出的一种改进的单向算法的设计思想,但必须增加一个限制条件,限定集合A中无相同元素。算法的非形式化描述如下。procedureRLMAX(1)每个结点Pi(1≤i≤p)求出其局部存储器中所保存的元素的极大值Mi。(2)每个结点Pi将Mi传给其左邻结点。(3)每个结点Pi收到来自其右邻结点的信息后进行下列操作:①当Mi小于收到的信息时,Pi将收到的信息传给其左邻结点;②当Mi大于收到的信息时,则将收到的信息丢弃;③当Mi等于收到的信息时,则Mi就是集合A中元素的极大值。endRLMAX根据环形网的结构性质、单向传递的约定和对集合A的限制条件,任何信息在返回到产生它的结点之前,必须经过其它所有结点,因此,当且仅当A中元素的最大值作为信息时,此信息才能返回到产生它的结点。由此可推出算法是正确的。关于RLMAX算法的时间复杂性和通信复杂性,有如下结论:定理11.13RLMAX算法的运算时间为O(p+n/p)。证明因为此算法的运算时间由通信时间和每个处理器求部分元素的极大值操作时间所决定,又由于p个处理器同时启动,所以通信时间是A中极大值通过环网一周的时间,即为O(p),由于求部分元素极大值操作的时间为O(n/p),所以整个算法的运算时间为O(p+n/p)。证毕。定理11.14RLMAX算法的通信复杂性在最好情况下为O(p),在最坏情况下为2O(p),在平均情况下为O(plogp)。证明因为最好情况是,除了极大值外,每个信息只传递一次,即沿传递方向各结点的信息值按由小到大的次序排列,所以总的信息传递数为:p+p-1=2p-1,即为O(p)。最坏情况是沿传递方向各结点的信息值按由大到小的次序排列。不妨设M1>M2>⋯p2>Mp,因此信息Mi传递的次数为p-i+1,所以总的信息传递次数为∑(p-i+1)=O(p)。i=1最后求在平均情况下信息传递次数。令P(gi,k)为第i小信息gi须传递k次的概率,因此它就是沿传递方向有连续k-1个近邻的部分极大值小于gi,同时gi的第k个近邻的部分极大值大于gi的概率。因为部分极大值小于gi的结点数为i-1,大于gi的节结点数为p-i,所以i-1k-1p-iP(gi,k)=·p-1p-kk-1\n306计算机算法基础因为信息为极大值时,它传递p次,所以只考虑p-1个信息。又由于它们每一个至多传递p-1次,所以第i小信息gi被传递的次数的数学期望为p-1Egi=∑k·P(gi,k)k=1对于所有的信息,其期望的传递数为p-1p-1E=p+∑∑k·P(gi,k)i=1k=1111=p(1+++⋯+)23p其中,调和级数的部分和为c+logp,所以信息平均传递数为O(plogp)。证毕。如果去掉对集合A的限制条件,显然RLMAX算法是不正确的。对于集合A中存在相同元素的情况,如何求A中元素的极大值,则留作习题。11.6.3网络生成树的并行算法对于MIMD异步通信计算模型,其通信网络可以看作是一个无向连通图。在此模型中,从某个结点把一个消息广播到整个网络中,有两种常见的方法:一种称之为洪水淹没法,这种方法的通信开销非常大;另一种是基于网络的一棵生成树进行广播。显然,它的信息交换次数等于网络中结点个数减去1。不难证明,这是网络中进行广播的最小通信开销,因此,该方法是网络中进行信息传递的好方法。利用网络的生成树进行广播,必须首先在网络中建立一棵网络生成树。由此可见,建立网络生成树的问题是MIMD异步通信计算模型中的一个极其重要而又非常基本的问题。求网络生成树的并行算法的基本思想是:首先在网络中任意选定一结点s作为树的根,然后s向它的邻接结点发送访问消息“V”。当一个结点m首次收到消息“V”后,把发送者当作自己的父结点F(m);若一个已访问过的结点收到消息“V”后,就给发送者回送一个应答消息“A”。同时,若m除父结点外还有其它邻接结点,则向这些邻接结点发送消息“V”。若m没有其它的邻接结点,则向父结点回送应答消息“R”。当根结点收到它所有邻接结点的应答消息后,向所有儿子结点发送消息“T”并终止其算法的执行。一个结点收到消息“T”后,若其是非叶子结点,则向儿子结点发送消息“T”并终止其算法;若是叶子结点,则终止其算法。算法的非形式化描述如下:procedureNST根结点s的算法:(1)向它的所有邻接结点发送消息“V”;(2)当收到消息“V”后,向发送者发应答消息“A”;(3)当收到来自某一邻接结点m的消息“R”后,则把结点m当其子结点即:S(s)∪{m}→S(s);∥S(s)是结点s的一个信息域,用以保存其子结点名∥(4)当收到全部邻接结点回送的消息“A”或消息“R”后,则向全部邻接结点发送消息“T”并且结束算法。非根结点t的算法:(1)当收到某一邻接结点m发送的消息“V”后,则\n第11章并行算法307①若结点t首次收到消息“V”,则认结点m为其父结点,并且在结点t的邻接表中去掉结点m。然后判定邻接表是否为空,若为空则向父结点m发消息“R”;若不为空,则向表中结点发消息“V”;②若结点t不是首次收到消息“V”,则向结点m发消息“A”。(2)当收到某一邻接结点m发送的消息“R”后,则①认m为其子结点;②若收到了它的全部邻接结点(除父结点外)发送的消息“A”或消息“R”,则向父结点发送消息“R”。(3)当收到某一邻接结点m发送的消息“A”后,结点t检查它的所有邻接结点(除父结点外)是否都已向t发送了消息“A”或“R”,若是,则向其父结点发送消息“R”。(4)当收到某一邻接结点m发送的消息“T”后,则结点t向其子结点发送消息“T”然后结束其算法。endNST算法正确性的简要说明:证明NST算法的正确性,关键是要验证下面4点:(1)对于除根结点s外,所有结点有且仅有唯一父结点;(2)算法能判定出叶子结点;(3)每个结点上的算法都能结束;(4)算法的认子结点过程正确。对于第(1)点,由于消息“V”从结点s开始,传遍所有结点,并且任一结点(不等于)只有当它首次收到消息“V”后才认父,所以除结点s外,每个结点有且仅有唯一父结点。对于第(2)点,算法是根据一结点t能否收到其全部邻接结点(除父结点外)发送的“A”信息来判别结点t是否是叶子,不难验证,这一条件正是判定一结点是否是叶子结点的充要条件,因此,算法能正确判定出叶子结点。对于第(3)点,任意结点t上的算法是否结束是根据以t为根的子树是否已完全形成来判定的。在算法中,一旦判定一结点是叶子,就可结束此结点上的算法,当一结点t为根的子树形成后,就向其父发送消息“R”,因此,任意一个结点,当收到它的全部邻接结点(除父结点外)发送来的消息“R”或消息“A”后,说明此结点为根的子树已完全形成。这里,消息“R”是由底向上,一级级传递的,当根结点s判定出树已形成后,再由顶向下发送消息“T”,通知各个结点结束其算法,因此每个结点上的算法都能结束;对于第(4)点,算法是根据消息“R”来认儿子的,由第(3)点的验证过程,不难看出算法的认儿过程是正确的。关于NST算法的通信复杂性和时间复杂性,有如下结论。定理11.15NST算法的通信复杂性为O(m),其中m为网络中通信链的条数。证明对于网络中每条通信链,不难验证消息“V”至多通过二次;消息“R”至多通过一次;消息“A”至多通过二次;消息“T”至多通过一次。因此,整个算法至多发送6m条消息,即算法的通信复杂性为O(m)。证毕。定理11.16NST算法的时间复杂性为O(n),其中n为网络中结点的个数。证明由算法本身可以看出,其操作时间由发送消息“V”“,R”“,A”和“T”的时间和判别邻接表中所有结点(除父结点外)是否都发送来消息“R”和“A”的时间开销所决定。因为网络中每个结点的度数有限,因此算法的操作时间为O(1)。设从起点到最远距离的结点间的长度为l,则算法的通信时间为O(l),因为在最坏情况下l=n,因此算法的时间复杂度为O(n)。证毕。\n308计算机算法基础习题十一11.1当处理机个数N不为2的幂时须对BROADCAST算法进行修改,证明修改后算法的运算时间和性能保持不变。11.2用形式化描述方式设计在SIMD-CREW模型上的并行归并分类算法。11.3分析在SIMD-EREW模型上的并行归并分类算法的运算时间及算法性能。11.4在SIMD共享存储模型上将串行插入分类算法并行化,并进行算法分析。11.5指出在Connected-Components算法中哪些语句存在读冲突。11.6在SIMD-CREW模型上设计连通图的宽度优先并行搜索算法,并进行算法分析。11.7在SIMD一维线性模型上设计几个数的并行求和算法,并进行算法分析。11.8在SIMD超立方模型上设计并行求极值算法,并分析算法的运算时间和性能。11.9设A,B分别是m×k和k×n矩阵,在MIMD共享存储模型上设计求矩阵C=A×B的并行算法,并对算法进行算法分析。11.10对于S={18,35,21,24,29,13,33,17,31,27,15,28,11,22,19,25,34,32,16,12,23,30,26,14,20},假定N=5,k=6,请用图表示二次取中并行选择算法的执行过程。11.11在PQS算法中,证明当n≥3时,其运算时间T(n)满足:nT(n)≤Cnlogn+2T4其中n是元素个数,C是一个常数。11.12对于序列S={20,15,24,11,17,22,13,19,16,25,12,21,26,18,23,14},模拟PQS算法的执行过程。11.13修改TCMAX算法,使之适合于一般的树形网络结构模型,并对修改后的算法进行性能分析。11.14当集合A中容许有相同元素时,编写在环形结构中求集合A中元素极大值的单向传递算法,并分析算法的时间复杂性和通信复杂性。\n参考文献1EHorowitz,SSahni.FundamentalsofComputerAlgorithms.NewYork:ComputerSciencePress,19782DEKnuth.TheArtofComputerProgramming,Vo13.London:Addison-WesleyPublishingCompany,19733SEGoodman.S.T.Hedetniemi.IntroductiontoTheDesignandAnalysisofAlgorithms.NewYork:McGraw-HillPublishingCompany19774A.V.Aho,JEHopcroft,JDUllman.TheDesignandAnalysisofComputerAlgorithms.London:Addi-son-WesleyPublishingCompany,19745游兆永.线性代数与多项式的快速算法.上海:上海科学技术出版社,19806洪帆.离散数学基础.武汉:华中工学院出版社,19837马仲蕃,魏权龄,赖炎连.数学规划讲义.北京:中国人民大学出版社,19818E.Horowitz.S.Sahni.FundamentalsofDataStructures.NewYork:ComputerSciencePress,Pitman,19769SaraBaase.ComputerAlgorithms.IntroductiontoDesignandAnalysis.London:Addison-WesleyPub-lishingCompany,197810K.Hwang,F.A.Briggs.ComputerArchitectureandParallelProcessingNewYork:McGraw-Hill,PublishingCompany,198411N.Deo,C.Y.Pang,R.E.Lord.TwoParallelAlgorithmsforShortestPathProblems.Int'lConf.onParallelProcessing.1980.244~25312E.F.Moore.TheShortestPathThroughaMaze.Proc.Int’lSymp.onTheoryofSwitching,2,1959,285~29213E.Chang,R.Roberts.AnImprovedAlgorithmforDecentralizedExtrema-FindinginCircularConfigu-rationsofProcesses,Cornm.ACM,22(5),1979,281~28314陈国良.并行算法———排序和选择.合肥:中国科学技术大学出版社,199015唐策善,梁维发.并行图论算法.合肥:中国科学技术大学出版社,1991

相关文档