刘汝佳，1982年12月生，高中毕业于重庆市外国语学校。

2000年3月获得NOI2000全国青少年信息学奥林匹克竞赛一等奖第四名，进入国家集训队，并因此保送到清华大学计算机科学与技术系。大一时获2001年ACM/ICPC国际大学生程序设计竞赛亚洲-上海赛区冠军和2002年世界总决赛银牌（世界第四），2005年获学士学位，2008年获硕士学位。

学生时代曾为中国计算机学会NOI科学委员会学生委员，担任IOI2002-2008中国国家队教练，并为NOI系列比赛命题十余道。现为NOI竞赛委员会委员，并在NOI 25周年时获得中国计算机学会颁发的“特别贡献奖”。

2004年至今共为ACM/ICPC亚洲赛区命题二十余道，担任6次裁判和2次命题总监，并应邀参加IOI和ACM/ICPC相关国际研讨会，发表论文两篇。

2004年初作为第一作者出版专著《算法艺术与信息学竞赛》，2009年出版译著《编程挑战》。

多年来在全国二十余个城市进行中学生竞赛培训工作，为北京、上海、吉隆坡等地的著名高校授课与宣讲，并多次与TopCoder、百度和网易有道等知名企业合作举办比赛，让更多的IT人才获得展示自我的平台。

陈锋，1982年9月生。毕业于华北水利水电学院机械设计专业。曾就职于微软全球技术支持中心，负责.net虚拟机以及Visual Studio开发技术支持。后进入金融IT行业，专注于银行网点平台的产品研发，曾分别负责基于.net和Eclipse的两代网点平台产品的开发以及架构设计。现就职于北京宇信易诚科技，任前端产品技术经理及架构师。

算法艺术与信息学竞赛

算法竞赛入门经典——训练指南

刘汝佳　陈　锋　编著

清华大学出版社

北　京

内容简介

本书是《算法竞赛入门经典》的重要补充，旨在补充原书中没有涉及或者讲解得不够详细的内容，从而构建一个较完整的知识体系，并且用大量有针对性的题目，让抽象复杂的算法和数学具体化、实用化。

本书共6章，分别为算法设计基础、数学基础、实用数据结构、几何问题、图论算法与模型和更多算法专题，全书通过近200道例题深入浅出地介绍了上述领域的各个知识点、经典思维方式以及程序实现的常见方法和技巧，并在章末和附录中给出了丰富的分类习题，供读者查漏补缺和强化学习效果。

本书题目多选自近年来ACM/ICPC区域赛和总决赛真题，内容全面，信息量大，覆盖了常见算法竞赛中的大多数细分知识点。书中还给出了所有重要的经典算法的完整程序，以及重要例题的核心代码，既适合选手自学，也方便教练组织学习和训练。

本书封面贴有清华大学出版社防伪标签，无标签者不得销售。

版权所有，侵权必究。侵权举报电话：010-62782989　13701121933

图书在版编目（CIP）数据

算法竞赛入门经典——训练指南／刘汝佳，陈锋编著．—北京：清华大学出版社，2012.9

（算法艺术与信息学竞赛）

ISBN 978-7-302-29107-7

I．①算…　II．①刘…②陈…　III．①电子计算机-算法理论-教学参考资料②C语言-程序设计-教学参考资料　IV．①TP301.6②TP312

中国版本图书馆CIP数据核字（2012）第130853号

责任编辑：朱英彪

封面设计：刘　超

版式设计：文森时代

责任校对：张兴旺

责任印制：

出版发行：清华大学出版社

　　　　　网　　址：http://www.tup.com.cn，http://www.wqbook.com

　　　　　地　　址：北京清华大学学研大厦A座

　　　　　邮　　编：100084

　　　　　社总机：　010-62770175

　　　　　邮　　购：010-62786544

　　　　　投稿与读者服务：010-62776969，c-service@tup.tsinghua.edu.cn

　　　　　质量反馈：010-62772015，zhiliang@tup.tsinghua.edu.cn

印刷者：

装订者：

经　　销：全国新华书店

开　　本：185mm×260mm

印　　张：33

字　　数：762千字

版　　次：2012年9月第1版

印　　次：2012年9月第1次印刷

印　　数：1～5000

定　　价：52.80元

产品编号：041622-01

序

翻了翻UVa（Valladolid大学）在线评测系统的数据库，关于汝佳的第一条记录是在2000年11月15日，UTC时间是13点58分00秒——他在那时刚刚注册。这个时间在中国已经不算早了，但对于汝佳来说这完全不是问题，因为据我所知他随时都在工作。在那个即将结束的一天中，他提交了10道题目的22份C++代码，其中第一份代码是在注册后的短短数分钟之内提交的，并且获得了AC（正确），通过了题目264（Count on Cantor）。

当然，他也会犯一些错，不过当天结束时他已经搞定了3道题目，并且第二天就解决了剩下的全部题目。在年底之前（其实只有45天），汝佳提交了90道题目的307份代码，其中通过了59道。那个时候他还很年轻（差不多就是个孩子，就像他现在的模样），但他的思路已经很清晰了——那就是勤加练习的结果。

尽管上面讲的这些已经是陈年旧事了，但从中我可以断定：你们手中的这本书是一个非比寻常的伴侣，将带领你们走进计算机编程世界，尤其是编程竞赛世界。遗憾的是，书是用中文写成的，因此唯有懂中文的人才能享受到汝佳创造的这份魔法。幸运的是，他用UVa在线评测系统作为书中题目的来源，这使得我们这些不懂中文的人也可以透过题目了解到本书的知识体系和教学计划。他的这一举措是我们的荣幸，因为这本书给UVa网站增加了一份特殊的价值。汝佳很爱学习，也很乐意把自身所学倾囊相授。有了这本书，我们的工作有了更大的意义。

对我们来说，这也是一项挑战，因为我们必须更加努力地完善评测系统和网站，以回馈那些因为本书而对UVa网站产生兴趣的读者。我们也乐于接受这个挑战，因为我们很高兴看到UVa网站除了可以用来备战竞赛之外，还可以用来帮助人们学习算法。事实上，我们已经开始着手开发一个新的评测系统，挖掘学生群体中新的需求。我们希望能有更多的人参与，比如你。

最后，我想向所有的中国读者表达一份特殊的欢迎——欢迎你们通过本书认识和了解UVa在线评测系统（如果能顺便了解一下Valladolid这座城市就更好了），同时感谢汝佳邀请我用这些平凡的文字为这本不平凡的书作序。

Miguel A. Revilla

UVa在线评测系统创始人

ACM-ICPC国际指导委员会成员、题目归档专家

Valladolid大学／西班牙

http://uva.onlinejudge.org

http://livearchive.onlinejudge.org

前　言

“请问新书《算法实践手册》什么时候出版？望眼欲穿啊……”

自《算法竞赛入门经典》（以下简称《入门经典》）出版以来，我收到的这样的来信已经不计其数。

不过，我心里有着自己的打算。《入门经典》的出版固然为广大算法爱好者提供了一些帮助，但其中的缺憾也是很明显的，如例题太少，习题没有中文翻译，而且限于篇幅，基础知识还没讲完……这样看来，《算法实践手册》的出版时机尚未成熟，还需要一本书来铺垫，弥补上述缺憾。可惜的是，由于创业的繁忙，这个想法一直未能实现。

2010年8月底，我收到了一封读者的E-mail，和我探讨《入门经典》中的一些问题，从此和本书的第二位作者陈锋相识。我万万没有想到，这位来自银行业的软件工程师、产品架构师学习编程的时间只有两三年，他对算法的热爱、严谨求实的态度和认真刻苦的专业精神绝不亚于有着多年算法和软件工程经验的行家。在与陈锋的交流过程中，我重新开始了对这本新书的构思。

事实上，在《入门经典》的写作过程中，完成的书稿远远不止印刷出来的220多页，只是因为篇幅和内容限制没有用到该书中。如果好好地把这些书稿加以整理，再算上笔者多年来外出讲课时制作的课件、题目翻译，那么本书的轮廓已经呼之欲出。这些东西对我来说已经是老生常谈，但接触算法不久的陈锋却觉得很新鲜。这样，我萌生出了一个有趣的念头：和陈锋一起合著一本书，我提供资料和总揽全局，而陈锋一边学习以前没有接触过的新知识，一边把这些东西按照适合初学者的方式重新进行组织和细化，并以“没参加过算法竞赛的软件工程师”这样一个独特的视角提出各种意见和建议。

细水长流了一年多之后，这个构想终于成为了现实。虽然大量的业余时间都奉献给了这本书，但我相信这是值得的。

参与本书编写和校对工作的还有中国人民大学的陈卓华和陈怡、北京大学的鲍由之（绘制了书中的几乎全部插图）等；一位不愿透露姓名的台湾朋友阅读了几乎全部书稿并给出了非常详细的修改意见。为了更好地配合本书，我在UVa上举办了3场专题比赛（数据结构、几何、实用程序），并将其中的典型题目收录在了正文例题或者习题当中。由于题目难度颇大，如果没有李益明、梁盾、沈业基、李耀、周而进、陈卓华、陈怡、唐迪、李晔晨、肖刘明镜、鲍由之等朋友的鼎力相助，这些比赛几乎不可能取得圆满成功。

另外，我还要感谢CCF（中国计算机学会）的杜子德秘书长、吴文虎教授、王宏教授等，还有NOI科学委员会及竞赛委员会的专家们，以及ACM/ICPC亚洲区主席黄金雄教授和中国指导委员会秘书长周维民教授。他们都是我的良师益友，在我接触NOI和ACM/ICPC以来的十多年里让我学到了很多东西。

感谢清华大学出版社辛勤劳动的编辑们，尤其是与我合作多年的朱英彪老师，在本丛书的编写、出版和推广方面都做了大量的工作，是一位真心实意为读者着想的好编辑。

最后，感谢我的爸爸妈妈。不管我做什么，不管别人怎么说怎么看，你们总是那样的支持我，让我可以全身心的投入自己喜爱的事业，没有半点后顾之忧。你们教给我的善良、感恩和奉献，正是我多年来坚持写书的源动力。

刘汝佳

许多计算机相关专业的人在毕业之后除了为应付面试外基本都很少再去碰算法，而在实际的产品或者项目开发过程中，大多数人也没有必要亲自去实现复杂的算法。因此，算法渐渐淡出程序员的日常生活。同时，在现实生活中有另外一种声音：程序员的生活太纠结，coding的速度永远跟不上需求变化的速度，提需求的客户似乎成了程序员的“天敌”，成了他们“苦逼”生活的罪魁祸首。

那么，一本讲算法比赛的书跟这又有多少关系？就从我自身的经历说起吧。我不是计算机科班出身，但因种种原因进入了这个行业，而且是从一个很低的起点进入。于是我像所有人一样，平时很难静下心来学习算法，有的面试就去临时抱本书突击一下。终于有一天我受不了这种循环，问自己难道一定是为了一个急功近利的目的才去付出自己的时间吗？佛家有句话叫“凡夫求果，菩萨求因”，我就想，既然成不了圣人，就学一回圣人吧。

因缘际会，我接触到了《入门经典》及其作者刘汝佳，于是一发不可收拾，写这本书的过程也变成了修行与学习的过程。慢慢地，我发现算法对于实际工作的人而言，有着比应付面试更大的价值。所谓的算法、组件、模式，就像是一些基础的原材料，对于优秀的建筑师来说，需要透彻的理解（不一定写得很熟练）它们的关键性。因为一个错误的设计，对于系统来说，所要付出的代价远比一般的程序bug要高得多。更进一步说，现在做软件的为啥苦，为啥抱怨需求变化快？因为解决问题的思维有偏差。需求分析绝对不是简单地拿着需求，直接翻译成代码——这是最低层次的。算法分析的意义，更多地不在于性能，不在于那些脑筋急转弯，而在于发现纷繁复杂的问题背后的不变式，而这正是本书要着力与大家分享的。

没有大家的支持和帮助，这本书几乎是不可能写好的。尤其要感谢我的家人和朋友，为了这本书的写作，他们投入了大量的时间和精力。这里尤其是在我工作非常繁忙的情况下，写作占用了大量跟家人在一起的时间，没有妻子梁明珠和女儿婉之的支持，我几乎不可能集中精力参与本书的写作过程。

另外，也感谢我的同事徐海波、张大用、周洲、王洪桥、朱宗耀、朱洁晨等，他们不仅在工作上给了我非常大的支持和帮助，也对我参与写作的章节提出了很多非常好的建议。

陈锋

声明：书中用到了大量的例题和习题，感谢这些命题者和竞赛组织方的辛勤劳动。笔者已经尽可能地找到他们并征求了题目的使用权，但如果你认为本书侵犯了你的权益，请与出版社和作者取得联系。在UVa网站上可以找到本书大多数例题／习题的作者。

阅读说明

如何阅读本书

欢迎大家阅读本书！为了最大限度地发挥本书的作用，强烈建议您在阅读正文之前仔细地读完《阅读说明》，还要时不时地在学习过程中重读这些文字，以确保自己不会因为纠缠于书中的一些细节而变得舍本逐末，甚至偏离了预定的学习航线。切记！

“覆盖面广，点到为止，注重代码”是本书的最大特点，而这3个特点都是为了向业界靠拢，注重广度而非深度。为了方便练习，书中所有题目来自于UVa在线评测系统和ACM/ICPC真题库（Live Archive, LA）。虽然这两个题库的题目不能包罗万象，但对于“点到为止”来说绰绰有余。书中的代码更多是为了提供一个容易理解、适合比赛的参考实例，并不推荐读者直接使用甚至背诵它们（关于这一点，在稍后还会有详细叙述）。

关于知识点

本书不是一本算法竞赛入门类图书，因此并不从程序设计语言、算法的概念、基础数据结构、渐进时间复杂度分析这些内容讲起。如果你是一个新手，建议先阅读《算法竞赛入门经典》（和本书搭配着阅读也可以）。

聪明的选手都是善于利用网络资源的，如在网上交流讨论、阅读牛人的博客，寻找解题报告和测试数据等。这些方法也是阅读本书的重要辅助手段。由于算法竞赛涉及的知识点非常广，并且读者所具备的知识水平参差不齐[1]，因此，不同读者在阅读本书时会遇到不同的困难。这些困难有时并不是读者的问题，而且也无法靠修改书稿来避免[2]，因此，最好的办法就是上网搜索！

本书是完全的题目导向，掌握了书中的所有例题，也就掌握了书中的主要知识点和方法、技巧。在习题中也有少量相对不那么重要、例题没有涉及的东西，在每章的小结部分会明确指出。换句话说，对于每个知识点，最好的方法是结合例题分析和代码去理解，上机实践，最终达到能独立解决同类问题的目的。

本书的学习顺序不是单一的，但最好先阅读第1章，因为其中不少思想和编码技巧适用于全书。接下来，数学（第2章）、数据结构（第3章）、几何（第4章）、图论（第5章）等内容可以根据读者需要按任意顺序进行学习。事实上，笔者建议大家先学习这些章节的重要内容，等对全书有了一个整体认识之后再精读精练，千万不要过早地陷入难懂的细节（每章都有一些相对难懂的内容）。第6章主要是一些零散的知识点和技巧、方法，可以与其他章节穿插阅读。

请重视每章后面的小结与习题部分。虽然每章前面的文字一视同仁地对所有内容进行了逐一讲解，但在算法竞赛中，这些内容并不是同等重要的，思维训练和编程训练的内容和比重也不尽相同。当你不知道接下来应该学些什么、练些什么时，这些内容会是你最好的帮手。比如，如果关于某个知识点的习题特别少，这通常意味着该知识点很少在竞赛中出现，或者只需要很少的练习就能掌握到其精髓[3]。

如何做题

书中的重要题目（包括例题和习题）均配有完整代码（限于篇幅，这些代码不一定会在书中出现）和测试数据，以方便读者学习，而其他题目也大都附有中文翻译，并且指明了题号、提交人数和通过比例等统计数据，以供读者训练参考。

书中题目数量不少，因此做题时必然要有一个先后顺序，建议初学者优先解决那些通过人数较多的题目，而已经开始专项训练的读者则可以根据需要选择难一些的题目。

考虑到很多读者并不是“久经沙场”的老手，这里以“蚂蚁”例题为例介绍做题的一般步骤。

蚂蚁（Piotr's Ants, UVa 10881）

一根长度为L厘米的木棍上有n只蚂蚁，每只蚂蚁要么朝左爬，要么朝右爬，速度为1厘米／秒。当两只蚂蚁相撞时，二者同时掉头（掉头时间忽略不计）。给出每只蚂蚁的初始位置和朝向，计算T秒之后每只蚂蚁的位置。

【输入格式】

输入第一行为数据组数。每组数据的第一行为3个正整数L，T，n（0≤n≤10000），以下n行每行描述一只蚂蚁的初始位置。其中，整数x为它距离木棍左端的距离（单位：厘米），字母表示初始朝向（L表示朝左，R表示朝右）。

【输出格式】

对于每组数据，输出n行，按输入顺序给出每只蚂蚁的位置和朝向（Turning表示正在碰撞）。在第T秒之前已经掉下木棍的蚂蚁（正好爬到木棍边缘的不算）输出Fell off。

【样例输入】

2

10 1 4

1 R

5 R

3 L

10 R

10 2 3

4 R

5 L

8 R

【样例输出】

Case #1:

2 Turning

6 R

2 Turning

Fell off

Case #2:

3 L

6 R

10 R

这是第1章中的例题5，是一道很有意思的题目。题目正文只有3句话，并不难理解，但正文并不是题目的全部。可以看到，一道完整的题目至少包含3个部分：题目描述、输入输出格式和样例输入输出，缺一不可[4]。

题目描述可以让你了解到你需要解决一个什么样的问题，但有些细节可能不会涉及。在第一次阅读时可以忽略掉不明白的地方，直接看输入输出格式，以了解具体的输入输出方法。对照输入输出格式和题目描述，可以更清楚地知道这道题目已知什么，要求什么。

输入输出格式往往有规律可循。例如，本书的题目全部采用ACM/ICPC比赛题目的格式，采用多数据输入输出。最常见的格式有以下两种。

格式一：输入第一行包含数据组数T。每组数据的第一行包含……主程序通常这样编写：

格式二：输入包含多组数据。每组数据的第一行包含一个整数n，输入结束标志为n＝0。主程序通常这样编写：

注意，上述代码用到了scanf的返回值，它返回的是成功读取的元素数目。换句话说，即使真实数据的最后并没有n＝0的“标志”，上述程序也会在文件结束后退出主循环。当然，正常情况下命题者不会忘记在数据末尾加上这个“标志”。但人非圣贤，孰能无过，即使在ACM/ICPC世界总决赛这样的重量级比赛中，也曾出现过数据不符合题目描述的情况。因此，作为选手来说，最好的方法是编写一个尽量鲁棒的程序，即使在数据有瑕疵的情况下仍然能够通过数据[5]。

输入输出格式的另一个作用是告知数据范围和约束。比如上述题目中，“n≤10000”这个约束实际上是在警告选手：O（n²）时间复杂度的算法也许会超时[6]。另一些重要的约束包括“输出保证不超过64位无符号整数的范围”、“输入文件大小不超过8MB”。前者通常意味着我们不必使用高精度整数[7]，而后者通常意味着我们需要注意输入数据的时间，不要采用太慢的输入方法（这个问题会在第1章中详细讨论）。

在很多题目中，正确的输出并不是唯一的。在这种情况下，题目要么会明确指出多解时任意输出一个解即可（此时会有一个称为special judge的程序来判断你的解是否正确），要么会告诉你输出哪一组解，以确保输出的唯一性。多数情况都是输出字典序最小或者最大的解。

看完题目描述和输入输出格式之后，还有一件重要的事要做，那就是阅读样例。很多题目的样例都是“可读”的，即至少包含一个简单到能够手算出来的例子。在这种情况下，强烈建议读者手算一下这个样例，因为这不仅可以帮助你理解题意、消除潜在的歧义，还能启发你的思路，使你从手算的过程中概括整理出本题的通用解法。如果有疑问，还可以很方便地向主办方提出[8]。

接下来，就可以设计算法并编写程序了。如何设计算法？如何编写程序？这正是本书的主题，所以在这里不再赘述，假定你已经顺利地写完程序。

接下来应当测试，看看你的程序是不是正确。编程的一大特点是“失之毫厘，谬以千里”，因此，哪怕只是写错了一个运算符，程序的结果也可能完全不对。所以，在提交程序之前应当好好测试一下。测试什么数据呢？最现成的当然就是样例了，但即使这样也远远不够。很多时候，样例并不具有典型性，通过样例也许只是碰巧；如果题目很难，就算样例很典型，也未必能覆盖到所有情况。也许你的程序几乎是完美的，但在一些特殊情况下也会出错，而样例并不包含这些特殊情况。因此，进行全面的测试是很有必要的。

一旦测试出错，就需要调试（debug）你的程序。调试的方法有很多种，如跟踪调试，即利用IDE的单步、断点、watch等功能，动态地检查程序的执行过程是否和预想中的一样。这样的技巧固然有用，但在算法竞赛中更常见的还是“断言＋输出中间结果”的方法，这在《算法竞赛入门经典》中已有详细叙述。需要特别注意的是，修改程序后最好测试一下以前测过的数据，以免“拆东墙补西墙”，修改了老bug的同时又引入了新bug。

不管是参加什么样的比赛，都有一个“提交代码”的过程[9]。为了尽可能地避免一些“非正常因素”的干扰，进行一些提交前的检查还是很有必要的。主要包括检查输入输出渠道是否正确（比如，文件输入还是标准输入，有没有忘记关闭文件）、调试用的中间结果是否已被屏蔽、数组有没有开得足够大、输出中的常量字符串有没有打错（一般来说，Yes打成yes将被判错）等。

本书的题目风格均为ACM/ICPC式的，因此每个程序只有“对”和“不对”两种结果（当然，还会告知“不对”的原因，详见《算法竞赛入门经典》），而没有“半对半错”之说。好在本书的所有题目均选自UVa在线评测系统和ACM/ICPC真题库（Live Archive，简称LA），这两个在线题库的操作几乎一样，都可以很方便地提交题目（只需要知道题号，通过网站左下方菜单中的“Quick Submit”命令即可提交）。在完成书后习题时，常常需要参考原题（如看样例或者输入输出格式），这里介绍两个“快捷方式”（以题目12345和2345为例）：

UVa题目链接：http://uva.onlinejudge.org/external/123/12345.html

LA题目链接：http://livearchive.onlinejudge.org/external/23/2345.html

当然，还可以直接用搜索引擎搜索“UVa 12345”或者“livearchive 2345”。

为了方便读者，书中给出了所有例题和习题的提交统计信息，比如511/80％是指有511个用户提交，其中约80％的用户通过。

请注意，题号越大，说明加入题库的时间越晚。新加入题库的题目，其提交量不会很大，因此统计数据并不一定能客观反映其真实难度。

很多选手都有过“一道题折腾了一个星期，交了100次才过”的经历，可见，在算法竞赛中，坚韧不拔的精神是多么地重要。当然了，笔者并不建议读者每道题目都错上100次以后才罢手，因此提供了重要题目的数据和代码。毕竟，在初学阶段，学习他人的代码以及“对着评测数据调试”都是重要的学习方法。但在达到一定水平之后，最好是独立编写程序，并且不借助评测数据——要知道，在真实比赛时，你能拿到的所有数据仅仅是样例。

关于范例代码

本书中有很多代码，如果善加利用，这些代码可以帮你很大的忙；但如果滥用，非但不能发挥它们的最大好处，还可能会起到不好的作用。

关于书中的范例代码，笔者的建议只有一点：不要直接使用。理由有如下3点。

第一，不理解的代码不好用。所有代码都有它自身的适用范围，如果不理解其中的原理，不但有可能错误地使用这些代码，而且一旦需要对这些代码进行一定的修改才能符合题目要求时，你就会束手无策。解决方法很简单：想用的代码，事先重写一遍。这样不仅能加深你的理解，用的时候也更放心。

第二，代码习惯因人而异。书中的代码符合笔者的思维习惯和编码风格，却不一定适合你。比如，书中的代码把某个东西从0开始编号，而你习惯把任何东西从1开始编号，则当你混用自己的代码和书中的代码时，不仅编程时必须小心翼翼，而且很容易出现一些难以找到的bug。

第三，代码风格与传统的工程代码有冲突。作为从事软件开发工作多年的工程师，笔者深知算法竞赛代码和工程代码的差异。在算法竞赛中，由于时间紧迫，很多东西都“从简”了，很多“规矩”也都被无视了。比如，算法竞赛中经常使用全局变量、内存往往是事先分配一大堆而不是按需分配、很少使用OO特性（顶多用几个带有成员函数的struct，所有成员都是public的，并且很少用继承）、单个函数通常比工程代码长，但代码总长度通常更短，标识符通常也更短……如果本书采用传统的工程风格来编写代码，不仅会让书的厚度成倍增加，还会让读者在阅读了大量“无关紧要”的代码之后仍然不得要领，找不到最关键、最核心的地方。因此，笔者宁可让代码紧凑些，一方面让读者很容易抓住问题的核心，另一方面也锻炼了读者对程序整体逻辑（而非表达方式）的“感觉”——这恰恰是很多程序员所缺乏的[10]。一旦真正理解了这些“紧凑代码”，将其改成工程代码并不是难事。如前所述，这些代码会更好用，更符合你的需要。

不过，笔者有一点需要澄清：虽然和传统的工程代码有如此多的差异，但这并不是说本书的代码风格完全不适合工程项目。相反，本书的写作目的之一是希望读者能把两种风格有机地结合起来。比如，算法竞赛的选手写出来的代码通常比较简洁，这使程序具有更好的可读性和易维护性。笔者不主张把可读性肤浅地理解成“变量名取得长且有意义”、“注释足够多”、“每个函数都不超过10行”以及“拥有一个看上去很棒的类层次结构”，而应该更加看重程序的逻辑关系和执行流程。例如，对于下面这一段代码：

任何一个具备一定算法基础的程序员都能够毫不犹豫地说出它的作用，不需要任何注释，也不需要给任何变量改一个“更有意义”的名字；相反，很多工整、干净甚至看起来很优美的“工程型”的算法代码，却用了1000行来完成100行就能搞定的功能[11]。随着代码长度的增加，潜在的bug数量、配套的单元测试数量以及人力成本等都会随之增长，而且增速通常快于线性函数。在这样的情况下，采用竞赛式的风格编写工程项目中的算法代码不仅是可行的，也是值得提倡的[12]。如果认真体会本书中的代码，你会发现它们比传统工程代码短的主要原因并不是变量名短、函数分得不细，而是因为逻辑更简单、清楚、直接。

考虑到参赛语言限制（ACM/ICPC只能使用C、C++和Java，NOI支持Pascal，但不支持Java），本书的正文选择了两种竞赛都支持的C++。但由于笔者在工作中还使用了大量的非C++语言（包括Java、C#、Python、JavaScript/CoffeeScript、ActionScript、Erlang、Scala等），因此，我们特别编写了一个附录来简单介绍其他3种语言——Java、C#和Python。强烈建议读者反复阅读这个特别的附录，它不仅能开阔你的眼界，还能帮你在竞赛和真实的软件开发之间架起一座桥梁。算法是软件开发的基石，但不是全部。

注释

[1]　笔者认为，本书的读者至少会覆盖从小学高年级学生到从事IT工作多年的工程师。

[2]　比如，有些术语在学术界并没有统一的叫法，或者在不同文献中的叫法不同，如果本书采用的名称和你所熟知的不同，反而会产生误导。

[3]　这两个原因并不是孤立的。一般来说，竞赛会偏爱那些精巧、灵活的内容，而非那些很难变形、扩展的死板东西。

[4]　有的题目还包含其他部分，比如背景介绍、样例解释，甚至解题提示，但这些都不是必需的。

[5]　书中多次强调的“把数组开大一些”也是基于这个考虑。

[6]　多数情况如此，但如果常数特别小，O（n²）时间算法也有可能通过n＝10000的数据。

[7]　但如果算法糟糕或者实现不好的话，中间结果有可能溢出！

[8]　如果你问：“这题是什么意思？”，通常不会有人回答你；如果你问“这个样例为什么输出1”，常常都能得到满意的回答。

[9]　有的比赛是上机结束后由机器自动收取，没有明显的“提交”过程，在此情况下，请把“提交”理解成“比赛结束”。

[10]　如果你是在进行逆向工程，面对的是大量二进制的机器码，不仅没有注释，连变量名都没了，所能把握的就只有逻辑了。

[11]　笔者一点都没有夸张。经验表明，10倍的比例是很正常的。

[12]　也许最大的问题是：如果仍然以LOC（代码行数）来衡量工作量，习惯于编写紧凑代码的程序员往往很吃亏。

第1章　算法设计基础

在《算法竞赛入门经典》一书中，已经讨论过算法分析、渐进时间复杂度等基本概念，以及分治、贪心、动态规划等常见的算法设计方法。本章是《算法竞赛入门经典》的延伸，通过例题介绍更多的算法设计方法和技巧。

1.1　思维的体操

例题1　勇者斗恶龙（The Dragon of Loowater, UVa 11292）

你的王国里有一条n个头的恶龙，你希望雇一些骑士把它杀死（即砍掉所有头）。村里有m个骑士可以雇佣，一个能力值为x的骑士可以砍掉恶龙一个直径不超过x的头，且需要支付x个金币。如何雇佣骑士才能砍掉恶龙的所有头，且需要支付的金币最少？注意，一个骑士只能砍一个头（且不能被雇佣两次）。

【输入格式】

输入包含多组数据。每组数据的第一行为正整数n和m（1≤n，m≤20000）；以下n行每行为一个整数，即恶龙每个头的直径；以下m行每行为一个整数，即每个骑士的能力。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出最少花费。如果无解，输出“Loowater is doomed!”。

【样例输入】

2 3

5

4

7

8

4

2 1

5

5

10

0 0

【样例输出】

11

Loowater is doomed!

【分析】

能力强的骑士开价高是合理的，但如果被你派去砍一个很弱的头，就是浪费人才了。因此，可以把雇佣来的骑士按照能力从小到大排序，所有头按照直径从小到大排序，一个一个砍就可以了。当然，不能砍掉“当前需要砍的头”的骑士就不要雇佣了。代码如下。

例题2　突击战（Commando War, UVa 11729）

你有n个部下，每个部下需要完成一项任务。第i个部下需要你花Bi分钟交待任务，然后他会立刻独立地、无间断地执行Ji分钟后完成任务。你需要选择交待任务的顺序，使得所有任务尽早执行完毕（即最后一个执行完的任务应尽早结束）。注意，不能同时给两个部下交待任务，但部下们可以同时执行他们各自的任务。

【输入格式】

输入包含多组数据，每组数据的第一行为部下的个数N（1≤N≤1000）；以下N行每行两个正整数B和J（1≤B≤10000，1≤J≤10000），即交待任务的时间和执行任务的时间。输入结束标志为N＝0。

【输出格式】

对于每组数据，输出所有任务完成的最短时间。

【样例输入】

3

2 5

3 2

2 1

3

3 3

4 4

5 5

0

【样例输出】

Case 1: 8

Case 2: 15

【分析】

直觉告诉我们，执行时间较长的任务应该先交待。于是我们想到这样一个贪心算法：按照J从大到小的顺序给各个任务排序，然后依次交待。代码如下。

上述代码直接交上去就可以通过测试了。

可是为什么这样做是对的呢？假设我们交换两个相邻的任务X和Y（交换前X在Y之前，交换后Y在X之前），不难发现其他任务的完成时间没有影响，那么这两个任务呢？

情况一：交换之前，任务Y比X先结束，如图1-1（a）所示。不难发现，交换之后X的结束时间延后，Y的结束时间提前，最终答案不会变好。

情况二：交换之前，X比Y先结束，因此交换后答案变好的充要条件是：交换后X的结束时间比交换前Y的结束时间早（交换后Y的结束时间肯定变早了），如图1-1（b）所示。这个条件可以写成B[Y]＋B[X]＋J[X]＜B[X]＋B[Y]＋J[Y]，化简得J[X]＜J[Y]。这就是我们贪心的依据。

图　1-1

例题3　分金币（Spreading the Wealth, UVa 11300）

圆桌旁坐着n个人，每人有一定数量的金币，金币总数能被n整除。每个人可以给他左右相邻的人一些金币，最终使得每个人的金币数目相等。你的任务是求出被转手的金币数量的最小值。比如，n＝4，且4个人的金币数量分别为1，2，5，4时，只需转移4枚金币（第3个人给第2个人两枚金币，第2个人和第4个人分别给第1个人1枚金币）即可实现每人手中的金币数目相等。

【输入格式】

输入包含多组数据。每组数据第一行为整数n（n≤1000000），以下n行每行为一个整数，按逆时针顺序给出每个人拥有的金币数。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出被转手金币数量的最小值。输入保证这个值在64位无符号整数范围内。

【样例输入】

3

100

100

100

4

1

2

5

4

【样例输出】

0

4

【分析】

这道题目看起来很复杂，让我们慢慢分析。首先，最终每个人的金币数量可以计算出来，它等于金币总数除以人数n。接下来我们用M来表示每人最终拥有的金币数。

假设有4个人，按顺序编号为1，2，3，4。假设1号给2号3枚金币，然后2号又给1号5枚金币，这实际上等价于2号给1号2枚金币，而1号什么也没给2号。这样，可以设x₂表示2号给了1号多少个金币。如果x₂＜0，说明实际上是1号给了2号-x₂枚金币。x₁，x₃和x₄的含义类似。注意，由于是环形，x₁指的是1号给4号多少金币。

现在假设编号为i的人初始有Ai枚金币。对于1号来说，他给了4号x₁枚金币，还剩Ai－x₁枚；但因为2号给了他x₂枚金币，所以最后还剩A₁－x₁＋x₂枚金币。根据题设，该金币数等于M。换句话说，我们得到了一个方程：A₁－x₁＋x₂＝M。

同理，对于第2个人，有A₂－x₂＋x₃＝M。最终，我们可以得到n个方程，一共有n个变量，是不是可以直接解方程组了呢？很可惜，还不行。因为从前n－1个方程可以推导出最后一个方程（想一想，为什么）。所以，实际上只有n－1个方程是有用的。

尽管无法直接解出答案，我们还是可以尝试着用x₁表示出其他的xi，则本题就变成了单变量的极值问题。

对于第1个人，A₁－x₁＋x₂＝M→x₂＝M－A₁＋x₁＝x₁－C₁（规定C₁＝A₁－M，下面类似）

对于第2个人，A₂－x₂＋x₃＝M→x₃＝M－A₂＋x₂＝2M－A₁－A₂＋x₁＝x₁－C₂

对于第3个人，A₃－x₃＋x₄＝M→x₄＝M－A₃＋x₃＝3M－A₁－A₂－A₃＋x₁＝x₁－C₃

…

对于第n个人，An－xn＋x₁＝M。这是一个多余的等式，并不能给我们更多的信息（想一想，为什么）。

我们希望所有xi的绝对值之和尽量小，即｜x₁｜＋｜x₁－C₁｜＋｜x₁－C₂｜＋…＋｜x₁－Cn－1｜要最小。注意到｜x₁－Ci｜的几何意义是数轴上点x₁到Ci的距离，所以问题变成了：给定数轴上的n个点，找出一个到它们的距离之和尽量小的点。

下一步可能有些跳跃。不难猜到，这个最优的x₁就是这些数的“中位数”（即排序以后位于中间的数），因此只需要排个序就可以了。性急的读者可能又想跳过证明了，但是笔者希望您这次能好好读一读，因为它实在是太优美、太巧妙了，而且不少其他题目也能用上（我们很快就会再见到一例）。

注意，我们要证明的是：给定数轴上的n个点，在数轴上的所有点中，中位数离所有顶点的距离之和最小。凡是能转化为这个模型的题目都可以用中位数求解，并不只适用于本题。

让我们把数轴和上面的点画出来，如图1-2所示。

图　1-2

任意找一个点，比如图1-2中的灰点。它左边有4个输入点，右边有2个输入点。把它往左移动一点，不要移得太多，以免碰到输入点。假设移动了d单位距离，则灰点左边4个点到它的距离各减少了d，右边的两个点到它的距离各增加了d，但总的来说，距离之和减少了2d。

如果灰点的左边有2个点，右边有4个点，道理类似，不过应该向右移动。换句话说，只要灰点左右的输入点不一样多，就不是最优解。什么情况下左右的输入点一样多呢？如果输入点一共有奇数个，则灰点必须和中间的那个点重合（中位数）；如果有偶数个，则灰点可以位于最中间的两个点之间的任意位置（还是中位数）。代码如下。

程序本身并没有太多技巧可言，但需要注意的是long long的输入输出。在《入门经典》中我们已经解释过了，％lld这个占位符并不是跨平台的，比如，Windows下的mingw需要用％I64d而不是％lld。虽然cin/cout没有这个问题，但是本题输入量比较大，cin/cout会很慢。有两个解决方案。一是自己编写输入输出函数（前面已经给过范例），二是使用ios::sync_ with_stdio（false），通过关闭ios和stdio之间的同步来加速，有兴趣的读者可以自行搜索详细信息。

中位数可以在线性时间内求出，但不是本例题的重点（代数分析才是重点），有兴趣的读者可以自行搜索“快速选择”算法的资料。另外，这个程序里的A数组实际上是不必保存的，你能去掉它吗？

例题4　墓地雕塑（Graveyard, NEERC 2006, LA 3708）

在一个周长为10000的圆上等距分布着n个雕塑。现在又有m个新雕塑加入（位置可以随意放），希望所有n＋m个雕塑在圆周上均匀分布。这就需要移动其中一些原有的雕塑。要求n个雕塑移动的总距离尽量小。

【输入格式】

输入包含若干组数据。每组数据仅一行，包含两个整数n和m（2≤n≤1000，1≤m ≤1000），即原始的雕塑数量和新加的雕塑数量。输入结束标志为文件结束符（EOF）。

【输出格式】

输入仅一行，为最小总距离，精确到10⁻⁴。

【样例输入】

2 1

2 3

3 1

10 10

【样例输出】

1666.6667

1000.0

1666.6667

0.0

【样例解释】

前3个样例如图1-3所示。白色空心点表示等距点，黑色线段表示已有雕塑。

图　1-3

【分析】

请仔细看看样例。3个样例具有一个共同的特点：有一个雕塑没有移动。如果该特点在所有情况下都成立，则所有雕塑的最终位置（称为“目标点”）实际上已经确定。为了简单起见，我们把没动的那个雕塑作为坐标原点，其他雕塑按照逆时针顺序标上到原点的距离标号，如图1-4所示。

图　1-4

注意，这里的距离并不是真实距离，而是按比例缩小以后的距离。接下来，我们把每个雕塑移动到离它最近的位置。如果没有两个雕像移到相同的位置，那么这样的移动一定是最优的。代码如下。

注意在代码中，坐标为pos的雕塑移动到的目标位置是floor（pos＋0.5），也就是pos四舍五入后的结果。这就是坐标缩小的好处。

这个代码很神奇地通过了测试，但其实这个算法有两个小小的“漏洞”：首先，我们不知道是不是一定有一个雕塑没有移动；其次，我们不知道会不会有两个雕塑会移动到相同的位置。如果你对证明不感兴趣，或者已经想到了证明，再或者迫不及待地想阅读更有趣的问题，请直接跳到下一个例题。否则，请继续阅读。

第一个“漏洞”的修补需要证明我们的猜想。证明思路在例题3中我们已经展示过了，具体的细节留给读者思考。

第二个“漏洞”有两种修补方法。第一种方法相对较容易实施：由于题目中规定了n，m≤1000，我们只需要在程序里加入一个功能——记录每座雕塑移到的目标位置，就可以用程序判断是否会出现“人多坑少”的情况。这段程序的编写留给读者，这里可以明确地告诉大家：这样的情况确实不会出现。这样，即使无法从理论上证明，也可以确保在题目规定的范围内，我们的算法是严密的。

第二种方法就是直接证明。在我们的程序中，当坐标系缩放之后，坐标为x的雕塑被移到了x四舍五入后的位置。如果有两个坐标分别为x和y的雕塑被移到了同一个位置，说明x和y四舍五入后的结果相同，换句话说，即x和y“很接近”。至于有多接近呢？差距最大的情况不外乎类似于x＝0.5，y＝1.499999…。即便是这样的情况，y－x仍然小于1（尽管很接近1），但这是不可能的，因为新增雕塑之后，相邻雕塑的距离才等于1，之前的雕塑数目更少，距离应当更大才对。

例题5　蚂蚁（Piotr's Ants, UVa 10881）

一根长度为L厘米的木棍上有n只蚂蚁，每只蚂蚁要么朝左爬，要么朝右爬，速度为1厘米／秒。当两只蚂蚁相撞时，二者同时掉头（掉头时间忽略不计）。给出每只蚂蚁的初始位置和朝向，计算T秒之后每只蚂蚁的位置。

【输入格式】

输入的第一行为数据组数。每组数据的第一行为3个正整数L，T，n（0≤n≤10000）；以下n行每行描述一只蚂蚁的初始位置，其中，整数x为蚂蚁距离木棍左端的距离（单位：厘米），字母表示初始朝向（L表示朝左，R表示朝右）。

【输出格式】

对于每组数据，输出n行，按输入顺序输出每只蚂蚁的位置和朝向（Turning表示正在碰撞）。在第T秒之前已经掉下木棍的蚂蚁（正好爬到木棍边缘的不算）输出Fell off。

【样例输入】

2

10 1 4

1 R

5 R

3 L

10 R

10 2 3

4 R

5 L

8 R

【样例输出】

Case #1:

2 Turning

6 R

2 Turning

Fell off

Case #2:

3 L

6 R

10 R

【分析】

假设你在远处观察这些蚂蚁的运动，会看到什么？一群密密麻麻的小黑点在移动。由于黑点太小，所以当蚂蚁因碰撞而掉头时，看上去和两个点“对穿而过”没有任何区别，换句话说，如果把蚂蚁看成是没有区别的小点，那么只需独立计算出每只蚂蚁在T时刻的位置即可。比如，有3只蚂蚁，蚂蚁1＝（1，R），蚂蚁2＝（3，L），蚂蚁3＝（4，L），则两秒钟之后，3只蚂蚁分别为（3，R）、（1，L）和（2，L）。

注意，虽然从整体上讲，“掉头”等价于“对穿而过”，但对于每只蚂蚁而言并不是这样。蚂蚁1的初始状态为（1，R），因此一定有一只蚂蚁在两秒钟之后处于（3，R）的状态，但这只蚂蚁却不一定是蚂蚁1。换句话说，我们需要搞清楚目标状态中“谁是谁”。

也许读者已经发现了其中的奥妙：所有蚂蚁的相对顺序是保持不变的，因此把所有目标位置从小到大排序，则从左到右的每个位置对应于初始状态下从左到右的每只蚂蚁。由于原题中蚂蚁不一定按照从左到右的顺序输入，还需要预处理计算出输入中的第i只蚂蚁的序号order[i]。完整代码如下。

例题6　立方体成像（Image Is Everything, World Finals 2004, LA 2995）

有一个n×n×n立方体，其中一些单位立方体已经缺失（剩下部分不一定连通）。每个单位立方体重量为1克，且被涂上单一的颜色（即6个面的颜色相同）。给出前、左、后、右、顶、底6个视图，你的任务是判断这个物体剩下的最大重量。

【输入格式】

输入包含多组数据。每组数据的第一行为一个整数n（1≤n≤10）；以下n行每行从左到右依次为前、左、后、右、顶、底6个视图，每个视图占n列，相邻视图中间以一个空格隔开。顶视图的下边界对应于前视图的上边界；底视图的上边界对应于前视图的下边界。在视图中，大写字母表示颜色（不同字母表示不同颜色），句号（.）表示该位置可以看穿（即没有任何立方体）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出一行，即物体的最大重量（单位：克）。

【样例输入】

【样例输出】

【分析】

这个问题看上去有点棘手，不过仍然可以找到突破口。比如，能“看穿”的位置所对应的所有单位立方体一定都不存在。再比如，如果前视图的右上角颜色A和顶视图的右下角颜色B不同，那么对应的格子一定不存在。如图1-5所示。

图　1-5

在删除这个立方体之后，我们可能会有新发现：C和D的颜色不同。这样，我们又能删除一个新的立方体，并暴露出新的表面。当无法继续删除的时候，剩下的立方体就是重量最大的物体。

可能有读者会对上述算法心存疑惑。解释如下：首先不难证明第一次删除是必要的（即被删除的那个立方体不可能存在于任意可行解中），因为只要不删除这个立方体，对应两个视图的“矛盾”将一直存在；接下来，我们用数学归纳法，假设算法的前k次删除都是必要的，那么第k＋1次删除是否也是必要的呢？由刚才的推理，我们不能通过继续删除立方体来消除矛盾，而由归纳假设，已经删除的立方体也不能恢复，因此矛盾无法消除。

下面给出完整代码。

程序用了一个get函数来表示第k个视图中，第i行j列、深度为len的单位立方体在原立方体中的坐标（x，y，z），另外还使用了宏REP精简程序。尽管用宏缩短代码在很多时候会降低程序可读性，但本题却不会（如果到处都是for循环，反而容易令人犯晕）。

1.2　问题求解常见策略

例题7　偶数矩阵（Even Parity, UVa 11464）

给你一个n×n的01矩阵（每个元素非0即1），你的任务是把尽量少的0变成1，使得每个元素的上、下、左、右的元素（如果存在的话）之和均为偶数。比如，如图1-6（a）所示的矩阵至少要把3个0变成1，最终如图1-6（b）所示，才能保证其为偶数矩阵。

图　1-6

【输入格式】

输入的第一行为数据组数T（T≤30）。每组数据的第一行为正整数n（1≤n≤15）；接下来的n行每行包含n个非0即1的整数，相邻整数间用一个空格隔开。

【输出格式】

对于每组数据，输出被改变的元素的最小个数。如果无解，应输出-1。

【分析】

也许最容易想到的方法就是枚举每个数字“变”还是“不变”，最后判断整个矩阵是否满足条件。遗憾的是，这样做最多需要枚举2²⁵⁵≈5×10⁶⁷种情况，实在难以承受。

注意到n只有15，第一行只有不超过2¹⁵＝32768种可能，所以第一行的情况是可以枚举的。接下来根据第一行可以完全计算出第二行，根据第二行又能计算出第三行（想一想，如何计算），以此类推，这样，总时间复杂度即可降为O（2n×n²）。代码如下。

例题8　彩色立方体（Colored Cubes, Tokyo 2005, LA 3401）

有n个带颜色的立方体，每个面都涂有一种颜色。要求重新涂尽量少的面，使得所有立方体完全相同。两个立方体相同的含义是：存在一种旋转方式，使得两个立方体对应面的颜色相同。

【输入格式】

输入包含多组数据。每组数据的第一行为正整数n（1≤n≤4）；以下n行每行6个字符串，分别为立方体编号为1～6的面的颜色（由小写字母和减号组成，不超过24个字符）。输入结束标志为n＝0。立方体的6个面的编号如图1-7所示。

图　1-7

【输出格式】

对于每组数据，输出重新涂色的面数的最小值。

【分析】

立方体只有4个，暴力法应该可行。不过不管怎样“暴力”，首先得搞清楚一个立方体究竟有几种不同的旋转方式。

为了清晰起见，我们借用机器人学中的术语，用姿态（pose）来代替口语中的旋转方法。假设6个面的编号为1～6，从中选一个面作为“顶面”，然后在剩下的4个面中选一个作为“正面”，则其他面都可以唯一确定，因此有6×4＝24种姿态。

在代码中，每种姿态对应一个全排列P。其中，P[i]表示编号i所在的位置（1表示正面，2表示右面，3表示顶面等，如图1-8所示）。如图1-9所示的姿态称为标准姿态，用排列｛1，2，3，4，5，6｝表示，因为1在正面，2在右面，3在顶面等。

图　1-8

图　1-9

图1-10是标准姿态向左旋转后得到的。对应的排列是｛5，1，3，4，6，2｝。

图　1-10

接下来有两种方法。一种方法是手工找出24种姿态对应的排列，编写到代码中。显然，这种方法比较耗时，且容易出错，不推荐使用。下面的方法可以用程序找出这24种排列，而且不容易出错。除了刚才写出的标准姿态向左翻之外，我们再写出标准姿态向上翻所对应的排列：｛3，2，6，1，5，4｝，如图1-11所示。

图　1-11

注意到旋转是可以组合的，比如，图1-11标准姿态先向左转再向上翻就是5→5，1→3，3→6，4→1，6→4，2→2，即｛5，3，6，1，4，2｝。因此，有了这两种旋转方式，我们就可以构造出所有24种姿态了（均为从标准姿态开始旋转）。

1在顶面的姿态：向上翻1次（此时1在顶面），然后向左转0～3次。

2在顶面的姿态：向左转1次（此时2在顶面），向上翻1次，然后向左转0～3次。

3在顶面的姿态：（3本来就在顶面）向左转0～3次。

4在顶面的姿态：向上翻2次（此时4在顶面），然后向左转0～3次。

5在顶面的姿态：向左转2次，向上翻一次（此时5在顶面），然后向左转0～3次。

6在顶面的姿态：向左转3次，向上翻一次（此时6在顶面），然后向左转0～3次。

这段代码应该写在哪里呢？一种方法是直接手写在最终的程序中，但是一旦这部分代码出错，非常难调；另一种方法是写到一个独立程序中，用它生成24种姿态对应的排列，而在最终程序中直接使用常量表。生成排列表的程序如下（注意，在代码中编号为0～5，而非1～6）。

下面让我们来看看如何“暴力”。一种方法是枚举最后那个“相同的立方体”的每个面是什么，然后对于每个立方体，看看哪种姿态需要重新涂色的面最少。但由于4个立方体最多可能会有24种不同的颜色，最多需要枚举24⁶种“最后的立方体”，情况有些多。

另一种方法是先枚举每个立方体的姿态（第一个作为“参考系”，不用旋转），然后对于6个面，分别选一个出现次数最多的颜色作为“标准”，和它不同的颜色一律重涂。由于每个立方体的姿态有24种，3个立方体（别忘了第一个不用旋转）的姿态组合一共有24³种，比第一种方法要好。程序如下（程序头部是生成的常量表，为了节省篇幅，合并了一些行）。

例题9　中国麻将（Chinese Mahjong, UVa 11210）

麻将是一个中国原创的4人玩的游戏。这个游戏有很多变种，但本题只考虑一种有136张牌的玩法。这136张牌所包含的内容如下。

饼（筒）牌：每张牌包括一系列点，每个点代表一个铜钱，如图1-12所示。本题中用1T、2T、3T、4T、5T、6T、7T、8T、9T表示。

图　1-12

索（条）牌：每张牌由一系列竹棍组成，每根棍代表一挂铜钱，如图1-13所示。本题中用1S、2S、3S、4S、5S、6S、7S、8S、9S表示。

图　1-13

万牌：每张牌代表一万枚铜钱，如图1-14所示。本题中用1W、2W、3W、4W、5W、6W、7W、8W、9W表示。

图　1-14

风牌：东、南、西、北风，如图1-15所示。本题中用DONG、NAN、XI、BEI表示。

图　1-15

箭牌：中、发、白，如图1-16所示。本题中用ZHONG、FA、BAI表示。

图　1-16

总共有9×3＋4＋3＝34种牌，每种4张，一共有136张牌。

其实麻将中还有如图1-17所示的8张花牌，所以共有136＋8＝144张牌，但是本题中不予考虑。

图　1-17

中国麻将的规则十分复杂，本题中只需考虑部分规则。在本题中，手牌（即每个人手里的牌）总是有13张。如果多了某张牌以后，整副牌可以拆成一个将（两张相同的牌）、0个或多个刻子（3张相同的牌）和0个或多个顺子（3张同花相连的牌。注意，风牌和箭牌不能形成顺子），我们就说这手牌“听”这张牌，即拿到那张牌以后就赢了，称为“和”（实战中还要考虑番数和特殊和法，在本题中可以忽略）。

比如，如图1-18所示的这手牌：

图　1-18

听牌alt、alt和alt，即1S、FA和4S。听牌alt的原因是：“发”做将，另有3个顺子（1S2S3S, 1S2S3S, 2S3S4S）。

【输入格式】

输入数据最多50组。每组数据由一行13张牌给出，输入保证给出的牌是合法的。输入结束标记为一行单个0。

【输出格式】

对于每组数据，输出所有“听”的牌，按照描述中的顺序列出（1T-9T，1S-9S，1W-9W，DONG，NAN，XI，BEI，ZHONG，FA，BAI）。每张牌最多被列出一次。如果没有“听”牌，输出Not ready。

【分析】

如果您和笔者一样对麻将很熟悉，不妨回忆一下自己平时打麻将时，是如何知道自己有没有听牌的。虽然多数情况都容易判断，但对于一些复杂的情况，新手容易看不出自己“听”牌了，或者看不全所有“听”的牌，而麻将老手却可以。原因在于，麻将老手擅长把手里的牌按照不同的方式进行组合。在程序里，我们也需要用一点“暴力”来枚举所有可能的组合方式。

一共只有34种牌，因此可以依次判断是否“听”这些牌。比如，为了判断是否“听”一万，只需要判断自己拿到这张一万后是否可以和牌。这样，问题就转化为了：给定14张牌，判断是否可以和牌。为此，我们可以递归求解：首先选两张牌作为“将”，然后每次选3张作为刻子或者顺子。如图1-19所示，即为一次递归求解的过程。

图　1-19

选将有5种方法（一二三四五万都可以做将）。如果选五万做将，一万要么属于一个刻子，要么属于一个顺子（二三四）。注意，这时不必考虑其他牌是如何形成刻子或者顺子的，否则会出现重复枚举（想一想，为什么）。

为了快速选出将、刻子和顺子，我们用一个34维向量来表示状态，即每种牌所剩的张数。除了第一次直接枚举将牌之外，每次只需要考虑编号最小的牌，看它能否形成刻子或者顺子（一定是以它作为最小牌。想一想，为什么），并且递归判断。本题唯一的陷阱是：每一种牌都只有4张，所以1S1S1S1S是不“听”任何牌的。

完整代码如下。

例题10　正整数序列（Help is needed for Dexter, UVa 11384）

给定正整数n，你的任务是用最少的操作次数把序列1，2，…，n中的所有数都变成0。每次操作可从序列中选择一个或多个整数，同时减去一个相同的正整数。比如，1，2，3可以把2和3同时减小2，得到1，0，1。

【输入格式】

输入包含多组数据。每组仅一行，为正整数n（n≤10⁹）。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出最少操作次数。

【分析】

拿到这道题目之后，最好的方式是自己试一试。经过若干次尝试和总结后，不难发现第一步的最好方式如图1-20所示。

图　1-20

换句话说，当n＝6的时候留下1，2，3，而把4，5，6同时减去min｛4，5，6｝＝4得到序列1，2，3，0，1，2，它等价于1，2，3（想一想，为什么）。换句话说，f（6）＝f（3）＋1。

一般地，为了平衡，我们保留1～n/2，把剩下的数同时减去n/2＋1，得到序列1，2，…，n/2，0，1，…，（n－1）/2，它等价于1，2，…，n/2，因此f（n）＝f（n/2）＋1。边界是f（1）＝1。代码如下。

例题11　新汉诺塔问题（A Different Task, UVa 10795）

标准的汉诺塔上有n个大小各异的盘子。给定一个初始局面（如图1-21所示），求它到给定目标局面（如图1-22所示）至少需要多少步。移动规则如下：一次只能移动一个盘子；在移动一个盘子之前，必须把压在上面的其他盘子先移走；编号大的盘子不得压在编号小的盘子上。

图　1-21

图　1-22

【输入格式】

输入包含不超过100组数据。每组数据的第一行为正整数n（1≤n≤60）；第二行包含n个1～3的整数，即初始局面中每个盘子所在的柱子编号；第三行和第二行格式相同，为目标局面。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出最少步数。

【分析】

考虑编号最大的盘子。如果这个盘子在初始局面和目标局面中位于同一根柱子上，那么根本不需要移动它，而如果移动了，反而不可能是最优解（想一想，为什么）。这样，我们可以在初始局面和目标局面中，找出所在柱子不同的盘子中编号最大的一个，设为k，那么k必须移动。

让我们设想一下，移动k之前的一瞬间，柱子上的情况吧。假设盘子k需要从柱子1移动到柱子2。由于编号比k大的盘子不需要移动，而且也不会碍事，所以我们直接把它们看成不存在；编号比k小的盘子既不能在柱子1上，也不能在柱子2上，因此只能在柱子3上。换句话说，这时柱子1只有盘子k，柱子2为空，柱子3从上到下依次是盘子1，2，3，…，k－1（再次提醒：我们已经忽略了编号大于k的盘子）。我们把这个局面称为参考局面。

由于盘子的移动是可逆的，根据对称性，我们只需要求出初始局面和目标局面移动成参考局面的步数之和，然后加1（移动盘子k）即可。换句话说，我们需要写一个函数f（P，i，final），表示已知各盘子的初始柱子编号数组为P（具体来说，P[i]代表盘子i的柱子编号），把盘子1，2，3，…，i全部移到柱子final所需的步数，则本题的答案就是f（start，k－1，6－start[k]－finish[k]）＋f（finish，k－1，6－start[k]－finish[k]）＋1。其中，start[i]和finish[i]是本题输入中盘子i的初始柱子和目标柱子，k是上面所说的“必须移动的编号最大的盘子”的编号。我们把柱子编号为1，2，3，所以“除了柱子x和柱子y之外的那个柱子”编号为6－x－y。

如何计算f（P，i，final）呢？推理和刚才类似。假设P[i]＝final，那么f（P，i，final）＝f（P，i－1，final）；否则需要先把前i－1个盘子挪到6－P[i]－final这个盘子做中转，然后把盘子i移动到柱子final，最后把前i－1个盘子从中转的柱子移到目标柱子final。注意，最后一个步骤是把i－1个盘子从一个柱子整体移到另一个柱子，根据汉诺塔问题的经典结论，这个步骤需要2i－1－1步，加上移动盘子i的那一步，一共需要2i－1步。换句话说，当P[i]不等于final的时候，f（P，i，final）＝f（P，i－1，6－P[i]－final）＋2i－1。

最后，注意答案需要用long long保存（想一想，为什么）。代码如下。

例题12　组装电脑（Assemble, NWERC 2007, LA 3971）

你有b块钱，想要组装一台电脑。给出n个配件各自的种类、品质因子和价格，要求每种类型的配件各买一个，总价格不超过b，且“品质最差配件”的品质因子应尽量大。

【输入格式】

输入的第一行为测试数据组数T（T≤100）。每组数据的第一行为两个正整数n（1≤n≤1000）和b（1≤b≤10⁹），即配件的数目和预算；以下n行每行描述一个配件，依次为种类、名称、价格和品质因子。其中，价格为不超过10⁶的非负整数；品质因子是不超过10⁹的非负整数（越大越好）；种类和名称则由不超过20个字母、数字和下划线组成。输入保证总是有解。

【输出格式】

对于每组数据，输出配件最小品质因子的最大值。

【分析】

在《入门经典》一书中，我们曾提到过，解决“最小值最大”的常用方法是二分答案。假设答案为x，如何判断这个x是最小还是最大呢？删除品质因子小于x的所有配件，如果可以组装出一台不超过b元的电脑，那么标准答案ans≥x，否则ans＜x。

如何判断是否可以组装出满足预算约束的电脑呢？很简单，每一类配件选择最便宜的一个即可。如果这样选都还超预算的话，就不可能有解了。代码如下。

例题13　派（Pie, NWERC 2006, LA 3635）

有F＋1个人来分N个圆形派，每个人得到的必须是一整块派，而不是几块拼在一起，且面积要相同。求每个人最多能得到多大面积的派（不必是圆形）。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为两个整数N和F（1≤N，F≤10000）；第二行为N个整数ri（1≤ri≤10000），即各个派的半径。

【输出格式】

对于每组数据，输出每人得到的派的面积的最大值，精确到10⁻³。

【分析】

这个问题并不是“最小值最大”问题，但仍然可以采用二分答案方法，把问题转化为“是否可以让每人得到一块面积为x的派”。这样的转化相当于多了一个条件，然后求解目标变成了“看看这些条件是否相互矛盾”。

会有怎样的矛盾呢？只有一种矛盾：x太大，满足不了所有的F＋1个人。这样，我们只需要算算一共可以切多少份面积为x的派，然后看看这个数目够不够F＋1即可。因为派是不可以拼起来的，所以一个半径为r的派只能切出[πr²/x]个派（其他部分就浪费了），把所有圆形派能切出的份数加起来即可。代码如下。

例题14　填充正方形（Fill the Square, UVa 11520）

在一个n×n网格中填了一些大写字母，你的任务是把剩下的格子中也填满大写字母，使得任意两个相邻格子（即有公共边的格子）中的字母不同。如果有多种填法，则要求按照从上到下、从左到右的顺序把所有格子连接起来得到的字符串的字典序应该尽量小。

【输入格式】

输入的第一行为测试数据组数T。每组数据的第一行为整数n（n≤10），即网格的行数和列数；以下n行每行n个字符，表示整个网格。为了清晰起见，本题用小数点表示没有填字母的格子。

【输出格式】

对于每组数据，输出填满字母后的网格。

【样例输入】

2

3

…

…

…

3

…

A..

…

【样例输出】

Case 1:

ABA

BAB

ABA

Case 2:

BAB

ABA

BAB

【分析】

首先说点题外话。一道题当可能有多个解时，为了确保答案唯一（比如，命题者不想写“输出检查器”，或者为了加大难度），题目通常会加上一些限制条件，其中“字典序最小”就是一个很常见的要求。

所谓“字典序”，就是“在字典中的顺序”。字典中的单词是如何排列的呢？首先按照第一个字母排序，即所有以a开头的单词排在以b开头的单词前面，而以b开头的单词排在以c开头的单词前面，以此类推。把这种方法扩展一下：对于任意两个序列，我们先比较第一个元素，再比较第二个元素……直到有一个元素不同，那么此元素小的序列，其字典序也小。剩下的元素全部不比较。注意，如果比较过程中恰好有一个序列结束，那么该序列较小。如果两个序列同时结束，说明两个序列完全相等，字典序自然也相等。下面是比较两个整数序列字典序的代码。

不难发现，对于定义了“小于”运算符的任意数据类型，由该类型元素组成的序列的字典序的比较方法是完全一样的。这样，我们可以把上述函数模板化。

除了阴影部分之外，这份代码和前面的代码完全一样。有了模板函数，不管你定义的是vector＜int＞a，b还是vector＜string＞a，b，甚至是vector＜vector＜int＞ ＞x，y，全部都可以用if（lexicographicallySmaller（a，b））…的方式直接使用上述函数，而不必针对各种类型各写一个函数。

既然只有序列才有字典序，题目中的这句“从上到下、从左到右”就不难理解了。它的意思是首先把每行看成一个字符串，然后从上到下顺次连接，要求得到的这个长长的字符串的字典序最小。

根据字典序的定义，我们可以从上到下、从左到右一位一位地求：先满足第一个元素最小，再满足第二个元素最小，以此类推。落实到本题中，我们只需从左到右、从上到下依次给所有的空格填上最小可能的字母即可，代码如下所示。

严谨的读者可能又要发问了：如果上述代码顺利执行完毕，即每个格子都有得填，得到的解自然是字典序最小的，但如果某个格子把A～Z的所有字母都尝试完，一个都填不了，该怎么办？这意味着必须推翻以前的决策，一下子让情况变得复杂起来。

幸运的是，这种情况不会发生，因为一个格子的上下左右只有4个格子，不可能包含A～Z这26个字母。因此，每个空格都能填上字母。

例题15　网络（Network, Seoul 2007, LA 3902）

n台机器连成一个树状网络，其中叶结点是客户端，其他结点是服务器。目前有一台服务器正在提供VOD（Video On Demand）服务，虽然视频质量本身很不错，但对于那些离它很远的客户端来说，网络延迟却难以忍受。你的任务是在一些其他服务器上也安装同样的服务，使得每台客户端到最近服务器的距离不超过一个给定的整数k。为了节约成本，安装服务的服务器台数应尽量少，如图1-23所示，当k＝2时还要在结点4处放一台服务器。

图　1-23

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为树中的结点数n（3≤n≤1000）；下一行包含两个整数s和k（1≤s≤n，1≤k≤n），其中s是已经放置好的VOD服务器的结点编号，k是叶子和服务器的距离上限；以下n－1行每行包含两个整数，即树中的一条边。

【输出格式】

对于每组数据，输出一个整数，即还需要放置的VOD服务器的个数的最小值。

【分析】

通常来说，把无根树变成有根树会有助于解题。何况在本题中，已经有了一个天然的根结点：原始VOD服务器。对于那些已经满足条件（即到原始VOD服务器的距离不超过k）的客户端，直接当它们不存在就可以了，如图1-24所示。

图　1-24

接下来，我们考虑深度最大的结点。比如结点8，应该在哪里放新的服务器来覆盖（“覆盖”一个叶子是指到该叶子的距离不超过k）它呢？只有结点5和结点4满足条件。显然，结点4比结点5划算，因为结点5所覆盖的叶子（6，7，8）都能被结点4所覆盖。一般的，对于深度最大的结点u，选择u的k级祖先是最划算的（父亲是1级祖先，父亲的父亲是2级祖先，以此类推）。证明过程留给读者自行思考。

下面给出上述算法的一种实现方法：每放一个新服务器，进行一次DFS，覆盖与它距离不超过k的所有结点。注意，本题只需要覆盖叶子，而不需要覆盖中间结点，而且深度不超过k的叶子已经被原始服务器覆盖，所以我们只需要处理深度大于k的叶结点即可。为了让程序更简单，我们可用nodes表避开“按深度排序”的操作，代码如下。

例题16　长城守卫（Beijing Guards, CERC 2004, LA 3177）

有n个人围成一个圈，其中第i个人想要ri个不同的礼物。相邻的两个人可以聊天，炫耀自己的礼物。如果两个相邻的人拥有同一种礼物，则双方都会很不高兴。问：一共需要多少种礼物才能满足所有人的需要？假设每种礼物有无穷多个，不相邻的两个人不会一起聊天，所以即使拿到相同的礼物也没关系。

比如，一共有5个人，每个人都要一个礼物，则至少要3种礼物。如果把这3种礼物编号为1，2，3，则5个人拿到的礼物应分别是：1，2，1，2，3。如果每个人要两个礼物，则至少要5种礼物，且5个人拿到的礼物集合应该是：｛1，2｝，｛3，4｝，｛1，5｝，｛2，3｝，｛4，5｝。

【输入格式】

输入包含多组数据。每组数据的第一行为一个整数n（1≤n≤100000）；以下n行按照圈上的顺序描述每个人的需求，其中每行为一个整数ri（1≤ri≤100000），表示第i个人想要ri个不同的礼物。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出所需礼物的种类数。

【分析】

如果n为偶数，那么答案为相邻的两个人的r值之和的最大值，即p＝max｛ri＋ri＋1｝（i＝1，2，…，n），规定rn＋1＝r₁。不难看出，这个数值是答案的下限，而且还可以构造出只用p种礼物的方案：对于一个编号为i的人，如果i为奇数，发编号为1～r的礼物ri；如果i为偶数，发礼物p－ri＋1～p，请读者自己验证它是否符合要求。

n为奇数的情况比较棘手，因为上述方法不再奏效。这个时候需要二分答案：假设已知共有p种礼物，该如何分配呢？设第1个人的礼物是1～r₁，不难发现最优的分配策略一定是这样的：编号为偶数的人尽量往前取，编号为奇数的人尽量往后取。这样，编号为n的人在不冲突的前提下，尽可能地往后取了rn样东西，最后判定编号为1的人和编号为n的人是否冲突即可。比如，n＝5，A＝｛2，2，5，2，5｝，p＝8时，则第1个人取｛1，2｝，第2个人取｛3，4｝，第3个人取｛8，7，6，5，2｝，第4个人取｛1，3｝，第5个人取｛8，7，6，5，4｝，由于第1个人与第5个人不冲突，所以p＝8是可行的。

程序实现上，由于题目并不要求输出方案，因此，只需记录每个人在[1～r₁]的范围内取了几个，在[r₁＋1～n]的范围里取了几个（在程序中分别用left[i]和right[i]表示），最后判断出第n个人在[1～r₁]里面是否有取东西即可。代码如下。

1.3　高效算法设计举例

例题17　年龄排序（Age Sort, UVa 11462）

给定若干居民的年龄（都是1～100之间的整数），把它们按照从小到大的顺序输出。

【输入格式】

输入包含多组测试数据。每组数据的第一行为整数n（0＜n≤2000000），即居民总数；下一行包含n个不小于1、不大于100的整数，即各居民的年龄。输入结束标志为n＝0。

输入文件约有25MB，而内存限制只有2MB。

【输出格式】

对于每组数据，按照从小到大的顺序输出各居民的年龄，相邻年龄用单个空格隔开。

【分析】

由于数据太大，内存限制太紧（甚至都不能把它们全读进内存），因此无法使用快速排序方法。但整数范围很小，可以用计数排序方法。下面是程序代码。

上述优化使得运行时间缩短了约2/3。一般情况下，当输入输出数据量很大时，应尽量用scanf和printf函数；如果时间效率还不够高，应逐字符输入输出，就像上面的readint和writeint函数[1]。不管怎样，在确信I/O时间成为整个程序性能瓶颈之前，不要盲目优化。测试方法也很简单：输入之后不执行主算法，直接输出一个任意的结果，看看运行时间是否过长。

例题18　开放式学分制（Open Credit System, UVa 11078）

给一个长度为n的整数序列A₀，A₁，…，An－1，找出两个整数Ai和Aj（i＜j），使得Ai－Aj尽量大。

【输入格式】

输入第一行为数据组数T（T≤20）。每组数据的第一行为整数的个数n（2≤n≤100000）；以下n行，每行为一个绝对值不超过150000的整数。

【输出格式】

对于每组数据，输出Ai－Aj的最大值。

【分析】

最简单的一种方法是用二重循环，代码如下。

可惜上述算法的时间复杂度是O（n²），在n＝100000的规模面前无能为力。怎么办呢？对于每个固定的j，我们应该选择的是小于j且Ai最大的i，而和Aj的具体数值无关。这样，我们从小到大枚举j，顺便维护Ai的最大值即可。代码如下。

不难发现，上述程序的时间复杂度为O（n）。和刚才的平方算法相比，这个算法快就快在每次用O（1）时间更新了MaxAi，而不是重新计算。

如果你已经理解了这个算法，不妨思考一下，如果题目要求输出对应的i和j，应该怎么办？另外，你能不用A数组实现边读边计算么？这样可以让附加空间从O（n）降低到O（1）。

例题19　计算器谜题（Calculator Conundrum, UVa 11549）

有一个老式计算器，只能显示n位数字。有一天，你无聊了，于是输入一个整数k，然后反复平方，直到溢出。每次溢出时，计算器会显示出结果的最高n位和一个错误标记。然后清除错误标记，继续平方。如果一直这样做下去，能得到的最大数是多少？比如，当n＝1，k＝6时，计算器将依次显示6、3（36的最高位），9、8（81的最高位），6（64的最高位），3……

【输入格式】

输入的第一行为一个整数T（1≤T≤200），即测试数据的数量。以下T行，每行包含两个整数n和k（1≤n≤9，0≤k＜10n）。

【输出格式】

对于每组数据，输出你能得到的最大数。

【分析】

题目已经暗示了计算器显示出的数将出现循环（想一想，为什么），所以不妨一个一个地模拟，每次判断新得到的数是否以前出现过。如何判断呢？一种方法是把所有计算出来的数放到一个数组里，然后一一进行比较。不难发现，这样每次判断需要花费非常多的时间，相当慢。能否开一个数组vis，直接读vis[k]判断整数k是否出现过呢？很遗憾，k的范围太大，开不下。在这种情况下，一个简便的方法是利用STL的集合，代码如下。

上述程序在UVa OJ上的运行时间为4.5秒。有经验的读者应该知道，STL的string很慢，stringstream更慢，所以需要考虑把它们换掉。

上述程序的运行时间降为1秒。

当然，也可以用哈希表（详见《入门经典》的相关部分），但和set一样，空间开销比较大。有没有空间开销比较小且速度也不错的方法呢？答案是肯定的。

想象一下，假设有两个小孩子在一个“可以无限向前跑”的跑道上赛跑，同时出发，但其中一个小孩的速度是另一个的两倍。如果跑道是直的（如图1-25（a）所示），跑得快的小孩永远在前面；但如果跑道有环（如图1-25（b）所示），则跑得快的小孩将“追上”跑得慢的小孩。

图　1-25

这个算法称为Floyd判圈算法，不仅空间复杂度将降为O（1），运行时间也将缩短到0.5秒。主程序如下。

例题20　流星（Meteor, Seoul 2007, LA 3905）

给你一个矩形照相机，还有n个流星的初始位置和速度，求能照到流星最多的时刻。注意，在相机边界上的点不会被照到。如图1-26所示，流星2、3、4、5将不会被照到，因为它们从来没有经过图中矩形的内部。

图　1-26

相机的左下角为（0，0），右上角为（w，h）。每个流星用两个向量p和v表示，其中，p为初始（t＝0时）位置，v为速度。在时刻t（t＞＝0）的位置是p＋tv。比如，若p＝（1，3），v＝（-2，5），则t＝0.5时该流星的位置为（1，3）＋0.5×（-2，5）＝（0，5.5）。

【输入格式】

输入的第一行为测试数据组数T。每组数据的第一行为两个整数w和h（1≤w，h≤100000）；第二行为流星个数n（1≤n≤100000）；以下n行每行用4个整数xi，yi，ai，bi（-200000≤xi，yi≤200000，-10≤ai，bi≤10）描述一个流星，其中（xi，yi）是初始位置，（ai，bi）是速度。ai和bi不同时为0。不同流星的初始位置不同。

【输出格式】

对于每组数据，输出能照到的流星个数的最大值。

【分析】

不难发现，流星的轨迹是没有直接意义的，有意义的只是每个流星在照相机视野内出现的时间段。换句话说，我们把本题抽象为这样一个问题：给出n个开区间（Li，Ri），你的任务是求出一个数t，使得包含它的区间数最多（为什么是开区间呢？请读者思考）。开区间（Li，Ri）是指所有满足Li ＜ x ＜Ri的实数x的集合。

把所有区间画到平行于数轴的直线上（免得相互遮挡，看不清楚），然后想象有一条竖直线从左到右进行扫描，则问题可以转化为：求扫描线在哪个位置时与最多的开区间相交，如图1-27所示。

图　1-27

不难发现，当扫描线移动到某个区间左端点的“右边一点点”时最有希望和最多的开区间相交（想一想，为什么）。为了快速得知在这些位置时扫描线与多少条线段相交，我们再一次使用前面提到的技巧：维护信息，而不是重新计算。

我们把“扫描线碰到一个左端点”和“扫描线碰到一个右端点”看成是事件（event），则扫描线移动的过程就是从左到右处理各个事件的过程。每遇到一个“左端点事件”，计数器加1；每遇到一个“右端点事件”，计数器减1。这里的计数器保存的正是我们要维护的信息：扫描线和多少个开区间相交，如图1-28所示。

图　1-28

这样，我们可以写出这样一段伪代码。

这段伪代码看上去挺有道理，但实际上暗藏危险：如果不同事件的端点相同，那么哪个排在前面呢？考虑这样一种情况——输入是两个没有公共元素的开区间，且左边那个区间的右端点和右边那个区间的左端点重合。在这种情况下，两种排法的结果截然不同：如果先处理左端点事件，执行结果是2；如果先处理右端点事件，执行结果是1。这才是正确答案。

这样，我们得到了一个完整的扫描算法：先按照从左到右的顺序给事件排序，对于位置相同的事件，把右端点事件排在前面，然后执行上述伪代码的循环部分。如果你对这个冲突解决方法心存疑虑，不妨把它理解成把所有区间的右端点往左移动了一个极小（但大于0）的距离。代码如下。

另外，本题还可以完全避免实数运算，全部采用整数：只需要把代码中的double全部改成int，然后在update函数中把所有返回值乘以lcm（1，2，…，10）＝2520即可（想一想，为什么）。

例题21　子序列（Subsequence, SEERC 2006, LA 2678）

有n个正整数组成一个序列。给定整数S，求长度最短的连续序列，使它们的和大于或等于S。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n和S（10＜n≤100000，S＜10⁹）；第二行为n个正整数，均不超过10000。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出满足条件的最短序列的长度。如果不存在，输出0。

【分析】

和《开放式学分制》一样，本题最直接的思路是二重循环，枚举子序列的起点和终点。代码如下（输入数据已存入数组A[1]～A[n]）。

很可惜，上述程序的时间复杂度是O（n³）的，因此，当n达到100000的规模后，程序将无能为力。有一个方法可以降低时间复杂度，即常见的前缀和技巧。令Bi＝A₁＋A₂＋…＋Ai，规定B₀＝0，则可以在O（1）时间内求出子序列的值：Ai＋Ai＋1＋…＋Aj＝Bj－Bi－1。这样，时间复杂度降为O（n²），代码如下。

遗憾的是，本题的数据规模太大，O（n²）时间复杂度的算法也太慢。不难发现，只要同时枚举起点和终点，时间复杂度不可能比O（n²）更低，所以必须另谋他路。比如，是否可以不枚举终点，只枚举起点，或者不枚举起点，只枚举终点呢？

我们首先试试只枚举终点。对于终点j，我们的目标是要找到一个让Bj－Bi－1≥S，且i尽量大（i越大，序列长度j－i＋1就越小）的i值，也就是找一个让Bi－1≤Bj－S最大的i。考虑图1-29所示的序列。

图　1-29

当j＝5时，B₅＝12，因此目标是找一个Bi－1≤12－7＝5的最大i。注意到B是递增的（别忘了，本题中所有Ai均为整数），所以可以用二分查找。如果使用STL的话，这里的i就是lower_bound（B，B＋j，B[j]－S）。代码如下。

上面代码的时间复杂度是O（nlogn）。可以将其继续优化到O（n）。由于j是递增的，Bj也是递增的，所以Bi－1≤Bj－S的右边也是递增的。换句话说，满足条件的i的位置也是递增的。因此我们可以写出这样的程序。

这段程序的时间复杂度如何？似乎答案并不那么明显，因为它是一个二重循环：外层循环j，内层循环i。这时我们需要一点技巧，用不同方式统计不同语句的执行次数。语句（1）和（2）的执行次数为n，因为每个不同的j都执行了一次；语句（2）的执行次数有些复杂，因为不同的j对应的执行次数不一样。但我们可以从另外一个角度考虑：i从未减小，一直递增，所以递增次数一定不超过n。换句话说，整个程序的时间复杂度为O（n）。

例题22　最大子矩阵（City Game, SEERC 2004, LA 3029）

给定一个m×n的矩阵，其中一些格子是空地（F），其他是障碍（R）。找出一个全部由F组成的面积最大的子矩阵，输出其面积乘以3后的结果。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为整数m和n（1≤m，n≤1000）；以下m行每行n个字符（保证为F或者R），即输入矩阵。

【输出格式】

对于每组数据，输出面积最大的、全由F组成的矩阵的面积乘以3后的结果。

【分析】

最容易想到的算法便是：枚举左上角坐标和长、宽，然后判断这个矩形是否全为空地。这样做需要枚举O（m²n²）个矩形，判断需要O（mn）时间，总时间复杂度为O（m³n³），实在是太高了。本题虽然是矩形，但仍然可以用扫描法：从上到下扫描。

我们把每个格子向上延伸的连续空格看成一条悬线，并且用up（i，j）、left（i，j）、right（i，j）表示格子（i，j）的悬线长度以及该悬线向左、向右运动的“运动极限”，如图1-30所示。列3的悬线长度为3，向左向右各能运动一列，因此左右的运动极限分别为列2和列4。

图　1-30

这样，每个格子（i，j）对应着一个以第i行为下边界、高度为up（i，j），左右边界分别为left（i，j）和right（i，j）的矩形。不难发现，所有这些矩形中面积最大的就是题目所求（想一想，为什么）。这样，我们只需思考如何快速计算出上述3种信息即可。

当第i行第j列不是空格时，3个数组的值均为0，否则up（i，j）＝up（i－1，j）＋1。那么，left和right呢？深入思考后，可以发现：

其中lo是第i行中，第j列左边的最近障碍格的列编号。如果从左到右计算left（i，j），则很容易维护lo。right也可以同理计算，但需要从右往左计算，因为要维护第j列右边最近的障碍格的列编号ro。为了节约空间，下面的程序用up[j]，left[j]和right[j]来保存当前扫描行上的信息。

程序的时空复杂度均为O（mn）。另外，本题可以用一个栈来代替left和right数组，有兴趣的读者可以自行研究。但不管采用怎样的程序实现，上述的递推、扫描思想都是解决问题的关键。

例题23　遥远的银河（Distant Galaxy, Shanghai 2006, LA 3695）

给出平面上的n个点，找一个矩形，使得边界上包含尽量多的点。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为整数n（1≤n≤100）；以下n行每行两个整数，即各个点的坐标（坐标均为绝对值不超过10⁹的整数）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出边界点个数的最大值。

【分析】

不难发现，除非所有输入点都在同一行或者同一列上（此时答案为n），最优矩形的4条边都至少有一个点（一个角上的点同时算在两条边上）。这样，我们可以枚举4条边界所穿过的点，然后统计点数。这样做的时间复杂度为O（n⁵）（统计点数还需要O（n）时间），无法承受。

和《子序列》一题类似，可以考虑部分枚举，即只枚举矩形的上下边界，用其他方法确定左右边界，过程如图1-31所示。

图　1-31

对于竖线i，我们用left[i]表示竖线左边位于上下边界上的点数（注意，不统计位于该竖线上的点），on[i]和on2[i]表示竖线上位于上下边界之间的点数（区别在于on[i]不统计位于上下边界上的点数，而on2[i]要统计）。这样，给定左右边界i和j时，矩形边界上的点数为left[j]－left[i]＋on[i]＋on2[j]。当右边界j确定时，on[i]－left[i]应最大。

枚举完上下边界后，我们先花O（n）时间按照从左到右的顺序扫描一遍所有点，计算left、on[i]和on2[i]数组，然后枚举右边界j，同时维护on[i]－left[i]（i＜j）的最大值。这一步本质上等价于例题《开放式学分制》。代码如下。

例题24　废料堆（Garbage Heap, UVa 10755）

有个长方体形状的废料堆，由A×B×C个废料块组成，每个废料块都有一个价值，可正可负。现在要在这个长方体上选择一个子长方体，使组成这个子长方体的废料块的价值之和最大。

【输入格式】

输入的第一行为数据组数T（T≤15）。每组数据的第一行为3个整数A，B，C（1≤A，B，C≤20）。接下来有A×B×C个整数，即各个废料块的价值，每个废料块的价值的绝对值不超过2³¹。如果给每个废料块赋予一个空间坐标（一个角为（1，1，1），对角线的另一端为（A，B，C）），则这些废料块在输入文件中的出现顺序为：（1，1，1），（1，1，2），…，（1，1，C），（1，2，1），…，（1，2，C），…，（1，B，C），…，（2，1，1），…，（2，B，C），…，（A，B，C）。

【输出格式】

对于每组数据，输出最大子长方体的价值和。

【分析】

还是老规矩，先想一个正确但低效的方法。枚举x，y，z的上下界x1，x2，y1，y2，z1，z2，然后比较这O（n⁶）个长方体的价值和，而每个长方体还需要O（n³）时间累加出价值和，所以总时间复杂度为O（n⁹），即使对于n≤20这样的规模，也太大了。

解决高维问题的常见思路是降维。让我们先来看看本题的二维情况：给定一个数字矩阵，求一个和最大的连续子矩阵。借用上题的思路，我们枚举上下边界y1和y2（规定x从左到右递增，y从上到下递增），则问题转化为了一维问题，如图1-32所示。

图　1-32

注意，右图这个一维问题中的一个元素对应左图4个灰色格子的数之和。比如，（-1）＋（-5）＋1＋9＝4，（-2）＋1＋3＋（-4）＝-2等。

为了节省时间，这4个元素不能再用一重循环来累加得到，否则时间复杂度会变成O（n⁴）。我们得想办法让这些元素可以在O（1）时间内得到，这样，二维问题才能在O（n³）时间内解决。

解决方法仍然是前面曾多次使用的递推法：设sum（x，y1，y2）表示满足y1≤y≤y2的所有格子（x，y）里的数之和，则当y1＜y2时，sum（x，y1，y2）＝sum（x，y1，y2－1）＋A[x][y2]。这样，可以事先在O（n³）时间内算出整个sum数组，则所有一维问题中的元素都可以在O（1）时间内得到，完整的二维问题在O（n³）时间内得到了解决。尽管这个方法在时间效率上不错，但却占据了较大空间。

有没有一种办法可在保持O（n³）时间复杂度的同时，降低空间开销呢？办法之一就是使用二维前缀和。设S（x，y）为满足x'≤x，y'≤y的所有A[x'][y']之和，即以（x，y）为右下角的矩形中的所有元素之和，这样所有子矩形的元素之和等于4个“前缀矩形”的元素之和经过加减之后得到。如图1-33所示，黑色部分的元素之和等于以1号、4号为右下角的前缀矩形的元素和减去以2号、3号为右下角的前缀矩形的元素和。

图　1-33

这个关系也可以用来递推出整个S数组（注意，x，y都从1开始编号）。

第二个方法是边枚举边递推。此时，需要用到一个辅助数组C。先按照升序枚举y1，对于每个y1，先清空C，再按照升序枚举y2；每枚举一个新的y2，先把所有C[x]都累加A[x][y2]，然后计算数组C的最大连续和。对于给定的（y1，y2），这个C[x]实际上就是sum（x，y1，y2），但是因为及时用新数据覆盖了旧数据（那些数据再也用不到了），所以辅助空间占用仅为O（n）。

上述两种方法都可以很方便地推广到三维情形，时间复杂度为O（n⁵）。因为三维情况下的n很小，因此前面所说的空间问题并不严重。下面是算法一的完整代码，它用三维数组S保存以（x，y，z）为“右下角”的长方体的元素和。代码效率不算高，但读者很容易把它推广到四维或更高维的情形。

例题25　侏罗纪（Jurassic Remains, NEERC 2003, LA 2965）

给定n个大写字母组成的字符串。选择尽量多的串，使得每个大写字母都能出现偶数次。

【输入格式】

输入包含多组数据。每组数据的第一行为正整数n（1≤n≤24），以下n行每行包含一个大写字母组成的字符串。

【输出格式】

对于每组数据，第一行输出整数k，即字符串个数的最大值。第二行按照从小到大的顺序输出选中的k个字符串的编号（字符串按照输入顺序编号为1～n）。

【样例输入】

6

ABD

EG

GE

ABE

AC

BCD

【样例输出】

5

1 2 3 5 6

【分析】

在一个字符串中，每个字符出现的次数本身是无关紧要的，重要的只是这些次数的奇偶性，因此想到用一个二进制的位表示一个字母（1表示出现奇数次，0表示出现偶数次）。比如样例的6个数，写成二进制后如图1-34所示。

图　1-34

此时，问题转化为求尽量多的数，使得它们的xor（异或）值为0。

最容易想到的方法是直接穷举，时间复杂度为O（2n），有些偏大。注意到xor值为0的两个整数必须完全相等，我们可以把字符串分成两个部分：首先计算前n/2个字符串所能得到的所有xor值，并将其保存到一个映射S（xor值→前n/2个字符串的一个子集）中；然后枚举后n/2个字符串所能得到的所有xor值，并每次都在S中查找。

如果映射用STL的map实现，总时间复杂度为O（2n/2logn），即O（1.44nlogn），比第一种方法好了很多。这样的策略称为中途相遇法（Meet-in-the-Middle）。密码学中著名的中途相遇攻击（Meet-in-the-Middle attack）就是基于这个原理。

1.4　动态规划专题

在《算法竞赛入门经典》中，我们已经接触过了不少动态规划题目，下面简单回顾一下。如果还没有系统地学习过动态规划，建议先熟读《算法竞赛入门经典》的第9章。本节是在该章基础之上的复习、拓宽与加深。

问题1：数字三角形。如图1-35（a）所示，有一个由非负整数组成的三角形，第一行只有一个数，除了最下行之外，每个数的左下方和右下方各有一个数。从第一行的数开始，每次可以往左下或右下走一格，直到走到最下行，把沿途经过的数全部加起来。如何走，可使得这个和最大？

分析：这是一个多段图上的最短路径问题，其中每行是一个阶段。设d（i，j）为从格子（i，j）出发能得到的最大和，则d（i，j）＝a（i，j）＋max｛d（i＋1，j），d（i＋1，j＋1）｝，边界是d（n＋1，j）＝0，各个格子的编号如图1-35（b）所示。

图　1-35

问题2：嵌套矩形。有n个矩形，每个矩形可以用两个整数a，b描述，表示它的长和宽。矩形X（a，b）可以嵌套在矩形Y（c，d）中的条件为：当且仅当a＜c，b＜d，或者b＜c，a＜d（相当于把矩形X旋转90°）。例如，矩形（1，5）可以嵌套在矩形（6，2）内，但不能嵌套在矩形（3，4）内。选出尽量多的矩形排成一行，使得除了最后一个之外，每一个矩形都可以嵌套在下一个矩形内。

分析：本题是DAG最长路问题。设d（i）为以矩形i结尾的最长链的长度，则d（i）＝max｛0，d（j）｜矩形j可以嵌套在矩形i中｝＋1。

问题3：硬币问题。有n种硬币，面值分别为V₁，V₂，…，Vn，每种都有无限多。给定非负整数S，可以选用多少个硬币，使得面值之和恰好为S？输出硬币数目的最小值和最大值。其中，1≤n≤100，0≤S≤10000，1≤Vi≤S。

分析：本题是DAG最长路和最短路问题。设f（i）和g（i）分别为面值之和恰好为i时，硬币数目的最小值和最大值，则f（i）＝min｛∞，f（i－Vj）＋1｜Vj≤i｝，g（i）＝max｛-∞，g（i－Vj）＋1｜Vj≤i｝，边界条件是f（0）＝g（0）＝0。

问题4：01背包问题。有n种物品，每种只有一个。第i种物品的体积为Vi，重量为Wi。选一些物品装到一个容量为C的背包，使得背包内物品在总体积不超过C的前提下重量尽量大。其中，1≤n≤100，1≤Vi≤C≤10000，1≤Wi≤10⁶。

分析：用f（i，j）表示“把前i个物品装到容量为j的背包中的最大总重量”，则状态转移方程为f（i，j）＝max｛f（i－1，j），f（i－1，j－Vi）＋Wi｜Vi≤j｝，边界为f（0，j）＝0。可以使用滚动数组优化空间，最终程序如下。

它的道理蕴含在图1-36中。

图　1-36

f是从上到下、从右到左（而不是从左到右）计算的，所以不会覆盖到以后需要的值。

问题5：点集配对问题。空间里有n个点P₀，P₁，…，Pn－1，把它们配成n/2对（n是偶数），使得每个点恰好在一个点对中。要求所有点对中，两点的距离之和应尽量小。其中，n≤20，｜xi｜，｜yi｜，｜zi｜≤10000。

分析：设d（S）为集合S配对后的最小距离和，则

再次强调，由于S中的最小元素i无论如何都是要配对的，所以无须枚举（否则时间复杂度会多乘上一个n）。为了进一步帮助读者理解，这里画出状态转移图的一部分，如图1-37所示。

图　1-37

其中，d（｛2，3，5，6｝）＝min｛d（｛5，6｝）＋｜P₂P₃｜，d（｛3，6｝）＋｜P₂P₅｜，d（｛3，5｝）＋｜P₂P₆｜｝，d（｛1，2，3，4，5，6｝）＝min｛d（｛3，4，5，6｝）＋｜P₁P₂｜，d（｛2，4，5，6｝）＋｜P₁P₃｜，…，d（｛2，3，4，5｝）｜P₁P₆｜｝。因为1必须配对，因此｛1，2，3，4，5，6｝的这5种决策涵盖了所有情况。程序中，集合用二进制表示，在《入门经典》中已有详细描述。

问题6：最长上升子序列问题（LIS）。给定n个整数A₁，A₂，…，An，按从左到右的顺序选出尽量多的整数，组成一个上升子序列（子序列可以理解为：删除0个或多个数，其他数的顺序不变）。比如，从序列1，6，2，3，7，5中，可以选出上升子序列1，2，3，5，也可以选出1，6，7，但前者更长。选出的上升子序列中相邻元素不能相等。

分析：设d（i）为以i结尾的最长上升子序列的长度，则d（i）＝max｛0，d（j）｜j＜i，Aj＜Ai｝＋1，最终答案是max｛d（i）｝。如果LIS中的相邻元素可以相等，把小于号改成小于等于号即可。上述算法的时间复杂度为O（n²），下面介绍一种可把时间复杂度优化到O（nlogn）的方法。

假设已经计算出的两个状态a和b满足Aa＜Ab且d（a）＝d（b），则对于后续所有状态i（即i＞a且i＞b）来说，a并不会比b差——如果b满足Ab＜Ai的条件，a也满足，且二者的d值相同；但反过来却不一定了，a满足Aa＜Ai的条件时，b却不一定满足。换句话说，如果我们只保留a，一定不会丢失最优解。

这样，对于相同的d值，只需保留A最小的一个。我们用g（i）表示d值为i的最小状态编号[2]（如果不存在，g（i）定义为正无穷）。根据上述推理可以证明

需要特别注意的是，上述g值是动态改变的。对于一个给定的状态i，我们只考虑在i之前已经计算过的状态j（即j＜i），上述g序列也是基于这些状态的。随着i的不断增大，我们要考虑的状态越来越多，g也随之发生改变。在给定状态i时，可以用二分查找得到满足g（k）≥Ai的第一个下标k，则d（i）＝k[3]，此时Ai＜g（k），而d（i）＝k，所以更新g（k）＝Ai。

问题7：最长公共子序列问题（LCS）。给出两个子序列A和B，如图1-38所示，求长度最大的公共子序列。比如，1，5，2，6，8，7和2，3，5，6，9，8，4的最长公共子序列为5，6，8（另一个解是2，6，8）。

图　1-38

分析：设d（i，j）为A₁，A₂，…，Ai和B₁，B₂，…，Bj的LCS长度，则当A[i]＝B[j]时，d（i，j）＝d（i－1，j－1）＋1；否则，d（i，j）＝max｛d（i－1，j），d（i，j－1）｝。时间复杂度为O（nm）[4]，其中n和m分别是序列A和B的长度。LCS问题也可以用滚动数组法进行优化。

问题8：最大连续和。给出一个长度为n的序列A₁，A₂，…，An，求一个连续子序列Ai，Ai＋1，…Aj，使得元素总和Ai＋Ai＋1＋…＋Aj最大。

分析：本题在《算法竞赛入门经典》中已经给出了一个利用前缀和的线性时间算法。用动态规划可以得到另一个线性算法：设d（i）为以i结尾的最大连续和，则d（i）＝max｛0，d（i－1）｝＋A[i]。

问题9：货郎担问题（TSP）。有n个城市，两两之间均有道路直接相连。给出每两个城市i和j之间的道路长度Li，j，求一条经过每个城市一次且仅一次，最后回到起点的路线，使得经过的道路总长度最短。其中，n≤15，城市编号为0～n－1。

分析：TSP是一道经典的NPC难题，不过因为本题规模小，可以用动态规划求解。首先注意到可以直接规定起点和终点为城市0（想一想，为什么），然后设d（i，S）表示当前在城市i，访问集合S中的城市各一次后回到城市0的最短长度，则

边界为d（i，｛｝）＝dist（0，i）。最终答案是d（0，｛1，2，3，…，n－1｝），时间复杂度为O（n²2n）。

问题10：矩阵链乘（MCM）。一个n×m矩阵由n行m列共n×m个数排列而成。两个矩阵A和B可以相乘的条件为：当且仅当A的列数等于B的行数。一个n×m的矩阵乘以一个m×p的矩阵等于一个n×p的矩阵，运算量为mnp。

矩阵乘法不满足分配律，但满足结合律，因此A×B×C既可以按顺序（A×B）×C进行，也可以按A×（B×C）来进行。假设A、B、C分别是2×3，3×4和4×5矩阵，则（A×B）×C的运算量为2×3×4＋2×4×5＝64，A×（B×C）的运算量为3×4×5＋2×3×5＝90。显然，第一种运算顺序更节省运算量。

给出n个矩阵组成的序列，设计一种方法把它们依次相乘，使得总运算量最小。假设第i个矩阵Ai是pi－1×pi的。

分析：设f（i，j）表示把Ai，Ai＋1，…，Aj乘起来所需要的乘法次数，枚举“最后一次乘法”是第k个乘号，则f（i，j）＝max｛f（i，k）＋f（k＋1，j）＋pi－1pkpj｝，边界是f（i，i）＝0，时间复杂度为O（n³）[5]。

问题11：最优排序二叉树问题（OBST）。给n个符号建立一棵排序二叉树[6]。虽然平衡树的高度最小，但如果各个符号的频率相差很大，平衡反而不好。比如，若有7个符号ABCDEFG，频率分别为729，243，81，27，9，3，1，如图1-39所示，则下面的平衡树的总检索次数（即所有关键字频率和深度的乘积之和）为27×1＋（243＋2）×2＋（729＋81＋9＋1）×3＝2977。

图　1-39

相比之下，图1-40所示的链状树反而好得多。

图　1-40

它的总检索次数仅为1636次。给定n个关键字的频率f₁，f₂，…，fn，要求构造一棵最优的排序二叉树，使得每个关键字的频率和深度的乘积之和最小。

分析：根据排序二叉树的递归定义，可以先选根，然后递归建立左右子树。记d（i，j）为符号i，i＋1，…，j所建立的排序二叉树的最小检索次数。如果选根为k，总检索次数应该如何计算？

树根k只需要检索一次，累加上fk。左子树在单独作为一棵树时，其总检索次数为d（i，k－1）；但在作为k的子树后，所有结点的深度都增加了1，因此总检索次数需要加上fi＋fi＋1＋…＋fk－1。右子树类似。这样，若记w（i，j）＝fi＋fi＋1＋…＋fj，状态转移方程为d（i，j）＝max｛d（i，k－1）＋d（k＋1，j）｝＋w（i，j）。状态有O（n²）个，每个状态的决策有O（n）个，总时间复杂度为O（n³）。

有一个方法可以把时间复杂度降为O（n²）。记K（i，j）为让d（i，j）取到最小值的决策，则可以证明K（i，j）≤K（i，j＋1）≤K（i＋1，j＋1）（i≤j），即K在同行和同列上都是递增的。证明需要用到四边形不等式，这里略去，有兴趣的读者可以自行查阅相关资料。

有了这个结论，我们在计算d（i，j）时，只需把决策枚举从i～j改成从K（i，j－1）～K（i＋1，j）即可。注意到后面两个状态都在d（i，j）之前已经算过，所以K（i，j－1）和K（i＋1，j）已经得到。

下面分析时间复杂度。当L＝j－i固定时，

d（1，L＋1）的决策是K（1，L）～K（2，L＋1）

d（2，L＋2）的决策是K（2，L＋1）～K（3，L＋2）

d（3，L＋3）的决策是K（3，L＋2）～K（4，L＋3）

…

全部合并起来，当L固定时的总决策为K（1，L）～K（n－L＋1，n），共O（n）个。由于L有O（n）个，总时间复杂度降为O（n²）。

例题26　约瑟夫问题的变形（And Then There Was One, Japan 2007, LA 3882）

n个数排成一个圈。第一次删除m，以后每数k个数删除一次，求最后一个被删除的数。当n＝8，k＝5，m＝3时，删数过程如图1-41所示。

图　1-41

【输入格式】

输入包含多组数据。每组数据包含3个整数n，k，m（2≤n≤10000，1≤k≤10000，1≤m≤n）。输入结束标志为n＝k＝m＝0。

【输出格式】

对于每组数据，输出最后一个被删除的数。

【分析】

本题是约瑟夫问题的变种，唯一的区别就是：原版问题中，从1开始数数，而在本题中，规定第一个删除的数是m。约瑟夫问题作为链表的经典应用，出现在很多数据结构与程序设计语言的书籍中。可惜链表法的时间复杂度为O（nk），无法承受本题这样大的规模。

如果像本题这样只关心最后一个被删除的编号，而不需要完整的删除顺序，则可以用递推法求解。假设编号为0～n－1的n个数排成一圈，从0开始每k个数删除一个，最后留下的数字编号记为f（n），则f（1）＝0，f（n）＝（f（n－1）＋k）％n。为什么呢？因为删除一个元素之后，可以把所有元素重新编号，如图1-42所示。

图　1-42

本题第一个删除的为m，因此答案为（m－k＋1＋f[n]）％n[7]。注意，本题虽然不是动态规划，但思路是相通的[8]。代码如下。

例题27　王子和公主（Prince and Princess, UVa 10635）

有两个长度分别为p＋1和q＋1的序列，每个序列中的各个元素互不相同，且都是1～n²之间的整数。两个序列的第一个元素均为1。求出A和B的最长公共子序列长度。

【输入格式】

输入的第一行为数据组数T（T≤10）。每组数据包含3行，第一行为3个整数n，p，q（2≤n≤250，1≤p，q≤n²）；第二行包含序列A，其中第一个数为1，其元素两两不同，且都是1～n²之间的整数；第三行包含序列B，格式同序列A。

【输出格式】

对于每组数据，输出A和B的最长公共子序列的长度。

【分析】

本题是LCS问题，但因为p和q可以高达250²＝62500，O（pq）的算法显然太慢。注意到A序列中所有元素均不相同，因此可以把A中元素重新编号为1～p＋1。例如，样例中A＝｛1，7，5，4，8，3，9｝，B＝｛1，4，3，5，6，2，8，9｝，因此把A重新编号为｛1，2，3，4，5，6，7｝，则B就是｛1，4，6，3，0，0，5，7｝，其中0表示A中没有出现过（事实上，可以直接删除这些元素，因为它们肯定不在LCS中）。这样，新的A和B的LCS实际上就是新的B的LIS。由于LIS可在O（nlogn）时间内解决，因此本题也可在O（nlogn）时间内得到解决。代码如下。

例题28　Sum游戏（Game of Sum, UVa 10891）

有一个长度为n的整数序列，两个游戏者A和B轮流取数，A先取。每次玩家只能从左端或者右端取一个数，但不能两端都取。所有数都被取走后游戏结束，然后统计每个人取走的所有数之和，作为各自的得分。两个人采取的策略都是让自己的得分尽量高，并且两人都足够聪明，求A的得分减去B的得分后的结果。

【输入格式】

输入包含多组数据。每组数据的第一行为正整数n（1≤n≤100），第二行为给定的整数序列。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出A和B都采取最优策略的情况下，A的得分减去B的得分后的结果。

【分析】

整数的总和是一定的，所以一个人得分越高，另一个人的得分就越低。不管怎么取，任意时刻游戏的状态都是原始序列的一段连续子序列（即被两个玩家取剩下的序列）。因此，我们想到用d（i，j）表示原序列的第i～j个元素组成的子序列（元素编号为1～n），在双方都采取最优策略的情况下，先手得分的最大值（只考虑i～j这些元素）。

状态转移时，我们需要枚举从左边取还是从右边取以及取多少个。这等价于枚举给对方剩下怎样的子序列：是（k，j）（i＜k≤j），还是（i，k）（i≤k＜j）。因此：

其中，sum（i，j）是元素i到元素j的数之和。注意，这里的“0”是“取完所有数”的决策，有了它，方程就不需要显式的边界条件了。

两人得分之和为sum（1，n），因此答案是d（1，n）－（sum（1，n）－d（1，n））＝2d（1，n）－sum（1，n）。注意，sum（i，j）的计算不需要循环累加，可以预处理S[i]为前i个数之和，则sum（i，j）＝S[j]－S[i－1]。

下面是完整代码。它采用了记忆化搜索的方式，显得更加自然。

状态有O（n²）个，每个状态有O（n）个转移，所以时间复杂度为O（n³），空间复杂度为O（n²）。对于本题的规模，这样的时间复杂度已经不错了，但其实还可以进一步改进。让我们回顾一下状态转移方程：

如果令f（i，j）＝min｛d（i，j），d（i＋1，j），…，d（j，j）｝，g（i，j）＝min｛d（i，j），d（i，j－1），…，d（i，i）｝，则状态转移方程可以写成：

f和g也可以快速递推出来：f（i，j）＝min｛d（i，j），f（i＋1，j）｝，g（i，j）＝min｛d（i，j），g（i，j－1）｝，因此每个f（i，j）的计算时间都降为了O（1）。下面我们用递推（而非记忆化搜索）的方法编写。代码如下。

新算法的时间复杂度为O（n²）。

例题29　黑客的攻击（Hacker's Crackdown, UVa 11825）

假设你是一个黑客，侵入了一个有着n台计算机（编号为0，1，…，n－1）的网络。一共有n种服务，每台计算机都运行着所有服务。对于每台计算机，你都可以选择一项服务，终止这台计算机和所有与它相邻计算机的该项服务（如果其中一些服务已经停止，则这些服务继续处于停止状态）。你的目标是让尽量多的服务器完全瘫痪（即：没有任何计算机运行该项服务）。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤16）；以下n行每行描述一台计算机的相邻计算机，其中第一个数m为相邻计算机个数，接下来的m个整数为这些计算机的编号。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出完全瘫痪的服务器的最大数量。

【分析】

本题的数学模型是：把n个集合P₁，P₂，…，Pn分成尽量多组，使得每组中所有集合的并集等于全集。这里的集合Pi就是计算机i及其相邻计算机的集合，每组对应于题目中的一项服务。注意到n很小，可以用《算法竞赛入门经典》中提到的二进制法表示这些集合，即在代码中，每个集合Pi实际上是一个非负整数。输入部分代码如下。

为了方便，我们用cover（S）表示若干Pi的集合S中所有Pi的并集（二进制表示），即这些Pi在数值上的“按位或”。

不难想到这样的动态规划：用f（S）表示子集S最多可以分成多少组，则

这里有一个重要的技巧：枚举S的子集S₀。详见下面的代码。

如何分析上述算法的时间复杂度呢？它等于全集｛1，2，…，n｝的所有子集的子集个数之和，也可以令c（S）表示集S的子集的个数（它等于2｜S｜），则本题的时间复杂度为sum｛c（S₀）｜S₀是｛1，2，3，…，n｝的子集｝。

注意到元素个数相同的集合，其子集个数也相同，我们可以按照元素个数“合并同类项”。元素个数为k的集合有C（n，k）个，其中每个集合有2k个子集，因此本题的时间复杂度为sum｛C（n，k）2k｝＝（2＋1）n＝3n，其中第一个等号用到了二项式定理（不过是反着用的）。

本题比较抽象，但对思维训练很有帮助，希望读者花点时间将它彻底搞懂。

例题30　放置街灯（Placing Lampposts, UVa 10859）

给你一个n个点m条边的无向无环图，在尽量少的结点上放灯，使得所有边都被照亮。每盏灯将照亮以它为一个端点的所有边。在灯的总数最小的前提下，被两盏灯同时照亮的边数应尽量大。

【输入格式】

输入的第一行为测试数据组数T（T≤30）。每组数据第一行为两个整数n和m（m＜n≤1000），即点数（所有点编号为0～n－1）和边数；以下m行每行为两个不同的整数a和b，表示有一条边连接a和b（0≤a，b≤n）。

【输出格式】

对于每组数据，输出3个整数，即灯的总数、被两个灯照亮的边数和只被一个灯照亮的边数。

【分析】

无向无环图的另一个说法是“森林”，它由多棵树组成。动态规划是解决树上的优化问题的常用工具，本题就是一个很好的例子。

首先，本题的优化目标有两个：放置的灯数a应尽量少，被两盏灯同时照亮的边数b应尽量大。为了统一起见，我们把后者替换为：恰好被一盏灯照亮的边数c应尽量小，然后改用x＝Ma＋c作为最小化的目标，其中M是一个很大的正整数。当x取到最小值时，x/M的整数部分就是放置的灯数的最小值；x％M就是恰好被一盏灯照亮的边数的最小值。

一般来说，如果有两个需要优化的量v₁和v₂，要求首先满足v₁最小，在v₁相同的情况下v₂最小，则可以把二者组合成一个量Mv₁＋v₂，其中M是一个比“v₂的最大理论值和v₂的最小理论值之差”还要大的数。这样，只要两个解的v₁不同，则不管v₂相差多少，都是v₁起到决定性作用；只有当v₁相同时，才取决于v₂。在本题中，可以取M＝2000[9]。

每棵树的街灯互不相干，因此可以单独优化，最后再把答案加起来即可。下面我们只考虑一棵树的情况。首先对这棵树进行DFS，把无根树转化为有根树，然后试着设状态d（i）为以i为根的子树的最小x值，看看能不能写出状态转移方程。

决策只有两种：在i处放灯和不在i处放灯。后继状态是i的各个子结点。可是问题来了：i处是否放灯将影响到其子结点的决策。因此，我们需要把“父结点处有没有放灯”加入状态表示中。新状态为：d（i，j）表示i的父结点“是否放灯”的值为j（1表示放灯，0表示没放），以i为根的树的最小x值（算上i和其父结点这条边）。

注意到各子树可以独立决策，因此可作出如下决策。

决策一：结点i不放灯。必须j＝1或者i是根结点时才允许作这个决策。此时d（i，j）等于sum｛d（k，0）｜k取遍i的所有子结点｝。如果i不是根，还得加上1，因为结点i和其父结点这条边上只有一盏灯照亮。

决策二：结点i放灯。此时d（i，j）等于sum｛d（k，1）｜k取遍i的所有子结点｝＋M。如果j＝0且i不是根，还得加上1，因为结点i和其父结点这条边只有一盏灯照亮。

用数学式子很难表达上面的状态转移，但用程序表达却可以很清晰。

例题31　捡垃圾的机器人（Robotruck, SWERC 2007, LA 3983）

有n个垃圾，第i个垃圾的坐标为（xi，yi），重量为wi。有一个机器人，要按照编号从小到大的顺序捡起所有垃圾并扔进垃圾桶（垃圾桶在原点（0，0））。机器人可以捡起几个垃圾以后一起扔掉，但任何时候其手中的垃圾总重量不能超过最大载重C。两点间的行走距离为曼哈顿距离（即横坐标之差的绝对值加上纵坐标之差的绝对值）。求出机器人行走的最短总路程（一开始，机器人在（0，0）处）。

【输入格式】

输入的第一行为数据组数。每组数据的第一行为最大承重C（1≤C≤100）；第二行为正整数n（1≤n≤100000），即垃圾的数量；以下n行每行为两个非负整数x，y和一个正整数w，即坐标和重量（重量保证不超过C）。

【输出格式】

对于每组数据，输出总路径的最短长度。

【分析】

如果把“当前垃圾序号”和“当前载重量”作为状态，则状态个数就已经高达O（NC），不管怎样优化状态转移，时间也无法承受。迫不得已，我们只得设d（i）为从原点出发、将前i个垃圾清理完并放进垃圾筒的最小距离，则

其中，dist（j＋1，i）表示从第j＋1个垃圾出发，依次经过垃圾j＋2、垃圾j＋3、…、最终到达垃圾i的总距离，dist2origin（i）表示垃圾i到原点的距离（即｜xi｜＋｜yi｜），w（i，j）表示第i～j个垃圾的总重量。

设total_dist（i）为从第1个垃圾开始，依次经过垃圾2，3，…，最终到达垃圾i的总距离，则dist（j＋1，i）＝total_dist（i）－total_dist（j＋1）。这样，上式可以改写为：

d[i]＝min｛d[j]－total_dist（j＋1）＋dist2origin（j＋1）｜w（j＋1，i）≤C｝＋total_dist（i）＋dist2origin（i）

如果令func（j）＝d[j]－total_dist（j＋1）＋dist2origin（j＋1），上式还可以进一步简化为

其中，阴影部分是问题的关键。注意到满足w（j＋1，i）≤C的所有j形成一个区间，而且随着i的增大，这个区间会往右移动（因为所有wi均为正数），我们常常把这个区间称为滑动窗口，则问题就转化为：维护一个滑动窗口中的最小值。

当滑动窗口的右边界增大时，相当于往滑动窗口里添加新元素；当滑动窗口的左边界增大时，相当于往滑动窗口里删除元素。这样，我们可以用一个数据结构维护滑动窗口，要求支持插入、删除、取最小值。在学习完本书的数据结构部分后，相信读者能够找到一个合适的数据结构，在O（logn）时间内完成上述3种操作。但其实这并不是最高效的方法。

假设滑动窗口中有两个元素1和2，且1在2的右边，会怎样？这意味着2在离开窗口之前一直会被1给“压迫着”，永远不可能成为最小值。换句话说，这个2是无用的，应当及时删除。当删除掉无用元素之后，滑动窗口中剩下的东西（有用元素）从左到右是递增的。我们把这些元素看成一个队列[10]，每次有新元素进来时，需要删除所有比新元素大的元素，如图1-43所示。

图　1-43

还需要及时把不在滑动窗口范围之内的元素移出队列。读者可能会问，如果老是要删除很多元素怎么办，时间复杂度会不会很差？不会的，因为每个元素最多被删除一次，所以总时间复杂度仍是O（n）。

例题32　分享巧克力（Sharing Chocolate, World Finals 2010, LA 4794）

给出一块长为x，宽为y的矩形巧克力，每次操作可以沿一条直线把一块巧克力切割成两块长宽均为整数的巧克力（一次不能同时切割多块巧克力）。

问：是否可以经过若干次操作得到n块面积分别为a₁，a₂，…，an的巧克力。如图1-44所示，可以把3×4的巧克力切成面积分别为6，3，2，1的4块。

图　1-44

【输入格式】

输入包含若干组数据。每组数据的第一行为一个整数n（1≤n≤15）；第二行为两个整数x和y（1≤x，y≤100）；第三行为n个整数a₁，a₂，…，an。输入结束标志为n＝0。

【输出格式】

对于每组数据，如果可以切割成功，输出“Yes”，否则输出“No”。

【分析】

注意到n的规模很小，可以把与n有关的子集作为动态规划状态的一部分。设f（r，c，S）表示r行c列的巧克力是否可以切割成面积集合S。样例1的答案为Yes，即f（3，4，｛6，3，2，1｝）＝1。第一刀把巧克力切成了3×3和3×1两块，即f（3，3，｛6，2，1｝）＝f（3，1，｛3｝）＝1。

不难得到下面的状态转移规则：f（r，c，S）＝1当且仅当

❑　存在1≤r₀＜r和S的子集S₀，使得f（r₀，c，S₀）＝f（r－r₀，c，S－S₀）＝1，或者

❑　存在1≤c₀＜c和S的子集S₀，使得f（r，c₀，S₀）＝f（r，c－c₀，S－S₀）＝1。

前者对应横着切，后者对应竖着切。状态有O（xy2n）个，每个状态转移到O（x＋y）个状态，总时间复杂度为O（（x＋y）xy2n），有些偏大。

其实，上述状态有些浪费。如果r×c不等于S中所有元素之和（记为sum（S）），显然f（r，c，S）＝0。换句话说，我们可以只计算r*c＝sum（S）的状态f（r，c，S）。另外，f（r，c，S）＝f（c，r，S），所以不妨设r≤c，然后用g（r，S）代替f（r，c，S）。这样，状态降为了O（x2n）个。在枚举决策时，一旦确定了S₀，实际上可以计算出r₀或者c₀（或者发现不存在这样的r₀或者c₀），因此总决策数为O（x3n），这也是本算法的时间复杂度。由于很多状态达不到，推荐用记忆化搜索实现，实际运算量往往远小于O（x3n）。

最后有一点需要注意，输入之后需要比较所有ai之和是否为x×y（想一想，为什么）。

代码如下。

1.5　小结与习题

本章介绍了不少问题求解和算法设计的方法和技巧。这些内容有难有易，不必强求第一次就全部看懂，需要反复阅读、细心体会。

1．问题求解策略

本章介绍了贪心法、暴力法、二分法等常用算法，以及各种思维方式。表1-1中列出了本章中的例题。

表　1-1

仅完成书中的例题还远远不够，下面将给出一定数量的习题，以方便读者练习和提高。限于篇幅，有些输入输出格式较为简单的题目省略了“输入格式”和“输出格式”两个部分，读者可自行参考英文原题。

首先是一些入门习题，如表1-2所示。

表　1-2

接下来是需要多一些思考的题目，如表1-3所示。

表　1-3

你好　世界！（Hello World!, UVa 11636）

你刚刚学会用“printf（"Hello World!\n"）”向世界问好了，因此非常兴奋，希望输出n条“Hello World”信息，但你还没有学习循环语句，因此只能通过复制／粘贴的方式用n条printf语句来解决。比如，经过一次复制／粘贴后，一条语句会变成两条语句，再经过一次复制／粘贴后，两条语句会变成4条语句……至少需要复制／粘贴几次，才能使语句的条数恰好为n？输入n（0＜n＜10001），输出最小复制／粘贴的次数。

提示：每次可以只复制／粘贴一部分语句。

设计建筑物（Building Designing, UVa 11039）

有n个绝对值各不相同的非0整数，选出尽量多的数，排成一个序列，使得正负号交替，且绝对值递增。输入整数n（1≤n≤500000）和n个整数，输出最长序列长度。

古老的密码（Ancient Cipher, NEERC 2004, LA 3213）

给定两个长度均为n的字符串，判断它们之间的26个字母能否一一对应，即做一个一一映射后使得两个字符串相同（比如ABB和CDD可以一一对应，方法是A→C，B→D，但ABC和DED不能一一对应）。输入两个字符串，输出YES或者NO。

DNA序列（DNA Consensus String, Seoul 2006, LA 3602）

给定m个长度均为n的DNA序列，求一个DNA序列，使其到所有序列的总Hamming距离尽量小。两个等长字符串的Hamming距离等于字符不同的位置个数。如有多解，求字典序最小的解。输入整数m和n（4≤m≤50，4≤n≤1000），以及m个长度为n的DNA序列（只包含字母A、C、G、T），输出让Hamming距离最小的DNA序列和其对应的距离。

大块巧克力（Big Chocolate, UVa 10970）

把一个m行n列的矩形巧克力切成mn个1×1的方块，需要切几刀？每刀只能沿着直线把一块巧克力切成两部分（不能用一刀同时去切两块巧克力）。输入m和n，输出最少需要的刀数。

子序列（All in All, UVa 10340）

输入两个字符串s和t，判断是否可以从t中删除0个或多个字符（其他字符顺序不变），得到字符串s。比如，abcde可以得到bce，但无法得到dc。

喷水装置（Watering Grass, UVa 10382）

有一块草坪，长为l，宽为w。在其中心线的不同位置处装有n个点状的喷水装置。每个喷水装置i可将以它为中心，半径为ri的圆形区域润湿（见图1-45）。请选择尽量少的喷水装置，把整个草坪全部润湿。

图　1-45

【输入格式】

输入包含多组数据。每组数据的第一行为整数n、l、w（1≤n≤10000）；以下n行每行包含两个整数pi和ri，即每个喷水装置的位置和半径。

【输出格式】

对于每组数据，输出需要打开的喷水装置数目的最小值。如果无解，输出-1。

孩子们的游戏（Children's Game, UVa 10905）

给定n个正整数，你的任务是把它们连接成一个最大的整数。比如，123、124、56、90有24种连接方法，最大的结果是9056124123。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤50）；第二行为n个正整数。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出可以得到的最大整数。

处理器（Processor, Seoul 2008, LA 4254）

有n个任务，每个任务有3个参数ri、di和wi，表示必须在时刻[ri，di]之内执行，工作量为wi。处理器执行的速度可以变化，当执行速度为s时，一个工作量为wi的任务需要执行wi/s个单位时间。另外，任务不一定要连续执行，可以分成若干块。你的任务是求出处理器在执行过程中最大速度的最小值。处理器速度可以是任意整数值。

假设有5个任务，ri和di分别是[1，4]，[3，6]，[4，5]，[4，7]，[5，8]，工作量分别为2，3，2，2，1，则图1-46是一个最优执行方案，最大速度为2。

图　1-46

【输入格式】

输入的第一行为数据组数T（T≤20）。每组数据的第一行为整数n（1≤n≤10000）；以下n行每行包含3个整数ri，di，wi（1≤ri＜di≤20000，1≤wi≤1000）。

【输出格式】

对于每组数据，输出在执行过程中处理器最大速度的最小值。

障碍滑雪比赛（Slalom, UVa 11627）

在一场滑雪比赛中，你需要通过n个旗门（均可看成水平线段）。第i个旗门左端的坐标为（xi，yi），所有旗门的宽度均为W。旗门海拔高度严格递减，即对所有1≤i＜n满足yi＜yi＋1。你有S双滑雪板，第j双的速度为sj（即向下滑行速度为sj米／秒）。你的水平速度在任何时刻都不能超过vh米／秒，但可以任意变速。如果起点和终点的水平坐标可以任意选择，用哪些滑雪板可以顺利通过所有旗门？

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为3个整数W、vh和N（1≤W≤10⁸，1≤vh≤10⁶，1≤N≤10⁵）；以下N行每行为两个整数xi和yi，即每个旗门左端的坐标（1≤xi，yi≤10⁸）。下一行包含一个整数S，即滑雪板的数量（1≤S≤10⁶）；以下S行每行一个整数sj，即每双滑雪板的速度（1≤sj≤10⁶）。

【输出格式】

对于每组数据，输出可以通过所有旗门的滑雪板的最大速度。

传说中的车（Fabled Rooks, UVa 11134）

你的任务是在n×n棋盘上放n辆车，使得任意两辆车不相互攻击，且第i辆车在一个给定的矩形Ri之内。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤5000）；以下n行每行用4个整数xli，yli，xri，yri（1≤xli≤xri≤n，1≤yli≤yri≤n）描述一个矩形，其中（xli，yli）是左上角坐标，（xri，yri）是右下角坐标。换句话说，第i个车的位置（x，y）必须满足xli≤x≤xri和yli≤y≤yri。输入结束标志为n＝0。

【输出格式】

对于每组数据，如果无解，输出IMPOSSIBLE；否则，输出n行，依次为第1，2，…，n个车的坐标。

旅行2007（The Trip, 2007, UVa 11100）

给定n个正整数，把它们划分成尽量少的严格递增序列（前一个数必须小于后一个数）。比如，6个正整数1，1，2，2，2，3至少要分成3个序列：｛1，2｝，｛1，2｝和｛2，3｝。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤10000），第二行是n个不超过1000000的正整数。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出序列个数的最小值k和这k个序列。如果有多种划分方法，任何一组解均可。

机场（Airport, Seoul 2009, LA 4725）

有一个客流量巨大的机场，却只有一条起飞跑道，如图1-47所示。

图　1-47

换句话说，每个时刻只能有一架飞机起飞（从E或者W通道进入起飞跑道），每个时刻也都有一些飞机到达E或者W通道中。在任意时刻，E通道和W通道里的飞机分别从0开始编号（图1-47中，E通道里的飞机编号为0和1，W通道里的飞机编号为0，1，2）。你的任务是在每个时刻选择一架飞机起飞，使得任意时刻飞机的最大编号最小。

例如，若飞机到达方式如表1-4所示。

表　1-4

最优策略是这样的：时刻1，飞机A1，A2，A3编号为0，1，2，飞机B1，B2编号为0，1，然后让B1起飞；时刻2，飞机B3，B4，B5编号为1，2，3，然后让A1起飞；时刻3，A4和A5编号为2，3。这样，飞机的最大编号为3，是所有可能的方案中最小的。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为时刻总数n（1≤n≤5000）；以下n行每行两个整数ai和bi（0≤ai，bi≤20），分别是该时刻到达W通道和E通道的飞机数量。

【输出格式】

对于每组数据，输出飞机最大编号的最小值。

安装服务（Installations, Daejon 2010, LA 4850）

工程师要安装n个服务，其中第i个服务Ji需要si单位的安装时间，截止时间为di。如果在截止时间之前完成任务，不会有任何惩罚；否则惩罚值为任务完成时间与截止时间之差。换句话说，如果实际完成时间为Ci，则惩罚值为max｛0，Ci－di｝。从t＝0时刻开始执行任务，但同一时刻只能执行一个任务。你的任务是让惩罚值最大的两个任务的惩罚值之和最小。

假定有两个任务，安装时间si和截止时间di所组成的二元组（si，di）分别为（1，7），（4，7），（2，4），（2，15），（3，5），（6，8）。如图1-48所示描述了一个最优解，其中惩罚值最小的两个任务分别为J₂和J₆，二者的惩罚值之和为6＋1＝7。

图　1-48

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为整数n（1≤n≤500）；以下n行每行两个整数si和di，即任务Ji的安装时间和截止时间（1≤si≤di≤10000）。

【输出格式】

对于每组数据，输出两个最大惩罚值之和的最小值。

田忌赛马（Tian Ji - The Horse Racing, Shanghai 2004, LA 3266）

田忌与齐王赛马，两人各出N匹马。赢一场比赛得200两银子，输了赔200银子，平局不赔不赚。已知两人每匹马的速度，问田忌至多能赢多少两银子。

【输入格式】

输入包含最多50组数据。每组数据的第一行为一个整数n（n≤1000）；第二行包含n个正整数，即田忌每匹马的速度；第三行包含n个正整数，即齐王每匹马的速度。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出田忌最多能赢多少两银子。

巴士司机问题（The Bus Driver Problem, UVa 11389）

有n个司机、n个下午路线和n个夜间路线。给每个司机安排一个下午路线和夜间路线，使得每条路线恰好被分配到一个司机，且需要支付给司机的总加班费尽量少。如果一个司机的行驶总时间（下午路线的时间与夜间路线的时间之和）不超过d，则没有加班费；超出的部分每小时需要支付r元的加班费。

【输入格式】

输入包含多组数据。每组数据的第一行包含3个整数n，d，r（1≤n≤100，1≤d≤10000，1≤r≤5）；第二行包含n个整数，即各条下午路线的行驶时间；第三行包含n个整数，即各夜间路线的行驶时间。所有行驶时间均为不超过10000的正整数。输入结束标志为n＝d＝r＝0。

【输出格式】

对于每组数据，输出最小总加班费。

梦之队（Wonder Team, Tehran 2007, LA 4094）

有n（1≤n≤50）支队伍比赛，每两支队伍打两场（主客场各一次），胜得3分，平得1分，输不得分。比赛结束之后会评选出一个梦之队（也可能空缺），它满足如下条件：进球总数最多（不能并列），胜利场数最多（不能并列），丢球总数最少（不能并列）。

求梦之队的最低可能排名。一支得分为p的球队的排名等于得分严格大于p的球队个数加1。

积木艺术（Cubist Artwork, Tokyo 2009, LA 4636）

用一些等大的立方体搭积木，每个立方体或者直接放在地面的网格上，或者放在另一个立方体的上面，给出正视图和侧视图，如图1-49所示。你的任务是判断最少要用多少个立方体。

图　1-49

图1-50是两种可能的方案，其中图1-50（b）所示的是最优方案（立方体数目最少）。

图　1-50

最后是一些需要暴力求解的题目，见表1-5。其中有些题目难度较大，请读者根据实际情况选择适合自己的题目完成，不必勉强。

表　1-5

箭术（Archery, Seoul 2008, LA 4253）

有n个平行于x轴的线段，每条线段代表一个靶子。你的任务是判断是否可以站在x轴上[0，W]区间内的某个位置射箭，使得箭能穿过所有靶子。假设箭沿直线飞行，直到无穷远处。不同靶子的高度（y坐标）不同。

如图1-51所示，站在B点可以射穿所有靶子，但站在A点不行。

图　1-51

【输入格式】

输入的第一行为数据组数T（T≤30）。每组数据的第一行为整数W（2≤W≤10000000）；第二行为整数n（2≤n≤5000），即靶子的个数，以下n行每行3个整数Di，Li，Ri（1≤Di≤W，0≤Li＜Ri≤W），表示有一个高度为Di，左右端点的x坐标分别为Li和Ri的靶子。不同靶子的高度Di保证不同。

【输出格式】

对于每组数据，如果可以射穿所有靶子，输出YES，否则输出NO。

刻度尺（Ruler, Beijing 2006, LA 3667）

给出n种距离di，设计一个有m个刻度的尺子，使得每个di都可以直接量出来（即存在某两个刻度之间的距离恰好为di）。要求在m尽量小的前提下保证尺子的总长度尽量短。

【输入格式】

输入包含多组数据。每组数据包含两行，第一行为一个整数n（1≤n≤50），第二行包含n个整数di（1≤di≤10⁶）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出m和这m个刻度（从小到大排列，第一个数必须为0）。输入保证m≤7。

美味的三角匹萨（Yummy Triangular Pizza, Shanghai 2011, LA5704）

用n（1≤n≤16）个等大的等边三角形，可以组成多少个形状不同的匹萨？匹萨必须是连通的，中间可以有洞。平移或旋转（不能翻转）之后能重合的只算一种。例如，n＝4时有4种组合方案，如图1-52所示。n＝10时有866种组合方案。

图　1-52

神奇的乘法（Anagram and Multiplication, UVa 10825）

有些m位n进制整数非常神奇：乘以2，3，…，m之后，所得到的数恰好是原数各数字的一个排列。比如，142857就是这样一个十进制6位整数。

2×142857＝285714

3×142857＝428571

4×142857＝571428

5×142857＝714285

6×142857＝857142

输入m、n，你的任务是找到这样一个整数。输入保证这样的整数最多只有一个。

【输入格式】

输入包含若干组数据。每组数据包含两个整数m和n（3≤m≤6，4≤n≤400）。输入结束标志为m＝n＝0。

【输出格式】

对于每组数据，输出这个整数。每位数字是0～n－1之间的整数，相邻两位数字之间用一个空格隔开。如果无解，输出“Not found.”。

正方形拼图（Square Puzzle, UVa 10639）

给出一些拼块，如图1-53所示。你的任务是判断它们是否可以拼成一个给定大小的正方形。拼块可以旋转，但不能翻转，所有拼块都必须使用，且拼块不能重叠。

图　1-53

每个拼块用多边形表示。所有顶点坐标都是整数，所有边都是横、竖或者斜向45°。

【输入格式】

输入的第一行为数据组数T（T≤20）。每组数据的第一行为两个整数n和m（1≤n，m≤6），其中n为拼块的个数，m为正方形边长。以下n行每行描述一个拼块，其中第一行为整数k，即多边形顶点数；接下来为k对整数，按照逆时针顺序给出各顶点的坐标。所有顶点坐标均为0～m的整数。

【输出格式】

对于每组数据，根据情况输出yes或者no。

天平难题（Mobile Computing, Tokyo 2005, LA 3403）

给出房间的宽度r和s个挂坠的重量wi。设计一个尽量宽，但宽度不能超过房间宽度r的天平，挂着所有挂坠。

每个天平的每一端要么挂一个挂坠，要么挂另外一个天平。如图1-54所示，设n和m分别是两端挂的总重量，要让天平平衡，必须满足n×a＝m×b。

图　1-54

例如，如果有3个重量分别为1，1，2的挂坠，有如图1-55所示3种平衡方案。

图　1-55

挂坠的宽度忽略不计，且不同的子天平可以相互重叠。如图1-56所示，宽度为（1/3）＋1＋（1/4）。

图　1-56

房间宽度r和挂坠数目s满足0＜r＜10和1≤s≤6，且每个挂坠的重量wi满足1≤wi≤1000。输入保证不存在天平的宽度恰好在r－10⁻⁵和r＋10⁻⁵之间。

Bingo游戏（Bingo, Tokyo 2005, LA 3406）

有P个M×M的数字矩阵（2≤P≤4，3≤M≤4）。你可以依次选择一些整数，每选一个整数后就把所有矩阵中的同一数字全部圈起来。如果某个矩阵中某一行、某一列或者某条主对角线（一共有两条）上的M个数字都被圈起来了，我们就说这个矩阵Bingo了。你的任务是找到一个最短的数字序列，使得最后所有的矩阵按照从左到右的顺序依次Bingo（相邻矩阵可以同时Bingo）。

图1-57是一个成功的序列。注意，5不能在16之前选择，否则第4列会在第2、3列之前Bingo，违反了题目规定。

图　1-57

快速幂计算（Power Calculation, Yokohama 2006, LA 3621）

给出x和正整数n（1≤n≤1000），问最少需要几次乘除法可以得到xn。比如，x³¹需要6次乘除计算：x²＝x×x，x⁴＝x²×x²，x⁸＝x⁴×x⁴，x¹⁶＝x⁸×x⁸，x³²＝x¹⁶×x¹⁶，x³¹＝x³²/x。计算过程中x的指数应当总是正整数（比如x⁻³＝x/x⁴是不允许的）。

2．高效算法设计

还有一些题目并不需要巧妙的思路和缜密的推理就能找到一个解决方案，只是时间效率难以令人满意。降低时间复杂度的方法有很多，本章的例题就覆盖了其中最常见的一些，如表1-6所示。

表　1-6

接下来仍然列举一些习题，以供读者练习和提高，如表1-7所示。

表　1-7

超级传输（Hypertransmission, NEERC 2003, LA 2963）

需要在n个星球上各装一个广播装置，作用范围均为R（即和它距离不超过R的星球能收听到它的广播）。每个星球广播A类节目或者B类节目。令N＋（i）表示星球i收听到的和自己广播相同节目的星球数（包括星球i自己），N⁻（i）表示星球i收听到的广播另一种节目的星球数。如果N＋（i）＜N-（i），我们说星球i是不稳定的。你是暗黑世界的间谍，因此希望选择R，使得不稳定的星球尽量多些。在此前提下，R应尽量小。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤1000），即星球个数；以下n行每行包含4个整数xi，yi，zi和pi，其中，（xi，yi，zi）是星球i的空间位置，pi＝0表示星球i广播A类节目，pi＝1表示星球i广播B类节目。所有坐标的绝对值不超过10000。不同星球的位置保证不同。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出两行。第一行为不稳定的星球个数。第二行为让不稳定星球数最大化的最小的R值，精确到10⁻⁴。

环面上的最大和（Maximum sum on a torus, UVa 10827）

把一个网格的第一行和最后一行粘起来，第一列和最后一列粘起来，可以得到一个环面。给定一个整数网格，求出所对应环面上的最大子矩形（该子矩形的所有元素之和最大）。如图1-58所示就是一个最大的子矩形。

图　1-58

【输入格式】

输入的第一行为数据组数T（T≤18）。每组数据的第一行为网格的行数和列数（它总是一个正方形）n（1≤n≤75）；以下n行每行n个-100～100之间的整数。

【输出格式】

对于每组数据，输出一行，即最大子矩形内元素的和。

和集（Sumsets, UVa 10125）

给定一个整数集合S，找出一个最大的d，使得a＋b＋c＝d，其中a，b，c，d是S中的不同元素。

【输入格式】

输入包含多组数据。每组数据的第一行为集合内的元素个数n（1≤n≤1000），以下n行每行一个-536870912～+536870911的整数。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出最大的d。如果无解，输出“no solution”。

交换学生（Foreign Exchange, UVa 10763）

有n个学生想交换到其他学校学习。为了简单起见，规定每个想从A学校换到B学校的学生必须找一个想从B学校换到A学校的“搭档”。如果每个人都能找到搭档（一个人不能当多个人的搭档），学校就会同意他们交换。你的任务是判断交换是否可以进行。

【输入格式】

输入包含多组数据。每组数据的第一行为学生个数n（1≤n≤500000）；以下每行包含两个不同的非负整数A和B，表示该学生想从A学校换到B学校。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出YES或者NO。

复合词（Compound Words, UVa 10391）

给定一个词典，要求找出其中所有的复合词，即恰好由两个单词连接而成的单词。

【输入格式】

输入只有一组数据，其中每行都是一个由小写字母组成的单词。输入已按照字典序排序，且不超过120000个单词。

【输出格式】

输出所有复合词，按照字典序排列。

Gergovia的酒交易（Wine trading in Gergovia, UVa 11054）

直线上有n个等距的村庄，每个村庄要么买酒，要么卖酒。把k个单位的酒从一个村庄运到相邻村庄需要k个单位的劳动力。问最少需要多少劳动力才能满足所有村庄的需求。

【输入格式】

输入包含多组数据。每组数据的第一行为村庄个数n（2≤n≤100000）；第二行从左到右给出各个村庄对酒的需求ai（-1000≤ai≤1000），其中ai＞0表示买酒，ai＜0表示卖酒。输入保证所有ai之和等于0。输入结束标志为n＝0。

【输出格式】

输出劳动力总和的最小值。输出保证在64位带符号整数的范围内。

平均值（Average, Seoul 2009, LA 4726）

给定一个长度为n的01序列，选一个长度至少为L的连续子序列，使得子序列中数字的平均值最大。如果有多解，子序列长度应尽量小；如果仍有多解，起点编号应尽量小。序列中的字符编号为1～n，因此[1，n]就是指完整的序列。

例如，对于长度为17的序列00101011011011010，如果L＝7，子序列[7，14]的平均值最大，为6/8（它的长度为8）；如果L＝5，子序列[7，11]的平均值最大，为4/5。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为两个整数n和L（1≤n≤100000，1≤L≤1000），第二行为一个长度为n的01序列。

【输出格式】

对于每组数据，输出满足条件的最优子序列的起点和终点编号。

餐厅（Restaurant, Daejon 2010, LA 4851）

有一个M×M的网格，左下角坐标为（0，0），右上角坐标为（M－1，M－1）。网格里有两个y坐标相同的宾馆A和B，以及n个餐厅。宾馆A和宾馆B里各有一个餐厅，编号为1和2，其他地方的餐厅编号为3～n。现在你打算开一家新餐厅，需要考查一下可能的位置。

一个位置p是“好位置”的条件为：当且仅当对于已有的每个餐厅q，要么p比q离A近，要么p比q离B近，即dist（p，A）＜dist（q，A）或者dist（p，B）＜dist（q，B）。如图1-59所示，A和B的坐标分别为（0，5）和（10，5），（7，4）是个好位置，但（4，6）不是好位置，因为位于（3，5）处的餐厅不管是到宾馆A还是到宾馆B，都比（4，6）要近。

图　1-59

你的任务是统计网格中好位置的个数。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为两个整数M和n（2≤M≤60000，2≤n≤50000）；以下n行每行包含两个数x和y（0≤x，y＜M），即各个餐厅的坐标。不同餐厅的坐标保证不同，宾馆A里的餐厅编号为1，宾馆B里的餐厅编号为2。餐厅1和餐厅2的y坐标保证相同。

【输出格式】

对于每组数据，输出好位置的个数。

火势控制系统（Fire-Control System, Hangzhou 2008, LA 4356）

在平面上有n个目标点，你的任务是找出一个圆心在（0，0）点处的扇形，至少覆盖其中的k个点，使得该扇形的面积最小。

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和k（1≤n≤5000，k≤n）；以下n行每行包含两个整数x和y，即每个目标点的坐标。坐标均为绝对值不超过1000的整数，且没有目标点在（0，0）。输入结束标志为n＝k＝0。

【输出格式】

对于每组数据，输出覆盖至少k个点的最小扇形的面积，保留两位小数。

基因组进化（Genome Evolution, Tehran 2010, LA 5052）

给出1～n的两个排列A和B，统计有多少个二元组（A'，B'）满足以下条件：A'是A的连续子序列，B'是B的连续子序列，且A'和B'包含的整数集完全相同。A'和B'均应至少包含两个元素。

例如，A＝｛3，2，1，4｝，B＝｛1，2，4，3｝时，有3组解：｛2，1｝，｛1，2｝；｛2，1，4｝，｛1，2，4｝；｛3，2，1，4｝，｛1，2，4，3｝。

【输入格式】

输入包含多组数据。每组数据的第一行为n（2≤n≤3000）；第二行和第三行各包含一个1～n的排列，分别为A和B。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出满足条件的二元组的个数。

DNA突变区域（DNA Regions, CERC 2006, LA 3716）

给出两条长度均为n的DNA链（字符串）A和B，你的任务是找出一段最长的区域，使得该区域内的突变位置不超过p％。换句话说，你需要找出一个尽量长的闭区间[L，R]，使得对于区间内的所有位置x（L≤x≤R），有不超过p％的x满足Ax≠Bx。

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和p（1≤n≤150000，1≤p≤99）；以下两行各为一条长度为n的DNA链，只由大写字母A、C、G、T组成。输入结束标志为n＝0。

【输出格式】

输出满足条件的区域长度的最大值。如果不存在，输出“No solution.”（不含引号）。

3．动态规划

动态规划是几乎所有算法竞赛的宠儿。理由很简单：动态规划对思维的要求比较高，常用来解决那些其他算法都不奏效的题目。本章的动态规划例题并不多，但却包含了不少重要的思想和方法，如表1-8所示。

表　1-8

下面仍然列举一些习题。数量虽不少，但却并不是前面例题的简单重复和改头换面。其中有些题目可以直接转化为经典题目，或者顺着经典问题的思路即可解决，但也有一些题目需要认真分析才能解决。动态规划题目对思维训练非常有帮助，请读者予以重视。

首先是相对简单的题目，如表1-9所示。

表　1-9

续表

划分成回文串（Partitioning by Palindromes, UVa 11584）

输入一个由小写字母组成的字符串，你的任务是把它划分成尽量少的回文串。比如，racecar本身就是回文串；fastcar只能分成7个单字母的回文串；aaadbccb最少可分成3个回文串：aaa、d、bccb。字符串长度不超过1000。

商人（Salesman, Seoul 2008, LA 4256）

给定一个包含n个点（n≤100）的无向连通图和一个长度为L的序列A（L≤200），你的任务是修改尽量少的数，使得序列中的任意两个相邻数或者相同，或者对应图中两个相邻结点。

波浪子序列（Wavio Sequence, UVa 10534）

给定一个长度为n的整数序列，求一个最长子序列（不一定连续），使得该序列的长度为奇数2k＋1，前k＋1个数严格递增，后k＋1个数严格递减。注意，严格递增／递减意味着该序列中的两个相邻数不能相同。n≤10000。

最小的块数（Fewest Flops, UVa 11552）

输入一个正整数k和字符串S，字符串的长度保证为k的倍数。把S的字符按照从左到右的顺序每k个分成一组，每组之间可以任意重排，但组与组之间的先后顺序应保持不变。你的任务是让重排后的字符串包含尽量少的“块”，其中每个块为连续的相同字母。比如，uuvuwwuv可分成两组：uuvu和wwuv，第一组可重排为uuuv，第二组可重排为vuww，连起来是uuuvvuww，包含4个“块”。

【输入格式】

输入的第一行包含一个整数t（t≤100），即测试数据的数量；以下t行每行包含整数k和字符串S。串长保证是k的整数倍。S由不超过1000个小写字母组成。

【输出格式】

对于每组数据，输出重排后的S所包含的最小“块”数。

回文子序列（Palindromic Subsequence, UVa 11404）

给定一个由小写字母组成的字符串，删除其中的0个或多个字符，使得剩下的字母（顺序不变）组成一个尽量长的回文串。如果有多解，输出字典序最小的解。

【输入格式】

输入包含多组测试数据。每组数据仅一行，为一个长度不超过1000的非空字符串。

【输出格式】

对于每组数据，输出所求的最长回文串。

蜂窝网络（Cellular Network, Seoul 2009, LA 4731）

手机在蜂窝网络中的定位是一个基本问题。假设蜂窝网络已经得知手机处于c₁，c₂，…，cn这些区域中的一个，最简单的方法是同时在这些区域中寻找手机。但这样做很浪费带宽。由于蜂窝网络中可以得知手机在这不同区域中的概率，因此一个折中的方法就是把这些区域分成w组，然后依次访问。比如，已知手机可能位于5个区域中，概率分别为0.3、0.05、0.1、0.3和0.25，w＝2，则一种方法是先同时访问｛c₁，c₂，c₃｝，再同时访问｛c₄，c₅｝，访问区域数的数学期望为3*（0.3＋0.05＋0.1）＋（3＋2）*（0.3＋0.25）＝4.1。另一种方法是先同时访问｛c₁，c₄｝，再访问｛c₂，c₃，c₅｝，访问区域数的数学期望为2×（0.3＋0.3）＋（3＋2）×（0.05＋0.1＋0.25）＝3.2。

【输入格式】

输入的第一行为数据组数T。每组数据的第一行为两个整数n和w（1≤w≤n≤100）；第二行为包含n个不超过10000的正整数u₁，u₂，…，un。手机在ci的概率为pi＝ui/（u₁＋u₂＋…＋un）。

【输出格式】

对于每组数据，输出访问区域数的数学期望的最小值。

洛克人的难题（Mega Man's Missions, UVa 11795）

洛克人最初只有一个武器“Mega Buster”。你需要按照一定的顺序消灭n个其他机器人。每消灭一个机器人将会得到他的武器，而某些机器人只能用特定的武器才能消灭。你的任务是计算出可以消灭所有机器人的顺序总数。

【输入格式】

输入的第一行为数据组数T（T≤50）。每组数据的第一行为机器人个数n（1≤n≤16）；以下n＋1行描述各种武器，其中第1行描述Mega Buster，第k＋1行描述消灭k号机器人后获得的武器。其中每行包含n个整数，分别表示该武器是否能消灭第1、2、3、…、n号机器人（1表示可以，0表示不可以）。注意，一个机器人的武器可能可以消灭自己，但不会影响到本题的答案，因为必须先消灭这个机器人，才能拿到武器。

【输出格式】

对于每组数据，输出可以消灭所有机器人的顺序总数。

跳跃（Jump, Seoul 2009, LA 4727）

把1～n按逆时针顺序排成一个圆圈，从1开始每k个数字删掉一个，直到所有数字都被删除。这些数的删除顺序记为Jump（n，k）（n，k≥1）。

例如，Jump（10，2）＝[2，4，6，8，10，3，7，1，9，5]，Jump（13，3）＝[3，6，9，12，2，7，11，4，10，5，1，8，13]，Jump（13，10）＝[10，7，5，4，6，9，13，8，3，12，1，11，2]，Jump（10，19）＝[9，10，3，8，1，6，4，5，7，2]。

你的任务是求出Jump（n，k）的最后3个数。

【输入格式】

输入的第一行为数据组数T。每组数据仅一行，为两个整数n和k （5≤n≤500000，2≤k≤500000）

【输出格式】

对于每组数据，按顺序输出Jump（n，k）的最后3个元素。

火星采矿（Martian Mining, LA 3530）

给出n×m网格中每个格子的A矿和B矿的数量，A矿必须由右向左运输，B矿必须由下向上运输，如图1-60所示。管子不能拐弯或者间断。要求收集到的A、B矿总量尽量大。

图　1-60

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和m（1≤n，m≤500），即行数和列数；以下n行每行m个整数，即每个格子中的A矿数量；再以下n行每行m个整数，即每个格子中的B矿数量。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出收集到的矿总量的最大值。

沙漏里的路径（Paths through the Hourglass, UVa 10564）

有一个沙漏，第一行有n个格子，第二行n－1个格子……最中间的行只有1个格子，然后它下面一行2个格子，再下面一行3个格子……最后一行n个格子，如图1-61所示。你可以从第一行开始往下走，每次往下走一行，往左或往右走一列，但不能走出沙漏。你的目标是让沿途经过的所有整数之和恰好为一个给定整数S。求出符合上述条件的路径条数和一条路径。

图　1-61

如果有多条路径，起点编号应尽量小（第一行格子从左到右编号为0～n－1）。如果仍有多解，移动序列（L代表左，R代表右）的字典序应最小。

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和S（2≤n≤20，0≤S＜500）。以下2n－1行为各个格子里的整数（均为0～9的整数）。

【输出格式】

对于每组数据，输出路径条数和路径（如果有解的话）。

校长的烦恼（Headmaster's Headache, UVa 10817）

某校有n个教师和m个求职者。已知每人的工资和能教的课程集合，要求支付最少的工资使得每门课都至少有两名教师教学。在职教师必须招聘。

【输入格式】

输入包含多组数据。每组数据的第一行为3个整数s、m和n（1≤s≤8，1≤m≤20，1≤n≤100），即科目的个数、在职教师个数和申请者个数；以下m行每行用一些整数描述一位在职教师，其中第一个整数c（10000≤c≤50000）是工资，接下来的若干整数是他能教的科目列表（课程编号为1～s之间的整数）；接下来的n行描述申请者，格式同上。输入结束标志为s＝0。

【输出格式】

对于每组数据，输出工资总额的最小值。

战略游戏（Strategic Game, SEERC 2000, LA 2038）

给定一棵树，选择尽量少的结点，使得每个没有选中的结点至少和一个已选结点相邻。

【输入格式】

输入包含多组数据。每组数据的第一行为结点数n（n≤1500）；以下n行每行描述一个结点的相邻点列表。结点编号为0～n－1，每条边恰好在输入中出现一次。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出最少需要选的结点数。

接下来的题目有一定难度，如表1-10所示。

表　1-10

续表

字符串“刷子”（String Painter, Chengdu 2008, LA 4394）

给定两个长度相等，只有小写字母组成的字符串s和t，每步可以把s的一个连续子串“刷”成同一个字母，问至少需要多少步才能把s变成t。比如，s＝bbbbbbb，t＝aaabccb，最少需要两步可实现将s变成t：bbbbbbb→aaabbbb→aaabccb。

【输入格式】

输入包含多组数据。每组数据包含两行，第一行为s，第二行为t。两个字符串的长度保证相等，且不超过100。

【输出格式】

对于每组数据，输出一个整数，即最少步数。

游行（Parade, Beijing 2008, LA 4327）

F城由n＋1个横向路和m＋1个竖向路组成。你的任务是从最南边的路走到最北边的路，使得走过的路上的高兴值和最大（注意，一段路上的高兴值可以是负数）。同一段路不能经过两次，且不能从北往南走，如图1-62所示。另外，在每条横向路上所花的时间不能超过k。

图　1-62

【输入格式】

输入包含多组数据。每组数据的第一行为3个整数n，m，k（1≤n≤100，1≤m≤10000，0≤k≤3000000）；以下n＋1行每行m个整数，按照从北到南、由西向东的顺序给出各段横向道路上的高兴值；再以下n＋1行每行m个整数，按照从北到南、由西向东的顺序给出各段横向道路的行走时间。

【输出格式】

对于每组数据，输出最大高兴值。

洞穴（Cave, Chengdu 2007, LA 4015）

一棵n个结点的有根树，树的边有正整数权，表示两个结点之间的距离。你的任务是回答这样的询问：从根结点出发，走不超过x单位距离，最多能经过多少个结点？同一个结点经过多次只算一个。

【输入格式】

输入包含多组数据。每组数据的第一行为结点数n（0≤n≤500）；以下n－1行每行3个整数i，j，d（1≤d≤10000），表示结点i的父亲为结点j，二者的距离为d；洞穴编号为0～n－1；下一行包含整数Q（1≤Q≤1000），即查询的个数；以下Q行每行包含一个整数x（0≤x≤5000000），即机器人可以行走的最大距离。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出Q行，每个查询占一行，即可以经过的结点数的最大值。

帮助布布（Help Bubu, Wuhan 2009, LA 4490）

书架上有n本书。如果从左到右写下书架上每本书的高度，我们能够得到一个序列，比如30，30，31，31，32。我们把相邻的高度相同的书看成一个片段，并且定义该书架的混乱程度为片段的个数。比如，30，30，31，31，32的混乱程度为3。同理，30，32，32，31的混乱程度也是3，但31，32，31，32，31的混乱程度高达5（请想象一下这个书架，确实够乱的吧。）。

为了整理书架，你最多可以拿出k本书，然后再把它们插回书架（其他书的相对顺序保持不变），使书架的混乱程度降至最低。

【输入格式】

输入包含不超过20组数据。每组数据的第一行为两个正整数n和k（1≤k≤n≤100）；第二行包含n个整数hi（25≤hi≤32），即初始时从左到右每本书的高度。输入结束标志为n＝k＝0。

【输出格式】

对于每组数据，输出在整理结束后书架混乱程度的最小值。

消灭妖怪（Masud Rana, UVa 11600）

某国有n个城市，编号为1～n。这些城市两两之间都有一条双向道路（一共有n（n－1）/2条），其中一些路上有妖怪，其他路是安全的。为了保证城市间两两可达，你第一天晚上住在城市1，然后每天白天随机选择一个新的城市，然后顺着它与当前所在城市之间的道路走过去，途中消灭这条道路上所有的妖怪，晚上住在这座城市。在平均情况下，需要多少个白天才能让任意两个城市之间均可以不经过有妖怪的道路而相互可达？

【输入格式】

输入的第一行为数据组数T（T≤100）。每组数据的第一行为两个整数n和m（1≤n≤30，0≤m≤n（n－1）/2），即城市数目和安全的道路数目；以下m行每行两个整数a和b（1≤a，b≤n），表示连接城市a和城市b的道路是安全的（即没有妖怪）。

【输出格式】

对于每组数据，输出平均情况下需要的白天数目。

疏散计划（Evacuation Plan, NEERC 2010, LA 4987）

战争时期，有n支施工队在修一条笔直的高速公路，其中第i支队伍离高速公路起点的距离为ai。另外还有m个避难所，其中第i个避难所离高速公路起点的距离为bi。给每只施工队分配一个避难所，以方便其在敌人轰炸时能够迅速逃往避难。假定施工队i分配到避难所j，施工队的移动距离为｜ai－bj｜。由于避难所的门只能从里面反锁，要求每个避难所至少应分配一支施工队。你的任务是确定分配方案，使得所有施工队移动的总距离最小。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤4000）；第二行包含n个整数，即a₁，a₂，…，an。第三行为整数m（1≤m≤n）。第四行包含m个整数，即b₁，b₂，…，bm。所有坐标均为不超过10⁹的正整数，且不同的施工队位置不同，不同的避难所位置也不同。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，第一行输出最小总距离，第二行输出每个施工队分配的避难所编号。避难所按照输入顺序编号为1～m。

独占访问2（Exclusive Access 2, NEERC 2009, LA 4593）

在一个庞大的系统里运行着n个守护进程。每个进程恰好用到两个资源。这些资源不支持并发访问，所以这些进程通过锁来保证互斥访问。每个进程的主循环如下。

注意，P和Q的顺序是至关重要的。如果某进程用到了消息队列和数据库，“先获取数据库的锁”与“先获取消息队列的锁”可能会产生截然不同的效果。给定每个进程所需要的两种资源，你的任务是确定每个进程获取锁的顺序，使得进程永远不会死锁，且最坏情况下，等待链的最大长度最短。

在本题中，一个长度为n的等待链是一个不同资源和不同进程的交替序列：R₀c₀R₁c₁…RncnRn＋1，其中进程ci已经获取Ri的锁，正在等待Ri＋1的锁。当R₀＝Rn＋1时死锁，否则说明已获取Rn＋1的锁的进程正在执行操作（而非等待中）。

【输入格式】

输入包含多组数据。每组数据的第一行为整数n（1≤n≤100）；以下n行每行两个L～Z之间的大写字符（即一共有15种资源），即该进程需要的两个资源。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，第一行输出最坏情况下等待链的最大长度。以下n行每行输出两个字符，表示该进程获取锁的顺序（先获取第一个字符对应资源的锁）。

基金管理（Fund Management, NEERC 2007, LA 4048）

你有c美元，但没有股票。给你m天时间和n支股票供你买卖（c≤10⁸，m≤100，n≤8），问最后最多能剩下多少钱（最后一天结束时不能持有任何股票）。

每天只能买卖一支股票，并且只能买一手或卖一手，对于第i支股票来说，已知第j天的价格为pi，j，一手为si股，且每天最多能持有ki手这支股票。所持股票的总“手”数每天都不能超过k。k≤8，si≤10⁶，ki≤k，0＜pi，j≤1000。

一个调度问题（A Scheduling Problem, Kaoshiung 2006, LA 3683）

有n（n≤200）个恰好需要一天完成的任务，要求用最少的时间完成所有任务。任务可以并行完成，但必须满足一些约束。约束分有向约束和无向约束两种，其中，A→B表示A必须在B之前完成，A－B表示A和B不能在同一天完成。输入保证约束图是将一棵树的某些边定向后得到的。

（提示：有这样一个定理。设由有向边组成的最长路长度为k，则答案为k或者k＋1）。

书架（Bookcase, NWERC 2006, LA 3637）

有n（3≤n≤70）本书，每本书有一个高度Hi和宽度Wi（150≤Hi≤300，5≤Wi≤30）。现在要构建一个3层的书架，你可以选择将n本书放在书架的哪一层。设3层高度（该层书的最大高度）之和为h，书架总宽度（即每层总宽度的最大值）为w，要求h×w尽量小。

修缮长城（Fixing the Great Wall, CERC 2004, LA 3181）

长城被看做一条直线段，有n个损坏点需要用机器人GWARR修缮。可以用三元组（xi，ci，di）描述第i个损坏点的参数，其中，xi是位置，ci是立刻修缮（即时刻＝0时开始修缮）的费用，di是单位时间增加的修缮。换句话说，如果在时刻ti开始修缮第i个损坏点，费用为ci＋tidi。修缮的时间忽略不计，GWARR的速度恒定为v，因此从修缮点i走到修缮点j需要｜xi－xj｜/v单位的时间。

【输入格式】

输入包含多组数据。每组数据和第一行包含3个整数：n，v，x（1≤n≤1000，1≤v≤100，1≤x≤500000），即损坏点的个数、GWARR的速度和初始坐标；以下n行每行包含3个整数xi，ci，di（1≤xi≤500000，0≤ci≤50000，1≤di≤50000），含义如题。输入保证损坏点的位置各不相同，且GWARR的初始位置不与任何一个损坏点重合。输入结束标志为n＝v＝x＝0。

【输出格式】

对于每组数据，输出一行，即最小费用（用截尾法保留整数部分）。输入保证最小费用不超过10⁹。

给孩子起名（The Best Name for Your Baby, Japan 2006, LA 3623）

给定一个包含n条规则的上下文无关文法和长度l，求出满足该文法的字符串中，长度恰好为l的字典序最小串。

“满足文法”是指可以不断使用规则，把单个大写字母S变成这个字符串。每条规则形如A→a，其中A是一个大写字母（表示非终结符），a是一个由大小写字母组成的字符串。该规则的含义是可以用字符串a来替换当前字符串中的大写字母A（如果有多个A，每次只替换一个）。

比如，有4条规则：S→aAB，A→，A→Aa，B→AbbA，那么aabb满足该文法，因为S→aAB（规则1）→aB（规则2）→aAbbA（规则4）→aAabbA（规则2）→（规则3）→aAabb（规则2）→aabb（规则2）。

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和l（1≤n≤50，0≤l≤20），即规则的个数和字符串的长度。接下来的n行每行为一条规则，其中每条规则的第一个字母为大写字母，然后是等号，然后是由大小写字母组成的字符串。等号右端的字符串长度不超过10，且可以为空串。规则内部无空白字符。输入结束标志为n＝l＝0。

【输出格式】

对于每组数据，输出满足条件的字典序最小串。如果不存在，输出一个字符'-'。

方块消除（Blocks, UVa 10559）

n（n≤200）个带颜色的方块排成一列。每次可以选择一段颜色相同的连续方块消除，得分为x²，其中x是这一段连续方块的个数。图1-63是两种可能的操作序列，其中第一个是最优的。

图　1-63

你的任务是消除所有的方块，以得到尽量多的分数。

越大越好（Bigger is Better, Xi'an 2006, LA 3782）

你的任务是用不超过n（n≤100）根火柴摆一个尽量大的，能被m（m≤3000）整除的正整数，如图1-64所示。例如n＝6和m＝3，解为666。无解时输出-1。

图　1-64

整数传输（Integer Transmission, Beijing 2007, LA 4031）

你要在一个仿真网络中传输一个n比特的非负整数k。各比特从左到右传输，第i个比特的发送时间为i。每个比特的网络延迟总是为1～d的实数（因此从左到右第i个比特的到达时间为i～i＋d之间）。若同时有多个比特到达，实际收到的顺序任意。求实际收到的整数有多少种，以及它们的最小值和最大值。

【输入格式】

输入包含多组数据。每组数据仅一行，为3个整数n，d，k（1≤n≤64，0≤d≤n，0≤k＜2n）。输入结束标志为n＝d＝k＝0。

【输出格式】

对于每组数据，输出可能收到的整数个数以及最小值和最大值。

压缩（Compressor, UVa 11521）

压缩一个字符串，在压缩串中，[S]k表示S重复k次，[S]k｛S₁｝t₁｛S₂｝t₂…｛Sr｝tr表示S重复k次，然后在其中第ti个S后面插入Si。压缩是递归进行的，因此上面的S，S₁，S₂，…也可以是压缩串。你的任务是使压缩串的长度最小。比如，I_am_WhatWhat_is_WhatWhat的最优压缩结果是I_am_[What]4｛_is_｝2。注意，上述k，t₁，t₂，…的长度均算作1，哪怕它们的十进制表示中包含超过1个数字。

【输入格式】

输入包含不超过20组数据。每组数据包含不超过200个可打印字符，但不含空白字符、括号（小括号（）、方括号[]或者花括号｛｝都算括号）或者数字。字母是大小写敏感的。

【输出格式】

对于每组数据，输出长度和压缩串。如果有多个解，任意输出一个压缩串即可。

注释

[1]　注意：上述readint和writeint只能处理非负整数，请读者自行编写适用于负整数的函数。

[2]　即满足d（j）＝i的最小j。

[3]　实际上是要找满足g[k']＜A[i]的最后一个下标k'，则d（i）＝k'＋1，令k＝k'＋1即可得到。

[4]　事实上，LCS问题存在渐进时间复杂度比O（nm）更低的算法，但超出了本书的范围。

[5]　事实上，本问题存在O（nlogn）时间的算法，但超出了本书的范围。

[6]　详见第3章。

[7]　需要把这个数改成1～n之间的。

[8]　事实上，很多人习惯把所有递推都叫做动态规划，不管它是否真的是在解决最优化问题。

[9]　M不要取得太大，以免算术运算溢出。

[10]　队列中的元素递增，因此也称为单调队列。

第2章　数学基础

算法竞赛不是数学竞赛，但它是一门离不开数学的竞赛。它涉及组合数学、数论、概率论、抽象代数、线性代数、微积分、游戏论等领域，要求选手有较为全面的数学基础。本章力图介绍算法竞赛中常用的数学知识点与思考方法，并通过题目和代码加深读者的理解。

2.1　基本计数方法

计数方法的两个最基础的原理是加法原理和乘法原理。

加法原理。做一件事情有n个办法，第i个办法有pi种方案，则一共有p₁＋p₂＋…＋pn种方案。

乘法原理。做一件事情有n个步骤，第i个步骤有pi种方案，则一共有p₁p₂…pn种方案。

乘法原理是加法原理的特殊情况（按照第一步骤进行分类），二者都可用于递推。注意应用加法原理的关键是分类，各类别之间必须没有重复、没有遗漏。如果有重复，可以使用容斥原理，如下所述。

容斥原理。假设班里有10个学生喜欢数学，15个学生喜欢语文，21个学生喜欢编程，班里一共有多少个学生呢？是10＋15＋21＝46个吗？不是的，因为有些学生可能同时喜欢数学和语文，或者语文和编程，甚至还有可能三者都喜欢。为了叙述方便，我们把喜欢语文、数学、编程的学生集合分别用A，B，C表示，则学生总数等于｜A∪B∪C｜。刚才已经讲过，如果把这三个集合的元素个数｜A｜、｜B｜、｜C｜直接加起来，会有一些元素重复统计了，因此需要扣掉｜A∩B｜、｜B∩C｜、｜C∩A｜，但这样一来，又有一小部分多扣了，需要加回来，即｜A∩B∩C｜。这样，我们就得到了一个公式：

一般地，对于任意多个集合，我们都可以列出这样一个等式，等式左边是所有集合的并的元素个数，右边是这些集合的“各种搭配”。每个“搭配”都是若干个集合的交集，且每一项前面的正负号取决于集合的个数——奇数个集合为正，偶数个集合为负。事实上，第1章例题24《废料堆》中的“加加减减”也符合这个规律。

容斥原理有一个变种：假设全集为S，另有3个集合A，B，C，不属于A、B、C任何一个集合，但属于全集S的元素一共有多少个呢？和前面的方法类似，我们首先扣除A，B，C，然后把｜A∩B｜、｜B∩C｜和｜C∩A｜加回来，最后再扣掉多加的｜A∩B∩C｜。

容斥原理还有其他变种，但思路万变不离其宗：都是加加减减，把重复的扣掉，再把扣多的加回来。如果很难一次性想清楚，可以先推导两个集合或者3个集合的例子，然后进行深入。

下面列举几个常见的计数问题。

问题1：排列问题。有n个不同的数，选k个排成一排，每个数最多选一次，问有多少种排列方法？

分析：记答案为P（n，k）。由乘法原理，每个步骤选一个数，第1个步骤有n种选择，第2个步骤有n－1种选择（不管第1步选了什么），…，第k个步骤有n－k＋1种选择，所以P（n，k）＝n（n－1）（n－2）…（n－k＋1）。用阶乘表示就是P（n，k）＝n!/（n－k）!。特别地，n个数的全排列P（n，n）＝n!

问题2：组合问题。有n个不同的数，选出k个（顺序无关），每个数最多选一次，问有多少种选法？

分析：记答案为C（n，k）。把n选k的排列问题看成两个步骤：首先选出k个数的组合，然后把这k个数进行全排列。由乘法原理知P（n，k）＝C（n，k）×P（k，k），因此C（n，k）＝P（n，k）/P（k，k）＝n!/（（n－k）!k!）。

C（n，k）在组合计数中占有极重要的地位。常用性质如下。

性质1：C（n，0）＝C（n，n）＝1

性质2：C（n，k）＝C（n，n－k）

证明：选了k个数以后剩下的数恰好有n－k个，因此选k个和选n－k个的方案一一对应。

性质3：C（n，k）＋C（n，k＋1）＝C（n＋1，k＋1）

这是组合数的递推公式，经常用于预处理，证明如下：n＋1个数里选k＋1有两类办法。要么选第1个数，要么不选第1个数。如果不选，则问题转化为n个数里选k＋1个数；如果选，则问题转化为n个数里选k个数。这两类办法是不重复不遗漏的，由加法原理得证。

问题3（二项式展开）：求（a＋b）n展开式的各项系数。

分析：根据二项式定理alt，只需求出所有的C（n，k）。不管是用定义（阶乘相除）还是性质3，时间复杂度都是O（n²）。有没有办法算得更快呢？答案是肯定的。这需要用到下面的性质4。

性质4：C（n，k＋1）＝C（n，k）×（n－k）/（k＋1）

证明：直接利用公式

两式相除得C（n，k＋1）/C（n，k）＝（n－k）/（k＋1）。

有了这个性质，从C（n，0）开始递推，只需要O（n）时间就能求出所有的C（n，0），C（n，1），C（n，2），…，C（n，n）。注意不要让运算过程中出现乘法溢出。

问题4：有重复元素的全排列。有k个元素，其中第i个元素有ni个，求全排列个数。

分析：令所有ni之和为n，再设答案为x。首先做全排列，然后把所有元素编号，其中第s种元素编号为1～ns（比如，有三个a，两个b，先排列成aabba，然后可以编号为a₁a₃b₂b₁a₂）。这样做以后，由于编号后所有元素均不相同，方案总数为n的全排列数n!。根据乘法原理，我们得到了一个方程：n₁!n₂!n₃!…nk!x＝n!，移项即可。

问题5：可重复选择的组合。有n个不同元素，每个元素可以选多次，一共选k个元素，有多少种方法？比如n＝3，k＝2有6种：（1，1），（1，2），（1，3），（2，2），（2，3），（3，3）。

分析：设第i个元素选xi个，问题转化为求方程x₁＋x₂＋…＋xn＝k的非负整数解的个数。令yi＝xi＋1，则答案为y₁＋y₂＋...＋yn＝k＋n的正整数解的个数。想象有k＋1个数字“1”排成一排，则问题等价于把这些“1”分成n个部分，有多少种方法？这相当于在k＋n－1个“候选分隔线”中选n－1个，即C（k＋n－1，n－1）＝C（n＋k－1，k）。最后一步用到了组合数的性质2。

问题6：单色三角形。给定空间里的n（n≤1000）个点，其中没有三点共线。每两个点之间都用红色或黑色线段连接。求3条边同色的三角形个数。

分析：直接统计需要O（n³）时间，需要优化。从反面考虑，只要求出了非单色三角形，就可以间接得到单色三角形的个数。在每个非单色三角形中，恰好有两个顶点连接两条异色边（不包含不在此三角形中的边），而且有一个公共点的两条异色边总是唯一对应一个非单色三角形，因此如果第i个点连接了ai条红边n－1－ai条黑边，则这些边属于ai（n－1－ai）个非单色三角形。每个非单色三角形考虑了两次，因此最终答案应除以2，即alt。

例题1　象棋中的皇后（Chess Queen, UVa 11538）

在2×2棋盘上放两个相互攻击的皇后（一白一黑），一共有12种方法，如图2-1所示。

图　2-1

如果棋盘是n×m的，有多少种方法放置两个相互攻击的皇后？例如n＝100，m＝223时答案为10907100。

【输入格式】

输入由不超过5000行组成，每行均为一组数据，包含两个整数n，m（0≤n，m≤10⁶）。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出在n×m棋盘上放两个相互攻击的皇后的方案数。输出保证不超过64位带符号整数的范围。

【分析】

因为只有两个皇后，因此相互攻击的方式只有两个皇后在同一行、同一列或同一对角线3种情况。这3种情况没有交集，因此可以用加法原理。设在同一行放两个皇后的方案数为A（n，m），同一列放两个皇后的方案数为B（n，m），同一对角线放两个皇后的方案数为D（n，m），则答案为A（n，m）＋B（n，m）＋D（n，m）。

A（n，m）的计算可以用乘法原理：放白后有nm种方法，放黑后有m－1种方法，相乘就是nm（m－1）。也可以理解为先选一行（有n种方法），然后在这一行中选两个位置做全排列，因此有m（m－1）种方案。根据乘法原理得到nm（m－1）。

B（n，m）其实就等于A（m，n）（想一想，为什么），也就是mn（n－1）。

求D（n，m）的思考过程会稍微麻烦一些。不妨设n≤m，所有／向的对角线，从左到右的长度依次为

考虑到还有另一个方向的对角线，上面的整个结果还要乘以2，即

程序如下。

《算法竞赛入门经典》中曾多次强调：计数问题一定要考虑算术运算溢出。我们用64位无符号整数保存n和m，最大可以保存2⁶⁴－1＞1.8×10¹⁹，则nm（m＋n－2）≤10⁶×10⁶×2×10⁶＝2×10¹⁸，不会溢出；而2n（n－1）（3m－n－1）≤2×10⁶×10⁶×3×10⁶＝6×10¹⁸也不会溢出，不难验证，最终答案也不会溢出[1]。虽然本题的输入保证答案不超过带符号64位整数范围内，但因为在运算结果中不会出现负数，使用无符号64位整数更加保险。

例题2　数三角形（Triangle Counting, UVa 11401）

有多少种方法可以从1，2，3，…，n中选出3个不同的整数，使得以它们为三边长可以组成三角形？比如n＝5时有3种方法，即（2，3，4），（2，3，5），（3，4，5）。n＝8时有22种方法。

【输入格式】

输入包含多组测试数据，每组数据的第一行为整数n（3≤n≤1000000）。输入用n＜3的标志结束。

【输出格式】

对于每组数据，输出其方案总数。

【分析】

三重循环的时间复杂度为O（n³），采用这种方法肯定超时。这样的规模即使是O（n²）时间的算法都很难承受，那么只能进行一些数学分析了。

用加法原理，设最大边长为x的三角形有c（x）个，另外两条边长分别为y和z，根据三角形不等式[2]有y＋z＞x。所以z的范围是x－y＜z＜x。

根据这个不等式，当y＝1时x－1＜z＜x，显然无解；y＝2时只有一个解（z＝x－1）；y＝3时有两个解（z＝x－1或者z＝x－2）……直到y＝x－1时有x－2个解。根据等差数列求和公式，一共有0＋1＋2＋…＋（x－3）＋（x－2）＝（x－1）（x－2）/2个解。

可惜，这并不是c（x）的正确数值，因为上面的解包含了y＝z的情况，而且每个三角形算了两遍（想一想，为什么）。解决方案很简单，首先统计y＝z的情况。y的取值从x/2＋1开始到x－1为止，一共有x－1－x/2＝（x－1）/2个解，然后把这部分解扣除，再除以2，即

原题要求的实际上是“最大边长不超过n的三角形数目”f（n）。根据加法原理，f（n）＝c（1）＋c（2）＋…＋c（n）。可以写成递推式f（n）＝f（n－1）＋c（n）。代码如下。

例题3　拉拉队（Cheerleaders, UVa 11806）

在一个m行n列的矩形网格里放k个相同的石子，问有多少种方法？每个格子最多放一个石子，所有石子都要用完，并且第一行、最后一行、第一列、最后一列都得有石子。

【输入格式】

输入第一行为数据组数T（T≤50），每组数据包含3个整数m，n，k（2≤m，n≤20，k≤500）。

【输出格式】

对于每组数据，输出方案总数除以1000007的余数。

【分析】

如果题目求的是“第一行、最后一行、第一列、最后一列都没有石子”的方案数，该有多好啊！这相当于一共只有m－2行n－2列，答案自然是C（（m－2）（n－2），k）了。幸运的是，利用容斥原理，我们可以把本题转化为上述问题。

设满足“第一行没有石子”的方案集为A，最后一行没有石子的方案集为B，第一列没有石子的方案集为C，最后一列没有石子的方案集为D，全集为S，则所求答案就是“在S中但不在A，B，C，D任何一个集合中”的元素个数，可以用容斥原理求解。

在程序中，我们用二进制来表示A，B，C，D的所有“搭配”（S对应于“空搭配”）。如果在集合A和B中，相当于少了一行；如果在集合C或D中，相当于少了一列。假定最后剩了r行c列，方法数就是C（rc，k）。

2.2　递推关系

与学习动态规划专题一节一样，我们先来回顾一些常见的递推题目。如果读者理解下面的内容有困难，请先阅读《算法竞赛入门经典》的相关章节。

问题1：兔子的繁殖。把雌雄各一的一对新兔子放入养殖场中。每对兔子从第二个月开始每月产雌雄各一的一对新兔子。试问第n个月后养殖场中共有多少对兔子？

分析：第n个月的兔子由两部分组成，一部分是上个月就有的老兔子，一部分是刚出生的新兔子。前一部分等于f（n－1），后一部分等于f（n－2）（第n－1个月时具有生育能力的兔子数等于第n－2个月的兔子总数）。根据加法原理，f（n）＝f（n－1）＋f（n－2），边界是f（1）＝f（2）＝1。

数列为1，1，2，3，5，8，…，称为斐波那契（Fibonacci）数列。

问题2：凸多边形的三角剖分数。给出一个凸n边形，用n－3条不相交的对角线把它分成n－2个三角形，求不同的方法数目。例如n＝5时，有5种剖分方法，如图2-2所示。

图　2-2

分析：设答案为f（n）。按照某种顺序给凸多边形的各个顶点分别编号为V₁，V₂，…，Vn。既然分成的是三角形，边V₁Vn在最终的剖分中一定恰好属于某个三角形V₁VnVk，所以可以根据k进行分类。不难看出，三角形V₁VnVk的左边是一个k边形，右边是一个n－k＋1边形，根据乘法原理，包含三角形V₁VnVk的方案数为f（k）f（n－k＋1）；根据加法原理有：f（n）＝f（2）f（n－1）＋f（3）f（n－2）＋…＋f（n－1）f（2），边界是f（2）＝f（3）＝1。

从f（3）开始的数列为：1，2，5，14，42，132，429，1430，4862，16796，…，称为卡特兰（Catalan）数。

问题3：火柴[3]。用n（1≤n≤2000）根火柴棍能组成多少个非负整数？火柴不必用完，组成的整数不能有前导零（但整数0是允许的）。比如，若你有3根火柴，可以组成1或者7；如果有4根，除了可以组成1和7之外，还可以组成4和11。

分析：把“已经使用过的火柴数i”看成状态，可以得到一个图。从前往后每添加一个数字x，就从状态i转移到i＋c[x]，其中c[x]代表数字x需要的火柴数。当i＝0的时候不允许使用数字0（最后当n≥6时，给答案单独加上1，代表整数0）。从结点0和x（x＞0）出发的边分别如图2-3（a）和图2-3（b）所示。

图　2-3

令d（i）为从结点0到结点i的路径条数，则答案f（n）＝d（1）＋d（2）＋d（3）＋…＋d（n）（因为火柴不必用完，所以使用的火柴数目可能是1，2，3，…，n）。

程序实现时，我们可以按照从小到大的顺序用d（i）更新所有的d（i＋c[j]）（j取遍数字0～9），代码如下（请注意，下面的代码中略去了高精度运算）。

问题4：立方数之和[4]。输入正整数n（n≤10000），求将n写成若干个正整数的立方之和有多少种方法。

比如21有3种写法：21＝1³＋1³＋1³＋…＋1³＝2³＋1³＋1³＋1³＋…＋1³＝2³＋2³＋1³＋1³＋1³＋1³＋1³。77有22种写法，9999有440022018293种写法。

分析：建立多段图。结点（i，j）表示“使用不超过i的整数的立方，累加和为j”这个状态，设d（i，j）为从（0，0）到（i，j）的路径条数，则最终答案为d（21，n）（因为对于题目范围，22³＞n）。

这个多段图的特点是每个结点一步只能走到下一个阶段的结点，因此我们可以一个阶段一个阶段的计算，代码如下。

这个程序是正确的，但时间复杂度可以进一步降低，实现方法留给读者思考（提示：d（i，j）＝d（i－1，j）＋d（i，j－i³））。

问题5：村民排队[5]。村子里现在有n（1≤n≤40000）个人，有多少种方式可以把他们排成一列，使得没有人排在他父亲的前面（有些人的父亲可能不在村子里）？输入n和每个人的父亲编号（村里的人编号为1～n），输出方案总数除以1000000007的余数。

分析：村民由父子关系组织成了一个森林。虽然森林中可能有多棵树，但只要设置一个虚拟的树根root，就可以成功地转化成树的情形。这个root的编号为0，其余结点编号为1～n。

下面考虑这棵拥有虚拟根的树。排第一的一定是“老祖宗”root；他的各棵子树相互独立，如图2-4所示。

图　2-4

假设根有上述3棵子树，那么一共有多少种排法呢？首先，各个子树相互独立，因此第一棵子树的3个结点在所有11个非根结点里可以任选3个位置，而第二棵子树的位置是8选6。这样，一共有C（11，3）×C（8，6）×5×4×1＝92400种方法。还可以这么想，先给每棵子树中的结点确定顺序，有5×4×1种方法，然后把3棵子树的结点“穿插”起来。这相当于把每棵子树中的所有结点看成相同元素，根据有重复元素的全排列公式共有11!/（3!×6!×2!）种方法。再根据乘法原理，一共有5×4×1×11!/（3!×6!×2!）＝92400种方法。

一般地，设以结点i为根的子树有f（i）种排法，则

其中cj是结点i的第j个儿子，结点i一共有k个儿子，s（i）表示以i为根的子树的结点总数。递归把所有非根结点的f带入上式，可以发现所有非根结点u以（s（u）－1）!的形式出现在分子一次，以s（u）!的形式出现在分母一次，约分后相当于分子为1，分母为s（u）。这样，我们得到一个简化的表达式

注意到s（root）＝n＋1（所有普通结点加上虚拟结点本身），所以最终答案等于n!除以所有的s（i）!，其中i取遍1～n的所有整数。剩下的唯一问题是如何快速求出这个表达式的值除以MOD＝1000000007的余数。注意到MOD是个素数[6]，所以在模MOD下，除以x等价于乘以x的逆。这样，只需要预处理出所有阶乘和它们的逆即可。求逆的方法在《算法竞赛入门经典》或下一小节中均有详述。

问题6：带标号连通图计数。统计有n（n≤50）个顶点的连通图有多少个。图的顶点有编号。例如n＝3时有4个不同的图，如图2-5所示；n＝4有38个图；n＝5、6时分别有728和26704个图。

图　2-5

分析：设f（n）为所求答案，g（n）为有n个顶点的非连通图，则f（n）＋g（n）＝h（n）＝2n（n－1）/2。g（n）可以这样计算：先考虑1所在连通分量包含哪些顶点。假设该连通分量有k个点，就有C（n－1，k－1）种集合。确定点集后，1所在连通分量有f（k）种情况，其他连通分量有h（n－k）种情况，因此有递推公式：g（n）＝sum｛C（n－1，k－1）×f（k）×h（n－k）｜k＝1…n－1｝。注意每次计算出g（n）后应立刻算出f（n）和h（n），时间复杂度为O（n²）（不考虑高精度）。

数列为：1，1，1，4，38，728，26704，1866256，251548592，…。在OEIS中编号为A001187[7]。

例题4　多叉树遍历（Exploring Pyramids, NEERC 2005, LA 3516）

给出一棵多叉树，每个结点的任意两个子结点都有左右之分。从根结点开始，每次尽量往左走，走不通了就回溯，把遇到的字母顺次记录下来，可以得到一个序列。如图2-6所示的5个图的序列均为ABABABA。给定一个序列，问有多少棵树与之对应。

图　2-6

【输入格式】

输入包含多组数据。每组数据仅一行，即由大写字母组成的访问序列。序列非空，且长度不超过300。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出满足条件的多叉树的数目除以10⁹的余数。

【分析】

设输入序列为S，d（i，j）为子序列Si，Si＋1，…，Sj对应的树的个数，则边界条件是d（i，i）＝1，且Si不等于Sj时d（i，j）＝0（因为起点和终点应是同一点）。在其他情况下，设第一个分支在Sk时回到树根（必须有Si＝Sk），则这个分支对应的序列是Si＋1，…，Sk－1，方案数为d（i＋1，k－1）；其他分支对应的访问序列为Sk，…，Sj，方案数为d（k，j）。这样，在非边界情况，递推关系为d（i，j）＝sum｛d（i＋1，k－1）×d（k，j）｜i＋2≤k≤j，Si＝Sk＝Sj｝，如图2-7所示。

图　2-7

例题5　数字和与倍数（Investigating Div-Sum Property, UVa 11361）

给定正整数a，b，k，你的任务是在所有满足a≤n≤b的整数n中，统计有多少个满足n自身是k的倍数，且n的各个数字（十进制）之和也是k的倍数？比如k＝7时，322满足条件，因为322和3＋2＋2都是7的整数倍。当a＝1，b＝1000，k＝4时，有64个整数满足条件。

【输入格式】

输入第一行为测试数据组数T（T＜100）。以下T行每行3个整数a，b，k（1≤a≤b≤2³¹，1≤k＜10000）。

【输出格式】

对于每组数据，输出满足条件的整数个数。

【分析】

这类题目的第一步大都相同，设f（x）表示不超过x的非负整数中满足条件的个数，则本题的答案等于f（b）－f（a－1）。这样，问题的关键就是计算f函数。假设我们要计算f（23456），就是要数一数0～12345有哪些数满足条件。由于题目中给出的a和b的范围很大，不能一一判断，可以考虑用加法原理，分段求和，如图2-8所示。

图　2-8

每一段都可以用一个包含一个固定前缀和若干星号作为后缀的“模板”，其中星号表示任选一个数字。比如模板“***”就是3个数字都任选，因此对应000～999；模板“31**”表示前两个数字必须为31，后两个数字任选，因此对应3100～3199。

设f（d，m₁，m₂）表示共d个数字，其中各数字之和除以k的余数为m₁，这些数字组成的整数除以k的余数为m₂的整数的个数，则每个模板对应的解个数都等于某个f（d，m₁，m₂）。比如，模板“22***”对应的解个数等于f（3，3，1），因为：

（1）一共有3个星号。

（2）2＋2＝4，因此剩下3个数字之和除以7余3，相加之后才能被7整除。

（3）22000除以7的余数是6，因此后3个数字组成的整数除以7的余数应为1。

f函数可以用递推求解，只需枚举最高位的数字（0～9）即可，递推式为：

注意，在上面的式子中，a mod k的结果必须保证为0～k－1之间，当a为负数时要当心。程序留给读者编写。

例题6　葛伦堡博物馆（Glenbow Museum, World Finals 2008, LA 4123）

对于一个边平行于坐标轴的多边形，我们可以用一个由R和O组成的序列来描述它：从某个顶点开始按照逆时针顺序走，碰到一个90°的内角（也就是左转）记R；碰到一个270°的内角（右转）记O。这样的序列称为角度序列。

给定正整数L，有多少个长度为L的角度序列至少可以对应一个星型多边形（即多边形中存在一个点可以看到多边形边界上每一个点）？多边形每条边的长度任意。注意，一个多边形有多条序列与之对应，比如图2-9中的多边形可以描述为RRORRORRORRO，或者RORRORRORROR，或者ORRORRORRORR。

图　2-9

【输入格式】

输入包含多组数据。每组数据仅一行，即角度序列的长度L（1≤L≤1000）。输入结束标志为L＝0。

【输出格式】

对于每组数据，输出满足条件的角度序列的个数。

【分析】

不难发现，多边形的顶点数n一定等于角度序列的长度。因此当n为奇数或n＜3答案为0，否则一定有（n＋4）/2个R和（n－4）/2个O（想一想，为什么）。

假设从星型多边形的核上的某个点（即可以“看到”整个多边形的某个点）往上下左右4个方向看，一定可以看到4条边，并且这4条边一定是RR；而且相邻两条RR边之间有一个“阶梯”，序列为OROROR…RORO，如图2-10所示。

图　2-10

不难发现，序列中会有4个子序列RR，但不会出现OO。另一方面，只要是R比O多4个，且没有两个O相邻，一定可以构造出上面这样的多边形（想一想，为什么）。这样，问题转化为：把（n＋4）/2个R和（n－4）/2个O排成一个环状序列，要求没有两个O相邻，有多少种方法？

可以递推计算：设f（i，j，k，p）表示i个R，j个O，第一个元素为R（k＝0）还是O（k＝1），最后一个数为R（p＝0）还是O（p＝1），且没有相邻O的方案总数，则边界为f（1，0，0，0）＝f（0，1，1，1）＝1，而在其他情况下，f（i，j，k，0）＝f（i－1，j，k，0）＋f（i－1，j，k，1），f（i，j，k，1）＝f（i，j－1，k，0），最终答案为f（（n＋4）/2，（n－4）/2，0，0）＋f（（n＋4）/2，（n－4）/2，0，1）＋f（（n＋4）/2，（n－4）/2，1，0）。

如果用递推的方式实现上述算法，时间复杂度为O（n²），因为合法的f（i，j，k，p）有O（n²）个，而每个f值需要O（1）时间计算。尽管这样的时间复杂度对于本题的范围来说已经足够，但其实这个算法离O（n）时间复杂度的算法只有一步之遥：满足i－j＞5的f值不会对答案产生影响，根本不用计算。这样，需要计算的f值减少到了O（n）个，总时间复杂度也降为了O（n）。

也可以直接写出一个O（n）时间的递推式。设d（i，j，k）表示共有i个R，其中有j对相邻的R（如果有3个R相邻，看成两对），第一个元素是k，最后一个元素是R，且没有相邻O的序列个数，则答案为d（（n＋4）/2，3，0）＋d（（n＋4）/2，4，1）＋d（（n＋4）/2，4，0），其中第一项对应于首末两个字母均为R，第二项对应于O开头R结尾，第三项对应于R开头O结尾（想一想，为什么）。类似于上面的推理，边界为d（1，0，1）＝1（序列OR），d（1，0，0）＝1（序列R），其他d（1，j，k）＝0；在非边界情况，d（i，j，k）＝d（i－1，j，k）＋d（i－1，j－1，k），其中第一项表示在末尾添加两个字母OR，第二项表示在末尾添加单个字母R。

代码如下。注意n＜4或者n为奇数时输出0，并且L＝1000时答案高达5208312500。

例题7　串并联网络（Series-Parallel Networks, UVa 10253）

串并联网络有两个端点，一个叫源，一个叫汇，递归定义如下。

（1）一条单独的边是串并联网络。

（2）若G1和G2是串并联网络，把它们的源和汇分别接在一起也能得到串并联网络。

（3）若G1和G2是串并联网络，把G1的汇和G2的源并在一起也能得到串并联网络。

其中规则2说的是并列，规则3说的是串联。

注意在串并联网络中两点之间可以有多条边，而且串联在一起的各个部分可以任意调换顺序，比如图2-11中（a）、（b）和（c）3种网络被看做相同的。

图　2-11

类似地，并联在一起的各个部分也可以任意调换顺序。比如图2-12中（a）、（b）和（c）3种网络也被看做相同的。

图　2-12

输入正整数n，你的任务是统计有多少个n条边的串并联网络。例如，n＝4时有10个，n＝15时有1399068个。

【输入格式】

输入包含多组数据，每组数据仅包含一个整数，即边数n（1≤n≤30）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出一行，即包含n条边的串并联网络的数目。

【分析】

每个串并联网络都可以看成一棵树：为每次串联或并联创建一个结点，并且把所有串联／并联部分看成该结点的子树，如图2-13所示。

图　2-13

假定根结点是并联结点，则第二层的所有非叶结点都是串联结点，第三层的非叶结点都是并联结点……这样就可以知道所有非叶结点的类型。同理，如果根结点是串联结点，也可以推导出其他所有非叶结点的类型。换句话说，其实我们根本不需要理会什么并联结点、串联结点，只要算出“共n个叶子，且每个非叶结点至少有两个子结点”的树的数目f（n），再乘以2就是本题的答案。

如何求f（n）呢？我们可以用分类统计的方法。如图2-14所示，根据根结点的子结点数目，可以把所有的“5叶子树”分成6种（注意，根结点不能只有一个子结点）。

图　2-14

为什么只有这6种呢？各子树的叶子数可以为1，2，1，1或者3，1，1吗？注意题目中规定“串并联的各个部分可以任意调换顺序”，为了避免重复，规定各个子树按照包含叶子的数目从小到大排列。

如何计算每种情况的数目呢？可以用递推。考虑这种情况，根结点有5棵子树，分别包含4，4，4，6，6个叶子，如图2-15所示。

图　2-15

问题转化为可重复选择的组合问题。注意到“4叶子”子树和“6叶子”子树相互独立，根据乘法原理，上述情况的树的个数为C（5＋3－1，3）×C（33＋2－1，2）。

这样，我们得到了算法一：枚举n的所有整数划分（即把n写成若干正整数之和的形式，其中各个加数按从小到大排列），然后直接按照上述方法计算出结果。由于n不大，整数划分不是很多，这个方法可以奏效。

事实上，本题还有更加高效的算法。设d（i，j）表示每棵子树最多包含i个叶子，一共有j个叶子的方案数，则所求的f（n）＝d（n－1，n）。

d（i，j）的递推方式和算法一的思路一致。假设恰好包含i个叶子的子树有p棵，那么这些树的组合数等于从f（i）棵树中选p棵的方案数，即C（f（i）＋p－1，p），因此

边界比较复杂。首先，当i≥0时d（i，0）＝1，其次，i≥1时d（i，1）＝1，但d（0，i）＝0。注意计算组合数时不要溢出。n＝30时的解为90479177302242。代码如下。

2.3　数　　论

《算法竞赛入门经典》中已经介绍了数论的一些基本概念和算法，包括唯一分解定理、欧几里得算法、扩展的欧几里得算法、同余模算术、线性不定方程和模方程。书中的组合数学部分还介绍了欧拉函数。本节将回顾这些数论基础知识，并补充一些新的内容。

2.3.1　基本概念

为了便于理解，下面先回顾一些基本概念和常用代码。

素数。素数也称质数，即恰好包含两个不同因子的整数。注意1不是素数。所有素数从小到大排成一行为2，3，5，7，11，13，17，19，…。素数表生成代码如下。

欧几里得算法。原始算法只能求出两个整数a和b的最大公约数d，扩展算法还可以求出两个整数x和y，使得ax＋by＝d。在此前提下｜x｜＋｜y｜取最小值。代码如下。

注意我们创建的自定义类型LL，它等价于long long。如果在很多地方都出现了long long类型，使用LL会方便许多。这样做还有一个好处，那就是如果题目范围使得int已经足够，无需用long long，只需把LL改成int的同义词，代码本身不需要修改。

模算术。最基本的加减法无须赘述。下面是乘积取模与幂取模。注意，在调用函数之前，应确保下面的a和b都满足0≤a，b＜n。

欧拉phi函数。欧拉函数phi（x）等于不超过x且和x互素的整数个数。《算法竞赛入门经典》中已经推导出phi函数的公式为

单个欧拉函数和函数表可以用下面代码求出。

剩余系。通俗地说，模n的完全剩余系就是｛0，1，2，…，n－1｝，而简化剩余系（也称缩系）就是完全剩余系中与n互素的那些元素。比如n＝12时，缩系中只有4个元素：1，5，7，11。模n的完全剩余系最常见的写法是Z/nZ，也可以写成Z/n或者Zn。为了简单，本书记为Zn。缩系记为Zn*。

Zn中的每个元素都代表所有与它同余的整数。比如n＝5时，Z₅中的元素“3”实际上代表了3，3＋5＝8，3＋10＝13，3＋15＝18等，所有这些整数除以5的余数都是3。我们把满足同余关系的所有整数看成一个同余等价类，比如3，8，13，18等都属于“模5等于3”这个等价类。

既然Zn中的每个元素代表着一个同余等价类，Zn中的加法自然不能是普通的加法，而应是“模加法”，乘法也不是普通的乘法，而是“模乘法”。因此，在Z₅中3＋4＝2，在Z₁₂中5*7＝11。

模乘法的逆。在某些情况下，Zn中的两个元素a和b满足ab＝1，比如在Z₁₅中，7*13＝1。在这种情况下，我们说a和b互为乘法的逆，记为b＝a⁻¹，a＝b⁻¹。这个逆很像“倒数”，因为在剩余系中，当a⁻¹存在时，“除以”一个数a等价于乘以它的乘法逆a⁻¹。比如在Z₁₅中7⁻¹＝13，因此3/7＝3*7⁻¹＝3*13＝9。

看到这里，读者可能会产生疑问：3/7甚至不是整数，怎么可能等于9？请注意，因为剩余系中的每个元素对应一个同余等价类。3/7＝9的实际含义是“假定有两个整数a和b，其中a/b是整数，且a和b除以15的余数分别为3和7，则a/b除以15的余数等于9”。比如a＝528，b＝22就是一例。

由于“乘法逆”太重要了，这里给出计算它的完整代码，代码中用到了扩展欧几里得算法。

在执行完gcd之后，x可能为负数，但因为｜x｜＋｜y｜最小，所以加上n以后一定非负。如果d不等于1，a的逆不存在。此时，上述函数会返回-1。

求逆的另一个方法是利用欧拉定理。给定整数n＞1，对于任意a∈Zn*，aΦ（n）≡1（mod n）。因此，a的逆就是aΦ（n）－1，只不过需要取个模，变成0～n－1之间的整数（当然，实际上模不会等于0）。如果n是素数的话，Φ（n）＝n－1，所以a的逆就是pow_mod（a，n－2，n）。

例题8　总是整数（Always an Integer, World Finals 2008, LA 4119）

组合数学主要研究计数问题。比如，从n个人中选两个人有多少种方法？圆周上有n个点，两两相连之后最多能把圆面分成多少部分？如图2-16所示。有一个金字塔，从塔顶开始每一层分别有1×1，2×2，…，n×n个小立方体，问一共有多少个小立方体？

图　2-16

很多问题的答案都可以写成n的简单多项式。比如上述第一个问题的答案是n（n－1）/2，也就是（n²－n）/2；第二个问题的答案是（n⁴－6n³＋23n²－18n＋24）/24；第三个问题的答案是n（n＋1）（2n＋1）/6，即（2n³＋2n²＋n）/6。

由于上述3个多项式是计数问题的答案，因此当n取任意正整数时，这些多项式的值都是整数。当然，对于其他多项式，这个性质并不一定成立。

给定一个形如P/D（其中P是n的整系数多项式，D是正整数）的多项式，判断它是否在所有正整数处取到整数值。

【输入格式】

输入包含多组数据。每组数据仅一行，即一个多项式（P）/D，其中P是若干个形如Cn^E的项之和，其中系数C和E满足以下条件。

（1）E是一个满足0≤E≤100的整数。如果E＝0，则Cn^E写成C；如果E＝1，则Cn^E写成Cn，除非C等于1或者-1（C＝1时写成n，C＝-1时写成-n）。

（2）C是一个整数。如果C等于1或者-1，且E不是0或者1，则Cn^E写成n^E或者-n^E。

（3）只有不在第一项的非负C的前面才会有一个加号。

（4）E的数值严格递减。

（5）C和D都在32位带符号整数范围内。

输入结束标志为单个句号。

【输出格式】

对于每组数据，如果满足条件，输出“Always an integer”，否则输出“Not always an integer”。

【样例输入】

（n^2－n）/2

（2n^3＋3n^2＋n）/6

（-n^14－11n＋1）/3

．

【样例输出】

Case 1: Always an integer

Case 2: Always an integer

Case 3: Not always an integer

【分析】

本题实际上是判断一个整系数多项式P的值是否总是正整数D的倍数。一个容易想到的方法是，随机代入很多整数计算P/D，如果全都是整数，那么很有可能是“Always an integer”；如果有的不是整数，那么答案必然是“Not always an integer”。

这个方法看起来有些投机取巧，但效果非常不错。事实上，不需要随机代入，只需要把n＝1，2，3，…，k＋1全试一遍就可以了，其中k是多项式中最高项的次数。

为什么可以这样做呢？让我们从k较小的情况开始研究。

当k＝0时，P里根本就没有n个变量，所以只需代入P（1）计算即可。

当k＝1时，P是n的一次多项式，设为an＋b，则P（n＋1）－P（n）＝a。如果把P（n）看成一个数列的第n项，则｛P（n）｝是一个首项为P（1），公差为整数a的等差数列，因此只要首项和公差均为D的倍数，整个数列的所有项都会是D的倍数。因此只需验证P（1）和P（2）。

当k＝2时，P是n的二次多项式，设为an²＋bn＋c，则P（n＋1）－P（n）＝2an＋a＋b。注意到这个2an＋a＋b是n的一次多项式，根据刚才的结论，只要n＝1和n＝2时它都是D的倍数，对于所有正整数n，它都将是D的倍数。这样，相邻两项的差为D的倍数，再加上首项也为D的倍数，则P（n）将总是D的倍数。整理一下，只要P（1），P（2）－P（1），P（3）－P（2）都是D的倍数即可。这等价于验证P（1），P（2）和P（3）。

看到这里，结论已经不难猜到了。对于k次多项式P（n），相邻两项之差P（n＋1）－P（n）是关于n的k－1次多项式，根据数学归纳法，命题得证。顺便说一句，数列dP（n）＝P（n＋1）－P（n）称为P（n）的差分数列（difference series）。而差分数列的差分数列为二阶差分数列d²P（n），依此类推。这样，k＝2的证明可以用图2-17说明。

图　2-17

如果P（1），P（2），P（3）都是D的倍数，意味着P（1），dP（1）和d²P（1）都是D的倍数。由于第三行（即所有的d²P（n））是常数，所以整个第三行都是D的倍数；根据差分的定义，可以推导出整个第二行都是D的倍数，再进一步得到第一行也都是D的倍数。

程序留给读者编写。

例题9　最大公约数之和——极限版II（GCD Extreme（II）, UVa 11426）

输入正整数n，求gcd（1，2）＋gcd（1，3）＋gcd（2，3）＋…＋gcd（n－1，n），即所有满足1≤i＜j≤n的数对（i，j）所对应的gcd（i，j）之和。比如n＝10时答案为67，n＝100时答案为13015，n＝200000时答案为143295493160。

【输入格式】

输入包含不超过100组数据，每组数据占一行，包含正整数n（2≤n≤4000000）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出一行，即所求和。答案保证在64位带符号整数范围内。

【分析】

设f（n）＝gcd（1，n）＋gcd（2，n）＋gcd（3，n）＋…＋gcd（n－1，n），则所求答案为S（n）＝f（2）＋f（3）＋…＋f（n）。只需求出f（n），就可以递推出所有答案：S（n）＝S（n－1）＋f（n）。因此，本题的重点在于如何计算f（n）。

注意到所有gcd（x，n）的值都是n的约数，可以按照这个约数进行分类，用g（n，i）表示满足gcd（x，n）＝i且x＜n的正整数x的个数，则f（n）＝sum｛i×g（n，i）｜i是n的约数｝。注意到gcd（x，n）＝i的充要条件是gcd（x/i，n/i）＝1，因此满足条件的x/i有phi（n/i）个，说明g（n，i）＝phi（n/i）。

问题到这里还没有结束。如果依次计算f（n），需要对每个n枚举它的约数i，速度较慢，但如果把思路逆转过来，对于每个i枚举它的倍数n（并且更新f（n）的值），时间复杂度将降为与素数筛法同阶。至此，问题得到了完整的解决。代码如下。

2.3.2　模方程

线性模方程。《算法竞赛入门经典》中曾介绍过模线性方程的求解方法，即把它转化为线性不定方程。下面让我们换一个角度看看这个问题。首先还是把ax≡b（mod n）转化为ax－ny＝b，当d＝gcd（a，n）不是b的约数时无解，否则两边同时除以d，得到a'x－n'y＝b'，即a'x≡b'（mod n'）（这里a'＝a/d，b'＝b/d，n'＝n/d）。此时a'和n'已经互素，因此只需左乘a'在模n'下的逆，则解为x≡（a'）⁻¹b'（mod n'）。这个解是模n'剩余系中的一个元素，但一般我们还需要把解表示成模n剩余系中的元素。应当怎么做呢？

令（a'）⁻¹b'＝p，上述解相当于x＝p，p＋n'，p＋2n'，p＋3n'…。对于模n来说，这无穷多个整数有多少个等价类呢？假定p＋in'和p＋jn'同余，则（p＋in'）－（p＋jn'）＝（i－j）n'是n的倍数，因此i－j必须是d的倍数。换句话说，在模n剩余系下，ax≡b（mod n）恰好有d个解，为p，p＋n'，p＋2n'，p＋3n'，…，p＋（d－1）n'。

中国剩余定理。如果有多个方程；变量还是只有一个，该怎么做呢？可以考虑用中国剩余定理（Chinese Remainder Theorem）。假定有方程组x≡ai（mod mi），且所有模mi两两互素。令M为所有mi的乘积，wi＝M/mi，则（wi，mi）＝1。用扩展欧几里得算法可以找到pi和qi使得wipi＋miqi＝1。然后令ei＝wipi，则方程组等价于单个方程x≡e₁a₁＋e₂a₂＋…＋enan（mod M）。换句话说，在模M剩余系下，原方程组有唯一解。

不难验证x₀＝e₁a₁＋e₂a₂＋…＋enan是一个解，把等式wipi＋miqi＝1两边模mi后立即可得ei mod mi＝1，而对于不等于i的j，wi是mj的倍数（想一想，为什么），因此ei mod mj＝0。这样，x₀对mi取模时，除了eiai这一项余数为1×ai＝ai之外，其余项的余数均为0。代码如下。

离散对数。首先回忆一下初等代数里的对数。如果ax＝b，就说x＝logab，即x是以a为底b的对数。在模算术里，也有类似的概念，但要比初等代数里的复杂一些。简单起见，这里只考虑一种简单的情况，即当n为素数时，解模方程ax≡b（mod n）。因为n是素数，只要a不为0，一定存在逆a⁻¹。

根据欧拉定理，只需检查x＝0，1，2，…，n－1是不是解即可。因为an－1≡1（mod n），当x超过n－1时ax就开始循环了。我们先检查前m项（m的取值稍后叙述），即a⁰，a¹，…，am－1模n的值是否为b，并把ai mod n保存在ei里，求出am的逆a-m。

下面考虑am，am＋1，…，a2m－1。这次不用一一检查，因为如果它们中有解，则相当于存在i使得ei×am≡b（mod n）。两边左乘a-m得ei≡b'（mod n），其中b'＝a-mb（mod n）。这样，只需检查是否真的有ei等于这个b'即可。为了方便，可以把ei保存在一个STL集合中，但考虑到还需要得到具体的下标i，我们用一个map＜int，int＞ x，其中x[j]表示满足ei＝j的最小的i（因为可能有多个ei的值相同）。在下面的代码中，b'采用递推计算，覆盖了输入参数b。

上面的代码中，m＝n1/2，这是为什么呢？因为计算e数组需要O（mlogm）时间，以后每一轮需要O（logm）时间，一共O（n/m）轮，因此总时间复杂度为O（（m＋n/m）logm），当m和n/m接近，即m＝n1/2时总时间较短，为O（n1/2logn）。这个算法称为Shank的大步小步算法（Shank's Baby-Step-Giant-Step Algorithm）。

当n不是素数时，可以把这个算法加以扩展，方法留给读者思考。

例题10　数论难题（Code Feat, UVa 11754）

有一个正整数N满足C个条件，每个条件都形如“它除以X的余数在集合｛Y₁，Y₂，…，Yk｝中”，所有条件中的X两两互素，你的任务是找出最小的S个解。

【输入格式】

输入包含若干组数据。每组数据第一行为两个整数C和S（1≤C≤9，1≤S≤10）。以下C行每行描述一个条件，首先是整数X和k（X≥2，1≤k≤100），然后是Y₁，Y₂，…，Yk（0≤Y₁，Y₂，…，Yk＜X）。所有X的乘积保证在32位带符号整数的范围内。输入结束标志为C＝S＝0。

【输出格式】

对于每组数据，输出S个最小正整数解，并按照从小到大顺序排列。

【样例输入】

3 2

2 1 1

5 2 0 3

3 2 1 2

0 0

【样例输出】

5

13

【分析】

“除以X的余数在集合｛Y₁，Y₂，…，Yk｝中”这个条件很不好处理。如果我们知道这个余数具体是Y₁，Y₂，…，Yk中的哪一个，问题就会简单很多。一种容易想到的方法是枚举每个集合中取哪个元素，它可以解决样例。样例有3个条件，即x mod 2＝1，x mod 5＝0或3，x mod 3＝1或2，一共有如下4种可能．

当所有k的乘积很大时这种方法会很慢，此时我们有另外一个方法，直接枚举x。找一个k/X最小的条件（比如样例中的第二个条件k＝2，X＝5），按照t＝0，1，2，…的顺序枚举所有的tX＋Yi（相同的t按照从小到大的顺序枚举Yi），看看是否满足条件。因为所有k的乘积很大，这个算法很快就能找到解。

有两个需要注意的地方。首先，如果用中国剩余定理求解，若得到的解不超过S个，需要把这些解加上M，2M，3M，…，直到解足够多（M为所有X的乘积）。其次，根据题意，0不能算作解，因为它不是正整数。但不要因此把M，2M，3M，…这些解也忽略了。代码如下。

例题11　网格涂色（Emoogle Grid, UVa 11916）

有这样一道题目，要给一个M行N列的网格涂上K种颜色，其中有B个格子不用涂色，其他每个格子涂一种颜色，同一列中的上下两个相邻格子不能涂相同颜色。给出M，N，K和B个格子的位置，求出涂色方案总数除以100000007的结果R。

本题的任务和这个相反：已知N，K，R和B个格子的位置，求最小可能的M。

【输入格式】

输入第一行为数据组数T（T≤150）。每组数据第一行为4个整数N，K，B，R（0≤R＜100000007）。以下B行每行为两个整数x和y（1≤x≤M，1≤y≤N），即每个不需要涂色的格子的行列编号。这些格子的位置各不相同。

【输出格式】

对于每组数据，输出最小的M。输入保证一定有解。

【分析】

一列一列地涂色，每列从上往下涂。如果一个格子位于第一行，或者它上面相邻格子不能涂色，则它有K种涂色方法；其他可涂色格有K－1种方法。虽然M是未知的，但由于M至少应当等于不能涂色的格子的行编号的最大值，因此可以把整个网格分成不变部分和可变部分，如图2-18所示。

图　2-18

假定不变部分以及可变部分的第一行一共有cnt种涂色法，则每加一行之后，涂色方案数都会乘以P＝（K－1）N。这样，我们得到了一个模方程cnt×PM＝R，移项得PM＝R×cnt⁻¹。用大步小步算法求解即可。注意要事先判断M＝L是否满足条件，否则不仅“可变部分”为空，“不变部分”也是不完整的。

2.4　组合游戏

考虑这样一个游戏，有3堆火柴，分别有a，b，c根，记为状态（a，b，c）。每次一个游戏者可以从任意一堆中拿走至少一根火柴，也可以整堆拿走，但不能从多堆火柴中同时拿。无法拿火柴的游戏者输。这个游戏称为Nim游戏。

举个例子，假设a＝1，b＝2，c＝3，也就是3堆火柴分别有1，2，3根。若是你先拿，你会怎么拿呢？

方案1：拿走第一堆唯一的一根火柴。这样的话只剩两堆火柴了，分别有2根和3根，即状态（0，2，3）。在这种情况下，对手只要在第三堆火柴中拿走一根，即变成（0，2，2），你就输了。为什么？因为以后不管你怎么拿，对手总能在另一堆里采用相同的拿法，所以对手肯定不会出现无法拿火柴的情况，因此输的只能是你。当然了，如果对手比较笨，不把火柴拿成（0，2，2）的状态，你仍然有机会获胜，但这不在讨论范围之内。我们假定游戏双方都是绝顶聪明的，在有必胜策略时一定采取必胜策略。

方案2：完全拿走第二堆或者第三堆，即变成（1，0，3）或者（1，2，0）。这样做其实和方案1是等价的（想一想，为什么），仍然是你输。

方案3：拿成（1，1，3），（1，2，2）或者（1，2，1）。不难发现，对手只需分别拿成（1，1，0），（0，2，2）和（1，0，1），又是你输。

至此，我们已经讨论完了所有6种可能的拿法，都是你输。所以我们说（1，2，3）是一个先手必败的状态，简称必败状态[8]。

上述思考方法不仅适用于Nim游戏，在本节中，我们将考虑这样一类组合游戏：

（1）两个游戏者轮流操作。

（2）游戏的状态集有限，并且不管双方怎么走，都不会再现以前出现过的状态。这保证了游戏总是在有限步后结束。

（3）谁不能操作谁输，另一个游戏者获胜。不难发现，在这样的规则下是不会出现平局的。

状态图。为了方便分析，我们可以把游戏中的状态画成图。每个结点是一个状态，每条边代表从一个状态转移到另一个状态的操作。本节只讨论公平游戏（impartial game），也就是说，如果一个游戏者可以把状态A变为状态B，另一个游戏者也可以。国际象棋不是公平游戏，因为白方可以移动白子，但黑方却不可以。

上面的例子可以画出如图2-19所示的状态图（不完整）。

图　2-19

把刚才的分析搬到图上来叙述，就是（1，2，3）的后继状态都是先手必胜状态，因此（1，2，3）是先手必败状态。注意，先手必胜状态（简称必胜状态）是指先手存在必胜策略，而不是先手不管怎么走都能赢。别忘了我们的假设是双方都足够聪明。这样不难得到如下两个规则。

规则1：一个状态是必败状态当且仅当它的所有后继都是必胜状态。

规则2：一个状态是必胜状态当且仅当它至少有一个后继是必败状态。

作为特例，没有后继状态的状态是必败状态（我们的规则是不能操作的游戏者输）。有了这两个规则，就可以用递推的方法判断整个状态图的每一个结点是必胜状态还是必败状态。因为状态图是无环的，所以如果按照拓扑序的逆序进行判断，在判断每个结点的时候，它的所有后继都已经判断过了。

Ferguson游戏。为了加深对状态图的理解，我们来考虑一个简单的游戏。有两个盒子，一开始其中一个有m颗糖而另一个有n颗糖，把这样的状态记为（m，n）。每次移动是将一个盒子清空而把另一个盒子的一些糖拿到被清空的盒子中，使得两个盒子至少各有一颗糖。显然唯一的终态为（1，1）。如果最后移动的游戏者获胜，那么状态为（m，n）的先手是胜还是败？根据上述定义，按照k＝m＋n从小到大的顺序判断即可。下面的代码输出了所有k＜20的必败状态。由于状态（n，m）和（m，n）是等价的，程序只输出了n≤m的必败状态。

Chomp!游戏。有一个m×n棋盘，每次可以取走一个方格并拿掉它右边和上面的所有方格。拿到左下角的格子（1，1）者输。例如在8×3棋盘中拿掉（6，2）和（2，3）后的状态，如图2-20所示。

图　2-20

给出m和n，问先手必胜还是必败。

分析：本题的结论有些出乎意料，除了（1，1）是先手必败之外，其他情况都是先手必胜。证明如下。

如果后手能赢，也就是说后手有必胜策略，使得无论先手第一次取哪个石子，后手都能获得最后的胜利。那么现在假设先手取最右上角的石子（n，m），接下来后手通过某种取法使得自己进入必胜的局面。但事实上，先手在第一次取的时候就可以和后手这次取的一样，抢先进入必胜局面，与假设矛盾。

约数游戏。有1～n个数字，两个人轮流选择一个数，并把它和它的所有约数擦去。擦去最后一个数的人赢，问谁会获胜。

分析：本题和“Chomp!游戏”有着异曲同工之妙，读者不妨试着证明一下（提示，假设先手选“1”）。

Nim游戏的解法。回到Nim游戏，我们实际上已经得到了一个判断任意状态（a，b，c）是否为必败状态的算法，可惜时间复杂度比较高，因为当a、b、c很大时，状态图的结点数和边数会很大。有没有直接判断的方法呢？L.Bouton在1902年给出了这样一个定理，状态（x₁，x₂，x₃）为必败状态当且仅当x₁ xor x₂ xor x₃＝0，这里的xor是二进制的逐位异或操作，也称为Nim和（Nim sum）。

举个例子，状态为（13，12，8）的情况是先手胜还是先手败，只需要计算

因为不是0，所以先手胜。那么应当怎样操作才能胜利呢？不难发现，应该给对手留下一个先手败的状态，即Nim和等于0的状态。比如，把13＝1101₂变成4＝100₂。

事实上，这个定理适合于任意堆的情况，即每堆火柴的数目全部放在一起求异或，当且仅当结果为0时，状态是先手必败的。下面用数学归纳法证明。

首先，如果每堆都没有火柴，显然先手败；否则根据前面介绍的规则，需要证明两个结论。

（1）对于必胜状态，一定有一个后继状态是必败的。

证明：

假设Nim和为X＞0，且X的二进制表示最左边的1在第k位，则一定存在一个该位为1的堆（想一想，为什么）。设这堆火柴的数量为Y，则只需要把它拿成Z＝Y xor X根火柴，得到的状态就是必败状态。为什么呢？我们需要证明两个结论：Z＜Y，以及新的Nim和等于0。注意，Y和X异或之后第k位变成了0，且第k位左边那些位均不变，因此Z一定比Y小；所有堆的Nim和原先等于X，现在Y变成了Z，新的Nim和等于X xor Y xor Z＝X or Y xor （Y xor X）＝0。

（2）对于必败状态，所有后继状态都是必胜的。

证明：

由于只能改变一堆火柴，不管修改它的哪一位，Nim和对应的那一位一定不为0，因此不可能是必败状态。

这样，我们就证明了Bouton定理。

组合游戏的和。假设有k个组合游戏G₁，G₂，…，Gk，可以定义一个新游戏，在每个回合中，当前游戏者可以任选一个子游戏Gi进行一次合法操作，而让其他游戏的局面保持不变，不能操作的游戏者输。这个新游戏称为G₁，G₂，…，Gk的和。

考虑这样3个游戏：

（1）有一堆火柴，共有3根。每次一个游戏者可以拿走至少一根火柴，也可以整堆拿走。无法拿火柴的游戏者输。

（2）有一堆火柴，共有7根。每次一个游戏者可以拿走至少一根火柴，也可以整堆拿走。无法拿火柴的游戏者输。

（3）有一堆火柴，共有8根。每次一个游戏者可以拿走至少一根火柴，也可以整堆拿走。无法拿火柴的游戏者输。

则这3个游戏的“和”就是下面这个Nim游戏。

有3堆火柴，分别有3，7，8根。每次一个游戏者可以从任意一堆中拿走至少一根火柴，也可以整堆拿走，但不能从多堆火柴中同时拿。无法拿火柴的游戏者输。

为什么呢？因为每个回合里，当前游戏者可以选择一个子游戏（即选一堆火柴），然后进行一次合法操作（从该堆中拿火柴），但其他子游戏的局面保持不变（不能从多堆火柴中同时拿）。

组合游戏的和通常是很复杂的[9]，下面介绍一个很好的工具，可以解决组合游戏的和的问题，那就是SG函数和SG定理。

SG函数和SG定理。对于任意状态x，定义SG（x）＝mex（S），其中S是x的后继状态的SG函数值集合，mex（S）表示不在S内的最小非负整数。比如，若x有3个后继状态，SG函数值分别为0，1，1，2，4，7，则SG（x）＝3，因为3是第一个没有出现在后继状态SG函数值集合中的非负整数。这样，终态的SG值显然为0（因为S是空集），而其他值可以递推算出。不难发现，SG（x）＝0当且仅当x为必败状态。

SG函数的威力需要借助于Sprague-Grundy定理（SG定理）才能发挥。游戏和的SG函数等于各子游戏SG函数的Nim和。这样，就可以把各个子游戏分而治之，大大简化了问题。顺便说一句，上述Bouton定理可以看做Sprague-Grundy定理在Nim游戏中的直接应用，因为单堆Nim游戏的SG函数满足SG（x）＝x（想一想，为什么）。

上述描述有些难懂，但即使你有些迷糊也没有关系，可以直接阅读下面的例题。在学习组合游戏时，例题往往能大大加深你对知识点的理解。

翻棋子游戏。一个棋盘上每个格子有一个棋子，每次操作可以随便选一个朝上的棋子（x，y）（代表第x行第y列的棋子），选择一个形如（x，b）或者（a，y）（其中b ＜ y，a ＜ x）的棋子，然后把它和（x，y）一起翻转，无法操作的人输。

分析：把坐标为（x，y）的棋子看做大小分别为x和y的两堆火柴，则本题转化为了经典的Nim游戏。如果难以把棋子“看做”火柴，可以先把Nim游戏中的一堆火柴看成一个正整数，则Nim游戏中的每次操作是把其中一个正整数减小或者删除。

除法游戏[10]。有一个n×m（1≤n，m≤50）矩阵，每个元素均为2～10000之间的正整数。两个游戏者轮流操作。每次可以选一行中的1个或多个大于1的整数，把它们中的每个数都变成它的某个真因子，比如12可以变成1，2，3，4或者6，不能操作的输（换句话说，如果在谁操作之前，矩阵中的所有数都是1，则他输）。

分析：考虑每个数包含的素因子个数（比如12＝2×2×3包含3个素因子），则让一个数“变成它的真因子”等价于拿掉它的一个或多个素因子。这样，每行对应一个火柴堆，每个数的每个素因子看成一根火柴，则本题就和Nim游戏就完全等价了。

例题12　石子游戏（Play with Stones, Jakarta 2010, LA 5059）

有n堆石子，分别有a₁，a₂，…，an个。两个游戏者轮流操作，每次可以选一堆，拿走至少一个石子，但不能拿走超过一半的石子。比如，若有3堆石子，每堆分别有5，1，2个，则在下一轮中，游戏者可以从第一堆中拿1个或2个，第二堆中不能拿，第三堆只能拿一个。谁不能拿石子，就算输。

【输入格式】

输入第一行为数据组数T（T≤100）。每组数据的第一行为整数n（1≤n≤100），第二行包含n个整数a₁，a₂，…，an（1≤ai≤2×10¹⁸）。

【输出格式】

对于每组数据，如果先手胜，输出YES，否则输出NO。

【分析】

本题和Nim游戏不同，但也可以看做n个单堆游戏之和。遗憾的是，即使是单堆游戏，由于ai的范围太大，也不能按照定义递推出所有的SG函数。尽管如此，我们还是可以先写一个递推程序，看看SG函数有没有规律。代码如下。

打印出来的结果是1 0 2 1 3 0 4 2 5 1 6 3 7 0 8 4 9 2 10 5 11 1 12 6 13 3 14 7 15。我们发现当n为偶数时，SG（n）＝n/2，但n为奇数时似乎没什么规律。但当把n为偶数的值全部删除后得到的数列是0 1 0 2 1 3 0 4 2 5 1 6 3 7…，和原数列是一样的。换句话说，当n为奇数时，SG（n）＝SG（n/2）（这里的n/2是向下取整），代码如下。

例题13　Treblecross游戏（Treblecross, UVa 10561）

有n个格子排成一行，其中一些格子里面有字符X。两个游戏者轮流操作，每次可以选一个空格，在里面放上字符X。如果此时有3个连续的X出现，则该游戏者赢得比赛。初始情况下不会有3个X连续出现。你的任务是判断先手必胜还是必败，如果必胜，输出所有必胜策略。

【输入格式】

输入第一行为数据组数T（T≤100）。每组数据为一行，有至少3个，最多200个字符，每个字符要么是“.”（表示空格），要么是X。保证不会有3个X字符连续出现。

【输出格式】

对于每组数据，第一行输出WINNING（先手胜）或者LOSING（先手败）。如果先手胜，在第二行输出所有必胜策略（即下一步放X的格子编号。格子从左到右编号为1，2，3，…）。如果先手败，第二行应为空。

【分析】

如果输入中已经有XX或者X.X出现，则一定先手胜，且必须选择那些和两个X相邻的格子。因此，在接下来的分析中，可以假定输入中不会出现XX或者X.X。不仅如此，我们还得出了一个结论，即聪明的游戏者不会在X的旁边或者旁边的旁边放X。这两种位置（最多4个格子）都属于不许放X的“禁区”。谁无法放X了，谁就输了。

这样，我们不难发现，整个游戏已经被X和它们旁边的“禁区”分成了若干个独立的棋盘片段，每次都可以选择一个片段进行游戏，谁无法继续游戏了，就算输。联想到了什么？没错，这就是若干个游戏的和。由于每个棋盘片段都是连续的，我们想到用一个正整数（即棋盘的长度）来表示状态，g（x）表示由连续的x个格子组成的棋盘所对应的游戏的SG函数值，则可以得到递推方程g（x）＝mex｛g（x－3），g（x－4），g（x－5），g（1）xor g（x－6），g（2）xorg（x－7）…｝。

边界为g（0）＝0，g（1）＝g（2）＝g（3）＝1。在上式中，g（x－3）对应“把X放在最左边的格子”这一决策，注意到最左边的3个格子都成为了“禁区”，实际能放的格子只有x－3个，而g（2）xor g（x－7）对应图2-21的局面，左边两个格子和右边x－7个格子是两个独立的子游戏，根据SG定理应该用异或。

图　2-21

有了g函数，很容易算出初始局面的SG函数，但如何找到必胜策略呢？枚举所有策略，那些使得后继状态SG值为0的决策就是所求策略。

2.5　概率与数学期望

《入门经典》中已经介绍过离散概率的基础知识，本节是这些知识的延伸。

全概率公式。把样本空间S分成若干个不相交的部分B₁，B₂，…，Bn，则P（A）＝P（A｜B₁）×P（B₁）＋P（A｜B₂）×P（B₂）＋…＋P（A｜Bn）×P（Bn）。这里的P（A｜B）是指在B事件发生的条件下，事件A发生的概率。

公式看上去复杂，但其实思想很简单。比如，参加比赛，得一等奖、二等奖、三等奖和优胜奖的概率分别为0.1，0.2，0.3和0.4，这4种情况下，你会被妈妈表扬的概率分别为1.0，0.8，0.5，0.1，则你被妈妈表扬的总概率为0.1×1.0＋0.2×0.8＋0.3×0.5＋0.4×0.1＝0.45。使用全概率公式的关键是“划分样本空间”，只有把所有可能情况不重复、不遗漏地进行分类，并算出每个分类下事件发生的概率，才能得出该事件发生的总概率。

数学期望。简单地说，随机变量X的数学期望EX就是所有可能值按照概率加权的和。比如一个随机变量有1/2的概率等于1，1/3的概率等于2，1/6的概率等于3，则这个随机变量的数学期望为1×1/2＋2×1/3＋3×1/6＝5/3。在非正式场合中，可以说这个随机变量“在平均情况下”等于5/3。在解决和数学期望相关的题目时，可以先考虑直接使用数学期望的定义求解。计算出所有可能取值，以及对应的概率，最后求加权和。如果遇到困难，则可以考虑使用下面两个工具。

（1）期望的线性性质：有限个随机变量之和的数学期望等于每个随机变量的数学期望之和。比如对于两个随机变量X和Y，E（X＋Y）＝EX＋EY。

（2）全期望公式：类似全概率公式，把所有情况不重复、不遗漏地分成若干类，每类计算数学期望，然后把这些数学期望按照每类的概率加权求和。

例题14　麻球繁衍（Tribbles, UVa 11021）

有k只麻球，每只活一天就会死亡，临死之前可能会生出一些新的麻球。具体来说，生i个麻球的概率为Pi。给定m，求m天后所有麻球均死亡的概率。注意，不足m天时就已全部死亡的情况也算在内。

【输入格式】

输入第一行为测试数据组数T。每组数据第一行为3个整数n，k，m（1≤n≤1000，0≤k≤1000，0≤m≤1000）。以下n行每行为一个概率，即P₀，P₁，…，Pn－1。

【输出格式】

对于每组数据，输出所求概率，保留小数点后七位。

【分析】

由于每只麻球的后代独立存活，只需求出一开始只有1只麻球，m天后全部死亡的概率f（m）。由全概率公式，有

其中Pjf（i－1）j的含义是这个麻球生了j个后代，它们在i－1天后全部死亡。注意这j个后代的死亡是独立的，而每个死亡的概率都是f（i－1），因此根据乘法公式，j个后代全部死亡的概率为f（i－1）j。同理，由于一开始共有k只麻球，且各只麻球的死亡是独立的，由乘法公式，最终答案是f（m）k。程序如下（注意m＝0的情况）。

例题15　和朋友会面（Joining with Friend, UVa 11722）

你和朋友都要乘坐火车，并且都会途径A城市。你们很想会面，但是你们到达这个城市的准确时刻都无法确定。你会在时间区间[t₁，t₂]中的任意时刻以相同的概率密度到达。你朋友则会在时间区间[s₁，s₂]内的任意时刻以相同的概率密度到达。你们的火车都会在A城市停留w分钟。只有在同一时刻，你们所在的火车都停在城市A的时候，才有可能会面。你的任务是计算出现这种情况的概率。

【输入格式】

输入第一行为测试数据组数T（T≤500）。接下来的T行每一行包含5个整数，分别为t₁，t₂，s₁，s₂和w（360≤t₁＜t₂＜1080，360≤s₁＜s₂＜1080，1≤w ≤90）。这些数都以分钟为单位。t₁，t₂，s₁，s₂指的是从午夜00:00开始算起的时间。

【输出格式】

对于每组数据，输出会面成功的概率。

【分析】

假设x和y分别是两辆火车到城市A的时间，则整个概率空间是平面上的一个矩形，而两人相遇的条件是｜x－y｜≤W。因此，这个矩形被两条直线y＝x－w和y ＝x＋w切割后中间的那个“类似菱形的图形”的面积除以整个矩形的面积就是所求概率，如图2-22所示。

图　2-22

注意，上图只是众多情况中的一种，需要分类讨论。程序留给读者编写。

例题16　玩纸牌（Expect the Expected, UVa 11427）

每天晚上你都玩纸牌游戏，如果第一次就赢了就高高兴兴地去睡觉；如果输了就继续玩。假设每盘游戏你获胜的概率都是p，且各盘游戏的输赢是独立的。你是一个固执的完美主义者，因此会一直玩到当晚获胜局数的比例严格大于p时才停止，然后高高兴兴地去睡觉。当然，晚上的时间有限，最多只能玩n盘游戏，如果获胜比例一直不超过p的话，你只能垂头丧气地去睡觉，以后再也不玩纸牌了。你的任务是计算出平均情况下，你会玩多少个晚上的纸牌。

【输入格式】

输入第一行为数据组数T（T≤3000），以下每行包含每盘游戏的获胜概率p和每晚上最多可以玩的盘数n。其中p表示成分数形式（p的分母不超过1000，1≤n≤100）。

【输出格式】

对于每组数据，输出平均情况下玩游戏的晚上的个数，用截尾法保留整数部分。答案保证为1～1000的整数。

【分析】

不难发现，每天晚上的情况相互独立，因此先研究一下单独一天的情况，计算出只玩一晚上纸牌时，“垂头丧气地去睡觉”的概率Q。

设d（i，j）表示前i局中每局结束后的获胜比例均不超过p，且前i局一共获胜j局的概率，则根据全概率公式，有：j/i≤p时d（i，j）＝d（i－1，j）×（1－p）＋d（i－1，j－1）×p，其他d（i，j）＝0，边界为d（0，0）＝1，d（0，1）＝0。则d（n，0）＋d（n，1）＋d（n，2）＋…就是所求的Q。

下面我们用数学期望的定义来计算游戏总天数X的数学期望。

X＝1概率为Q。

X＝2概率为Q（1－Q）：第一天高高兴兴（概率为1－Q），第二天垂头丧气。

X＝3概率为Q（1－Q）²：前两天高高兴兴（概率为（1－Q）²），第三天垂头丧气。

…

X＝k概率为Q（1－Q）k－1：前k－1天高高兴兴（概率为（1－Q）k－1），第k天垂头丧气。

因此，X的数学期望EX＝Q＋2Q（1－Q）＋3Q（1－Q）²＋4Q（1－Q）³…注意，这是个无穷级数，需要一点数学技巧来计算。

首先，令

则

（1）－（2）得

有个方法可以更简单地得到上述结论。设数学期望为e天，把情况分成两类，第一天晚上垂头丧气，概率为Q，期望为1；第一天晚上高高兴兴，概率为1－Q，数学期望为e＋1（想一想，为什么）。根据全期望公式，我们得到一个方程e＝Q×1＋（1－Q）×（e＋1），解得e＝1/Q。

例题17　得到1（Race to 1, UVa 11762）

给出一个整数N，每次可以在不超过N的素数中随机选择一个P，如果P是N的约数，则把N变成N/P，否则N不变。问平均情况下需要多少次随机选择，才能把N变成1？比如N＝3时答案为2，N＝13时答案为6。

【输入格式】

输入第一行为数据组数T（T≤1000），以下T行每行一个整数N（1≤N≤1000000）。

【输出格式】

对于每组数据，输出平均情况需要的操作次数。

【分析】

本题可以看做一个随机转移的状态机，如图2-23所示，转移弧旁的数字为转移概率，每个状态出发的所有转移概率之和等于1。N的每个约数（包括N和1）对应一个状态。各状态转移都有一定的概率。从每个状态出发的各转移的概率和总是1[11]。

图　2-23

设f（i）表示当前的数为i时接下来需要选择的次数，则根据数学期望的线性和全期望公式可以为每个状态列出一个方程，例如：

等式右边最前面的“1”是指第一次转移，而后面的几项是后续的转移，用全期望公式展开。一般地，设不超过x的素数有p（x）个，其中有g（x）个是x的因子，则

边界为f（1）＝0。移项后整理得

因为x/y＜x，可以用记忆化搜索的方式计算f（x），核心代码如下。

最后结果是dp（n）。在调用之前，需要求出不超过1000000的所有素数，放到primes数组中，素数个数为primt_cnt。注意，数组vis只需在程序开始时清零，每次新处理一组数据时不必清零，这样可以加快处理速度（想一想，为什么）。

2.6　置换及其应用

简单来说，置换（permutation）就是把n个元素做一个全排列。比如1，2，3，4分别变成3，1，2，4，或者分别变成4，3，2，1。一般地，1变a₁，2变a₂，…的置换记为

置换实际上就是一一映射。在程序上，可以用一个数组f=｛a₁，a₂，…，an｝来表示1～n的一个置换，其中f[i]表示元素i所映射到的数（f[0]未使用）。这个f也可以看成是定义域和值域为｛1，2，3，…，n｝的函数，其中f（1）＝a₁，f（2）＝a₂，f（3）＝a₃，…，f（n）＝an。由于不同的元素映射到不同的数，这个函数是可逆的。

置换之间可以定义乘法，对应于函数复合（function composition）。比如置换f=｛1，3，2｝和g=｛2，1，3｝，乘积fg=｛2，3，1｝，因为各个元素的变化为1→1→2，2→3→3，3→2→1。在数学上，函数复合总是满足结合律，所以置换乘法也满足结合律。注意，置换乘法不满足交换律，比如在上面的例子中gf=｛3，1，2｝。如果你对上面的描述仍有疑问，请看下式。

置换f=｛1，5，6，2，4，3｝，g=｛6，2，4，3，5，1｝，则fg=｛6，5，1，2，3，4｝。比如，5先由f映射到4，这个4再由g映射到3，所以总的来说，5映射到了3。同理，1先映射到1，再映射到6；2先映射到5，再映射到5；3先映射到6，再映射到1等。

为了处理方便，常常把置换分解成循环（permutation cycle）的乘积，其中每个循环代表一些元素“循环移位”。比如（1 4 3）这个循环表示1→4，4→3，3→1。下面是一个把置换分解成循环乘积的例子

为什么任意置换都可以这样分解呢？因为我们把每个元素看成是一个结点，如果a变成b，连一条有向边a→b，则每个元素恰好有一个后继结点和一个前驱结点（想一想，为什么），借用图论的术语就是每个点的出度和入度均为1。不难发现，这样的图只能是若干个有向圈，其中每个圈对应一个循环。如上面的例子，一共有5条有向边：1→3，2→5，3→1，4→4，5→2。

这个思维过程实际上对应着一个循环分解算法。任取一个元素，顺着有向边走，最终一定会走成一个环，然后换一个没被访问过的元素如法炮制，直到所有元素都被访问过。

虽然在一般情况下置换乘法不满足交换律，但对于不相交的循环来说，按照任意顺序相乘都是等价的。在忽略顺序的情况下，刚才已经证明，任意置换只有唯一的方法表示成不相交循环的乘积。我们称循环分解中循环的个数为该置换的循环节。比如（13）（25）（4）的循环节为3。

等价类计数问题。有这样一个经典问题，给2×2方格中涂黑白两色，有几种方法？答案是16种，如图2-24所示。

图　2-24

但如果定义一种“旋转操作”，规定逆时针旋转90°、180°或者270°后相同的方案算作一种，那么答案就变成6种了，如图2-25所示。

图　2-25

这样的问题称为等价类计数问题。也就是说，题目中会定义一种等价关系，满足等价关系的元素被看成同一类，只统计一次。等价关系需要满足自反性（每个元素和它自身等价）、对称性（如果A和B等价，则B和A等价）和传递性（如果A和B等价，B和C等价，则A和C等价）。比如上面的“旋转后相同”就是一个等价关系。有了等价关系，所有元素会分成若干个等价类，每个等价类里的所有元素相互等价，不同等价类里的元素不等价。

为了统计等价类的个数，首先需要用一个置换集合F来描述等价关系。如果一个置换把其中一个方案映射到另一个方案，就说二者是等价的。比如“逆时针转90°”这个置换把上面的C3映射到C4。注意，F中任意两个置换的乘积也应当在F中，否则F无法构成置换群（读者暂时不必追究细节，只需记住即可）。例如，不能定义F＝｛逆时针旋转0°，逆时针旋转90°，逆时针旋转270°｝，因为两个“逆时针旋转90°”叠加的效果是旋转180°，不在F内。

对于一个置换f，若一个着色方案s经过置换后不变，称s为f的不动点。将f的不动点数目记为C（f），则可以证明等价类数目为所有C（f）的平均值。此结论称为Burnside引理。

例如，在本题中“逆时针旋转180度”的不动点有C1，C2，C11，C12这4个，“逆时针旋转90°”和“逆时针旋转270°”都只有C1和C2两个不动点，“逆时针旋转0°”有16个不动点（任意方案经过“不旋转”这样的置换后都不变），根据Burnside引理，等价的方案有（4＋2＋2＋16）/4＝6个。

如何求C（f）呢？我们先把格子编号为如图2-26所示的形式。

图　2-26

比如“逆时针旋转180°”这个置换写成循环的乘积就是（13）（24），即1和3互变，2和4互变。不难发现，1和3的颜色必须相同，2和4的颜色也必须相同，而1－3和2－4的颜色互不相干。根据乘法原理，有2×2＝4种方案，即C（逆时针旋转180°）＝4。

一般地，如果置换f分解成m（f）个循环的乘积，那么每个循环内所有格子的颜色必须相同（想一想，为什么），假设涂k种颜色，则有C（f）＝km（f）。代入Burnside引理的表达式之后得到Pólya定理：等价类的个数等于所有置换f的km（f）的平均数。

Pólya定理很容易用Burnside引理证明，因此建议读者记住Burnside引理，一般的等价类计数问题均可以用它解决。

例题18　项链和手镯（Arif in Dhaka（First Love Part 2）, UVa 10294）

项链和手镯都是由若干珠子穿成的环形首饰，区别在于手镯可以翻转，但项链不可以。换句话说，图2-27中（a）、（b）两个图，如果是手镯则看做相同，项链则看做不同。当然，不管是项链还是手镯，旋转之后一样的看做相同。

图　2-27

输入整数n和t，输出用t种颜色的n颗珠子（每种颜色的珠子个数无限制，但珠子总数必须是n）能制作成的项链和手镯的个数。比如n＝5，t＝3时，项链有51个，手镯有39个。

【输入格式】

输入包含多组数据。每组数据仅一行，包含两个整数n和t（1≤n≤50，1≤t≤10）。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出项链的个数和手镯的个数。

【分析】

本题是等价类计数问题，可以用Pólya定理或者Burnside引理解决。一共有两种置换，即旋转和翻转，其中项链只有第一种置换，而手镯则是两种都有。为了叙述方便，假定所有珠子按照逆时针顺序编号为0～n－1。

旋转：如果逆时针旋转i颗珠子的间距，则珠子0、i、2i、…构成一个循环。这个循环有n/gcd（i，n）个元素（想一想，为什么）。根据对称性，所有循环的长度均相同，因此一共有gcd（i，n）个循环。这些置换的不动点总数为alt

翻转：需要分两种情况讨论。当n为奇数时，对称轴有n条，每条对称轴形成（n－1）/2个长度为2的循环和1个长度为1的循环，即（n＋1）/2个循环。这些置换的不动点总数为alt。当n为偶数时，有两种对称轴。穿过珠子的对称轴有n/2条，各形成n/2－1个长度为2的循环和两个长度为1的循环；不穿过珠子的对称轴有n/2条，各形成n/2个长度为2的循环。这些置换的不动点总数为alt

根据Pólya定理，项链总数为a/n，手镯总数为（a＋b）/2n。代码如下。

本题的规模比较小，还有一种更简单的方法，那就是直接写出这些置换，用程序，而非数学推导计算循环节，代码留给读者编写。

例题19　Leonardo的笔记本（Leonardo's Notebook , NWERC 2006, LA 3641）

给出26个大写字母的置换B，问是否存在一个置换A，使得A²＝B。

【输入格式】

输入第一行为数据组数T（T≤500），以下每行为一组数据，即一个长度为26的字符串，也就是大写字母的一个置换。

【输出格式】

对于每组数据，如果存在A，输出Yes，否则输出No。

【分析】

在解决这个问题之前，先来看一下置换A与A²究竟有着怎样的关系。先把置换做循环分解，比如A＝（a₁a₂a₃）（b₁b₂b₃b₄）。那么

A²＝（a₁a₂a₃）（b₁b₂b₃b₄）（a₁a₂a₃）（b₁b₂b₃b₄）

注意：不相交的循环的乘法满足交换律，所以我们重新写成

A²＝（a₁a₂a₃）（a₁a₂a₃）（b₁b₂b₃b₄）（b₁b₂b₃b₄）

根据置换乘法的结合律，前面两个循环的乘积和后面两个循环的乘积可以分别算。计算得

（a₁a₂a₃）（a₁a₂a₃）＝（a₁a₃a₂）

（b₁b₂b₃b₄）（b₁b₂b₃b₄）＝（b₁b₃）（b₂b₄）

不难总结出如下规律：两个长度为n的相同循环相乘，当n为奇数时结果也是一个长度为n的循环；当n为偶数时分裂为两个长度为n/2的循环。相反，对于任意一个长度n为奇数的循环B，都能找到一个长度为n的循环A使得A²＝B；对于任意两个长度为n（n不一定为偶数）的不相交循环B和C，都能找到一个长度为2n的循环A使得A²＝BC。

现在，问题已经解决了大部分。把题目中给出的B分解成不相交循环的乘积。注意，长度n为奇数的循环既可能是两个长度为n的相同循环乘出来的，也可能是两个长度为2n的循环分裂成的，但长度n为偶数的循环只能是两个长度为2n的循环分裂成的。所以对于任意偶数长度，循环的个数必须是偶数才能配对。反过来，只要能成功配对，根据刚才的结论，一定有解。

例如，若B＝（1 3 4）（2 5）（6 9）（7 8），则无解，因为长度为2的循环有3个，无法两两配对。而B＝（1 3 4）（2 5）（6 9）（7）有解，因为（2 5）（6 9）＝（2 6 5 9）（2 6 5 9），而（1 3 4）＝（1 4 3）（1 4 3），（7）＝（7）（7）。代码如下。

例题20　排列统计（Find the Permutations, UVa 11077）

给出1～n的一个排列，可以通过一系列的交换变成｛1，2，3，…n｝。比如｛2，1，4，3｝需要两次交换（1和2，3和4），｛4，2，3，1｝只需要一次（1和4），｛2，3，4，1｝需要3次，而｛1，2，3，4｝本身一次都不需要。给定n和k，统计有多少个排列至少需要交换k次才能变成｛1，2，…，n｝。

【输入格式】

输入包含多组数据。每组数据仅一行，包含两个整数n和k（1≤n≤21，0≤k＜n）。输入结束标志为n＝k＝0。

【输出格式】

对于每组数据，输出满足条件的排列数目。

【分析】

首先来考虑一个简单一些的问题。任意给出一个排列P，至少需要交换几次才能变成｛1，2，…，n｝？这个次数也等于从｛1，2，…，n｝变换到该排列所需的次数（想一想，为什么）。因此，直接把排列P理解成一个置换，并且分解成循环，则各循环之间独立。

不难发现，单元素循环是不需要交换的，两个元素的循环需要交换一次，3个元素的循环需要交换两次，…，c个元素的循环需要交换c－1次。这样，如果排列P的循环节为x，则总的交换次数为n－x次。

有了上述结论，就不难进行递推了。设f（i，j）表示满足“至少需要交换j次才能变成｛1，2，3，…，i｝”的排列的个数，则f（i，j）＝f（i－1，j－1）＋f（i－1，j）×（i－1），因为元素i要么自己形成一个循环，要么加入前面任意一个循环的任意一个位置。边界为f（1，0）＝1，其他f（1，j）＝0。

代码如下。注意n＝21，k＝18时答案为13803759753640704000。

例题21　像素混合（Pixel Shuffle, CERC 2005/SWERC 2005, LA 3510）

在一个n×n黑白位图上反复执行同一个像素混合序列，问重复做几次后会得到原图像？每个混合序列是由若干个混合命令组成，这些命令如下。

❑　id：不变。

❑　rot：逆时针旋转90°。

❑　sym：水平翻转。

❑　bhsym：下一半的图像水平翻转。

❑　bvsm：下一半图像垂直翻转。

❑　div：第0，2，4，…，n－2变成第0，1，2，…，n/2－1行，第1，3，5，…，n－1行变成第n/2，n/2＋1，…，n－1行。

❑　mix：行混合。新图像的第2k行依次是原图像的（2k，0），（2k＋1，0），（2k，1），（2k＋1，1），…，（2k，n/2－1），（2k＋1，n/2－1）这些像素，第2k＋1行依次是原图像的（2k，n/2），（2k＋1，n/2），（2k，n/2＋1），（2k＋1，n/2＋1），…，（2k，n－1），（2k＋1，n－1）这些像素。

对字母A使用这8种混合法后的效果如图2-28所示。

图　2-28

比如，当n＝256时，“rot- div rot div”只需重复8次，但“bvsym div mix”却需要重复63457次。

【输入格式】

输入第一行为数据组数，以下每组数据第一行为位图边长n（2≤n≤2¹⁰），第二行为不超过k（k≤32）个混合命令。如果一个命令后面有一个减号“－”，表示应该执行该命令的逆。命令从右往左执行。比如对字母A执行“bvsym rot-”后的结果如图2-29所示。

图　2-29

【输出格式】

对于每组数据，输出最小重复次数，使得任意位图执行完这些像素混合之后都会恢复原状。输入保证这个最小重复次数小于2³¹。相邻两组数据的输出之间应有一个空行。

【分析】

虽然有这么多复杂的混合命令，但每个命令其实是所有像素的一个置换，而一个混合序列就是这些置换的乘积。虽然过程比较复杂，需要一些耐心，但求出这个乘积的难度并不大，时间复杂度为O（n²k）是可以承受的。接下来，问题就转化为给出一个置换A，求最小的正整数m，使得Am为全等置换（即所有元素映射到自己）。

还是先把置换分解为不相交循环的乘积，不难发现：对于一个长度为L的循环A，当且仅当m是L的整数倍时Am才是全等置换，所以最终答案是每个循环的长度的最小公倍数。

2.7　矩阵和线性方程组

矩阵（matrix）是一个由数排列成的矩形，例如

是一个2×3矩阵A＝（aij），其中i＝1，2，j＝1，2，3。第i行第j列的元素用aij表示。一般用大写字母表示矩阵，小写字母表示它的元素。A的转置AT通过翻转A的行和列得到，例如，对于上面的A，有

矩阵的运算。加法和减法简单的定义为逐个元素进行加减。注意，两个矩阵的行数、列数分别相等时，加减法才有定义。矩阵的乘法比较复杂，当A的列数等于B的行数，则可以定义乘法C＝AB。如果A是m×n矩阵，B是n×p矩阵，那么C是一个m×p矩阵，其中cij满足

根据这个定义容易得到一个O（n³）时间的矩阵乘法。虽然有更快的，但是在算法竞赛中，立方时间的矩阵乘法已经足够。

如果你觉得矩阵乘法很奇怪，想一想下面这个线性方程组吧。

也就是

这3个矩阵分别为A，x和b（注意x和b都是n×1的矩阵，也称列向量），则线性方程组可以简单的写成Ax＝b。这样是不是很方便呢？建议读者仔细研究一下这个Ax＝b。它意味着A把一个列向量x变成了另外一个列向量b。也就是说，矩阵A描述的是一个线性变换，它把向量x变换到了向量b。在算法竞赛中，只要遇到“把一个向量v变成另一个向量v'，并且v'的每一个分量都是v各个分量的线性组合”的情况[12]，就可以考虑用矩阵乘法来描述这个关系。这里的线性组合是指每个元素乘以一个常数后相加。比如2x＋3y－z就是x、y、z的线性组合，但5xy和x²－3y都不是。矩阵乘法最重要的性质就是满足结合律，即（AB）C＝A（BC），因此和幂取模一样，An也可以用倍增法加速。

逆矩阵。在数论部分我们曾证明，在模n的缩系中，每个元素a都存在唯一的逆元a⁻¹使得a×a⁻¹＝a⁻¹×a＝1（我们还介绍了用扩展欧几里得算法计算这个a⁻¹的方法）。类似地，在n阶方阵（即n×n矩阵）的集合中，也存在一些特殊的矩阵A存在唯一逆元A⁻¹使得A×A⁻¹＝A⁻¹×A＝In，其中In是n阶单位矩阵（identity matrix）

我们暂且不管怎样的矩阵是可逆的，如果已知矩阵A可逆，并且逆矩阵为A⁻¹，那么方程组Ax＝b是很容易求解的。两边左乘A⁻¹后得到x＝A⁻¹b。这说明方程组有唯一解。

“矩阵可逆”有一个常用的充分必要条件：所有行向量是线性无关的。什么叫线性无关呢？就是任意一个向量都不是其他向量的线性组合。比如3个向量（1，2，3），（1，3，5），（1，1，1）不是线性无关的，因为（1，1，1）＝2×（1，2，3）－（1，3，5）。而（1，0，0），（0，1，0）和（0，0，1）就是线性无关的。

这个条件和“矩阵可逆”等价的严格证明超出了本书的范围，但其中的思想还是很直观的。比如

为什么上面的方程组无解？因为（1，1，1）是（1，2，1）和（1，3，5）的线性组合，所以得到了两个左边是x＋y＋z的方程，并且右边不同（一个等于11，一个等于8），所以无解（我们称这两个方程为矛盾方程）。就算是等号右边恰好相同，也说明两个方程重复了（其中一个是多余方程），相当于两个方程3个未知数，于是有无穷多解。注意我们用方程x＋y＋z＝11代替了原来的x＋3y＋5z＝9（而不是添加一个新的方程）。这是因为由x＋2y＋z＝10和x＋y＋z＝11可以推导出x＋3y＋5z＝9。这样的变换保持了方程组的解集不变，称为同解变换。

顺便说一句，虽然逆矩阵看上去很吸引人，但实际上在多数情况下我们都可以避开矩阵求逆。事实上，算法竞赛中极少会用到矩阵求逆的过程，因此本书不加以讨论（尽管求逆的过程几乎就是一个高斯消元）。

高斯消元计算举例。理论知识已经介绍得较充分了，还是来求解方程组吧！在中学时，我们学过高斯消元（gauss elimination）。在算法领域，这个方法仍然可以用来解方程组。简单起见，先假设系数矩阵A是一个n阶可逆矩阵。例如

写成矩阵的形式为

这个矩阵称为增广矩阵（augmented matrix），其中最后一列并不是系数矩阵，而是等式右边的常数列。

从整体上来说，我们从上到下依次处理每一行，处理完第i行后，让Aii非0，而Aji（j＞i）均为0，过程如下。

其中最后一个增广矩阵的系数部分是上三角阵（0用空白表示），而且主对角元aii均非0。该矩阵对应的方程组为

直接可得z＝-1，把z的值代入第二个方程得y＝3；把y和z的值代入第一个方程得x＝2。

这是一个比较直观的例子，求解过程还需要细化。在消元部分中，假设正在处理第i行，则首先需要找一个r＞i且绝对值最大的ari，然后交换第r行和第i行。因为每行对应一个方程，交换两个方程的位置不会对解产生任何影响，但可以提高数值稳定性[13]。当A可逆时，可以保证交换后aii一定不等于0。这种方法称为列主元法。

接下来进行所谓的“加减消元”。比如在下面的例子中，要用第一个方程（-3，1，2－11）消去第二个方程（2，1，-1，8）的第一列，方法是把第二个方程中的每个数都减去第一行对应元素的-2/3倍。

一般情况下，如果要用第i个方程来消去第k个方程的第i列，那么第k行的所有元素A[k][j]都应该减去A[i][j]的A[k][i]/A[i][i]倍。

下一个过程是回代。现在A已经是一个上三角矩阵了，即第1、2、3…行的最左边非0元素分别在第1、2、3…列。这样，最后一行实际上已经告诉我们xn的值了；接下来像前面说的那样不停地回代计算，最终会得到每个变量的唯一解。代码如下。

消元中的精度问题。需要注意的是上述代码的消元部分中有一个中间变量f，保存的是为了使A[k][i]＝0，第i行所乘以的倍数。注意，f的类型是double，有可能会损失精度。解决这个问题，有一种办法是不使用中间变量，但前提是A[k][i]不会过早地被破坏，所以应写成按照列编号从大到小的顺序消元，代码如下。

当然，对于大多数题目，前面的逐行消元法已经可以满足要求，但对于精度要求很高的题目，不妨采用上面的方法。

高斯-约当消元法。在消元过程中，还可以把系数矩阵变成对角阵而非上三角阵，从而省略回代过程（因为第i行只有A[i][i]和A[i][n]两项非0，所以直接可得x[i]＝A[i][n]/A[i][i]）。这个方法称为高斯-约当消元法（Gauss-Jordan elimination），运算量比高斯消元略大，但代码更简单（因为少了回调过程）。

A不可逆的情形。上述讨论和代码都只针对A可逆的情况。当A不可逆时，会引入一些复杂性，下面我们继续讨论。

一般情况下，消元过程的目标是在解不变的情况下把线性方程组的系数矩阵A变成阶梯矩阵（Row Echelon Form, REF），也就是说，除了第一行外，每一行最左边的非0元素在上一行最左边非0元素的右边（即前者的列编号严格大于后者的列编号），比如：

注意，那些A矩阵全0的行所对应的方程要么是多余方程（它们总是成立），要么是矛盾方程（整个方程组无解），应放在所有非全0行的下方。

上面的矩阵应该怎么“回代”呢？把方程三的x₅＝2代入方程二的x₃－x₄－2x₅＝0只能得到x₃－x₄＝4，无法得到x₃和x₄的具体数值。但只要知道了x₄的值，就能算出x₃的值。继续“回代”，可以得到方程x₁＋2x₂＝a，其中a取决于x₄的具体数值。如果知道了x₂的值，就可以算出x₁。换句话说，这个方程组有两个自由变量（free variable）x₂和x₄，这两个变量本身可以取任意数值，而且只要这两个变量的取值确定了，所有其他变量（也就是每行最左边的非0元素所对应的那些变量）的取值也能确定。换句话说，设消元后矩阵有r个非0行，且不存在矛盾方程（对应于系数部分全为0，但常数不为0的行），则一共有r个非自由变量，即有界变量（bound variable）。这个r称为系数矩阵的秩（rank）。另外，不难发现，自由变量集的选择并不是唯一的。比如在刚才的例子中，也可以选x₁和x₃为自由变量。但不管怎么选，自由变量的数量不会改变。

例题22　递推关系（Recurrences, UVa 10870）

考虑线性递推关系f（n）＝a₁f（n－1）＋a₂f（n－2）＋a₃f（n－3）＋…＋adf（n－d），最著名的例子是Fibonacci数列f（1）＝f（2）＝1，f（n）＝f（n－1）＋f（n－2），因此d＝2，a₁＝a₂＝1。你的任务是计算f（n）除以m的余数。

【输入格式】

输入包含若干组数据。每组数据第一行为3个整数d，n，m（1≤d≤15，1≤n≤2³¹－1，1≤m≤46340）。第二行为d个非负整数a₁，a₂，…，ad。第三行为d个非负整数f（1），f（2），…，f（d）。这些非负整数均不超过2³²－1。输入结束标志为d＝n＝m＝0。

【输出格式】

对于每组数据，输出f（n）除以m的余数。

【分析】

线性递推关系是组合计数中很常见的一种递推关系。直接利用递推式，需要O（nd）时间才能算出f（n），时间无法承受。

现在已经有了f（n）＝a₁f（n－1）＋a₂f（n－2）＋a₃f（n－3）＋…＋adf（n－d），再加上f（n－1）＝f（n－1），f（n－2）＝f（n－2）等显然成立的式子，可以得到如下关系式

其中A称为相伴矩阵，也称友矩阵[14]（companion matrix）或者Q矩阵。这正是“把一个向量变成另一个向量”。根据矩阵乘法的定义，F（n）＝An－dF（d）。利用快速矩阵幂，本题的总时间复杂度为O（d³logn）。

程序留给读者编写。唯一需要说明的是矩阵幂的写法。在数论中，幂取模都是用递归写法，但这里推荐用迭代写法，因为矩阵的数据量较大，如果递归求幂，一不注意就会占用更多的空间，相比之下迭代写法更自然。

例题23　细胞自动机（Cellular Automaton, NEERC 2006, LA 3704）

一个细胞自动机包含n个格子，每个格子的取值为0～m－1。给定距离d，则每次操作后每个格子的值将变为到它的距离不超过d的所有格子在操作之前的值之和除以m的余数，其中i和j的距离为min｛｜i－j｜，n－｜i－j｜｝。给出n，m，d，k和自动机各格子的初始值，你的任务是计算k次操作以后各格子的值。

如图2-30所示，n＝5，m＝3，d＝1，一次操作将把图2-30（a）图变为图2-30（b）图。比如，与格子3距离不超过1的格子（即格子2，3，4）在操作前的值分别为2，2，1，因此操作后格子3的值为（2＋2＋1）mod 3＝2。

图　2-30

【输入格式】

输入包含多组数据。每组数据有两行，第一行包含4个整数n，m，d，k（1≤n≤500，1≤m≤10⁶，0≤d＜n/2，1≤k≤10⁷）。第二行包含n个0～m－1的整数，即各格子的初始值。

【输出格式】

对于每组数据，输出一行，包含n个0～m－1的整数，即k次操作以后各格子的值。

【分析】

如果我们把t次操作以后的各格子值写成列向量vt，不难发现vt＋1的每一维都是vt中各维的线性组合（注意，这里的加法是剩余系中的加法，即模加法），因此可以用矩阵乘法来刻画。比如，设vt＋1＝Avt，当d＝1时有

其中加法和乘法在模m的剩余系中完成。最终答案vk＝Akv₀，其中v₀是初始向量。如果直接用倍增法计算Ak，总时间复杂度为O（n³logk），有些高（注意n≤500，而且有多组数据）。注意A比较特殊，从第二行开始每一行都是上一行循环右移（即移出右边界后从左边移进来）一列的结果。我们把这样的矩阵称为循环矩阵（circulant matrix）。

可以证明，两个循环矩阵的乘积仍然是循环矩阵，因此在存储时只需保存第一行，而计算矩阵乘法时也只需算出第一行即可。这样，矩阵乘法的时间复杂度降为O（n²），总时间降为O（n²logk），可以承受。

例题24　随机程序（Back to Kernighan-Ritchie, UVa 10828）

给出一个类似下图的程序控制流图，从每个结点出发到每个后继结点的概率均相等。当执行完一个没有后继的结点后，整个程序终止。程序总是从编号为1的结点开始执行。你的任务是对于若干个查询结点，求出每个结点的期望执行次数。

比如，在图2-31中，如果从结点U开始执行，那么结点U，V和W的期望执行次数分别为2，2，1。

图　2-31

【输入格式】

输入包含不超过100组数据。每组数据第一行为整数n（1≤n≤100）即控制流图的结点数。结点编号为1～n。以下若干行每行包含两个整数a，b，表明从结点a执行完后可以紧接着执行结点b。这部分以两个0结束。下一行为整数q（q≤100），即查询的个数。以下q行每行为一个结点编号。输入结束标志为n＝0。

【输出格式】

对于每组数据的每个查询，输出该结点执行次数的数学期望。

【分析】

在数学期望部分，我们曾经接触过一个类似的题目：《得到1》。虽然所求的结果略有差别，但思路一致，且都是关于马尔可夫过程的。那道题目的做法是列方程，由于方程有特殊性，可以直接解出递推关系，采用记忆化搜索求解。本题也可以这样列方程，但是无法直接解除递推关系，只能列出方程组后用高斯消元。

设结点i的出度为di，期望执行次数为xi。对于一个拥有3个前驱结点a，b，c的结点i，可以列出方程xi＝xa/da ＋xb/db ＋xc/dc。

为什么这个方程成立呢？a结点的期望执行次数为xa，而执行完结点a后，接下来执行结点i的概率是1/da，所以弧a→i的期望经过次数为xa/da。由于要到达结点i，必须先经过弧a→i、b→i或者c→i，根据期望的线性性质，结点i的期望执行次数就是这3条弧的期望经过次数之和。

对于题目中的例子，设x₁，x₂和x₃分别为结点U，V，W的期望执行次数，则可以列出下面的方程组

注意，结点1比较特殊，可以将其理解为还有一个虚拟的结点0，它才是真正的程序入口，并且结点0以概率1转移到结点1。因为x₀＝d₀＝1，所以在方程中就是一个数字1。

方程组都列出来了，似乎本题已经基本解决了？很可惜，不是这样的。注意到输出可能是“无穷大”，我们在解方程组的时候就需要考虑这一点。可不可以先解方程，然后再判断解出来的值是不是无穷大呢？这很困难。比如一个3个结点的图，包含两条边1→3和3→2，则消元后的增广矩阵为

直接解出x₃＝INF，回代x₂＝0－0×INF。0和无穷大的乘积是一个不定式，但计算机里储存的值是确定的。写一段小程序就能发现，假定x＝1.0/0.0，那么x×0.0的值是一个NaN。在《算法竞赛入门经典》中曾提到，NaN指“不是数”（Not a Number），它的最大特点是不等于任何数，包括它自己（即，如果x是NaN，那么x＝＝x不成立）。

除了矛盾方程之外，本题还有可能出现多余方程。比如一个有3个结点的图，包含两条边2→3和3→2，则消元后的增广矩阵为

注意，从方程本身，我们无法确定x₃的值，只知道x₃＝x₄。换句话说，需要根据题目背景确定结果。哪些结点的期望执行次数是无穷大的呢？不难发现，就是那些无法到达“终态”（即没有后继的点）的结点。哪些点的期望执行次数是0呢？不难发现，就是那些起点到不了的点。这样，就可以先用Floyd算法求出传递闭包，先找到无穷点和零点，然后用高斯消元法求出其他变量的解。根据本题的物理意义，其他点的解一定唯一。

如何求解其他点呢？是不是需要把那些解唯一的点重新编号，然后再高斯消元？不需要。我们可以改用前面介绍过的高斯-约当消元法，省略回代过程，从而避免那些“无穷大值”的干扰。事实上，用这个方法还可以避开刚才所说的预处理（即先用Floyd算法求传递闭包），当A[i][i]＝A[i][n]＝0时xi＝0，而A[i][i]＝0但A[i][n]＞0时xi为正无穷（这个结论并不显然，有兴趣的读者可以自行思考）。

例题25　乘积是平方数（Square, UVa 11542）

给出n个整数，从中选出1个或多个，使得选出的整数乘积是完全平方数。一共有多少种选法？比如｛4，6，10，15｝有3种选法：｛4｝、｛6，10，15｝和｛4，6，10，15｝。

【输入格式】

输入第一行为一个整数T（1≤T≤30），即测试数据的数量。每组数据包含两行，第一行为整数n（1≤n≤100），第二行包含n个整数。所有整数均不小于1，不大于10¹⁵，并且不含大于500的素因子。

【输出格式】

对于每组数据，输出方案总数。输入保证总数不超过带符号64位整数的范围。

【分析】

“不含大于500的素因子”提示我们考虑每个数的唯一分解式，用01向量表示一个数，再用n个01变量xi来表示我们的选择，其中xi＝1表示要选第i个数，xi＝0表示不选它，则可对每个素数的幂列出一个模2的方程。

这话听起来比较抽象，让我们分析一下题目中的例子。4个整数4，6，10，15的素因子只有2，3，5这3种，首先把这些整数写成01向量的的形式，即4＝2²×3⁰×5⁰→（2，0，0），6＝2¹×3¹×5⁰→（1，1，0），10＝2¹×3⁰×5¹→（1，0，1），15＝2⁰×3¹×5¹→（0，1，1）。

选出来的数乘积为22x₁＋x₂＋x₃·3x₂＋x₄·5x₃＋x₄。如果要让这个数是完全平方数，每个幂都应该是偶数，即

注意，这也是一个线性方程组，只是代数系统变成了Z₂（模2的剩余系）。可是第一个方程里的2x₁不见了。这是因为2x₁总是偶数，所以没必要写在方程里。同理，3x₁会变成x₁，任意变量的系数非0即1。还可以把这个方程组看成是如下xor方程组

xor方程组是很好消元的，因为不需要做乘法和除法，只需要做xor；每次也不需要找绝对值最大的系数ari（每个ari不是0就是1），任意一个ari＝1即可实现消元。

最后，假设自由变量有f个，则线性方程组的解共有2f个，因为每个自由变量可以取0和1。比如，刚才的方程组对应的增广矩阵消元后为

有两个自由变量x₁和x₄，有两个有界变量x₂和x₃，因此一共有2²＝4种选法。注意，本题不允许一个整数都不选，因此最终答案需要减1，代码如下。

本题的规模很小，上述代码的速度已经非常快了。如果变量增多，xor方程组还有一个常见的优化，即位压。即把32列合并到一个无符号32位整数中，然后只需用一次逐位xor就可以处理32列了。另外，在上述代码中并没有判断是否存在矛盾方程，这是为什么？留给读者思考。

2.8　数值方法简介

很多在数学上难以求出封闭形式解的问题，都可以用数值的方法算出近似解。数值算法有很多，算法竞赛中最常用的是非线性方程求根、凸函数求极值、数值积分。下面直接介绍例题。

例题26　解方程（Solve It!, UVa 10341）

解方程pe-x＋qsin（x）＋rcos（x）＋stan（x）＋tx²＋u＝0，其中0≤x≤1。

【输入格式】

输入包含不超过2100组数据。每行为一组数据，包含6个整数p，q，r，s，t，u（0≤p，r≤20，-20≤q，s，t≤0）。

【输出格式】

对于每组数据，输出所有解，按照从小到大顺序排列，每个解均保留小数点后4位。如果无解，输出No solution。

【分析】

在0≤x≤1时，pe-x＋qsin（x）＋rcos（x）＋stan（x）＋tx²＋u的前5个加数都是减函数（注意各个系数的符号），而最后一项是常数，所以当f（0）≥0且f（1）≤0时f（x）＝0有唯一解，否则无解，如图2-32所示。

图　2-32

有解的时候可以用二分法求解。如图2-32所示，图2-32（a）中的f（1/2）≥0，所以根在[1/2，1]内；图2-32（b）中的f（1/2）≤0，所以根在[0，1/2]内。一般地，如果已知解在[x，y]内，只需判断f（（x＋y）/2）的符号即可把解的范围缩小一半。由于实数区间可以无限二分，通常当区间长度小于特定值（比如10⁻⁵）时终止，也可以当迭代次数超过一定值（比如100次）时终止[15]。

例题27　误差曲线（Error Curves, Chengdu 2010, LA 5009）

已知n条二次曲线Si（x）＋aix²＝bix＝（c ai≥0），定义F（x）＋max｛Si（x）｝，求出F（x）在[0，1000]上的最小值。

【输入格式】

输入第一行为测试数据组数T（T＜100）。每组数据第一行为正整数n（n≤10000），以下n行每行包含3个整数a，b，c（0≤a≤100，｜b｜，｜c｜≤5000）。

【输出格式】

对于每组数据，输出F（x）在[0，1000]中的最小值，保留4位小数。

【分析】

首先来分析一下简单的情况。n＝1的时候，F（x）是一条抛物线，怎么求最小值？注意到a≥0，所以它要么退化成了直线，要么是一条开口向上的抛物线。后者是标准的下凸函数，而直线也可以看成是下凸的，所以n＝1的时候就是一个下凸函数求极值的问题。

n＝2时是怎样的情况呢？似乎要复杂很多。比如它可能是由两段甚至三段组成，如图2-33所示。

图　2-33

尽管如此，图形仍然是下凸的。n＞2时候图形更复杂，但最后的图形总是下凸的（想一想，为什么）。下凸函数的最小值可以用三分法（ternary search）来求解，具体方法如下。取区间[L，R]的两个三分点m₁和m₂，比较F（m₁）和F（m₂）的大小，如图2-34所示。

图　2-34

在图2-34（a）中，如果F（m₁）＜F（m₂），说明解在[L，m₂]中，否则如图2-34（b）所示，说明解在[m₁，R]中。注意，三分法不仅适用于凸函数，还适用于所有单峰函数（unimodal function）。所谓单峰函数就是先严格递增再严格递减（此时存在唯一的最大值），或者先严格递减再严格递增（此时存在唯一的最小值）的函数。

注意上面的100次迭代上限是实验得出的，比赛中可以多尝试几个值。如果答案不正确，则加大迭代次数；如果超时，则减小迭代次数。如果以上两种方法都不行，最好把终止条件改写成解区间的长度小于阈值[16]。

另外，本题的目标是“最大值最小”，是否可以用二分答案的办法做呢？答案是肯定的。有兴趣的读者不妨实验一下，并且和三分法的效果进行比较。

最后有一点要告诉读者，黄金分割法（gold section search，也称优选法）和Fibonacci搜索（Fibonacci search）也可以求单峰函数的极值（前者适合连续情形，后者适合离散情形），而且函数计算的次数小于三分法，不过在绝大多数算法竞赛题目中，三分法已经足够了。

例题28　桥上的绳索（Bridge, Hangzhou 2005, LA 3485）

你的任务是修建一座大桥。桥上等距地摆放着若干个塔，塔高为H，宽度忽略不计。相邻两座塔之间的距离不能超过D。塔之间的绳索形成全等的对称抛物线。桥长度为B，绳索总长为L，如图2-35所示求建最少的塔时绳索的最下端离地的高度y。

图　2-35

【输入格式】

输入第一行为测试数据组数T。每组数据包含4个整数D，H，B，L（B≤L）。

【输出格式】

对于每组数据，输出绳索底部离地高度，保留两位小数。

【分析】

本题需要用到一些微积分基础知识，如求导法则和定积分的概念，但不要求有很强的解题能力。

为了叙述方便，我们把每两个相邻塔之间的部分称为“间隔”。不难发现，塔的数目最小时，间隔数为n＝ceil（B/D），每个间隔宽度D₁＝B/n，每段绳索长度L₁＝L/n。接下来就需要根据D₁和L₁计算底部离地高度y了。

y似乎很难直接计算，所以考虑解方程。设宽度为w，高度为h的抛物线的长度为p（w，h），需要解一个关于h的方程p（D₁，h）＝L₁，然后计算y＝H－h。注意到p（w，h）关于h单调递增，所以可以二分求解。因此，最关键的问题就是：p（w，h）怎么求？

根据微积分知识，可导函数f（x）在区间[a，b]的弧长为

为了写出函数f（x）和它的导函数f'（x），首先要加一个坐标轴，如图2-36所示。

图　2-36

因为顶点在（0，0），因此b＝c＝0，抛物线为f（x）＝ax²。又因为f（-w/2）＝h，解得a＝4h/w²。根据对称性，弧长为

这个定积分怎么算？根据定积分表里的公式

把上述公式变形为

然后就可以套公式了。代码如下。

题目虽解完了，但我们心有余悸。上面的解法中关键问题要用数学方法求解，如果下次遇到一个积不出来的函数怎么办？事实上，很多函数的积分都是没有封闭形式的，无法像上面这样推出公式以后再“套”。这种情况下，数值积分（numerical integral）是一个很好的工具。

假定要求这样一个积分

如果把f（x）画到平面直角坐标系下，则答案为曲线f（x）下方的面积。有很多种办法可以近似求出曲线下方的面积，其中一种既容易理解又容易实现的方法是利用辛普森公式（见图2-37）。

图　2-37

先在整个区间[a，b]中等距地取奇数个点a＝x₀，x₁，x₂，…，xn＝b，相邻两个点的距离为△x，再设yi＝f（xi），然后套用上面的公式。点取得越多，近似程度就越高，但计算量也越大。取多少个点合适呢？这是一个棘手的问题。好在辛普森公式有一个重要的“变种”，称为自适应辛普森法（adaptive Simpson's rule），只需要设一个精度需要eps，算法就可以根据情况递归地划分区间。容易近似的地方少划几份，不容易近似的地方多划几份。具体来讲，设区间[a，b]中点为c，则当且仅当｜S（a，c）＋S（c，b）－S（a，b）｜＜15eps时直接返回结果，否则递归调用。这里的S（a，b）是指区间[a，b]的“三点辛普森公式”值。

有了自适应辛普森公式，本题的求解就很简单了。主程序不变，其他代码如下。

虽然代码略长，但思维难度大大降低，而且代码依然非常快。最后需要说明的一点是，上述自适应辛普森公式的代码并不是最优的写法，因为有很多重复的函数计算（请读者找一找）。请读者试着修改这份代码，把每次递归调用中的函数计算从6次降为2次（提示：递归过程需要多3个参数，即端点和中点的函数值）。

2.9　小结与习题

本节介绍了很多数学专题，但这些知识并不能代替扎实的数学基础。请读者首先确认表2-1中的问题大部分都能较为轻松地解决。

表　2-1

最大公约数和最小公倍数（GCD LCM, UVa 11388）

输入两个整数G，L（1≤G，L＜2³²），找出两个正整数a和b，使得二者的最大公约数为G，最小公倍数为L。如果有多解，输出a≤b且a最小的解。无解输出-1。

最小公倍数（Benefit, UVa 11889）

输入两个整数A和C（1≤A，C≤10⁷），求最小的整数B使得lcm（A，B）＝C。如果无解，输出“NO SOLUTION”（不含引号）。

全加和（How do you add?, UVa 10943）

把K个不超过N的非负整数加起来，使得它们的和为N，有多少种方法？比如N＝5，K＝2，有6种方法，即0＋5，1＋4，2＋3，3＋2，4＋1，5＋0。输入N和K（1≤N，K≤100），输出方法总数除以10⁶的余数。

幂和阶乘（Again Prime? No time., UVa 10780）

输入两个整数n和m（1＜m＜5000，0＜n＜10000），求最大的整数k使得mk是n!的约数。

LCM的个数（LCM Cardinality, UVa 10892）

输入正整数n（n≤2000000000），统计有多少对正整数a≤b，满足lcm（a，b）＝n。比如有8对整数满足lcm（a，b）＝12，即（1，12），（2，12），（3，12），（4，12），（6，12），（12，12），（3，4），（4，6）。输出n和满足条件的整数对数。

超级幂（Super Power, UVa 11752）

如果一个数至少是两个不同的正整数的幂，那么称它为超级幂。比如64就是一个超级幂，因为64＝8²＝4³。你的任务是找出1～2⁶⁴－1之间的所有超级幂，按照升序输出（本题无输入）。

排列之和（Add Again, UVa 11076）

输入n个数字（1≤n≤12），这些数字的任何一种排列都是一个整数。你的任务是求出所有这些整数之和。比如3个数字2，2，4能组成3个整数224，242，422，它们的和是888。

组队（Teams, UVa 11609）

有n个人，选一个或多个人参加比赛，其中一名当队长，有多少种方案？如果参赛者完全相同，但队长不同，算作不同方案。输入n（1≤n≤10⁹），输出方案总数除以1000000007的余数。

回文数（Palindrome Numbers, Dhaka 2003, LA 2889）

回文数从小到大排列为：1，2，3，4，5，6，7，8，9，11，22，33…。输入n（1≤n≤2×10⁹），求第n小的回文数。如第24小的数为151。

整数游戏（Integer Game, UVa 11489）

给出一个数字串N，两个人轮流从中取出一个数字，要求每次取完之后剩下的数是3的倍数，不能取数者输。如果两个游戏者都足够聪明，谁会获胜？输入非空数字串N（N由不超过1000个非0的数字组成），如果先手胜，输出S，否则输出T。

和最小的LCM（Minimum Sum LCM, UVa 10791）

输入正整数n（n≤2³¹－1），找到至少两个正整数，使得它们的LCM为n，并且和最小。

完全平方数（Square Numbers, UVa 11461）

如果一个整数可以写成一个整数的平方，则说它是一个完全平方数。比如1，4，81都是完全平方数。输入正整数a，b（0＜a≤b≤100000），有多少个完全平方数介于a和b之间（包括a和b本身）？

1．组合计数

本章介绍了加法原理、乘法原理、容斥原理和递推等组合计数的常见技巧。下面先给出相关例题，如表2-2所示。

表　2-2

下面是习题和翻译，如表2-3所示。和动态规划一样，很多组合数学问题对思维锻炼都很有帮助。如果练习完这些题目仍意犹未尽的话，请读者在附录A中寻找更多的组合数学题目。

表　2-3

有多少个0（How many 0's? UVa 11038）

输入两个非负整数m和n（m≤n＜2³¹），求将m～n的所有整数写出来，一共要写多少个数字0？

超级平均数（Supermean, UVa 10883）

给出n（n≤50000）个数，每相邻两个数求平均数，将得到n－1个数。这n－1个数每相邻两个数求平均数，将得到n－2个数。依次类推，最后得到1个数。求这个数。

高速公路（Highway, CERC 2006, LA 3720）

有一个n行m列（1≤n，m≤300）的点阵，问一共有多少条非水平非竖直的直线至少穿过其中两个点？如图2-38所示，n＝2，m＝4时答案为12，n＝m＝3时答案为14。

图　2-38

霓虹灯广告牌（Neon Sign, Daejon 2011, LA 5846）

圆周上有n（3≤n≤1000）个点，两两相连，只能涂红色或蓝色。求单色三角形（即3条边的颜色相同的三角形）的个数。

数三角形（Counting Triangles, Dhaka 2005, LA 3295）

输入正整数m和n（m，n≤1000），数出m行n列的网格顶点能组成多少个三角形。比如，m＝1，n＝2，可以组成18个，如图2-39所示。

图　2-39

数四边形（Counting Quadrilaterals, UVa 11139）

在图2-40的10×10网格中，你可以看到5个四边形（注意，四边形的4条边不能相交，而且没有三点共线）。

图　2-40

当然，还可以连出其他网格四边形。你的任务是统计出n×n（n≤120）网格里可以连出多少个网格四边形。如n＝2时有94个，n＝10时有12046294个。

奇怪的税（Strange Tax Calculation, UVa 11529）

一个城市里有n（n≤1200）个建筑物，每个建筑物用平面上一个点来表示。不存在3个建筑物在同一条直线上的情况，因此任意3个建筑物都可以组成一个三角形，称为Bermuda块。计算平均每个Bermuda块内部包含多少个建筑物。任意两个建筑物的坐标都不相同。

铁轨（Magnetic Train Tracks, Dhaka 2007, LA 4064）

给定平面上n（3≤n≤1200）个无三点共线的点，问这些点共组成了多少个锐角或直角三角形。

堆计数（Counting Heaps, CERC 2008, LA 4390）

给出一棵n（1≤n≤500000）个结点的有根树，要求给结点标号为1～n，使得不同结点的标号不同，且每个非根结点的标号比父结点小。求方案总数除以m的余数（2≤m≤10⁹）。

Pinary数（Pinary, Seoul 2005, LA 3357）

把所有不含前导0和连续1的二进制串从小到大排列，即1，10，100，101，1000，1001，…，求第k个串。k≤90000000。

连续的比特（Bits, UVa 11645）

定义A（n）为n的二进制表示中，“连续两个1”出现的次数，比如A（12）＝1（12的二进制为1100），A（15）＝3（15的二进制为1111），A（27）＝2（27的二进制为11011）。输入整数n（0≤n≤2⁶³－2），你的任务是计算S＝A（0）＋A（1）＋…＋A（n）的值。

重排问题（Arrange the Numbers, UVa 11481）

可以把序列1，2，3，…，n任意重排，但重排后的前m（m≤n）个位置中恰好要有k（k≤m）个不变。比如，若n＝5，m＝3，k＝2，可以把序列重排成1，4，3，2，5，其中前m＝3个位置（1，4，3）中恰好有k＝2个（1、3）没有变。输入整数n，m，k（k≤m≤n≤1000），输出重排方法数除以1000000007的余数。

互不攻击的象（Bishops, UVa 10237）

两个象互不攻击，当且仅当它们不处于同一条斜线上。输入整数n（n≤30），统计在一个n×n的棋盘上放k个互不攻击的象有多少种方法。如n＝8，k＝6时有5599888种。

金属（Metal, Seoul 2008, LA 4258）

平面上有n（3≤n≤50）个点，任意两个点的x坐标不同。你的任务是统计有多少种方法把它们连成一个单调多边形。注意，多边形的非相邻边不能有公共点，且任意两条边不能重叠。在本题中，单调多边形是指，如果把多边形看成实心物体，该物体与任意竖线的相交部分是单点或者一条线段。如图2-41所示，左边的点集能连成4个不同的单调多边形。

图　2-41

外接矩形（Persephone, UVa 10884）

用n（n≤100）根长度为1的木条拼出一个周长为n的，各边与坐标轴平行的多边形，并要求其最小外接矩形周长也是n，如图2-42所示。求满足条件的方案数。

图　2-42

排列统计（Permutation Counting, Harbin 2010, LA 5092）

给定1～n的排列｛a₁，a₂，…，an｝，满足ai＞i的下标i的个数称为此排列的E值。例如｛1，3，2，4｝的E值为1，｛4，3，2，1｝的E值为2。给定整数n和k（1≤n≤1000，0≤k≤n），求E值恰好为k的排列个数。

Delta脑电波（Delta Wave, Tianjin 2010, LA 5028）

有一种特殊生物的脑电波是一条从（0，0）到（n，0）的折线。这条折线每向右延伸一个单位长度，高度要么不变，要么加1，要么减1。例如，n＝4时有如图2-43所示的9种折线。

图　2-43

已知n（3≤n≤10000），求折线种数除以10¹⁰⁰的余数。

二进制整数（Binary Integer, Hangzhou 2008, LA 4352）

你的任务是把整数S变成T，方法是选K个（不能多选也不能少选）不同的“恰有3个比特为1”的N（K＝N≤40）位二进制整数Ni，使得S与所有Xi的异或值为T。0≤K≤min｛20，C（N，3）｝

例如，S＝1101，T＝1001，则只有一种方法，即选择N＝｛1110，1101，0111｝。S xor N₁ xor N₂ xor N₃＝1101 xor 1110 xor 1101 xor 0111＝1001（上述数字均为二进制）。

输入N，K，S和T，输出方案总数除以10007的余数。

数学老师的作业（Math Teacher's Homework, Fuzhou 2010, LA 5101）

给出整数k和n个整数m₁，m₂，…，mn，问有多少组整数x₁，x₂，…，xn满足0≤xi≤mi，且x₁ xor x₂ xor x₃ xor … xor xn＝k。1≤n≤50，0≤k，m₁，m₂，…，mn≤2³¹－1。

输出解的数目除以1000000003的余数。

2．数论

数论有很多主题。本书只涉及到了一些最常用的，而把一些特殊的专题放在了第6章，如表2-4所示。虽然例题只有4道，但却相当经典，请读者认真学习。

表　2-4

下面是习题和翻译，如表2-5所示。数论题目多而杂，有时还需要一些灵感，有兴趣的读者可以在附录A中找到更多数论题目。

表　2-5

玩转Floor和Ceil（Play with Floor and Ceil, UVa 10673）

对于任意的整数x和k，存在两个整数p和q满足

输入正整数x和k（x，k＜10⁸），找出符合条件的p和q。如果有多组答案，输出任意一组即可。

格点判定（Lattice Point or Not, UVa 11768）

给定两个点A（x₁，y₁）和B（x₂，y₂），其中x₁，y₁，x₂，y₂均为0.1的整数倍，且绝对值均不超过200000。你的任务是统计线段AB穿过多少个整点。A和B的坐标都是0.1的整数倍。

虚张声势（Just Another Problem, UVa 11490）

你有S个士兵，并打算把他们排成一个r行c列，但有两个“洞”的矩形方队，以迷惑敌人（从远处看，敌人可能误以为一共有r×c个士兵）。洞是两个大小相同的正方形，为了让隐蔽性更强，方队边界（即第一行、最后一行、第一列、最后一列）的所有士兵都得在场，且每个洞的4个方向的士兵“厚度”总是相同。如图2-44所示，洞的边长是2，周围士兵的厚度为3。缺失的士兵数为8。

图　2-44

输入士兵的实际个数S（1≤S≤10¹²），计算出缺失士兵数的所有可能值。

已知因子之和（Alternate Task, UVa 11728）

输入一个正整数S（S≤1000），求一个最大的正整数N，使得N的所有正因子之和为S。

超大数取模（Huge Mod, UVa 10692）

指数运算和四则元素不太一样，因为它的计算顺序是从上到下，而非从下到上。如2^3^2指的是2（3²）＋2⁹＋512，而非（2³）²＋8²＋64。

输入正整数a₁，a₂，…，an和模m（2≤m≤10000，1≤n≤10，1≤ai≤1000），求a₁^a₂^…^an mod m的值。

多项式的最大公约数（Polynomial GCD, UVa 10951）

给定两个Zn上的多项式f（x）和g（x），求出它们的GCD，即Zn上的一个多项式r（x），使得它同时整除f（x）和g（x），并且次数尽量大。你找到的多项式的最高项系数应当为1。例如，2x³＋2x²＋x＋1和x⁴＋2x²＋2x＋2的GCD为x²＋2x＋1。

注：Zn上多项式的所有系数都为0～n－1的整数，并且加法和乘法均在模n意义下进行。

离散平方根（Discrete Square Roots, LA 4270）

在模n意义下，非负整数x的离散平方根是满足0≤r＜n且r²≡x（mod n）的整数。和每个正实数恰好有两个平方根不同，一个整数可能有更多的离散平方根。比如n＝12时，则有4个离散平方根，即1，5，7，11。你的任务是在已知一个离散平方根的情况下求出其他所有的离散平方根。

输入3个整数x，n，r（1≤x＜n，2≤n≤10⁹，1≤r＜n），输出数据编号和所有离散平方根（包括输入的r），按照从小到大排序。输入保证r是模n下的x的离散平方根。

高斯素数（Gauss Prime, Changchun 2007, LA 4079）

输入正整数a和b（0≤a≤10000，0＜b≤10000），判断高斯整数alt是否为高斯素数。当且仅当不存在两个高斯整数（0，1，-1除外）之积为x时，我们说x为高斯素数。注意，整数0，1，-1都不算高斯素数，但alt是高斯素数。

例如，alt不是高斯素数，因为alt

密码学问题（Cryptography Reloaded, Hangzhou 2008, LA 4353）

一般RSA算法是这样的：选两个素数p和q，计算n＝pq，然后选一个整数e，满足gcd（e，（p－1）（q－1））＝1，最后根据de＝1（mod （p－1）（q－1））中计算出d。

需要加密时，用（n，e）作公钥，任意消息x（0≤x＜n）可以加密成y＝xe mod n；需要解密时，用d作私钥，因为x＝yd mod n。

由于手持设备的计算能力比较弱，通常会选取一个相对较小的e，但这也造成了安全隐患。即如果同时得知n，d和e，很容易算出p和q（即分解n）。你的任务是通过n，d，e计算p和q（应满足n＝pq且p＜q），且n≤10¹⁰⁰，3≤e≤31。输入保证符合题目限制。

制作水晶（Make a Crystal, UVa 11014）

三维空间中有一个N×N×N的立方体（N为偶数），坐标范围是-N/2≤x，y，z≤N/2。你的任务是选择尽量多的网格点（除坐标原点O），使得对于任意两个选出的点P和Q，OPQ不共线。输入N（N≤200000），输出能选出的点数的最大值。

敲钟（The Bells are Ringing, Dhaka 2007, LA 4060）

输入整数N，统计有多少组t₁，t₂，t₃满足：0＜t₁＜t₂＜t₃≤1000000，t₃－t₁≤25，且t₁，t₂，t₃的最小公倍数是N。N在64位带符号整数范围内。

素数k元组（Prime k-tuple, Hanoi 2007, LA 3998）

如果k个相邻素数p₁，p₂，p₃，…，pk（pi＜pi＋1）满足pk－p₁＝s，称这些素数组成一个距离为s的素数k元组。例如，k＝4，s＝8时｛11，13，17，19｝是一个距离为8的素数4元组。输入4个整数a，b，k，s（a，b＜2×10⁹，k＜10，s＜40），输出区间[a，b]内距离为s的素数k元组的个数。

不同的数字（Different Digits, Shanghai 2004, LA 3262）

输入正整数n（n＜65536），求它的最小倍数m，使得m包含的不同的数字种类尽量少（例如，1334有3种数字：1，3，4）。如果有多解，m的值应尽量小。例如，n＝101时答案为1111。

Vivian的难题（Vivian's Problem, Guangzhou 2003, LA 2955）

输入k（1≤k≤100）个正整数p₁，p₂，…，pk（1 ＜ pi ＜ 2³¹），找出k个非负整数ei使得alt（x为正整数）。注意，由于x＞0，ei不能全为0。如果无解输出NO，否则输出最大的x。

简单加密法（Simple Encryption, Kuala Lumper 2010, LA 4998）

输入正整数K₁（K₁≤50000），找一个12位正整数K₂（不能含有前导0）使得alt。例如，K₁＝78和99时，K₂分别为308646916096和817245479899。

最大公约数版猜字游戏（GCD Guessing Game, NEERC 2011, LA 5916）

输入正整数n（2≤n≤10000），要求猜出一个1～n之间的正整数p。每次可以猜一个正整数x（1≤x≤n），然后告诉你p和x的最大公约数。要求在最坏情况下需要猜测的次数最小。

例如，n＝6时的最优策略为先猜6，如果回答1，则p＝1或5；如果回答2，则p＝2或4；如果回答3，则p＝3；如果回答6，则p＝6。无论在哪种情况下，最多猜两次就能知道p是多少。

约瑟夫的数论问题（Joseph's Problem, NEERC 2005, LA 3521）

输入正整数n和k（1≤n，k≤10⁹），计算alt

YAPTCHA（YAPTCHA, CERC 2008, LA4382）

输入正整数n（1≤n≤10⁶），计算alt其中[x]表示不超过x的最大整数。

名片切割（Business Card, CERC 2008, LA4384）

输入正整数a，b，c，d，1≤a，b，c，d≤10⁹，判断c×d的纸片是否可以切割成若干个完整的a*b名片，如图2-45所示。比如a＝2，b＝3，c＝9，d＝6时有解。

图　2-45

3．组合游戏

本章介绍了经典的组合游戏理论，包括Nim游戏和SG定理。当然，还有一些组合游戏的题目既不能转化为Nim游戏，也用不上SG定理，但需要一些推理甚至灵感，请读者注意相关例题如表2-6所示。

表　2-6

下面是习题和翻译，如表2-7所示。这些题目只涉及了最基本的组合游戏理论和SG定理。有兴趣的读者可以在附录A中找到更多其他类型的组合游戏题目。

表　2-7

盒子游戏（Box Game, UVa 12293）

有两个相同的盒子，其中一个盒子里有n个球，另一个盒子里有1个球。Alice和Bob两人轮流操作，每次清空球较少的那个盒子（如果球数相同，任意清空一个），然后从另一个盒子里拿些球到这个空盒子中，使得操作后两个盒子都至少有一个球。如果无法操作（当且仅当两个盒子都只有1个球时），则当前游戏者输。输入n（n≤10⁹），你的任务是判断先手（Alice）胜还是后手（Bob）胜。

类Nim游戏（ENimEN, UVa 11892）

有N（N≤20000）堆石子，第i堆有ai个（1≤ai≤10⁹）。两人轮流取，每次可以选择一堆石子，取一个或多个（可以把整堆都取走），但不能同时在两堆或更多堆石子中取。第一个人可以任选一堆取，但后面每次取石子的时候须遵守以下规则：如果对手刚才没有把一堆石子全部取走，则他只能继续在这堆石子里取；只有当对手把一堆石子全部取走时，他才能换一堆石子取。谁取到最后一个石子，谁就赢。假定游戏双方都绝顶聪明，谁会赢呢？

有趣的石子游戏（A Funny Stone Game, LA 3668）

有n（n≤23）堆石子，编号为0～n－1。第i堆初始有si个石子（0≤si≤1000）。两个游戏者轮流操作。每次可以选3堆i，j，k，使得i＜j≤k，且第i堆至少还剩一个石子，然后从第i堆拿走一个石子，再往第j堆和第k堆各放一个石子（若j＝k，则相当于往第j堆放了两颗石子）。谁不能操作，就算输。判断先手必胜还是必败。如果必胜，求出字典序最小的必胜操作（i，j，k）。

擦数游戏（Alice and Bob, Chengdu 2011, LA 5760）

黑板上有n（n≤50）个不超过1000的正整数，Alice和Bob两人轮流操作，Alice先操作。每次操作可以把一个数减1，或者擦掉两个数，然后写上它们的和。当一个数变成0时，会被自动擦掉，不能操作者输。问题是，如果双方均采用最优策略，谁会赢？

4．概率

概率是一个很有意思的数学分支，有时候很直观，有时候却相反；很多概率问题可以转化为组合计数问题，但也有一些问题需要用概率自身的工具和方法解决，如表2-8所示。

表　2-8

下面是习题和翻译，如表2-9所示。题目并不多，建议读者认真思考其中的每个问题。有兴趣的读者可以在附录A中找到更多概率问题，其中一些题目具有相当的难度。

表　2-9

条件概率（Probability｜Given, UVa 11181）

有n个人准备去逛超市，其中第i个人买东西的概率是Pi。逛完以后得知有r个人买了东西。根据这一信息，请计算每个人实际买了东西的概率。输入n（1≤n≤20）和r（0≤r≤n），输出每个人实际买了东西的概率。

奶牛和轿车（Cows and Cars, UVa 10491）

有这样一个电视节目：你的面前有3扇门，其中两扇门后面是奶牛，另外一扇门后则藏着奖品——一辆豪华小轿车。在你选择一扇门之后，门并不会被立即打开。这时，主持人会给出一个提示，具体方法是打开一扇有奶牛的门（不会打开你已经选择的那个门，即使里面是牛）。接下来你有两种可能的决策，即保持先前的选择，或者换成另外一扇未开的门。当然，你最终选择打开的那扇门后面的东西就归你了。

在这个例子里面，你能得到轿车的概率是2/3（难以置信吧），方法是总改变自己的选择。2/3这个数是这样得到的：如果你选择了两头牛之一，你肯定能换到车前面的门，因为主持人已经让你看了另外一头牛；而如果你开始选择的就是车，就会换成剩下的牛并且输掉奖品。由于你的最初选择是任意的，因此选错的概率是2/3。也正是这2/3的情况让你能换到那辆车（另外1/3的情况你会从车切换到牛）。

现在把问题推广一下，假设有a头牛，b辆车（门的总数为a＋b），在最终选择前主持人会替你打开c个有牛的门（1≤a≤10000，1≤b≤10000，0≤c＜a），输出在总是换门的策略下，赢得车的概率。

背单词（Garbage Remembering Exam, UVa 11637）

你刚刚背熟了一个单词表，里面有n个单词。你担心自己的记忆依赖于各个单词在词表中的相对位置（这是很常见的），因此准备把这些单词随机排列后检验记忆效果。给定整数k，如果某两个单词在单词表和随机排列中的距离均不超过k，就说这两个单词是无效的（因为二者的位置关系可能帮助了你的记忆）。输入n和k（1≤k≤n≤100000），你的任务是计算平均情况下，无效单词的个数。

注意，因为你在背了最后一个单词后紧接着又会背第一个单词，词表被看做是环状的而随机排列是线状的。换句话说，词表中的第1个单词和第n个单词的距离为1，而随机排列中第1个单词和第n个单词的距离为n－1。

三维网格中的灯（Lights inside a 3D Grid, UVa 11605）

给一个M×N×P的三维网格，每个格子里有一盏灯。每次均匀随机选择一个格子A和格子B（二者可以相同），然后把以A和B为对角顶点的长方体内所有灯的状态改变（开变关，关变开）。具体来说，如果A的坐标为（x₁，y₁，z₁），B的坐标为（x₂，y₂，z₂），则所有改变状态的灯是那些满足min（x₁，x₂）≤x≤max（x₁，x₂），min（y₁，y₂）≤y≤max（y₁，y₂）和min（z₁，z₂）≤z≤max（z₁，z₂）的格子（x，y，z）。一开始所有灯都是灭的，经过K次操作后，平均情况下有多少灯是亮着的？输入M，N，P，K（0＜M，N，P≤100，0≤K≤10000），输出亮着的灯的平均数量。

你想当2n元富翁吗？（So you want to be a 2n-aire?, UVa 10900）

在一个电视娱乐节目中，开始你有1元钱，主持人会问你n个问题，每次你听到问题后有两个选择：一是放弃回答该问题，退出游戏，拿走奖金；二是回答问题。如果回答正确，奖金加倍；如果回答错误，游戏结束，你一分钱也拿不到。如果正确的回答完所有n个问题，你将拿走所有的2n元钱，称为2n元富翁。

当然，回答问题是有风险的。每次听到问题后，你可以立刻估计出答对的概率。由于主持人会随机提问，你可以认为每个问题的答对概率在t和1之间均匀分布。输入整数n和实数t（1≤n≤30，0≤t≤1），你的任务是求出在最优策略下，拿走的奖金金额的期望值。这里的最优策略是指让奖金的期望值最大。

传球游戏（Game, NEERC 2007, LA 4049）

有n（n≤50）个人围成一圈。一开始第k个人手上有一个球。然后每个人随机往左或往右传球（往右传的概率为pi，往左传的概率为1－pi）。当每个人都拿过球时游戏结束，最后一个拿到球的人获胜。求第n个人的获胜概率。

5．置换

和置换、置换群相关的题目主要是两类，一是等价类计数；二是把置换分解成循环的乘积，然后分析。首先是例题，如表2-10所示。

表　2-10

如表2-11所示是习题和翻译。题目并不多，建议读者认真思考每道题目，但不必强求一定要解出来。

表　2-11

立方体（Cubes, UVa 10601）

有12根等长的小木棍，每根木棍都被涂上一种颜色。输入每根木棍的颜色编号（为1～6的整数），你的任务是统计出用它们能拼成多少种不同的立方体。旋转之后完全相同的立方体被认为是相同的。

世界末日（Doom's Day, UVa 11774）

有一个3n×3m的矩阵，一开始按照行优先的顺序填好1～3n＋m的整数。然后每次按照行优先的顺序把所有数取出，再按列优先的顺序放回，比如：

输入整数n和m（1≤n，m≤10⁹），你的任务是计算出多少次操作后，矩阵会和初始状态相同。输出保证在64位带符号整数范围内。

安迪的鞋架（Andy's Shoes, UVa 11330）

小Andy有很多鞋，穿完了到处扔。后来，他把所有鞋都放回了鞋架排成一排，并且左右交替。遗憾的是，所有颜色混在一起。你的任务是用最少的交换（一次交换任意两只鞋）次数，将每双鞋的左右脚放在一起。

苏丹的吊灯（Sultan's Chandelier, UVa 11540）

苏丹的吊灯是一种树状结构，树上的每个结点处都有一个彩色的灯泡，且该结点的所有子结点等距地挂在以它为圆心的圆周上。这些子结点可以旋转，因此一组吊灯可能在旋转后看起来像另一组吊灯。我们把这两组吊灯看成是本质相同的。

例如，对于如图2-46所示的吊灯结构。

图　2-46

如果灯泡只有红、绿两种颜色，则有如图2-47所示的6种本质不同的吊灯。

图　2-47

吊灯结构用如下方式表达。

换句话说，每组吊灯（subtree）要么只有一个灯泡，要么还有多个子吊灯（tree list）。

输入吊灯的结构（由“[”、“，”和“]”组成的字符串）和灯泡颜色的数量C（C≤100），输出方案总数除以1000000007的余数。每个吊灯的结点数不超过100。

6．矩阵与线性方程组

和矩阵、线性方程组有关的题目大都有规律可循。表2-12中的几道例题相当经典，请读者认真学习。

表　2-12

如表2-13所示的习题有一定难度，但能很好地锻炼思维和编程能力。

表　2-13

n次方之和（Contemplation! Algebra, UVa 10655）

输入非负整数p，q，n，求an＋bn的值，其中a和b满足a＋b＝p，ab＝q。注意，a和b不一定是实数。

矩阵的幂（Power of Matrix, UVa 11149）

输入一个n×n矩阵A，计算A＋A²＋A³＋…＋Ak的值。

有理数电阻（Rational Resistors, UVa 10808）

给你一个包含n个结点，m条导线的电阻网络，求结点a和b之间的等效电阻（单位：欧姆）。a和b的等效电阻等于有一安培电流从a流进，b流出时，ab之间的电压值（单位：伏特）。注意两个结点之间可以有多条导线相连，一条导线的两端也可以是同一个结点。输出用最简分数表示（无穷大用1/0表示）。

怪盗基德（Kid's Problem, Beijing 2004, LA 3139）

你的任务是帮助怪盗基德打开如图2-48所示的一个密码锁。

图　2-48

该锁一共有k（1≤k≤20）个按钮和k个齿轮，每个齿轮上有n（2≤n≤10）个齿，分别印有整数1～n，其中最上面的那个齿上的数字在外部可见。初始时所有齿轮的可见数（即最上面齿的整数）均为1。每个按钮可以控制多个齿轮，用序列｛a₁，b₁，a₂，b₂，…，ap，bp｝来描述，表示该按钮控制p个齿轮：a₁，a₂，…，ap，其中每按一下这个按钮，齿轮ai将逆时针旋转bi个齿。当每个齿轮的可见数组成的序列恰好等于密码，锁将会打开。密码保证不是全1，因此初始情况下密码锁是锁上的。

7．数值方法

表2-14中的3道例题覆盖了算法竞赛中3个最常见的数值方法：二分法、三分法、数值积分。

表　2-14

下面是习题和翻译。请读者在完成如表2-15所示的题目时注意精度问题。有些式子在数学上是等价的，但写成程序时对误差的敏感程度却不同。

表　2-15

变长的木棍（Expanding Rods, UVa 10668）

把一个长度为L的细木棍加热n度，它的长度会变为L'＝（1＋nC）L，其中C为伸缩系数。如果把木棍的两端固定好，木棍在变长的过程中会变成圆弧形。输入L，n，C，你的任务是计算出木棍中心的位移。输入保证木棍长度增量不超过原长的一半。

铁人两项（Duathlon, UVa 10385）

有n（n≤20）个选手参加一个“铁人两项”比赛，比赛项目是先跑步r千米，再骑车k千米。总赛程是t（t≤100）千米，跑步和骑车的距离可以调整，但必须保证r＋k＝t。已知每个人的跑步速度和骑车速度（假设速度不变且不随距离变化），并且指定其中一名选手。是否可以通过调整r和k的值，使得这个指定选手取胜？如果可以，还要让他和第二名的差距（时间）尽量大。

体积（Volume, Harbin 2010, LA 5096）

有两个底面半径为R，高为H的全等圆柱体在中心重合的位置正交，如图2-49所示。

图　2-49

给出R和H，求相交部分体积。注意R可以小于H的一半。

高精度十进制小数计算器（Big Decimal Calculator, UVa 12413）

你的任务是实现一个高精度十进制小数的计算器，支持以下15个运算。

❑　双目运算：add, sub, mul, div, pow, atan2。

❑　单目运算：exp, ln, sqrt, asin, acos, atan, sin, cos, tan。

在所有三角函数／反三角函数中，角均用弧度表示。

提示：在使用泰勒展开时，需要配合一些数值技巧以保证级数快速收敛。相关信息参见http://www.tc.umn.edu/~ringx004/sidebar.html。

注释

[1]　注意，“除以3”是在乘法之后，所以应当判断除以3之前的部分是不是会溢出。

[2]　即两边之和大于第三边。

[3]　UVa 11375

[4]　UVa 11137。

[5]　UVa 11174。

[6]　如果你无法注意到MOD是素数，至少应该注意编程判断题目中给出的模是否为素数。这是一个好习惯。

[7]　参考http://oeis.org/A001187。

[8]　再如，在上面的讨论中我们实际上已经得出（x，x，0）总是必败状态。

[9]　例如，单堆Nim游戏很简单（直接把所有火柴拿走就赢了），但n堆火柴就复杂得多了。

[10]　UVa 11859。

[11]　这样的随机过程称为马尔可夫过程（markov process）。尽管这个名称本身不会对解题带来什么帮助，但要知道它以后会对阅读某些文献有所帮助。

[12]　请反复阅读这句拗口的话。

[13]　算法不同，稳定性的精确定义也有所不同，但是都与算法的精确性与正确性相关。

[14]　请不要和伴随矩阵（adjugate matrix）混淆。

[15]　如果是在二分int，最多32次就够了；同理，因为float和double存在精度极限，也可以计算出最大迭代次数。

[16]　如果不愿意实验，直接计算出迭代次数也可以，但需要注意的是，本题输出的是函数值而不是x的值，因此计算迭代次数并不是想象中那么简单。

第3章　实用数据结构

在《算法竞赛入门经典》中，我们已经接触到了很多数据结构，如队列、栈、链表、二叉树、优先队列、并查集等，本章会给出更多关于它们的实例。而对于算法竞赛中涉及的一些更为复杂的数据结构，本章也会予以详细介绍。

与算法设计及数学问题相比，数据结构题目有更多规律可循，但编程难度通常也会更大。对于不能带资料的比赛（如IOI）来说，现场编写复杂数据结构是一种很大的挑战。

3.1　基础数据结构回顾

3.1.1　抽象数据类型（ADT）

《算法竞赛入门经典》中已经介绍过不少常用的数据结构，但重点在于知识点的讲授而不是应用。在算法竞赛中，往往是先思考需要一个怎样的数据结构，再考虑如何去实现（或者用已有的）。通俗地说，这个与实现无关的数据结构称为抽象数据类型（Abstract Data Type, ADT），它描述的是数据结构的外部特征，即使用方法。

例题1　猜猜数据结构（I Can Guess the Data Structure!, UVa 11995）

你有一个类似“包包”的数据结构，支持两种操作，如表3-1所示。

表　3-1

给出一系列操作以及返回值，你的任务是猜猜这个“包包”到底是什么。它可能是一个栈（后进先出），队列（先进先出），优先队列（数值大的整数先出）或者其他什么奇怪的东西。

【输入格式】

输入包含多组数据。每组数据第一行为一个整数n（1≤n≤1000）。以下n行每行要么是一条类型1的命令，要么是一条类型2的命令后面跟着一个整数x（1≤x≤100）。这个整数x表示执行完这条类型2的命令后，包包无错地返回了x。输入结束标志为文件结束符（EOF）。输入文件大小不超过1MB。

【输出格式】

对于每组数据，输出一行。一共有5种可能的输出，如表3-2所示。

表　3-2

【分析】

本题考查了栈、队列和优先队列3种ADT的概念。只要熟悉这些概念，本题不难解决。事实上，STL中已经封装好了这3种数据结构，分别是stack, queue和priority_queue。这样，本题只需要依次判断输入是否有可能是栈、队列或优先队列，然后综合起来即可。注意到题目中说的“无错的返回”，因此在执行pop操作的时候要调用一下empty（），否则可能会异常退出。

例题2　一道简单题（Easy Problem from Rujia Liu?, UVa 11991）

给出一个包含n个整数的数组，你需要回答若干询问。每次询问两个整数k和v，输出从左到右第k个v的下标（数组下标从左到右编号为1～n）。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤100000），第二行包含n个不超过10⁶的正整数，即待查询的数组。以下m行每行包含两个整数k和v（1≤k≤n，1≤v≤10⁶）。输入结束标志为文件结束符（EOF）。输入文件不超过5MB。

【输出格式】

对于每个查询，输出查询结果。如果不存在，输出0。

【分析】

本题有很多做法，下面描述一种编程复杂度小，时间效率也满足题目要求的方法。从查询的角度讲，如果能把输入组织成一个可以“直接读结果”的数据结构，是最好不过了。比如，如果data[v][k]就是答案，那该有多好！

事实上，这样的数据结构是存在的。首先，因为v的范围很大，这里的data不应该是一个数组，而是一个STL的map。也就是说，data[v]是指在data这个map中键v所对应的“值”。由于我们还要以data[v][k]这样的方式访问，data[v]的“值”应当是一个数组，保存整数v从左到右依次出现的下标（因此第k次出现的下标就是data[v][k]）。

由于不同整数出现的次数可以相差很大，data[v]不应是一个定长数组，否则会有大量的空间浪费。换句话说，data[v]应该是一个变长数组，如vector＜int＞。

这样，我们就可以利用vector和map这两个现成的数据结构简洁地解决本题，代码如下。

预处理每个元素的时间复杂度为O（logn）（这是map查找的时间复杂度），总时间复杂度为O（nlogn）。而查询的时间复杂度为O（logn）（这也是map查找的时间复杂度，因为vector＜int＞的随机存取是O（1）的）。如果把map改成hash_map（一个非标准的STL容器，用hash实现），则运行时间还会缩短。

3.1.2　优先队列

例题3　阿格斯（Argus, Beijing 2004, LA 3135）

你的任务是编写一个称为Argus的系统。该系统支持一个Register命令

Register Q_num Period

该命令注册了一个触发器，它每Period秒钟就会产生一次编号为Q_num的事件。你的任务是模拟出前k个事件。如果多个事件同时发生，先处理Q_num小的事件。

【输入格式】

输入仅包含一组数据。前若干行是Register命令，以“#”结尾；最后一行是整数k。对于每条命令，1≤Q_num，Period≤3000。k≤10000。命令条数n不超过1000。

【输出格式】

输出k行，即前k个事件的Q_num。

【分析】

用优先队列来维护每个触发器的“下一个事件”，然后每次从中取出最早发生的一个事件，重复k次即可。任意时刻优先队列中都是n个元素，因此总时间复杂度为O（klogn）。

推而广之，我们实际上已经学会解决多路归并问题，即把k个有序表合并成一个有序表（假定每个表都是升序排列）——用优先队列维护每个表的“当前元素”。如果一共有n个元素，则时间复杂度为O（nlogk）。

例题4　K个最小和（K Smallest Sums, UVa 11997）

有k个整数数组，各包含k个元素。在每个数组中取一个元素加起来，可以得到kk个和。求这些和中最小的k值（重复的值算多次）。

【输入格式】

输入包含多组数据。每组数据第一行为一个整数k（1≤k≤750）。以下k行每行包含k个不超过10⁶的正整数。输入结束标志为文件结束符（EOF）。输入文件不超过5MB。

【输出格式】

对于每组数据，输出k个最小和的值，并按照从小到大的顺序排序。

【分析】

在解决这个问题之前，先看看它的简化版：给出两个长度为n的有序表A和B，分别在A和B中任取一个数并相加，可以得到n²个和。求这些和中最小的n个和。

这个问题可以转化为前面介绍过的多路归并问题。这需要我们把这n²个和组织成如下n个有序表。

表1：A₁＋B₁≤A₁＋B₂≤A₁＋B₃≤…

表2：A₂＋B₁≤A₂＋B₂≤A₂＋B₃≤…

表3：An＋B₁≤An＋B₂≤An＋B₃≤…

其中第a张表里的元素形如Aa＋Bb。我们用二元组（s，b）来表示一个元素，其中s＝Aa＋Bb。为什么不保存A的下标a呢？因为我们用不到a的值。如果我们需要得到一个元素（s，b）在表a中的下一个元素（s'，b＋1），只需要计算s'＝Aa＋Bb＋1＝Aa＋Bb－Bb＋Bb＋1＝s－Bb＋Bb＋1，并不需要知道a是多少。代码里可以用如下结构体来表示。

因为在任意时刻，优先队列中恰好有n个元素，一共取了n次最小值，因此时间复杂度为O（nlogn）。代码如下。

本题不是两个表，而是k个表，怎么办呢？两两合并就可以了（想一想，为什么），代码如下。

注意（*）处，combine函数对A[0]又读又写，会有问题吗？这个问题请读者思考。程序的时间复杂度为O（k²logk）。另外，没有必要在一开始就把所有k²个元素保存在二维数组A中，而是可以每次只读k个元素，然后合并，从而大大降低空间复杂度。

3.1.3　并查集

例题5　易爆物（X-Plosives, LA 3644）

有一些简单化合物，每个化合物都由两种元素组成的（每个元素用一个大写字母表示）。你是一个装箱工人，从实验员那里按照顺序依次把一些简单化合物装到车上。但这里存在一个安全隐患：如果车上存在k个简单化合物，正好包含k种元素，那么它们将组成一个易爆的混合物。为了安全起见，每当你拿到一个化合物时，如果它和已装车的化合物形成易爆混合物，你就应当拒绝装车；否则就应该装车。编程输出有多少个没有装车的化合物。

【输入格式】

输入包含多组数据。每组数据包含若干行，每行为两个不同的整数a，b（0≤a，b≤10⁵），代表一个由元素a和元素b组成的简单化合物。所有简单化合物按照交给你的先后顺序排列。每组数据用一行-1结尾。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出没有装车的化合物的个数。

【分析】

我们把每个元素看成顶点，则一个简单化合物就是一条边。当整个图存在环的时候，组成环的边对应的化合物是危险的，反之则是安全的。

这样，我们可以用一个并查集来维护图的连通分量集合，每次得到一个简单化合物（x，y）时检查x和y是否在同一个集合中。如果是，则拒绝，反之则接受。

例题6　合作网络（Corporative Network, LA 3027）

有n个结点，初始时每个结点的父结点都不存在。你的任务是执行一次I操作和E操作，格式如下。

❑　Iuv：把结点u的父结点设为v，距离为｜u－v｜除以1000的余数。输入保证执行指令前u没有父结点。

❑　Eu：询问u到根结点的距离。

【输入格式】

输入第一行为测试数据组数T。每组数据第一行为n（5≤n≤20000）。接下来有不超过20000行，每行一条指令，以“O”结尾。I指令的个数小于n。

【输出格式】

对于每条E指令，输出查询结果。

【分析】

因为题目只查询结点到根结点的距离，所以每棵树除了根结点不能换之外，其他结点的位置可以任意改变，这恰好符合并查集的特点，但是需要记录附加信息。如果记录每个结点到根的距离，那么每次I操作都要更新很多结点的信息，时间复杂度难以保证，因此考虑记下每个结点到父结点的距离为d[i]，然后在路径压缩时维护这个d数组，如图3-1所示。

图　3-1

上图是修改后的查找过程。注意，由于本题的合并操作指定了父结点和子结点，所以不能使用启发式合并。完整代码如下。

3.2　区间信息的维护与查询

连续和查询问题。给定一个n个元素的数组A₁，A₂，…，An，你的任务是设计一个数据结构，支持一个查询操作Query（L，R）：计算AL＋AL＋1＋…＋AR。

如何做呢？如果每次用循环来计算，单次查询需要O（n）的时间，效率太低；如果借助前缀和思想，可以花O（n）时间事先计算好Si＝A₁＋A₂＋…＋Ai（定义S₀＝0），因为Query（L，R）＝SR－SL－1，每次查询都只需O（1）时间。我们用O（n）－O（1）来描述这样的时间复杂度：预处理时间为O（n），单次查询时间为O（1）。

上面的问题本身虽然不难，但是只要稍作修改，就可以引申出一系列经典算法和数据结构。下面将一一介绍。

3.2.1　二叉索引树（树状数组）

动态连续和查询问题。给定一个n个元素的数组A₁、A₂、…，An，你的任务是设计一个数据结构，支持以下两种操作。

❑　Add（x，d）操作：让Ax增加d。

❑　Query（L，R）：计算AL＋AL＋1＋…＋AR。

如何让Query和Add都能快速完成呢？如果依照刚才的思路，每次执行Add操作都要修改一批Si，还是会很慢。有一种称为二叉索引树（Binary Indexed Tree, BIT）[1]的数据结构可以很好地解决这个问题。为此，我们需要先介绍lowbit。

对于正整数x，我们定义lowbit（x）为x的二进制表达式中最右边的1所对应的值（而不是这个比特的序号）。比如，38288的二进制是1001010110010000，所以lowbit（38288）＝16（二进制是10000）。在程序实现中，lowbit（x）＝x&－x。为什么呢？回忆一下，计算机里的整数采用补码表示，因此-x实际上是x按位取反，末尾加1以后的结果，如图3-2所示。

图　3-2

二者按位取“与”之后，前面的部分全部变0，之后lowbit保持不变。

如图3-3所示是一棵典型的BIT，由15个结点组成，编号为1～15。

图　3-3

灰色结点是BIT中的结点（白色长条的含义稍后叙述），每一层结点的lowbit相同，而且lowbit越大，越靠近根。图中的虚线是BIT中的边[2]。注意编号为0的点是虚拟结点，它并不是树的一部分，但是它的存在可以让算法理解起来更容易一些。

对于结点i，如果它是左子结点，那么父结点的编号就是i＋lowbit（i）；如果它是右子结点，那么父结点的编号是i-lowbit（i）（请读者打草稿验证一下）。搞清楚树的结构之后，构造一个辅助数组C，其中

换句话说，C的每个元素都是A数组中的一段连续和。到底是哪一段呢？在BIT中，每个灰色结点i都属于一个以它自身结尾的水平长条（对于lowbit＝1的那些点，“长条”就是那个结点自己），这个长条中的数之和就是Ci。比如结点12的长条就是从9～12，即C₁₂＝A₉＋A₁₀＋A₁₁＋A₁₂。同理，C₆＝A₅＋A₆。这个等式极为重要，请读者花一些时间来验证“Ci就是以i结尾的水平长条内的元素之和”这一事实。

有了C数组后，如何计算前缀和Si呢？顺着结点i往左走，边走边“往上爬”（注意并不一定沿着树中的边爬），把沿途经过的Ci累加起来就可以了（请读者仔细验证，沿途经过的Ci所对应的长条不重复不遗漏地包含了所有需要累加的元素），如图3-4所示。

图　3-4

而如果修改了一个Ai，需要更新C数组中的哪些元素呢？从Ci开始往右走，边走边“往上爬”（同样不一定沿着树中的边爬），沿途修改所有结点对应的Ci即可（请读者验证，有且仅有这些结点对应的长条包含被修改的元素），如图3-5所示。

图　3-5

两个操作的代码如下。

不难证明，两个操作的时间复杂度均为O（logn）。预处理的方法是先把A和C数组清空，然后执行n次add操作，总时间复杂度为O（nlogn）。

例题7　乒乓比赛（Ping pong, Beijing 2008, LA 4329）

一条大街上住着n个乒乓球爱好者，经常组织比赛切磋技术。每个人都有一个不同的技能值ai。每场比赛需要3个人：两名选手，一名裁判。他们有一个奇怪的规定，即裁判必须住在两名选手的中间，并且技能值也在两名选手之间。问一共能组织多少种比赛。

【输入格式】

输入第一行为数据组数T（1≤T≤20）。每组数据占一行，首先是整数n（3≤n≤20000），然后是n个不同的整数，即a₁，a₂，…，an（1≤ai≤100000），按照住所从左到右的顺序给出每个乒乓爱好者的技能值。

【输出格式】

对于每组数据，输出比赛总数的值。

【分析】

考虑第i个人当裁判的情形。假设a₁到ai₁中有ci个比ai小，那么就有（i－1）－ci个比ai大；同理，假设ai＋1到an中有di个比ai小，那么就有（n－i）－di个比ai大。根据乘法原理和加法原理，i当裁判有ci（n－i－di）＋（i－ci－1）di种比赛。这样，问题就转化为求ci和di。

ci可以这样计算：从左到右扫描所有的ai，令x[j]表示目前为止已经考虑过的所有ai中，是否存在一个ai＝j（x[j]＝0表示不存在，x[j]＝1表示存在），则ci就是前缀和x[1]＋x[2]＋…＋x[ai－1]。初始时所有x[i]＝0，在计算ci时，需要先设x[ai]＝1，然后求前缀和。换句话说，我们需要动态地修改单个元素值并求前缀和——这正是BIT的标准用法。这样，就可以在O（nlogr）（这里的r是ai的上限）的时间内计算出所有ci，类似地，可以计算出di，然后在O（n）时间里累加出最后的答案。

3.2.2　RMQ问题

范围最小值问题（Range Minimum Query, RMQ）。给出一个n个元素的数组A₁，A₂，…，An，设计一个数据结构，支持查询操作Query（L，R）：计算min｛AL，AL＋1，…，AR｝。

每次用一个循环来计算最小值显然不够快，前缀和的思想也不能提高效率（想一想，为什么），怎么办呢？实践中最常用的是Tarjan的Sparse-Table算法，它预处理的时间是O（nlogn），但是查询只需要O（1），而且常数很小。最重要的是，这个算法非常好写，并且不易写错[3]。

令d（i，j）表示从i开始的，长度为2j的一段元素中的最小值，则可以用递推的方法计算d（i，j）：d（i，j）＝min｛d（i，j－1），d（i＋2j－1，j－1）｝，原理如图3-6所示。

图　3-6

注意2j≤n，因此d数组的元素个数不超过nlogn，而每一项都可以在常数时间计算完毕，故总时间为O（nlogn）。代码如下。

查询操作很简单，令k为满足2k≤R－L＋1的最大整数，则以L开头、以R结尾的两个长度为2k的区间合起来即覆盖了查询区间[L，R]。由于是取最小值，有些元素重复考虑了几遍也没关系，如图3-7所示（注意，如果是累加，重复元素是不允许的）。

图　3-7

查询代码如下。

这样就在O（nlogn）预处理后，做到了O（1）时间查询。

例题8　频繁出现的数值（Frequent Values, UVa 11235）

给出一个非降序排列的整数数组a₁，a₂，…，an，你的任务是对于一系列询问（i，j），回答ai，ai＋1，…，aj中出现次数最多的值所出现的次数。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和q（1≤n，q≤100000）。第二行包含n个非降序排列的整数a₁，a₂，…，an（-100000≤ai≤100000）。以下q行每行包含两个整数i和j（1≤i≤j≤n），输入结束标志为n＝0。

【输出格式】

对于每个查询，输出查询结果。

【分析】

应注意到整个数组是非降序的，所有相等元素都会聚集到一起。这样就可以把整个数组进行游程编码（Run Length Encoding，RLE）。比如-1，1，1，2，2，2，4就可以编码成（-1，1），（1，2），（2，3），（4，1），其中（a，b）表示有b个连续的a。用value[i]和count[i]分别表示第i段的数值和出现次数，num[p]、left[p]、right[p]分别表示位置p所在段的编号和左右端点位置，则在下图的情况，每次查询（L，R）的结果为以下3个部分的最大值：从L到L所在段的结束处的元素个数（即right[L]－L＋1）、从R所在段的开始处到R处的元素个数（即R－left[R]＋1）、中间第num[L]＋1段到第num[R]－1段的count的最大值，如图3-8所示（除了最小值变成了最大值，这几乎就是一个RMQ）。

图　3-8

特殊情况：如果L和R在同一段中，则答案是R－L＋1。

3.2.3　线段树（1）：点修改

动态范围最小值问题。给出一个有n个元素的数组A₁，A₂，…，An，你的任务是设计一个数据结构，支持以下两种操作。

❑　Update（x，v）：把Ax修改为v。

❑　Query（L，R）：计算min｛AL，AL＋1，…，AR｝。

如果还是使用Sparse-Table算法，每次Update操作都需要重新计算d数组，时间无法承受。为了解决这个问题，这里介绍一种灵活的数据结构：线段树（segment tree）[4]。

如图3-9所示是线段[1，8]所对应的线段树。每个非叶子结点都有左右两棵子树，分别对应该线段的“左半”和“右半”。为了方便，我们按照从上到下、从左到右的顺序给所有结点编号为1，2，3，…，则编号为i的结点，其左右子结点的编号分别为2i和2i＋1。

图　3-9

假定根结点是一个长度为2h的区间，不难发现：第i层有2i个结点，每个结点对应一个长度为2h－i的区间。最大层编号为h，结点总数为1＋2＋4＋8＋…2h＝2h＋1－1，略小于区间长度的两倍。当整个区间长度不是2的整数幂时，虽然叶子不全在同一层，但树的最大层编号和结点总数仍然满足上述结论。

在不同的题目中，线段可以有不同的含义，如数轴上的一条真实的线段，或者一个序列中的连续子序列。在动态范围最小值问题中，用线段[L，R]代表子序列AL，AL＋1，…，AR，则整个序列对应线段[1，n]，它是线段树的根结点。

上图只展示了线段树中各结点所对应的线段，但对于用到线段树的大部分题目来说，这些线段所拥有的附加信息才是重头戏。本题需要维护“最小值”信息，因此可以在每个结点上记录该线段中所有元素的最小值。比如，在写有“[5，8]”的结点里保存元素5，6，7，8的最小值，即min｛A₅，A₆，A₇，A₈｝。在程序中，可以用一个数组minv保存这个附加信息，其中minv[o]表示结点o所对应的区间中所有元素的最小值。比如，[5，8]的编号为3，因此minv[3]＝min｛A₅，A₆，A₇，A₈｝。

在查询时，我们从根结点开始自顶向下找到待查询线段的左边界和右边界，则“夹在中间”的所有叶子结点不重复不遗漏地覆盖了整个待查询线段。如图3-10所示。

图　3-10

从图中不难发现，树的左右各有一条“主线”，虽有分叉，但每层最多只有两个结点继续向下延伸（整棵树的左右子树各一个），因此“查询边界”结点不超过2h个，其中h是线段树的最大层编号。这实际上把待查询线段分解成了不超过2h个不相交线段的并。比如图3-10中，[2，5]＝[2]＋[3，4]＋[5]。在后文中，凡是遇到这样的区间分解，就把分解得到的各个区间叫做边界区间，因为它们对应于分解过程的递归边界。

如何更新线段树呢？显然需要更新线段[i，i]对应的结点，然后还要更新它的所有祖先结点。不难发现，其他结点的值并没有改变。

下面是代码。这里的o是当前结点编号，L和R是当前结点的左右端点（比如，当o＝3时，L＝5，R＝8）。查询时，全局变量ql和qr分别代表查询区间的左右端点；修改时，全局变量p和v分别代表修改点位置和修改后的数值。

最后叙述一下建树过程。一种方法是每读入一个元素x后执行修改操作A[i]＝x，则时间复杂度为O（nlogn）。其实只需要事先设置好每个叶结点的值，自底向上递推即可（也可以写成递归）。每个结点仅计算了一次，因此时间复杂度为O（n）。

值得一提的是，除了上述方法之外，线段树还有一种自底向上的写法，速度比上述递归方法快。但由于理解难度有所增加，而且在实际比赛中极少出现递归写法不够快的情况，这里不予介绍，有兴趣的读者可以自行查阅相关资料。

例题9　动态最大连续和（Ray, Pass me the Dishes, LA 3938）

给出一个长度为n的整数序列D，你的任务是对m个询问作出回答。对于询问（a，b），需要找到两个下标x和y，使得a≤x≤y≤b，并且Dx＋Dx＋1＋…＋Dy尽量大。如果有多组满足条件的x和y，x应该尽量小。如果还有多解，y应该尽量小。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤500000），即整数序列的长度和查询的个数。第二行包含n个整数，依次为D₁，D₂，…，Dn的值。这些整数的绝对值均不超过10⁹。以下m行每行为一个查询，包含两个整数a和b。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出数据编号，然后为每个查询输出一行，包含两个整数x和y。

【分析】

本题看上去很像RMQ问题，但稍微琢磨一下就会发现本题和RMQ的重要区别：整个区间的解不能简单地通过各个子区间的解合并得到（想一想，为什么），所以Sparse-Table算法中的预处理和查询部分均不适用于本题。

《算法竞赛入门经典》中讨论过本题的静态版本，即最大连续和子序列，并且介绍过它的分治算法：最优解要么完全在左半序列，要么完全在右半序列，要么跨越中点。由于线性算法的存在，这个算法可能并没有引起很多读者的重视，但这个思路却是解决本题的关键。

构造一棵线段树，其中每个结点维护3个值：最大连续和max_sub、最大前缀和max_ prefix与最大后缀和max_suffix。具体来说，max_sub（a，b）是满足a≤x≤y≤b且Dx＋Dx＋1＋…＋Dy最大的二元组（x，y）；max_prefix（a，b）是满足a≤x≤b且Da＋Da＋1＋…＋Dx最大的整数x；max_suffix（a，b）是满足a≤x≤b且Dx＋Dx＋1＋…＋Db最大的整数x。

比如，n＝64，询问为（20，50），则线段[20，50]在线段树的根结点处被分成了两条线段[20，32]和[33，50]。则max_sub（20，50）的起点和终点有3种情况。

情况一：起点和终点都在[20，32]中，则max_sub（20，50）＝max_sub（20，32）。

情况二：起点和终点都在[33，50]中，则max_sub（20，50）＝max_sub（33，50）。

情况三：起点在[20，32]中，终点在[33，50]中，则max_sub（20，50）＝（max_suffix（20，32），max_prefix（33，50））（想一想，为什么）。

类似地，max_prefix和max_suffix也可以这样递推。建树的时间复杂度为O（n），单组查询的时间复杂度为O（logn）。

3.2.4　线段树（2）：区间修改

第3.2.3节介绍的是支持点修改和区间查询的线段树。事实上，线段树还可以做得更多。本节讨论区间修改问题，比3.2.3节的内容更难懂，但也更巧妙。

快速序列操作I。给出一个n个元素的数组A₁，A₂，…，An，你的任务是设计一个数据结构，支持以下两种操作。

❑　Add（L，R，v）：把AL，AL＋1，…，AR的值全部增加v。

❑　Query（L，R）：计算子序列AL，AL＋1，…，AR的元素和、最小值和最大值。

根据查询需要，不难设计出线段树中需要维护的3个信息sum，min，max，分别对应3个查询值。但如何实现修改呢？注意，本题的add操作是区间修改，而不是前面介绍的点修改。点修改只会影响logn个结点，但区间修改在最坏情况下会影响到树中的所有结点。比如，如果对整个区间执行add操作，所有结点的sum都会改变。怎么办呢？

回忆前面讲区间查询时曾给出一个结论：任意区间都能分解成不超过2h个不相交区间的并（这里h依然是指线段树的最大层编号）。利用这个结论，我们可以“化整为零”，把一个add操作分解成不超过2h个操作，记录在线段树的结点中。图3-11展示了执行完add（1，7，5）和add（3，6，2）之后的情形。注意结点[5，6]中执行了两个add操作，分别加了2和5。这等价于一个add（7）操作。

图　3-11

维护的信息也需要修改，理由前面已经讲过了。如果仍然用sum[o]表示“结点o对应区间中所有数之和”，则add操作在最坏情况下会修改所有的sum（不管add有没有分解）。解决方法是把sum[o]的定义改成“如果只执行结点o及其子孙结点中的add操作，结点o对应区间中所有数之和”。这样的附加信息仍可以方便地维护，而且每个原始add所影响到的结点数目变为了O（h）（想一想，为什么）。

信息维护的代码如下。

在执行add操作时，哪些结点需要调用上述maintain函数呢？很简单，递归访问到的结点全部要调用，并且是在递归返回后调用。代码如下。

查询操作如何实现呢？仍然是把查询区间递归分解为若干不相交子区间，把各个子区间的查询结果加以合并，但需要注意的是每个边界区间的结果不能直接用，还得考虑祖先结点对它的影响。为了方便，我们在递归查询函数中增加一个参数，表示当前区间的所有祖先结点add值之和，方法如下。

怎么样，是不是比点修改复杂多了？还有更复杂的。

快速序列操作II。给出一个有n个元素的数组A₁，A₂，…，An，你的任务是设计一个数据结构，支持以下两种操作。

❑　Set（L，R，v）：把AL，AL＋1，…，AR的值全部修改为v（v≥0）。

❑　Query（L，R）：计算子序列AL，AL＋1，…，AR的元素和、最小值和最大值。

不难想到把set操作也进行分解，记录在结点中，但很快就会发现一个新问题，即add操作的时间顺序不会影响结果，但set操作会。比如先执行add（1，4，1）再执行add（2，3，2）等价于先执行add（2，3，2）再执行add（1，4，1），但先执行set（1，4，1）再执行set（2，3，2）却不等价于先执行set（2，3，2）再执行set（1，4，1）。怎么办呢？

完整的解决方案有些复杂，但大体思路是清晰的：除了对本操作进行分解之外，还要修改以前分解好的操作，使得任意两个set操作都不存在祖先后代关系[5]。因此，不管这些set操作以何种顺序进行，结果总是唯一的。

举例说明。在一棵根结点为[1，8]的线段树上先执行set（1，8，1）操作，再执行set（1，3，2）操作，会怎样呢？首先，set（1，8，1）就是简单设置根结点的set值为1，但set（1，3，2）就比较复杂了，分为3个步骤。

先把根结点的set（1）操作往下传递给左右子结点，如图3-12所示。

图　3-12

接下来把[1，4]的set（1）继续往下传（想一想，为什么[5，8]不用传），如图3-13所示。

图　3-13

注意，现在[1，2]不需要往下传，因为set（1，3，2）完整地覆盖了区间[1，2]，因此只需要把它的set值直接设为2即可，然后把[3，4]的set值往下传，最后覆盖[3，3]的set值，如图3-14所示。

图　3-14

这样，修改操作代码如下。

上述代码有两个需要注意的地方。首先是pushdown函数。它的作用是把set值往下传递，代码如下。

另一个值得注意的地方是，与上一题相比，代码中多了两处maintain的调用。这是因为只要标记下传，该子树的附加信息就必须重新计算（想一想，为什么）。对于本来就要递归访问的子树，递归访问结束之后自然会调用maintain，因此只需要针对不进行递归访问的子树调用maintain。如图3-15所示。

图　3-15

递归访问顺序是1→3→7，但是除了这3个结点之外，结点2和结点6也需要重新计算统计信息。

介绍完了set操作，可能有读者会产生疑问，如果先执行set（1，3，2）再执行set（1，8，1）会怎样呢？最终的线段树如图3-16所示。

图　3-16

这违反了前面讲的“任意两个set操作不会存在祖先-后代关系”。的确如此。上面的方法并不一定满足上述要求，但这又如何？我们只需规定在这种情况下，以祖先结点上的操作为准即可，在递归查询时，一旦碰到一个set操作就立即停止，代码如下。

例题10　快速矩阵操作（Fast Matrix Operations, UVa 11992）

有一个r行c列的全0矩阵，支持以下3种操作，如表3-3所示。

表　3-3

子矩阵（x1，y1，x2，y2）是指满足x1≤x≤x2，y1≤y≤y2的所有元素（x，y）。输入保证任意时刻矩阵所有元素之和不超过10⁹。

【输入格式】

输入包含多组数据。每组数据第一行为3个整数r，c，m（1≤m≤20000），其中r为行数，c为列数，m是查询个数。矩阵不超过20行，元素总数不超过10⁶。输入结束标志为文件结束符（EOF），输入文件大小不超过500KB。

【输出格式】

对于每条类型3的操作，输出3个整数，即该子矩阵的元素和、最小值和最大值。

【分析】

矩阵不超过20行，矩阵元素却可能多达10⁶个，可以想到每行建一棵线段树，则本题转化为一维问题。

本题有两个操作，add和set，因此需要两个标记addv和setv，含义同前。规定同时有两个标记时，表示先执行set再执行add，传递代码如下。

其他代码留给读者编写。注意update函数的递归边界代码，对于set操作要清除结点上的addv标记；但对于add操作不清除setv标记；在maintain函数中先考虑setv再考虑addv，而在query操作中要综合考虑setv和addv。

3.3　字符串（1）

3.3.1　Trie

我们常常用Trie（也叫前缀树）来保存字符串集合。如图3-17所示就是一个Trie。

图　3-17

上图表示的字符串集合为｛a, to, tea, ted, ten, i, in, inn｝，每个单词的结束位置对应一个“单词结点”。反过来，从根节点到每个单词结点的路径上所有字母连接而成的字符串就是该结点对应的字符串。在程序上，将根结点编号为0，然后把其余结点编号为从1开始的正整数，然后用一个数组来保存每个结点的所有子节点，用下标直接存取。

具体来说，可以用ch[i][j]保存结点i的那个编号为j的子结点。什么叫“编号为j”呢？比如，若是处理全部由小写字母组成的字符串，把所有小写字母按照字典序编号为0，1，2，…，则ch[i][0]表示结点i的子结点a。如果这个子结点不存在，则ch[i][0]＝0[6]。用sigma_size表示字符集的大小，比如，当字符集为全体小写字母时，sigma_size＝26。

使用Trie的时候，往往需要在单词结点上附加信息，其中val[i]表示结点i对应的附加信息。例如，如果每个字符串有一个权值，就可以把这个权值保存在val[i]中。简单起见，下面的代码中假定权值大于0，因此val[i]＞0当且仅当结点i是单词结点。

Trie的定义和插入代码如下。

查询和插入类似，代码留给读者编写。

例题11　背单词（Remember the Word, LA 3942）

给出一个由S个不同单词组成的字典和一个长字符串。把这个字符串分解成若干个单词的连接（单词可以重复使用），有多少种方法？比如，有4个单词a、b、cd、ab，则abcd有两种分解方法：a＋b＋cd和ab＋cd。

【输入格式】

输入包含多组数据。每组数据第一行为小写字母组成的待分解字符串，长度L不超过300000。第二行为单词个数S（1≤S≤4000）。以下S行每行为一个单词，由不超过100个小写字母组成。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出分解方案数除以20071207的余数。

【分析】

不难想到这样的递推法：令d（i）表示从字符i开始的字符串（即后缀S[i..L]）的分解方案数，则d（i）＝sum｛d（i＋len（x））｜单词x是S[i..L]的前缀｝。

如果先枚举x，再判断它是否为S[i..L]的前缀，时间无法承受（最多可能有4000个单词，判断还需要一定的时间）。可以换一个思路，先把所有单词组织成Trie，然后试着在Trie中“查找”S[i..L]。查找过程中每经过一个单词结点，就找到一个上述状态转移方程中的x，最多只需要比较100次就能找到所有的x。

例题12　strcmp（）函数（"strcmp（）" Anyone?, UVa 11732）

strcmp（）是C/C++的一个库函数，它的作用是比较两个字符串的字典序大小。在本题中，考虑strcmp（）的如下实现。

如上所述，比较操作一直进行到两个字符串的对应位置处的字符不相同为止（假定字符串总是以'\0'结尾）。比如，strcmp（"than"，"that"）和strcmp（"there"，"the"）各需要7次比较（注意s[i]＝＝t[i]和s[i]＝＝'\0'各有一次比较），如图3-18所示。

图　3-18

输入n个字符串，两两调用一次strcmp（）（即一共调用n（n－1）/2次），问字符比较的总次数是多少？

【输入格式】

输入最多包含10组测试数据，每组数据第一行为一个整数N（0＜N＜4001），即字符串总数。以下N行每行包含一个由不超过1000个数字和大小写字母组成的非空字符串。输入结束标志为n＝0。输入文件大小约为23MB。

【输出格式】

对于每组数据，输出字符比较的总次数T。输入保证T在64位带符号整数范围内。

【分析】

字符串很多，数据量大，按题意两两比较显然不现实。如果把所有单词插入到一棵Trie里面会怎样呢？考虑than和that对应的结点，如图3-19所示。

图　3-19

than和that的前3个字符相同，但第4个字符不同，因此比较次数为7。不仅如此，任何两个在灰色结点处分叉的字符串都需要比较7次。想到这里，知道该怎么做了吗？

3.3.2　KMP算法

下面考虑字符串匹配问题。假设文本是一个长度为n的字符串T，模板是一个长度为m的字符串P，且m≤n。需要求出模板在文本中的所有匹配点i，即满足T[i]＝P[0]，T[i＋1]＝P[1]，…，T[m－1]＝P[m－1]的非负整数i（注意字符串下标从0开始）。如图3-20所示，P在T中有且只有一个匹配点，即位置3。

图　3-20

最朴素的方法是依次判断每个位置s是不是一个匹配点。检查匹配点需要O（m）时间（每个字符逐一比较），而可能的匹配点有O（n－m）个，所以最坏情况时间复杂度为O（m（n－1））。有一个简单的优化：在检查匹配点的合法性时只要有一个字符不同，立即停止比较，换下一个匹配点。但最坏情况下仍然需要O（m（n－1））时间[7]。

和朴素算法相比，KMP算法的时间效率就强多了。它首先用O（m）的时间对模板进行预处理，然后用O（n）时间完成匹配。从渐进意义上说，这样的时间复杂度已经是最好的了（至少需要O（m＋n）时间，因为需要检查文本串和模板的每个字符）。

虽然代码很短，但KMP算法的细节并不容易理解。考虑到网上已经有很多介绍KMP的资料，这里只对它进行简单介绍，作为学习Aho-Corasick自动机的铺垫。

KMP算法的精髓蕴含在图3-21中。

图　3-21

假定在匹配的过程中正在比较文本串*位置的字符和模板串abbaaba的最后一个字符，发现二者不同（称为失配），这时，朴素算法会把模板串右移一位，重新比较abbaaba的第一个字符和文本串!!位置的字符。

KMP算法认为，既然!!位置已经比较过一次，就不应该再比一次了。事实上，我们已经知道灰色部分就是abbaab，应该可以直接利用模板串本身的特性判断出右移一位一定不是匹配。同理，右移两位或者三位也不行，但是右移四位是有可能的。这个时候，需要比较*处的字符和abbaaba的第三个字符。

图3-21下方的那条链是一个状态机，其中编号为i的结点表示已经匹配了i个字符[8]。匹配开始时当前状态是0，成功匹配时状态加1（表示多匹配了一个字符），而失配时沿着“失配边”走。比如在这个例子中，如果在状态6时失配，应转移到状态2。为方便起见，这里用失配函数（failure function）f[i]表示状态i失配时应转移到的新状态，要特别注意的是f[0]＝0。

有了失配函数后，KMP算法不难写出，代码如下。

这个代码的时间复杂度如何？答案可能并不明显。失配的时候也许会反复向左走很多次，会不会太慢？不会。可以这样计算时间复杂度。每次j＋＋的时候伴随着一次i＋＋，而每次j＝f[j]的时候j至少会减1。最坏情况下j增加了n次，因此j＝f[j]的次数不会超过n，因此总时间复杂度为O（n）。

状态转移图的构造是KMP算法的关键，也是它最巧妙的地方。算法的思想是“用自己匹配自己”，根据f[0]，f[1]，…，f[i－1]递推f[i]，代码和匹配部分非常相似，如下所示。

如表3-4所示是ABRACADABRA的状态转移表，供读者理解和模拟刚才介绍的算法。

表　3-4

上述算法其实并不是完整的KMP算法[9]，但因为现在的算法已经达到了时间复杂度的下限，并且足够使用，这里不再继续探讨，有兴趣的读者可自行查阅相关资料。

例题13　周期（Period, SEERC 2004, LA3026）

给定一个长度为n的字符串S，求它每个前缀的最短循环节。换句话说，对于每个i（2≤i≤n），求一个最大的整数K＞1（如果K存在），使得S的前i个字符组成的前缀是某个字符串重复K次得到。输出所有存在K的i和对应的K。

比如对于字符串aabaabaabaab，只有当i＝2，6，9，12时K存在，且分别为2，2，3，4。

【输入格式】

输入包含多组数据。每组数据第一行为正整数n（2≤n≤10⁶），第二行为一个字符串S。输入结束标志为n＝0。

【输出格式】

对于每组数据，按照从小到大的顺序输出每个i和对应的K，一对整数占一行。

【分析】

如图3-22所示，根据后缀函数的定义，“错位部分”长度为i－f[i]。

图　3-22

如果这i个字符组成一个周期串，那么“错位”部分恰好是一个循环节，因此i－f[i]＝k*i（注意k＞1，因此i－f[i]不能等于i，即必须有f[i]＞0）。不难证明反过来也成立。代码如下。

3.3.3　Aho-Corasick自动机

在模式匹配问题中，如果模板有很多个，KMP算法就不太合适了。因为每次查找一个模板，都要遍历整个文本串。可不可以只遍历一次文本串呢？可以，方法是把所有模板建成一个大的状态转移图（称为Aho-Corasick自动机，简称AC自动机），而不是每个模板各建一个状态转移图。注意到KMP的状态转移图是线性的字符串加上失配边组成的，不难想到AC自动机是Trie加上失配边组成的。

图3-23是｛he, she, his, hers｝的Trie。

图　3-23

图3-24是对应的Aho-Corasick自动机。

图　3-24

如果已经构造好AC自动机，匹配算法几乎和KMP是一样的，代码如下。

代码中出现了一个陌生的数组last，下面解释一下。和Trie一样，我们认为所有val[j]＞0的结点j都是单词结点，反之亦然。但和Trie不同的是，同一个结点可能对应多个字符串的结尾，如图3-25所示。

图　3-25

结点B不仅意味着找到串101，还意味着找到串01。换句话说，当找到一个模板后，应该顺着失配指针往回走，看看有没有其他串。当然，失配指针不一定指向一个单词结点（比如，两个串是101和010，那么上图中的结点A不是单词结点），为了提高效率，这里增设一个指针last[j]，表示结点j沿着失配指针往回走时，遇到的下一个单词结点编号。这个last[j]在正规文献里叫做后缀链接（suffix link）。

计算失配函数的方式和KMP很接近，只是把线性递归改成了按照BFS顺序递推，代码如下。

由于失配过程比较复杂，要反复沿着失配边走，在实践中常常会把上述AC自动机改造一下，把所有不存在的边补上，即把计算失配函数中的语句“if（!u） continue;”改成：

这样，就完全不需要失配函数，而是对所有的转移一视同仁。也就是说，find函数中的语句“while（j && !ch[j][c]） j＝f[j];”可以直接完全删除。

例题14　出现次数最多的子串（Dominating Patterns, LA 4670）

有n个由小写字母组成的字符串和一个文本串T，你的任务是找出哪些字符串在文本中出现的次数最多。例如字符串aba在ababa中出现2次，但字符串bab只出现了1次。

【输入格式】

输入包含多组数据，每组数据第一行为字符串个数n（1≤n≤150）。以下n行每行包含一个字符串，长度为1～70之间的整数。下一行是文本串T，长度为1～10⁶之间的整数。输入结束标志为n＝0。

【输出格式】

对于每组数据，第一行输出最多出现的次数，接下来每行包含一个出现次数最多的字符串，按照输入顺序排列。

【分析】

本题模板多但长度短，而文本串又很长，正适合用AC自动机，只需要把“递归打印”的print函数改成如下所示即可[10]。

另一个容易忽略的地方是重复出现的模板。如果模板有重复，后一个子串会覆盖前一个（想一想，为什么），因此需要用其他方法判重，其中一个简单的方法[11]是建一个字符串到编号的索引map＜string，int＞ ms，每次在初始化的时候清空，代码如下。

插入的时候更新（在val[u]＝v; 之后加一条ms[string（s）]＝v;），最后在输出的时候使用，方法如下。

例题15　子串（Substring, UVa 11468）

给出一些字符和各自对应的选择概率，随机选择L次后将得到一个长度为L的随机字符串S（每次独立随机）。给出K个模板串，计算S不包含任何一个串的概率（即任何一个模板串都不是S的连续子串）。

【输入格式】

输入第一行为数据组数T（T≤50）。每组数据第一行为模板串个数K（K≤20）。以下K行每行包含一个模板串（长度不超过20）。下一行为整数N，即字符个数。以下N行每行为一个不同的字符（保证为大小写字母或者数字）和选择它的概率pi。所有pi之和保证为1。最后一行为生成的字符串长度L。模板串保证只由上述N个字符组成。

【输出格式】

对于每组数据，输出生成的串不包含任何一个模板串的概率。

【分析】

构造出AC自动机之后，每随机生成一个字母，相当于在AC自动机中随机走一步。所有单词结点标记为“禁止”，则本题就是求从结点0开始走L步，不进入任何禁止结点的概率。设d（i，j）表示当前在结点i，还需要走j步，不碰到任何禁止结点的概率，由全概率公式不难得到下面的记忆化搜索过程，代码如下。

这个过程利用的是改造之后的AC自动机，因此所有字符一视同仁。上面的match[i]表示结点i是否为单词结点，有了它，就不需要维护val和last数组了。在insert函数中，新插入字符串结尾的match等于1，其他结点的match设为0，计算last的语句改成match[u]｜＝match[f[u]]即可。

例题16　矩阵匹配器（Matrix Matcher, UVa 11019）

给出一个n×m的字符矩阵T，你的任务是找出给定的x×y的字符矩阵P出现了多少次。即需要在二维文本串T中查找二维模式串P。

【输入格式】

输入第一行为测试数据组数。每组数据第一行为整数n和m（1≤n，m≤1000），以下n行每行m个字符，即字符矩阵T。下一行是整数x和y（1≤x，y≤100），以下x行每行y个字符，即字符矩阵P。

【输出格式】

对于每组数据，输出P在T中出现的次数。

【分析】

要想整个矩阵匹配，至少各行都得匹配。所以先把P的每行看做一个模式串构造出AC自动机，然后在T中的各行逐一匹配，找到P中每一行的所有匹配点。

只要在匹配时做一些附加的操作，就可以把匹配出来的单一的行拼成矩形。用一个count[r][c]表示T中以（r，c）为左上角、与P等大的矩形中有多少个完整的行和P对应位置的行完全相同。当P的第i行出现在T的第r行，起始列编号为c时，意味着count[r－i][c]应当加1（行列均从0开始编号）。所有匹配结束后，count[r][c]＝x（P的行数）的那些（r，c）就是一个二维匹配点。

这个算法的时间复杂度为O（nm＋xy），达到了理论下限（最坏情况下，必须检查T和P的每个字符）。

3.4　字符串（2）

3.4.1　后缀数组

3.3节讲到Aho-Corasick自动机可以解决多模板匹配问题，但前提是事先知道所有的模板。在实际应用中，很多时候是无法事先知道查询的（如搜索引擎）。这时需要预处理文本串T，而不是模板。

假定文本串为BANANA，可以在它的末尾加一个字符$（代表一个没在串中出现过的字符），然后把它的所有后缀（BANANA$，ANANA$，NANA$，ANA$，NA$，A$）插入到一颗Trie中。由于尾字符$的存在，字符串的每个后缀和叶结点一一对应。如图3-26所示。

图　3-26

有了这个后缀Trie，查找一个长度为m的模板时只需要进行一次普通的Trie查找，时间复杂度为O（m），比如查找字符串ANAN，如图3-27所示。

图　3-27

在实际应用中，会把后缀Trie中没有分支的链合并到一起，得到所谓的后缀树（suffix tree），但由于后缀树的构造算法比较复杂难懂，且容易写错（虽然最终代码并不长），在算法竞赛中很少使用。相比之下，后缀树的一个简单替代品——后缀数组却是选手必备的有利武器，它不仅时间效率高，而且代码简单，不易写错。

读者也许已经注意到了，在绘制上面的后缀Trie时，我们故意把每个结点的所有子结点排好序，字典序小的在左边，字典序大的在右边（规定$比所有其他字符都小），然后在叶结点里标上该后缀首字符的下标。比如后缀ANA开始于BANANA的下标3（注意下标从0开始），因此后缀ANA所对应的叶结点标有“3”。为叙述方便，直接把“以下标k开头的后缀”叫做“后缀k”。换句话说，对于文本串BANANA，后缀3就是ANA。

现在只需自左向右把所有叶子的编号排列出来，就可以得到后缀数组（suffix array）。比如，BANANA的后缀数组为SA＝｛5，3，1，0，4，2｝，它就是所有后缀按照字典序从小到大排序后的结果[12]。

根据定义，后缀数组可以直接通过一次快速排序得到，但是在最坏情况下，直接排序需要的时间是O（n²logn）（虽然比较次数是O（nlogn），但两个字符串的比较不是O（1）的，而是O（n）的）。下面介绍Manber和Myers发明的倍增算法，它的时间复杂度是O（nlogn）。

首先把所有单个字符排序，计算出每个字母的“名次”，最小的字母是第1名，第二小的字母是第2名，依此类推，如图3-28所示。

图　3-28

下面来给所有后缀的前两个字符排序（之前实际上是给所有后缀的第1个字符排序），如图3-29所示。

图　3-29

这一步等价于给一些二元组排序，其中每个二元组就是一个后缀的前两个字符的名次。

接下来给所有后缀的前4个字符排序（注意不是前3个），如图3-30所示。

图　3-30

注意，这次的排序对象仍然是二元组，而不是四元组。注意到每个后缀k的前4个字符是由“后缀k的前2个字符”和“后缀k＋2的前2个字符”连接而成的，如果要把它和某个后缀k'相比较，应该先比较“后缀k的前2个字符”和“后缀k'的前2个字符”，如果相同再比较“后缀k＋2的前2个字符”和“后缀k'＋2的前2个字符”。这正是在比较一个二元组。请仔细阅读这段话，再对照图3-30，直到完全弄懂为什么这样排序是对的。

还需要给所有后缀的前8个字符排序吗？不必了，因为所有名次已经两两不同（请读者思考，为什么名次两两不同以后就不用排序了）。不过为了加深理解，这里仍然给出第4次排序的图，如图3-31所示。

图　3-31

如果这个图和你想象的不同，说明你并未真正理解这个算法，请重新阅读前一段文字。

下面考虑程序实现。由于比较的字符数每次翻倍，不难发现最多需要O（logn）次排序。每次排序需要多长时间呢？第一次是给单个字符排序，第二次是给二元组排序，如果采用快速排序，各需要O（nlogn）时间，总时间复杂度为O（nlog²n）。注意到字符种类数最多只有n，所以可以用基数排序[13]，算法每一轮的时间复杂度降为O（n），总时间复杂度为O（nlogn）。程序的具体实现有很多技巧，这里仅给出最终代码，细节留给读者推敲。

上述代码除了输入的字符串s之外，还需要4n个辅助空间，因此空间复杂度为O（n）。后缀数组还有线性时间复杂度的算法，但不好理解，不仅运行效率没有显著改善，所占用的空间也大幅提高。在算法竞赛中，倍增算法是后缀数组构造的不错选择。

有了后缀数组，就能处理在线的多模板匹配问题了。直接在后缀数组里进行二分查找即可。下面是一个示例代码，找到其中一个匹配位置，读者不难把它改写成找出所有位置的代码。

每次查询的时间复杂度为O（mlogn），其中n是文本串的长度，m是模板串的长度。

3.4.2　最长公共前缀（LCP）

只有上面的sa这一个数组，能做的事情并不多。我们通常还需要两个辅助数组：rank和height，其中rank[i]代表后缀i在sa数组中的下标，height[i]定义为sa[i－1]和sa[i]的最长公共前缀（Longest Common Prefix, LCP）长度。两个字符串的LCP长度为k，意味着这两个字符串的前k个字符都相同，但第k＋1个字符不同（或者某个字符串一共只有k个字符）。

对于两个后缀j和k，不妨设rank[j]＜rank[k]，则不难证明后缀j和k的LCP长度等于height[rank[j]＋1]，height[rank[j]＋2]，…，height[rank[k]]中的最小值，即RMQ（height，rank[j]＋1，rank[k]）。

比如在图3-32中，后缀1（abaaaab）和后缀4（aaab）的LCP长度等于min｛2，3，1，2｝＝1。

图　3-32

如果根据定义，计算height[i]需要进行一次字符串比较，整个height数组需要O（n²）时间，比后缀数组的构造时间还要长。下面给出一个O（n）时间计算height数组的代码，它用到了一个辅助数组h[i]＝height[rank[i]]，然后按照h[1]，h[2]，…的顺序递推计算。递推的关键是这样一个性质：h[i]≥h[i－1]－1。这样做，就不必每次都从头比较了。

如果你很好奇这个性质为什么成立，下面将给出一个简单的证明，如图3-33所示。

图　3-33

在图3-33中，设排在后缀i－1前一个的是后缀k。后缀k和后缀i＋1分别删除首字符之后得到后缀k＋1和后缀i，因此后缀k＋1一定排在后缀i的前面，并且最长公共前缀长度为h[i－1]－1，如图3-34所示。

图　3-34

这个h[i－1]－1是一系列h值的最小值，这些h值中包括后缀i和排在它前一个的后缀p的LCP长度，即h[i]。因此h[i]≥h[i－1]－1。

前面讲过，有了height数组之后，任意两个后缀的LCP相当于一次RMQ，因此可以用前面介绍的Sparse-Table算法，在O（nlogn）时间的预处理之后用O（1）时间回答每次询问。利用这个方法可以加速多模式匹配，把时间复杂度由前面的O（mlogn）降为O（m＋logn），当n很大的时候效果显著。具体应该怎么做，请读者思考。

例题17　生命的形式（Life Forms, UVa 11107）

输入n个DNA序列，你的任务是求出一个长度最大的字符串，使得它在超过一半的DNA序列中连续出现。如果有多解，按照字典序从小到大输出所有解。

【输入格式】

输入包含多组数据。每组数据第一行为DNA串的个数n（1≤n≤100），以下n行每行为一个由不超过1000个小写字母组成的非空字符串。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出所有解，按照字典序从小到大排列。如果无解，输出一行“?”（不含引号）。

【分析】

这是一道经典的题目，解法有很多，这里仅介绍一种易于理解的方法。首先用不同的分隔字符把所有输入字符串（以下简称原串）拼起来，如abcdefg、bcdefgh、cdefghi可以拼成abcdefgXbcdefghYcdefghZ（注意最后也要加一个字符）。

求这个新串的后缀数组和height数组，然后二分答案，每次只需要判断是否有一个长度为p的串在超过一半的串中连续出现，判断方法是扫描一遍height数组，把它分成若干段。每当height[i]小于p时开辟一个新段，则每一段的最初p个字符均相同。只要某一段中包含了超过n/2个原串的后缀，p就是满足条件的，如图3-35所示。

图　3-35

如何进行上述判断呢？最简单的方法是用一个数组标记当前段中是否包含了各个原串的后缀（比如，可以用flag[i]＝1表示当前段中包含原串i的后缀），然后在每次开启一个新段时检查flag为1的i的个数是否超过n/2，然后清空flag数组[14]。这样，每次判断的时间复杂度为O（L）（L为拼接串的总长度），总时间复杂度为O（LlogL）。

本题还可以做到O（n）时间复杂度，有兴趣的读者可以自行思考。提示：扫描一次height数组即可，需要一个栈。

3.4.3　基于哈希值的LCP算法

在本节内容结束之前，来看一个另类的LCP算法。除了能够实现LCP算法之外，它的思路还可以用来解决其他很多问题。

还记得哈希函数吗？为每个后缀计算一个哈希值，满足递推式H（i）＝H（i＋1）x＋s[i]（其中0≤i＜n，H（n）＝0），例如

一般地，H（i）＝s[n－1]xn－1－i＋s[n－2]xn－2－i ＋…＋s[i＋1]x＋s[i]

对于一段长度为L的字符串s[i]～s[i＋L－1]，定义它的哈希值Hash（i，L）＝H（i）－H（i＋L）xL，展开后等于alt

上述表达式只和s[i]～s[i＋L－1]以及L有关，适合作为这一段子串的哈希值。而且当预处理H和xL之后，Hash（i，L）是可以在常数时间内算出的。注意到这里的Hash值可能很大，但一般只把它存在unsigned long long里，这样在算术运算时会自然溢出，相当于模2⁶⁴。看到这里，读者可能会问，这样能保证字符串不同时，哈希函数值也不同吗？不一定。不同的字符串，哈希函数值有可能相同，但概率很小。可以随机多取几个不同的x来验证，如果算出的哈希函数值全部相同，字符串不同的概率就更小了。

有了哈希数组Hash，回答LCP（i，j）问题就很容易了。首先二分答案L，然后判断Hash（i，L）和Hash（j，L）是否相等。如果相等，就认为从i和j出发，长度为L的字符串相同，即LCP（i，j）≥L，否则LCP（i，j）＜L。

怎么样，简单吧。这个方法通常适合在没有时间编写复杂的字符串算法时使用，而且常常比“标准算法”效率更高。对概率算法的深入讨论已经超出了本书的范围，但可以告诉大家的是，基于哈希值的字符串算法真的很不错。

例题18　口吃的外星人（Stammering Aliens, SWERC 2009, LA 4513）

有一个口吃的外星人，说的话里包含很多重复的字符串，比如babab包含两个bab。给定这个外星人说的一句话，找出至少出现m次的最长字符串。

【输入格式】

输入包含多组数据。每组数据第一行为整数m，第二行为一个仅包含小写字母的字符串，长度在m～40000之间。输入结束标志为m＝0。

【输出格式】

对于每组数据，如果不存在，则输出“none”，否则输出两个整数，即最长字符串的长度及其起始位置的最大值。

【分析】

二分答案L，然后判断是否有长度为L的字符串出现了至少m次。判断方法很简单，从左到右计算出所有起始位置的长度为L的字符串的哈希值，一旦该哈希值出现了至少m次，就说明有解。代码如下。

这种方法虽然不如后缀数组快（其实也只差了两三倍），但代码短且易于编写，不失为一种实用的方法。作为练习，请读者再用后缀数组解一遍本题，感受一下二者的区别。

3.5　排序二叉树

3.5.1　基本概念

排序二叉树（Binary Search Tree，BST，也称二叉搜索树、二分检索树）是一种数据结构，它的每个结点都保存着一个可以比较大小r的东西（如整数、字符串），并且对于任意结点u来说，u的左子树中的所有结点（如果存在的话）都比根结点小，u的右子树的所有结点（如果存在的话）都比根结点大。

注意，这个定义涉及了元素的比较操作，因此要求任意两个结点都可以比较大小。

BST最原始的用法是实现集合。这要求BST支持3种操作，即插入、删除和查找。根据定义，查找过程可以从根开始递归进行。假定要查找的元素为x，如果x比根小，递归在左子树中查找x；如果比根大，递归在右子树中查找x。如果根和x相等，直接返回结果即可。不难发现，在最坏情况下，查找的时间复杂度为O（h），其中h是树的高度。

插入过程类似，假设要插入的元素为x，如果x比根小，递归在左子树插入x；如果比根大，递归在右子树中插入x。如果根和x相等，插入失败（集合中不能有相同元素）。这个过程和查找过程最大的区别是，如果在递归查找／插入前发现对应的子树并不存在，查找过程的做法是返回“元素不存在”，而插入过程的做法是在该子树的位置创建新结点。删除比较复杂，暂不讨论。

同样的结点集合，可以构造出很多种不同的BST。注意到每次插入结点时，实际上只有一种正确的插入位置，所以BST不唯一的原因在于结点插入顺序不同。比如，按照不同顺序插入1，2，3，…，n，就会得到高度不同的BST。在极端情况下，它甚至会成为一条链，那么查找元素就和线性查找没什么两样了。所以实用的BST必须是平衡的。可问题来了，加入每次插入结点方式是唯一的，而插入结点的顺序又不是我们所能控制的（这取决于BST的使用者），如何让BST平衡呢？答案是通过旋转，让BST在保持合法的前提下改变形态。

图3-36为BST的两种旋转方式，分别为左旋和右旋。注意，这里的“左右”指的是旋转方向（如果觉得难以理解，可认为左是逆时针，右是顺时针）。不管是左旋还是右旋，旋转对象都是根结点，旋转结束以后，根结点发生变化。

图　3-36

不难发现，旋转前后各子树的左右位置保持不变（a＜o＜x＜k＜b），因此仍然是棵合法的排序二叉树。

遗憾的是，只学会旋转还不够，还需要知道在什么情况下如何旋转，才能让BST保持平衡，而这并不是一件容易的事。幸运的是，主流编程语言大多数都提供了直接可用的BST，比如STL中的set和map。本着“不要重新发明轮子”的作风，如果二者（或者multiset/ multimap）已经可以满足要求，建议不要实现自己的平衡BST。下面的例题19就是一个直接使用“现成BST”的例子。

例题19　优势人群（Efficient Solutions, UVa 11020）

有n个人，每个人有两个属性x和y。如果对于一个人P（x，y），不存在另外一个人（x'，y'），使得x'＜x，y'≤y，或者x'≤x，y'＜y，我们说P是有优势的。每次给出一个人的信息，要求输出在只考虑当前已获得的信息的前提下，多少人是有优势的。

【输入格式】

输入第一行为测试数据组数T。每组数据第一行为整数n（0≤n≤15000），以下n行每行两个整数x，y，分别为每个人的两个属性（不超过10⁹的非负整数）。

【输出格式】

对于每组数据，输出获得每条信息后，有优势的人数。

【分析】

注意到人是只增不减的，因此一个现在有优势的人，以后可能会失去优势，而且一旦失去优势，便再也不会重新获得优势。这样可以动态维护优势人群集合。如果用平面坐标上的点表示一个人，那么优势人群对应的集合会是什么样的呢？

首先，根据定义，不会有两个不同点的x坐标或者y坐标相同。其次，对于任意两个点A和B，如果A在B的左边（即A的x坐标严格小于B的x坐标），那么A一定在B的上边（即A的y坐标严格大于B的y坐标）。这样，从左到右看，各个点的y坐标越来越小，如图3-37所示。

图　3-37

新增一个点P后，会是什么情况呢？

情况一：这个点可能本身没有优势，因此直接忽略此点即可。判断方法很简单，只需判断它左边相邻点的y坐标是否比它小即可。如图3-38（a）所示。

情况二：这个点有优势，则把它加入到集合中。注意这个点可能会让一些其他点失去优势，从而被删除，如图3-38（b）所示。

图　3-38

这里采用STL中的multiset（可重集）来表示这个点集（因为集合中可以有相同点，代表属性完全相同的人），则第一种情况只需要比较lower_bound（P）和P的y坐标，而第二种情况只需要从upper_bound（P）开始，删除所有没有优势的点。代码如下。

由于每个点最多被删除一次，每次删除需要O（logn）时间复杂度，因此总时间复杂度为O（nlogn）。

3.5.2　用Treap实现名次树

在主流STL版本中，set都是用BST实现的，具体来说是用的一种称为红黑树（Red-black tree）的动态平衡BST，但红黑树在竞赛中并不常用。为什么呢？因为红黑树太复杂。它的插入有5种情况，删除有6种情况，不仅代码量大，编写的过程中也容易写错。相比之下，有一种称为Treap的动态平衡BST，不仅插入和删除非常简单直观，速度也不错，很好地平衡了编码复杂度和时间效率，是算法竞赛中同类数据结构中的首选。

简单地说，Treap是一棵拥有键值、优先级两种权值的树。对于键值而言，这棵树是排序二叉树；对于优先级而言，这棵树是堆。即在这棵树的任意子树中，根结点的优先级是最大的（这个性质称为堆性质）。图3-39就是一棵treap，其中结点内的数字表示键值，结点旁的数字表示优先级（该数值越大，优先级越高）。

图　3-39

不难证明，如果每个结点的优先级事先给定且互不相等，整棵树的形态也唯一确定了，和元素的插入顺序无关。在Treap的插入算法中，每个结点的优先级是随机确定的，因此各操作的时间复杂度也是随机的。幸运的是，可以证明插入、删除和查找的期望时间复杂度均为O（logn），实际表现也不错。

下面是Treap结点的定义。

下面是旋转。为方便起见，请结合图3-40来分析。

图　3-40

注意上述代码中的小技巧。因为d为0或1，d^1等价于1－d，但计算速度更快。另一个关键之处在于o的类型。首先，o是指向一个Treap结点的指针，因此为Node*类型；其次，o本身是可以修改的，因此o是引用。比如可以对某个结点u的左子结点进行旋转，即rotate（u－＞ch[0]），如果o不是引用，那么在旋转结束后，u的左子结点并没有改变，就会引起错误。当然，可以让rotate返回旋转后的结点指针，但代码会变长，且直观性会降低。

插入结点时，首先随机给新结点一个优先级，然后执行普通的插入算法（根据键值大小判断插在哪棵子树中）。执行完毕后用左右旋让这个结点“往上走”，从而维持堆性质。比如在图3-40中插入结点18，随机优先级为6，插入步骤如下。

首先按照标准的BST插入算法，不考虑优先级，如图3-41所示。

图　3-41

新结点违反了堆性质，它的优先级比它的父结点大。解决方法是进行一次左旋，如图3-42所示。

图　3-42

现在堆性质得到了满足，插入过程结束。一般情况下，Treap的插入操作分为BST插入和旋转两个部分。如果采用递归实现，则只需在递归插入之后判断是否需要旋转，代码如下。

基本逻辑是，如果x比o的值小，应当插入到左子树。插入完毕后，如果o的左子树的优先级比o高，就应该对o执行右旋操作，没有别的选择。x比o的值大的情况进行类似处理，旋转方向和插入位置相反。

删除时，首先找到待删除结点。如果它只有一棵子树，情况就简单了。直接用这棵子树代替这个待删除结点成为根即可（注意o是叶子的情况也符合这个条件）。麻烦的是o有两棵子树的情况，我们先把这两棵子树中优先级高的一棵旋转到根，然后递归在另一棵子树中删除结点o。比如，如果左子结点的优先级比较高，就必须右旋，否则会违反堆性质；如果右子结点优先级高，就必须左旋。代码如下。

有意思的是，删除和插入是对称的。事实上，前面那个例子从前往后看是插入，从后往前看是删除。二者的共同特点是“只有一种旋转方法”，所以即使不想记住上述代码，自己推导也不难。

Treap的所有难点都已经被攻破（怎么样，简单吧），请读者把它改写成一个“山寨set＜int＞”，并且和STL中的set＜int＞比一下速度[15]。注意，上面的代码没有处理“待插入值已经存在”和“待删除值不存在”这两种情况，因此在调用相应函数之前请进行一次查找，代码如下。

如果Treap只能用来仿写STL中的set方法，本书就不必用这么多篇幅介绍它，直接使用set即可。事实上，STL对set的过度封装使得一些用Treap可以实现的功能用STL的set却实现不了。这里仅举名次树（rank tree）一例。

在名次树中，每个结点均有一个附加域size，表示以它为根的子树的总结点数，如图3-43所示。

图　3-43

名次树可以支持两个新操作。

❑　Kth（k）：找出第k小元素。

❑　Rank（x）：值x的“名次”，即比x小的结点个数加1。

用Treap实现名次树的方法是这样的：给结点新增一个成员变量s（就是上述size域），然后编写一个maintain函数，方法如下。

注意这里并没有判断ch[0]和ch[1]是否为NULL，为什么呢？因为在实际编码中，为了减少出错的可能性，一般用一个真实的指针null代替空指针NULL，方法如下。

这样即使读写了null的内容也没有影响，因为null指向一个实际存在的东西。但是有一点需要注意，null的变量s必须始终为0，并且Treap相关代码中所有用到NULL的地方，包括初始化和比较，全部要改成null。当然，之前那种使用NULL的方法也完全可行，但maintain函数需要采用如下写法。

旋转操作也要修改。当s可能发生改变时，要调用maintain重新计算，方法如下。

了解了上述知识，实现kth和rank操作并非难事。细节留给读者思考（也可参考例题20的代码）。

例题20　图询问（Graph and Queries, Tianjin 2010, LA 5031）

有一张n个结点m条边的无向图，每个结点都有一个整数权值。你的任务是执行一系列操作。操作分为3种，如表3-5所示。

表　3-5

操作序列的结束标志为单个字母E。结点编号为1～n，边编号为1～m。

【输入格式】

输入第一行为两个整数n和m（1≤n≤20000，0≤m≤60000），以下n行每行有一个绝对值不超过10⁶的整数，即各结点的初始权值，以下m行每行有两个整数，即一条边的两个端点。接下来是各条指令，以单个字母E结尾。保证Q指令和C指令均不超过200000条。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出所有Q指令的计算结果的平均值，精确到小数点后6位。

【分析】

本题只需要设计离线算法，因此可以把操作顺序反过来处理，先读入所有操作，执行所有的D操作得到最终的图，然后按照逆序把这些边逐步插入，并在恰当的时机执行Q和C操作。用一棵名次树维护一个连通分量中的点权，则C操作对应于名次树中一次修改操作（可以用一次删除加一次插入来实现），Q操作对应Kth操作，而执行D操作时，如果两个端点已经是同一连通分量则无影响，否则将两个端点对应的两棵名次树合并。树合并的时间复杂度是多少呢？假设有两棵树T₁和T₂，结点数分别为n₁和n₂，若n₁＜n₂，显然把T₁合并到T₂里比较高效，即把T₁中的所有结点一一插入到T₂中，时间复杂度为O（n₁logn₂）。这样的策略叫启发式合并。如果使用启发式合并，对于任意结点来说，每当它被移动到新树中时，该结点所在树的大小至少加倍。由于树的结点数不超过n，任意结点至多“被移动”log₂n次，而每次移动需要O（logn）时间，因此总时间复杂度为O（nlog²n）。

完整代码如下。由于本题非常经典，强烈建议读者实现自己的程序，仅把下面的代码当做参考。

3.5.3　用伸展树实现可分裂与合并的序列

另一种常用的BST是伸展树。前面已经介绍了Treap了，为什么还要讲伸展树呢？因为它除了具有Treap的所有功能之外，还能快速地分裂与合并[16]。

伸展树最重要的操作是伸展操作，它把一个指定结点x自底向上逐步旋转到根结点。具体来说，这个旋转过程要分3种情况考虑。

情况一：x的父结点是根结点。这样进行一次单旋即可。如果它是父结点的左子结点，就右旋，否则左旋，如图3-44所示。

图　3-44

情况二：x的父结点不是根结点，且x、x的父结点、x父结点的父结点“三点共线”。这样需要两次相同方向的单旋（先旋转x的父结点，再旋转x），如图3-45所示。

图　3-45

情况三：剩下的情况是把情况二的“共线”改成“不共线”。这样需要两次方向相反的旋转（x旋转两次），如图3-46所示。

图　3-46

反复执行上述操作，直到x成为树根结点后，整个伸展过程结束。再次强调，伸展操作的作用就是把一个特定结点旋转到根结点。

前面提过，伸展树可以用来实现一种“可分裂与合并的序列”（以下简称“序列”），它支持以下3种操作。

❑　build（A）：把数组A转化为序列，返回这个序列

❑　merge（S₁，S₂）：把序列S₁和S₂连接在一起，S₁在左边，S₂在右边。返回新序列。

❑　split（S，k）：把序列S分裂成两个连续的子序列，左子序列包含左边k个元素，右子序列包含其余的元素。

为了支持这3种操作，我们需要像名次树那样增加一个附加信息s，然后在旋转代码中增加对maintain函数的调用。由于在很多地方都会用到“找到序列的左数第k个元素并伸展到根”这个过程，下面把查找和伸展过程写到一起，代码如下。

上述代码和前面讲过的3种情况对应，建议读者仔细阅读，看看二者是如何对应起来的。有了伸展操作，就不难实现分裂与合并了，其中分裂如图3-47（a）所示，合并如图3-47（b）所示。

图　3-47

在合并过程中，先把left中的最大元素伸展到根结点，则伸展之后的根结点一定没有右子树（想一想，为什么）。这时，只需要把right作为left的右子树即可。注意要重新计算left的附加信息。

在分裂过程中，直接把待分裂序列o的第k小元素伸展到根，然后断开树根及其右子树的链接关系，就得到了两个子序列。

可分裂合并序列的一个典型的应用是文本编辑器。由于需要在文本的任意位置插入和删除字符，链表与数组都是不合适的，因为链表无法快速定位，而数组无法实现快速插入和删除（将引起大量元素移动）。虽然在实践中更常用的方法是把二者组合起来，形成链式数组（将在第6章中讨论），但存在一个理论时间复杂度更优的解决方案，即把整个文本用一个可分裂合并的序列来表示，则插入对应一次split和两次merge，删除对应两次split和一次merge，如图3-48所示。

图　3-48

插入的时候先把待插入文本转化成序列mid，并且把当前文本分裂成left和right，然后合并left，mid，再与right合并。

删除的时候直接分裂两次，把当前文本分裂成left、mid和right，然后合并left和right，最后丢掉mid。

虽然单次伸展可能会比较慢，但可以证明平均情况下，伸展操作的时间复杂度是O（logn）的，实际效果也很好。

例题21　排列变换（Permutation Transformer, UVa 11922）

你的任务是根据m条指令改变排列｛1，2，3，…，n｝。每条指令（a，b）表示取出第a～b个元素，翻转后添加到排列的尾部。

【输入格式】

输入仅一组数据。第一行为整数n和m（1≤n，m≤100000）。以下m行每行为一条指令a，b（1≤a≤b≤n）。

【输出格式】

输出n行，即最终排列。

【分析】

用一个可分裂合并的序列来表示整个序列，则“截取出序列并放到序列末尾”的操作是不难实现的。如何实现“翻转”呢？根据前面线段树部分的经验，可以用标记来完成。给结点增加一个flip标记（表示以本结点为根的子树有没有旋转过），也可以写出与前面类似的pushdown函数，代码如下。

核心代码如下。

例题22　魔法珠宝（Jewel Magic, UVa 11996）

给定一个长度为n的01串，你的任务是依次执行如表3-6所示的m条指令。

表　3-6

在这里指令中，L代表字符串的当前长度。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤200000）。第二行为一个长度为n的01串。以下m行每行为一条指令。输入结束标志为文件结束符。输入文件大小不超过5MB。

【输出格式】

对于每个类型4操作，输出对应的结果。

【分析】

这是一个和LCP有关的问题。经过思考，我们发现后缀数组很难派上用场，因为修改操作太乱，很难维护后缀数组。前面讲过LCP可以用哈希法快速求出，本题是不是也可以用此方法呢？答案是肯定的。用一棵伸展树维护这个01序列，其中每个结点除了记录子树大小和flip标记之外还要记录该子树所对应连续序列的Hash值，及其反转序列的Hash值。根据Hash值的递推关系，仍然可以在O（1）时间内完成附加域的维护，插入和删除也可以快速完成（细节请读者思考），那LCP应如何计算呢？同样用二分法，用merge和split提取子树，然后比较Hash值是否相等。总时间复杂度为O（mlogn）。

需要注意的是，理论上Hash值相同的字符串不一定相同，因此即使程序完全正确地实现了本算法，答案也有可能出现错误。如果出现这种情况，可以修改Hash函数或者加长Hash值。当Hash函数足够好，Hash值足够长时，错误概率将降到足够小。

3.6　小结与习题

数据结构的实践性很强，下面直接给出本章的主要知识点和对应的例题、习题，供读者参考。

1．基础数据结构

如表3-7所示是本章中只用到基础数据结构的例题。

表　3-7

如表3-8所示是一些只用到基础数据结构的题目。它们并不简单，需要一些灵感和分析，但对思维和实践很有帮助。

表　3-8

破损的键盘（又名：悲剧文本）（Broken Keyboard （a.k.a. Beiju Text）, UVa 11988）

你有一个破损的键盘。键盘上的所有键都可以正常工作，但有时候Home键或者End键会自动按下。你并不知道键盘存在这一问题，而是专心地打稿子，甚至没有打开显示器。当你打开显示器之后，展现在你面前的是一段混乱的文本。你的任务是在打开显示器之前计算出这段混乱文本。

【输入格式】

输入包含多组数据。每组数据占一行，包含不超过100000个字母、下划线、字符“[”或者“]”。其中字符“[”表示Home键，“]”表示End键。输入结束标志为文件结束符（EOF）。输入文件大小不超过5MB。

【输出格式】

对于每组数据，输出一行，即屏幕上的混乱文本。

促销活动（Hoax or what, UVa 11136）

沃尔玛举行了一个促销活动，要求每个参加活动的顾客在小票上签字，然后放到一个特制的箱子里。活动第一天早上，这个箱子是空的。每天超市关门后，沃尔玛从箱子里取出购物金额最大和最小的两张小票，前者对应的顾客将得到沃尔玛提供的价值max-min的礼品，其中max和min分别为最大和最小的购物金额，然后把这两张小票扔掉（箱子里的其他小票留在箱子里）。你的任务是计算在整个活动中沃尔玛提供的奖品总价值。

【输入格式】

输入包含多组数据。每组数据第一行为活动天数n（1≤n≤5000）。接下来的n行每行描述一天所收到的所有小票，其中第一个整数k（0≤k≤10⁵）是小票个数，接下来的k个整数是各小票的金额。输入保证每天结束后，箱子里至少有两张小票。每张小票金额均为不超过10⁶的正整数，小票总个数不超过10⁶。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出活动期间沃尔玛提供的奖品总价值。

异或（Exclusive-OR, Wuhan 2009, LA 4487）

有n个小于2²⁰的非负整数X₀，X₁，...，Xn－1，但你并不知道它们的值。我会逐步提供一些信息，你的任务是根据这些信息回答问题。

有两种信息和一种问题如表3-9所示。

表　3-9

【输入格式】

输入最多包含10组数据。每组数据第一行为两个整数n和Q（1≤n≤20000，2≤Q≤40000）。以下Q行按顺序给出每条信息或者问题，其中参数k为不超过15的正整数，v是小于2²⁰的非负整数。信息和问题混在一起，在回答每个问题时只能依据那之前给出的信息。输入结束标志为n＝Q＝0。

【输出格式】

对于每组数据，输出测试数据编号，然后是各个问题的答案。如果某个问题的答案不能唯一确定，输出“I don't know.”；如果某条信息和之前的信息矛盾，输出“The first i facts are conflicting.”然后忽略之后所有的问题（但不要忽略这之前的问题）。每组数据之后输出一个空行。

几乎就是并查集（Almost Union-Find, UVa 11987）

你的任务是实现一个并查集的变种。它也是需要维护一些不相交集合，并且支持如表3-10所示3种操作。

表　3-10

初始时，一共有n个单元素集合：｛1｝，｛2｝，｛3｝，…，｛n｝。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤100000）。以下m行每行为一条指令（格式见题目，1≤p，q≤n）。输入结束标志为文件结束符（EOF）。输入大小不超过5MB。

【输出格式】

对于每条类型3的指令，输出p所在集合的元素个数。

2．区间信息维护

包括Fenwick树、线段树和RMQ问题。如表3-11所示的4道例题极为经典，建议读者做到熟练编写程序。

表　3-11

如表3-12所示是习题和翻译。这些题目多数都不难，建议读者一一完成。

表　3-12

电位计（Potentiometers, Dhaka 2006, LA 2191）

给定n个整数，你的任务是实现如表3-13所示的两种操作。

表　3-13

【输入格式】

输入最多包含两组数据。每组数据第一行为整数n（1＜＝n＜＝200000）。以下n行每行一个不超过1000的非负整数。接下来包含不超过200000个操作，以END结束。

【输出格式】

对于每组数据，输出测试数据编号和每个M操作的答案。

带循环移动的RMQ（RMQ with shifts, UVa 12299）

在传统的RMQ问题中，有一个不变的数组A，然后需要对每个询问（L，R）（L≤R），输出A[L]，A[L＋1]，…，A[R]中的最小值。

在本题中，A是可变的：我们还需要支持一种询问移动操作，即shift（i₁，i₂，…，ik），表示把元素A[i₁]，A[i₂]，…，A[ik]循环移动（1≤i₁＜i₂＜…＜ik≤n，k＞1）。例如，若A＝｛6，2，4，8，5，1，4｝，则执行shift（2，4，5，7）后得到｛6，8，4，5，4，1，2｝，再执行shift（1，2）后得到｛8，6，4，5，4，1，2｝。

【输入格式】

输入仅包含一组数据，第一行为两个整数n和q（1≤n≤100000，1≤q≤250000），即数组A中的元素个数和操作个数。第二行包含n个不超过100000的正整数，即数组A各个元素的初始值A[1]，A[2]，…，A[n]。以下q行每行为一个字符串格式的操作，长度不超过30，并且不含空白字符。所有操作保证合法。

【输出格式】

对于每个query操作，输出范围最小值（而不是下标）。

轮廓线（Skyline, Singapore 2007, LA 4108）

我们要在地平线（看成数轴）上依次建造n座建筑物。建筑物的修建按照从后往前的顺序，因此新建筑物可能会挡住一部分老建筑物。

修建完一座建筑物之后，统计它在多长的部分是最高的（可以和其他建筑物并列最高），并把这个长度称为该建筑物的“覆盖度”。如图3-49所示，假定最先修建的是灰色建筑物，然后是黑色，最后是白色，则三者的覆盖度依次为6，4，4。例如，白色建筑物在3～5，11～13是最高的，因此覆盖度为（5－3）＋（13－11）＝4。

图　3-49

【输入格式】

输入第一行为数据组数。每组数据第一行为建筑物个数n（1≤n＜100000）。以下n行按照先后顺序给出n座建筑物的左边界li，右边界ri和高度hi（0＜li＜ri≤100000）。

【输出格式】

对于每组数据，输出所有建筑物的总覆盖度（保证不超过2000000）。

排列（Permutation, UVa 11525）

给定整数n和k，输出1～k的所有排列中，按照字典序从小到大排序后的第n个（编号从0开始）。由于n可能很大，本题用k个整数S₁，S₂，...，Sk来间接给出n，方式如下。

【输入格式】

输入第一行为测试数据组数T（T≤10），每组数据第一行为一个整数k（1≤k≤50000），第二行包含k个整数Si（0≤Si≤k－i）。

【输出格式】

对于每组数据，输出满足条件的1～k的排列。

王国（Kingdom, Seoul 2009, LA 4730）

平面上有n个城市，初始时城市之间没有任何双向道路相连。你的任务是依次执行以下指令。

❑　road A B：在城市A和城市B之间连接一条双向道路，保证这条道路不和其他道路在非端点处相交。

❑　line C：询问一条y＝C的水平线和多少个州相交，以及这些州一共包含多少座城市。在任意时刻，每一组连通的城市形成一个州。在本指令中，C的小数部分保证为0.5。

例如，在图3-50中，y＝4.5穿过两个州，共8个城市；y＝6.5穿过一个州，共5个城市。

图　3-50

【输入格式】

输入第一行为数据组数T。每组数据第一行为一个整数n（1≤n≤100000）。以下n行每行两个整数x，y（0≤x，y≤1000000），即各城市坐标。城市编号为0～n－1。下一行为指令总数m（1≤m≤200000）。以下m行每行一条指令，其中A和B为不同的整数，且0≤A，B＜n；C是小数部分保证为0.5的实数，且0＜C＜1000000。不会有两条道路连接一对相同的城市。每组数据至少有一条line指令。

【输出格式】

对于每条line指令，输出y＝C穿过的州的数目和这些州包含的城市总数。

3．字符串算法

字符串算法很有使用价值，广泛的用在信息检索、文本处理、生物信息学等领域。本章涉及的Trie、KMP、AC自动机、后缀树／后缀数组、LCP等问题和算法是其中最为经典和实用的。如表3-14所示的8道例题道道经典，请读者认真学习。

表　3-14

如表3-15所示的习题也很经典，但其中有一些难度较大。

表　3-15

超级前缀集合（Hyper Prefix Sets, UVa 11488）

给定一个字符串集合S，定义P（S）为所有字符串的公共前缀长度与S中字符串个数的乘积。比如P（｛000，001，0011｝）＝6。给n个01串，从中选出一个集合S，使得P（S）最大。

【输入格式】

输入第一行为数据组数T（T≤20）。每组数据第一行为字符串总数n（n≤50000），以下n行每行为一个01串，长度不超过200。

【输出格式】

对于每组数据，输出P（S）的最大值。

破解密码（Password Suspects, World Finals 2008, LA 4126）

你想破解一个长度已知、仅由小写字母组成的密码。通过各种途径，你知道了它的一些连续子串（但不知道出现的位置。这些子串可能相互重叠）。比如，若密码长度为10，包含两个连续子串hello和world，则只有两种可能，即helloworld和worldhello。

你的程序应首先输出可能的密码个数，如果最多只有42种可能，则按照字典序输出所有可能的密码。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n≤25，0≤m≤10），即密码长度和已知的子串数量。以下m行每行一个长度不超过10的，仅由小写字母组成的字符串。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，首先输出可能的密码个数。如果个数不超过42，输出所有可能的密码。密码个数保证不超过10¹⁵。

L-空隙子串（L-Gap Substrings, UVa 10829）

如果一个字符串可以写成UVU的形式，其中U非空，V恰好包含L个字符，我们说这个字符串是一个L-Gap串。如abcbabc是个1-Gap串，xyxyxyxyxy既是2-Gap串，又是6-Gap串，但不是10-Gap串（因为U必须非空）。

给定字符串S和正整数g，你的任务是统计S的g-Gap连续子串的个数。相同的串在不同的位置出现算作不同的子串。

【输入格式】

输入第一行为测试数据组数t（t≤10），每组数据仅一行，为整数g（1≤g≤10）和字符串S。S由不超过50000个小写字母组成。

【输出格式】

对于每组数据，输出它的g-Gap子串的个数。

生成器（Generator, Hangzhou 2005, LA3490）

从空串开始，我们可以不断地从前n个大写字母中随机选出字母，然后加到串的末尾，直到这个串包含一个给定的连续子串时停止。假定每次随机都是均匀且独立的，求停止时字符串长度的数学期望。比如，若n＝2，出现连续子串ABA时停止，则字符串长度的数学期望为5。

【输入格式】

输入第一行为测试数据组数t（t≤10）。每组数据仅一行，包含整数n（1≤n≤26）和一个长度不超过12的字符串。该字符串中的所有字符均为前n个大写字母。

【输出格式】

对于每组数据，输出字符串长度的数学期望。

魔法阵（Casting Spells, CERC 2010, LA 4975）

强大的魔法阵一般都拥有一个十分拗口的名字，比如abrahellehhelleh。研究发现，一个魔法阵的威力正比于名字中形如wwRwwR的连续子串的最大长度（wR是指将w反着写以后得到的串）。比如，abrahellehhelleh的威力值为12，因为hellehhelleh就是一个满足条件的连续子串（其中w＝hel）。注意，根据定义，魔法阵的威力值总是4的倍数。

【输入格式】

输入第一行为数据组数T（T≤40）。每组数据包含一个长度不超过3×10⁵的字符串，仅包含大小写字母。

【输出格式】

对于每组数据，输出以该字符串命名的魔法阵的威力值。

4．排序二叉树

需要直接用到排序二叉树的题目并不多（有一些特定算法会用到，但超出了本章的范围，见第6章），如表3-16所示的4道例题堪称经典。

表　3-16

下面是习题和翻译，要么和二叉树本身相关，要么需要用二叉树求解。其中一些题目有不止一种做法（比如，还可以用线段树或者第6章中介绍的分块表），但用排序二叉树的方法往往最简洁、效率也最高，如表3-17所示。

表　3-17

排序二叉树（Binary Search Tree, Daejon 2010, LA 4847）

给定一个1～n的排列P，首先把它们依次插入到一棵空的排序二叉树中。你的任务是统计有多少个排列（包括P本身）插入到空排序二叉树中，将得到一棵完全相同的排序二叉树，如图3-51所示。

图　3-51

在图3-51（a）中插入结点80，将得到图3-51（b）中的排序二叉树。插入树后能得到图3-51（c）的排列有8种，比如2，1，4，3，5或者2，4，3，1，5。

【输入格式】

输入第一行为数据组数T。每组数据第一行为整数n（1≤n≤20），第二行为排列P。

【输出格式】

对于每组数据，输出排列总数除以9999991的余数。

西部枪手（Wild West, CERC 2005, LA 3525）

有n个已知三元组（xi，yi，zi），你的任务是统计有多少个三元组（x，y，z）满足：对于任意已知三元组（xi，yi，zi），要么x＞xi，要么y＞yi，要么z＞zi（可以有多个条件同时满足）。上述所有3元组的三个分量都是1～m的正整数。比如，若m＝1，已知一个三元组（2，2，2），有19个三元组满足条件。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n≤100000，2≤m≤100000）。以下n行每行包含3个整数xi，yi，zi（1≤xi，yi，zi≤m）。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出满足条件的三元组个数。输出保证不超过10¹⁸。

机器排序（Robotic Sorting, CERC 2007, LA3961）

一个实验室里有n个长短不一的试管。你的任务是编写一段程序，用机器臂把它们按照高度从小到大的顺序排列。对于高度相同的试管，排序前后的相对位置应保持不变。排序方法如图3-52所示。

图　3-52

排序需要n次操作，其中第i次操作是“反转”序列i～Pi，其中Pi是目标状态中第i个试管当前所在的位置。比如，在上图中，初始时P₁＝4，因此反转试管1～4就能把最左边的试管归位。类似地，第2次操作前P₂＝6，因此反转试管2～6就能把左数第2个试管归位。

你的任务是输出P₁，P₂，…，Pn的值，以便控制机器臂移动。注意i＝Pi时实际上不需要反转，但仍然需要输出Pi。

【输入格式】

输入包含多组数据。每组数据第一行为试管个数n（1≤n≤100000），第二行从左到右依次为各个试管的高度。

【输出格式】

对于每组数据，输出一行，依次为P₁，P₂，…，Pn。

防线（Defense Lines, CERC 2010, LA 4976）

给出一个序列，你的任务是删除一个连续子序列，使剩下的序列中有一个长度最大的连续递增子序列。比如将序列｛5，3，4，9，2，8，6，7，1｝中的｛9，2，8｝删除，得到的序列｛5，3，4，6，7，1｝包含一个长度为4的连续递增子序列｛3，4，6，7｝。

【输入格式】

输入第一行为数据组数T（T≤25）。每组数据第一行为序列长度n（n≤200000），第二行为序列本身（每个数均为不超过10⁹的正整数）。

【输出格式】

对于每组数据，输出最长连续递增子序列的长度。

内存分配（Heap Manager, UVa 12419）

内存以内存单元为基本单位，每个内存单元用一个固定的整数作为标识，称为地址。地址从0开始连续排列，地址相邻的内存单元被认为是逻辑上连续的。我们把从地址i开始的s个连续的内存单元称为首地址为i长度为s的地址片。

运行过程中有若干进程需要占用内存，对于每个进程有一个申请时刻t，需要内存单元数m及运行时间p。在运行时间p内（即t时刻开始，t＋p时刻结束），这m个被占用的内存单元不能再被其他进程使用。

假设在t时刻有一个进程申请m个单元，且运行时间为p，则：

（1）若t时刻内存中存在长度为m的空闲地址片，则系统将这m个空闲单元分配给该进程。若存在多个长度为m的空闲地址片，则系统将首地址最小的那个空闲地址片分配给该进程。

（2）如果t时刻不存在长度为m的空闲地址片，则该进程被放入一个等待队列。对于处于等待队列队头的进程，只要在任一时刻，存在长度为m的空闲地址片，系统马上将该进程取出队列，并为它分配内存单元。注意，在进行内存分配处理过程中，处于等待队列队头的进程的处理优先级最高，队列中的其他进程不能先于队头进程被处理。

现在给出一些描述进程的数据，请编写一程序模拟系统分配内存的过程。

【输入格式】

输入包含多组数据。每组数据第一行为3个整数n，b（10≤n≤10⁹，0≤b≤1），表示一共有n个内存单元。b＝1表示需要输出详细事件，b＝0表示不需要。接下来有不超过200000行，每行为3个整数t，m，p（m≤n，0≤t≤10⁹，0＜p≤10⁹），描述一个进程。进程按照t从小到大排列，不同进程的t保证不同。t＝m＝p为数据结构标记，整个输入结束标记为文件结束符（EOF）。输入总大小不超过10MB。

【输出格式】

对于每组数据，如果b＝1，先输出详细事件列表，按照先后顺序排列。每个事件格式为T i a，表示时刻T时，进程i（按照输入顺序从1开始编号）分配到了以第a为首地址的地址片。不管有没有输出事件列表，最后两行应各输出一个整数，分别为所有进程执行完毕的时刻，以及曾经被放入过等待队列的进程个数。

注释

[1]　俗称树状数组，或者Fenwick树，因为作者是Fenwick。

[2]　在代码中并不需要储存这些边。画出来仅仅是为了在概念上能更好地理解BIT。

[3]　RMQ问题可以做到O（n）预处理，O（1）查询，但有些麻烦，这里不进行介绍。

[4]　据笔者所知，这个数据结构在学术界并没有统一的术语，但线段树是最常见的叫法。其他叫法包括区间树（interval tree）、范围树（range tree）等，但这些属于在特定的场合（如计算几何）中有着特殊含义。本书中只采用“线段树”这一名称。

[5]　这样说其实并不准确。但我们很快就会看到正确的说法。

[6]　这并不会引起误会，因为任何结点的子结点都不可能是根结点。

[7]　注意，尽管最坏情况下朴素匹配算法表现不佳，但实际上对于随机数据，它的表现非常好，这一点希望读者注意。

[8]　如果你像本书这样采用C字符数组的方式保存字符串，也可以表示“正在匹配第i号字符”。

[9]　这是MP算法。KMP算法还需要对失配函数进行优化。

[10]　修改以后的函数还叫print就不合适了。如果你在代码中对这个函数进行了改名，别忘了在调用这个函数的地方也进行修改。

[11]　这里当然也可以用一棵Trie，但没有必要。另一个方法是用repr[i]表示所有与第i个模板相同的模板中，编号的最小值。

[12]　这里用每个后缀的编号代表了后缀本身。

[13]　限于篇幅，这里不介绍基数排序，请读者自行参考相关资料。

[14]　其实还有一个不用清空flag数组，也不用每次重新统计flagh[x]＝1的个数的方法，留给读者思考。提示：别让flag[x]只保存01值。

[15]　如果你的代码比STL的set＜int＞快，也千万不要得意，因为你的代码可能并不鲁棒，缺少错误处理，或者缺少一些细小的功能。这个比较只是让你对Treap的性能有一个直观的认识。

[16]　其实Treap也能很快地分裂与合并，但实践中伸展树用的更多。

第4章　几何问题

几何问题是高水平算法竞赛中不可或缺的题型。由于背景知识多，内容杂乱，因此《算法竞赛入门经典》几乎没有涉及真正意义上的几何问题。本章通过介绍一些几何中的常见问题和算法，力图让读者具备一定的几何解题能力，并感受到几何的美。

4.1　二维几何基础

简单地说，向量（vector）就是有大小和方向的量，如速度、位移等物理量都是向量。向量最基本的运算是加法，满足平行四边形法则，如图4-1所示。

图　4-1

在平面坐标系下，向量和点一样，也用两个数x、y表示。它等于向量的起点到终点的位移，也相当于把起点平移到坐标原点后，终点的坐标。尽管如此，请读者不要在概念上把点和向量弄混。比如，点－点＝向量，向量＋向量＝向量，点＋向量＝点，而点＋点是没有意义的。在第6章中，我们会介绍齐次坐标的概念，从而在程序上区分开点和向量，但在本章中，点和向量都用两个数x、y表示。

下面是它们的常用定义。

注意上面的“相等”函数用到了“三态函数”dcmp，减少了精度问题。另外，向量有一个所谓的“极角”，即从x轴正半轴旋转到该向量方向所需要的角度。C标准库里的atan2函数就是用来求极角的，如向量（x，y）的极角就是atan2（y，x）（单位：弧度）。

4.1.1　基本运算

点积。两个向量v和w的点积等于二者长度的乘积再乘上它们夹角的余弦。如图4-2所示这里的θ是指从v到w逆时针旋转的角，因此当夹角大于90°时点积为负。

图　4-2

余弦是偶函数，因此点积满足交换率。如果两向量垂直，点积等于0。不难推导出：在平面坐标系下，两个向量OA和OB的点积等于xAxB＋yAyB。下面是点积计算方法，以及利用点积计算向量长度和夹角的函数。

叉积。简单地说，两个向量v和w的叉积等于v和w组成的三角形的有向面积的两倍。什么叫“有向面积”呢（见图4-3）？

图　4-3

顺着第一个向量v看，如果w在左边，那么v和w的叉积大于0，否则小于0。如果两个向量共线（方向相同），那么叉积等于0（三角形退化成线段）。不难发现，叉积不满足交换率。事实上，cross（v，w）＝-cross（w，v）。在坐标系下，两个向量OA和OB的叉积等于xAyB－xByA。代码如下。

两个向量的位置关系。把叉积和点积组合到一起，我们可以更细致地判断两个向量的位置关系。如图4-4所示，括号里的第一个数是点积的符号，第二个数是叉积的符号，第一个向量v总是水平向右，另一个向量w的各种情况都包含在了图中。比如，当w的终点在下图左上方的第二象限时点积为负但叉积均为正，用（-，+）表示。

图　4-4

向量旋转。向量可以绕起点旋转，公式为x'＝xcosaysina，y'＝xsina＋ycosa。其中a为逆时针旋转的角。代码如下。

作为特殊情况，下面的函数计算向量的单位法线，即左转90°以后把长度归一化。

基于复数的几何计算。在本节结束前，笔者还想介绍另外一种实现点和向量的方法，即使用C++里的复数。

这样定义之后，我们自动拥有了构造函数、加减法和数量积。用real（p）和imag（p）访问实部和虚部，conj（p）返回共轭复数，即conj（a＋bi）＝a－bi。相关函数如下。

上述函数的效率不是很高，但是相当方便、好记。

4.1.2　点和直线

直线的参数表示。直线可以用直线上一点P₀和方向向量v表示（虽然这个向量的大小没什么用处）。直线上所有点P满足P＝P₀＋tv，其中t称为参数。如果已知直线上的两个不同点A和B，则方向向量为B－A，所以参数方程为A＋（B－A）t。

参数方程最方便的地方在于直线、射线和线段的方程形式是一样的，区别仅仅在于参数。直线的t没有范围限制，射线的t＞0，线段的t在0～1之间。这样，很多对于直线适用的公式可以很方便地用在射线和线段上。

直线交点。下面介绍参数方程下的直线交点公式。设直线分别为P＋tv和Q＋tw，设向量u＝QP，交点在第一条直线的参数为t₁，第二条直线上的参数为t₂，则x和y坐标可以列出一个方程，解得（过程略）

代码如下。

需要提醒读者注意的是，从上述公式可以看到，如果P，v，Q，w的各个分量均为有理数，则交点坐标也是有理数。在精度要求极高的情况下，可以考虑自定义分数类。

点到直线的距离。点到直线的距离是一个常用的函数，可以用叉积算出，即用平行四边形的面积除以底，代码如下。

点到线段的距离。点到线段的距离有两种可能，如图4-5所示。

图　4-5

简单地说，设投影点为Q，如果Q在线段AB上，则所求距离就是P点直线AB的距离（左图）；如果Q在射线BA上，则所求距离为QA距离；否则为QB距离。判断Q的位置可以用点积进行（还记得那张四个象限的图吗）。

还有其他方法可以求出上述距离，但不仅速度慢，而且也更容易产生精度误差。

点在直线上的投影。虽然上面的运算避免了求出P在直线AB上的投影点Q，但有时候还是需要把它求出来的。为此，我们把直线AB写成参数式A＋tv（v为向量AB），并且设Q的参数为t₀，那么Q＝A＋t₀v。根据PQ垂直于AB，两个向量的点积应该为0，因此Dot（v，P－（A＋t₀v））＝0。根据分配率，有：Dot（v，P－A）－ t₀×Dot（v，v）＝0，这样就可以解出t₀，从而得到Q点。

线段相交判定。最后介绍线段相交判定，即给定两条线段，判断是否相交。我们定义“规范相交”为两线段恰好有一个公共点，且不在任何一条线段的端点[1]。线段规范相交的充要条件是：每条线段的两个端点都在另一条线段的两侧（这里的“两侧”是指叉积的符号不同）。代码如下。

如果允许在端点处相交，情况就比较复杂了：如果c1和c2都是0，表示两线段共线，这时可能会有部分重叠的情况；如果c1和c2不都是0，则只有一种相交方法，即某个端点在另外一条线段上。为了判断上述情况是否发生，还需要如下一段判断一个点是否在一条线段上（不包含端点）的代码[2]。

4.1.3　多边形

如何计算多边形的有向面积？如果多边形是凸的，可以从第一个顶点出发把凸多边形分成n－2个三角形，然后把面积加起来，代码如下。

其实这个方法对非凸多边形也适用。由于三角形面积是有向的，在外面的部分可以正负抵消掉。实际上，可以以任意点出发进行划分，如图4-6所示。

图　4-6

可以取p[0]点为划分顶点，一方面可以少算两个叉积（0和任意向量的叉积都等于0），另一方面也减少乘法溢出的可能性，还不用特殊处理（i＝n－1的时候，下一个顶点是p[0]而不是p[n]，因为p[n]不存在）。代码如下。

也可以取坐标原点为划分点，乘法次数减少，代码留给读者编写。

4.1.4　例题选讲

例题1　Morley定理（Morley's Theorem, UVa 11178）

Morley定理是这样的：作三角形ABC每个内角的三等分线，相交成三角形DEF，则DEF是等边三角形，如图4-7所示。

图　4-7

如图4-7所示，你的任务是根据A、B、C3个点的位置确定D、E、F 3个点的位置。

【输入格式】

输入第一行为测试数据组数T（T≤5000）。每组数据包含一行6个整数xA，yA，xB，yB，xC，yC，即A、B、C3个点的坐标。输入保证三角形ABC的面积非0。所有坐标为不超过1000的非负整数。A、B、C按照逆时针顺序排列。

【输出格式】

对于每组数据，输出6个整数xD，yD，xE，yE，xF，yF。

【分析】

本题没有什么算法可言，主要是根据题意计算，作为练习。考虑到对称性，只需要知道如何求D点即可。首先需要计算∠ABC的值a，然后把射线BC逆时针旋转a/3，得到直线BD。同理可以得到直线CD，求交点即可。代码如下。

主程序代码很简单，如下所示。

例题2　好看的一笔画（That Nice Euler Circuits, Shanghai 2004, LA 3263）

平面上有一个包含n个端点的一笔画，第n个端点总是和第一个端点重合，因此图案是一条闭合曲线。组成一笔画的线段可以相交，但是不会部分重叠，如图4-8所示。求这些线段将平面分成多少部分（包括封闭区域和无限大区域）。

图　4-8

【输入格式】

输入包含多组数据。每组数据第一行为n（4≤n≤300），第二行为n对整数，依次为一笔画上各顶点的坐标（均为绝对值不超过300的整数）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出平面被分成的区域数。

【分析】

若是要直接找出所有区域，会非常麻烦，而且容易出错。但用欧拉定理可以将问题进行转化，使解法更容易。

欧拉定理：设平面图的顶点数、边数和面数分别为V，E和F，则V＋F－E＝2。

这样，只需求出顶点数V和边数E，就可以求出F＝E＋2－V。

该平面图的结点由两部分组成，即原来的结点和新增的结点。由于可能出现三线共点，需要删除重复的点。代码如下。

例题3　狗的距离（Dog Distance, UVa 11796）

甲和乙两条狗分别沿着一条折线奔跑。两只狗的速度未知，但已知它们同时出发，同时到达，并且都是匀速奔跑。你的任务是求出甲和乙在奔跑过程中的最远距离与最近距离之差。

【输入格式】

输入第一行为数据组数I（I≤1000）。每组数据第一行为整数A和B，分别为甲和乙线路上的顶点数。第二行用2×A个整数描述甲的路线，即X₁，Y₁，X₂，Y₂，…，XA，YA。其中（X₁，Y₁）和（XA，YA）分别是路线的起点和终点。第三行用2×B个整数描述乙的路线，格式同上。

【输出格式】

对于每组数据，输出所求值，四舍五入到最近的整数。

【分析】

先来看一种简单的情况：甲和乙的路线都是一条线段。因为运动是相对的，因此也可以认为甲静止不动，乙自己沿着直线走，因此问题转化为求点到线段的最小或最大距离。

有了简化版的分析，只需模拟整个过程。设现在甲的位置在Pa，刚经过编号为Sa的拐点；乙的位置是Pb，刚经过编号为Sb的拐点，则我们只需要计算他俩谁先到拐点，那么在这个时间点之前的问题就是我们刚才讨论过的“简化版”。求解完毕之后，需要更新甲乙的位置，如果正好到达下一个拐点，还要更新Sa和／或Sb，然后继续模拟。因为每次至少有一条狗到达拐点，所以子问题的求解次数不超过A＋B。代码如下。

4.1.5　二维几何小结

至此，二维几何基础已经告一段落了。内容很散乱，因此有必要小结为表4-1。

表　4-1

4.2　与圆和球有关的计算问题

4.2.1　圆的相关计算

圆上任意一点拥有唯一的圆心角，所以在定义圆的时候，可以加一个通过圆心角求坐标的函数，代码如下。

直线和圆的交点。假定直线为AB，圆的圆心为C，半径为r。第一种方法是解方程组。设交点为P＝A＋t（B－A），代入圆方程整理后先得到（at＋b）²＋（ct＋d）²＝r²，进一步整理后得到一元二次方程et²＋ft＋g＝0。根据判别式的值可以区分3种情况，即无交点（相离）、一个交点（相切）和两个交点（相交）。代码如下（变量a，b，c，d，e，f，g对应于上述方程中的字母）。

函数返回的是交点的个数，参数sol存放的是交点本身。注意上述代码并没有清空sol，这给很多题目带来方便：可以反复调用这个函数，把所有交点放在一个sol里。

另一种方法是几何法，即先求C在AB上的投影p，再求向量AB对应的单位向量v，则两个交点分别为p－Lv和p＋Lv，其中L为p到交点的距离（p到两个交点等距），可以由勾股定理算出，如图4-9所示，代码略。

图　4-9

两圆相交。假定圆心分别为C₁和C₂，半径为r₁和r₂，圆心距为d，根据余弦定理可以算出C₁C₂到C₁P₁的角da，根据向量C₁C₂的极角a，加减da就可以得到C₁P₁和C₁P₂的极角。有了极角，就可以很方便地计算出P₁和P₂的坐标了，如图4-10所示。

图　4-10

计算向量极角的方法如下。

两圆相交的代码如下。

过定点作圆的切线。先求出pq的距离和pc的夹角ang，则向量pc的极角加减ang就是两条切线的极角。注意切线不存在和只有一条的情况，如图4-11所示。

图　4-11

两圆的公切线。根据两圆的圆心距从小到大排列，一共有6种情况。

情况一：两圆完全重合。有无数条公切线。

情况二：两圆内含，没有公共点。没有公切线。

情况三：两圆内切。有1条外公切线。

情况四：两圆相交。有2条外公切线。

情况五：两圆外切。有3条公切线，其中一条内公切线，两条外公切线。

情况六：两圆相离。有4条公切线，其中内公切线两条，外公切线两条。

可以根据圆心距和半径的关系辨别出这6种情况，然后逐一求解。情况一和情况二没什么需要求的，情况三和情况五中的内公切线都对应于“过圆上一点求圆的切线”，只需连接圆心和切点，旋转90°后即可知道切线的方向向量。这样，问题的关键是求出情况四、五中的外公切线和情况六中的内公切线。

先考虑情况六中的内公切线，它对应于两圆相离的情况，如图4-12所示。

图　4-12

根据三角函数定义不难求出角度a，然后和前面一样通过极角进行计算即可。

外公切线类似。假定r1≥r2，不管两圆是相离还是相交，a的余弦值都是（r1－r2）/d。剩下的过程又和前面一样了，如图4-13所示。代码如下[3]。

图　4-13

例题4　二维几何110合一！（2D Geometry 110 in 1!, UVa 12304）

这是一个拥有110₂个子问题的2D几何问题集。

❑　CircumscribedCircle x1 y1 x2 y2 x3 y3：求三角形（x1，y1）－（x2，y2）－（x3，y3）的外接圆。这3点保证不共线。答案应格式化成（x，y，r），表示圆心为（x，y），半径为r。

❑　InscribedCircle x1 y1 x2 y2 x3 y3：求三角形（x1，y1）－（x2，y2）－（x3，y3）的内切圆。这3点保证不共线。答案应格式化成（x，y，r），表示圆心为（x，y），半径为r。

❑　TangentLineThroughPoint xc yc r xp yp：给定一个圆心在（xc，yc），半径为r的圆，求过点（xp，yp）并且和这个圆相切的所有切线。每条切线格式化为angle，表示直线的极角（角度，0≤angle＜180）。整个答案应格式化为列表（见后）。如果无解，应打印空列表。

❑　CircleThroughAPointAndTangentToALineWithRadius xp yp x1 y1 x2 y2 r：求出所有经过点（xp，yp）并且和直线（x1，y1）－（x2，y2）相切的半径为r的圆。每个圆格式化为（x，y），因为半径已经给定。整个答案应格式化为列表（同上）。

❑　CircleTangentToTwoLinesWithRadius x1 y1 x2 y2 x3 y3 x4 y4 r：给出两条不平行直线（x1，y1）－（x2，y2）和（x3，y3）－（x4，y4），求所有半径为r并且同时和这两条直线相切的圆。每个圆格式化为（x，y），因为半径已经给定。整个答案应格式化为列表（同上）。

❑　CircleTangentToTwoDisjointCirclesWithRadius x1 y1 r1 x2 y2 r2 r：给定两个相离的圆（x1，y1，r1）和（x2，y2，r2），求出所有和这两个圆外切的，半径为r的圆。注意，因为是外切，求出的圆不能把这两个给定圆包含在内部。每个圆格式化为（x，y），因为半径已经给定。整个答案应格式化为列表（同上）。

对于上述所有直线，输入的两个点保证不重合。当格式化实数列表时，所有数应从小到大排列；当格式化二元组（x，y）时，先按x从小到大排序，当x相同时按y从小到大排序。

【输入格式】

输入包含不超过1000个子问题，每个占一行，格式同题目描述。所有坐标均为绝对值不超过1000的整数。输入结束标志为文件结束符（EOF）。

【输出格式】

对于输入的每行，按题目要求格式化输出。每个实数保留小数点后6位。列表用方括号，元组用圆括号。每行的输出中不应有空白符。

【分析】

本题的6个子问题相互独立，可以逐一求解。前两个问题可以用前面介绍的基本二维几何工具计算，也可以直接使用下面的代码。

第三个问题在前面已经叙述过，注意在得到向量以后要计算极角。

第四个问题解法如下：因为已知半径为r，所以要想和直线L相切，圆心到直线的距离一定为r。满足这个条件的点的轨迹是两条直线。而要想过定点，圆心点该点的距离一定为r，满足这个条件的点的轨迹是一个圆。求出圆和这两条直线的交点即可。

第五个问题解法类似，根据每条直线得到两条新直线，再两两求交点即可。

第六个问题解法也类似，根据两个圆得到两个新的圆，求交点即可。

需要特别注意的是精度问题，普通的误差不会影响到输出（只需要精确到小数点后6位），但第四个子问题比较特殊：圆和直线相切的判定很容易受到误差影响，因为这里的直线是计算出来的，而不是题目中输入的。解决方法是特殊判断圆心到输入直线的距离，当然也可以通过调整eps的数值来允许一定的误差[4]。

例题5　圆盘问题（Viva Confetti, Kanazawa 2002, LA 2572）

我已经把n个圆盘依次放到了桌面上。现按照放置顺序依次给出各个圆盘的圆心位置和半径，问最后有多少圆盘可见？如图4-14所示。

图　4-14

【输入格式】

输入包含多组数据，每组数据第一行为圆盘数n（1≤n≤100），以下n行每行3个实数x，y和r，按照放置顺序（即第一个圆盘是最底部的，最后一个圆盘是最顶部的）给出各个圆盘的圆心坐标和半径。保证在对输入数据进行微小扰动后答案不变。

【输出格式】

对于每组数据，输出可见圆盘的个数。

【分析】

题目说“保证在对输入数据进行微小扰动后答案不变”，意味着圆盘的可见部分不会太小。不难发现，每个可见部分都是由一些“小圆弧”围成的，因此可以先求出所有小圆弧，然后判断每段小圆弧是否可见[5]。小圆弧可见，意味着它所在的圆是可见的。接下来，对于所有可见的小圆弧，看看这段圆弧中点的正下方都有哪些圆盘，则其中最顶部的圆盘也是可见的（想一想，为什么）。

如何求出所有小圆弧？所有圆两两求交点，则每个圆上任两个相邻交点之间的圆弧就是所求。需要注意的是，如果一个圆不和其他所有圆相交，则整个圆是一条所求圆弧。

4.2.2　球面相关问题

经纬度转换为空间坐标。经线圈（范围：-180°～180°）和纬线圈（范围：0°～360°）的概念相信大家并不陌生，但如何把经纬度转化为对于球心的空间坐标呢？可以先算出z坐标：用半径乘以sin（纬度）就可以了，然后乘以cos（纬度），投影到x，y平面，再按照平面上的方法乘以经度的正余弦，得到x和y坐标。即

球面距离。已知球面两点，如何求出它们的最短路？注意，只能沿着球面走，不能穿过球的内部。从表面走的话，最近的路径是走圆弧，具体来说是走大圆（Great Circle）圆弧。用一个穿过球心的平面截这个球，截面就是一个大圆。怎么求大圆弧长呢？你无需想象那个大圆的空间位置，而可以把它们想象成一个平面问题：求半径为r，弦长为d的圆弧长度。如图4-15所示，圆心角为2arcsin（d/2r），因此弧长为2arcsin（d/2r）r。

图　4-15

4.3　二维几何常用算法

与直线、多边形相关的问题有很多。本节介绍点与多边形的位置关系、直线切割多边形、凸包、半平面交等基本问题。

4.3.1　点在多边形内判定

直观地讲，一个多边形就是二维平面上被一系列首尾相接、闭合的折线段围成的区域，在程序中一般用顶点数组表示，其中各个顶点按照逆时针顺序排列。

给定一个多边形和一个点，如何判断该点是否在多边形内？主要有两种方法。一是射线法，即判断穿越数（Crossing Number）。具体来说，就是从某个判定点出发，任意引一条射线。如果和边界相交奇数次，说明点在多边形内；如果相交偶数次，说明点在多边形外。注意射线如果在端点处和多边形相交，或者穿过一条完整的边，则需要重新引一条射线[6]。

第二种方法是转角法，基本思想就是看多边形相对于这个点转了多少度，如图4-16所示。具体来说，我们把多边形每条边的转角加起来，如果是360°，说明在多边形内；如果是0°，说明在多边形外；如果是180°，说明在多边形的边界上（想一想，为什么）。

图　4-16

这个方法比射线法更方便，因为如果多边形的一些边是弧形的，转角法丝毫不受影响，只需要把每一段的终点到起点的转角累加起来即可。另外，这个多边形甚至可以不是简单多边形（即可以自交）。

如果直接按照定义实现，需要计算大量的反三角函数，不仅速度慢，而且容易产生精度误差。算法竞赛中我们通常不这样做，而是假想有一条向右的射线，统计多边形穿过这条射线正反多少次，把这个数记为绕数wn（Winding Number），逆时针穿过时，wn加1，顺时针穿过时，wn减1。

注意在程序实现的时候，判断是否穿过，以及穿过方向时，需要用叉积判断输入点在边的左边还是右边。代码如下。

点在凸多边形内的判定更简单，只需要判断是否在所有边的左边（假设各顶点按照逆时针顺序排列）即可。

4.3.2　凸包

顾名思义，凸包就是把给定点包围在内部的、面积最小的凸多边形，它在计算几何中有着极其重要的作用。如图4-17所示就是点集的凸包。

图　4-17

在网上很容易找到很多关于凸包算法的资料，这里仅简单叙述一下基于水平序的Andrew算法[7]。首先把所有点按照x从小到大排序（如果x相同，按照y从小到大排序），删除重复点后得到序列p₁，p₂，…，然后把p₁和p₂放到凸包中。从p₃开始，当新点在凸包“前进”方向的左边时继续，否则依次删除最近加入凸包的点，直到新点在左边。

如图4-18所示，新点P₁₈在向量P₁₀P₁₅（即当前“前进方向”）的右边，因此需要从凸包上删除P₁₅和P₁₀，让P₈的下一个点为P₁₈。重复这个过程，直到碰到最右边的pn，就求出了“下凸包”。然后反过来从pn开始再做一次，求出“上凸包”，合并起来就是完整的凸包。

图　4-18

这个算法在排序后仅仅是从左到右和从右到左各扫描了一次，时间复杂度为O（n）。加上排序后时间复杂度也仅为O（nlogn）。

例题6　包装木板（Board Wrapping, UVa 10652）

有n块矩形木板，你的任务是用一个面积尽量小的凸多边形把它们包起来，并计算出木板占整个包装面积的百分比，如图4-19所示。

图　4-19

【输入格式】

输入第一行为数据组数T（T≤50）。每组数据第一行为木板个数n（2≤n≤600）。以下n行每行5个实数x，y，w，h，j（0≤x，y，w，h≤10000，-90≤j≤90），其中（x，y）是木板中心的坐标，w是宽，h是高，j是顺时针旋转的角度（j＝0表示不旋转，此时长度为w的那条边应该是水平方向）。木板保证互不相交。

【输出格式】

对于每组数据，输出木板总面积占包装面积的百分比，保留小数点后1位。

【分析】

学过凸包之后，本题应当是不难解决的。每块木板的4个点都作为输入，求出的凸包就是包装。代码如下。

例题7　飞机场（Airport, UVa 11168）

给出平面上n个点，找一条直线，使得所有点在直线的同侧（也可以在直线上），且到直线的距离之和尽量小。

【输入格式】

输入第一行为测试数据组数T（T≤65）。每组数据第一行为正整数n（n≤10000），以下n行分别包含一个点的坐标。所有坐标均为绝对值不超过80000的整数。

【输出格式】

对于每组数据，输出距离之和的最小值。

【分析】

要求所有点在直线同侧，因此直线不能穿过凸包。不难发现，选择凸包的边所在的直线，比选择和凸包相离的直线更划算。由于凸包上的边不超过n条，如果可以在O（1）时间内求出每条边对应的总距离，就能在O（n）时间内解决本题。

如何在O（1）时间内求出总距离呢？似乎用向量的方式思考有些困难。我们改用解析几何的思路，设直线的一般式方程为Ax＋By＋C＝0，则点（x₀，y₀）到该直线的距离为

注意由于所有点在Ax＋By＋C＝0的同侧，所有Ax₀＋By₀＋C的正负号相同。这样，我们预处理算出所有点的x坐标和y坐标之和，就可以在O（1）时间内算出总距离了。

现在只剩下一个问题了：如何把直线的两点式转化为一般式？这个问题留给读者思考。

例题8　点集划分（The Great Divide, UVa 10256）

平面上有n个红点和m个蓝点，是否存在一条直线，使得任取一个红点和一个蓝点都在直线的异侧？这条直线不能穿过红点或者蓝点。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤500）。以下n行每行两个整数x和y（-1000≤x，y≤1000），即各红点坐标。以下m行为各蓝点坐标，格式同红点。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，如果直线存在，输出Yes，否则输出No。

【分析】

先求红点的凸包和蓝点的凸包，则分离两个点集的充要条件是分离两个凸包。进一步可以发现，两个凸包可分离的充要条件是两个凸包的边界和内部没有公共部分（哪怕是一个点也不行）。这只需判断两件事：

（1）任取红凸包上的一条线段和蓝凸包上的一条线段，判断二者是否相交。如果相交（不一定是规范相交。有公共点就算相交），则无解。

（2）任取一个红点，判断是否在蓝凸包的内部。如果是，则无解。类似地，任取一个蓝点，判断是否在红凸包的内部。如果是，则无解。

注意，如果其中一个凸包退化成点或者线段，还需要进行特殊判断，留给读者思考。

例题9　正方形（Square, Seoul 2009, LA 4728）

给定平面上n个边平行于坐标轴的矩形，在它们的顶点中找出两个欧几里得距离最大的点。如图4-20所示，距离最大的是S₁的左下角和S₈的右上角。正方形可以重合或者交叉。

图　4-20

你的任务是输出这个最大距离的平方。

【输入格式】

输入第一行为数据组数T。每组数据第一行为一个整数n（1≤n≤100000）。以下n行每行3个整数x，y，w（0≤x，y≤10000，1≤w≤10000），其中（x，y）为正方形的左下角顶点，w是边长。

【输出格式】

对于每组数据，输出所有正方形的顶点中，两点最大距离的平方。

【分析】

首先把矩形的所有顶点求出，得到一个点集，则本题的目标就是要求出这个点集的直径（diameter），即点之间的最大距离（因此本题也叫最远点对）。两两枚举的方法需要O（n²）时间，对于本题的规模来说无能为力。

有一个办法可以更快的求出点集的直径。首先求点集的凸包，则最大距离一定来自于凸包上的两个顶点（想一想，为什么）。由于凸包上点的个数往往比原始点少很多，就算还是两两枚举，速度也比直接枚举快很多。当然，在最坏情况下的时间复杂度仍是O（n²），所以需要继续改进。

假设我们已经找到了直径，端点为Pi和Pj。现在我们分别从Pi和Pj出发各作一条垂直于PiPj的直线，如图4-21所示。

图　4-21

可以证明：整个凸包都被夹在了这两条直线中间。如若不然，不妨设Pi的下方有一个凸包上的点P'，则连接P'Pj，假设和下面那条直线交于Q，则P'Pj ＞ PjQ ＞PiPj（想一想，为什么），与PiPj是直径矛盾。为了方便，我们把两条直线看成有向直线，使得凸包位于两条直线的左侧。

像PiPj这样存在两条分别穿过这两个点的平行直线，把凸包夹在中间的点被称为对踵点对（antipodal pair）。给定一个角，有无数条以它为倾角的平行直线，但其中只有两条能把凸包紧紧的夹在中间，因此对踵点最多只有4对（想一想，为什么）。这样，我们可以用一种称为旋转卡壳（rotating calipers）的方式找出所有对踵点对，具体方法如下。

初始时，有两条有向直线把凸包夹在中间，一条水平向右，一条水平向左。这时可以求出初始对踵点为Pi和Pj（就是y坐标最小和最大点）。接下来，逆时针旋转两条直线，看看对踵点对是如何变化的。假设穿过Pi的有向直线还需要逆时针旋转θi角度才能贴住边PiPi＋1，类似定义θj，则当旋转的角度同时小于θi和θj时，对踵点对始终不变。

如果θi＜θj，当旋转角度等于θi时，穿过pi的直线会贴住边PiPi＋1，对踵点对中的Pi会变成Pi＋1；同理，如果θi＞θj，当旋转角度等于θj时，穿过pj的直线会贴住边PjPj＋1，对踵点对中的Pj会变成Pj＋1。如果θi＝θj，则两条直线同时贴住新的边，因此Pi和Pj分别变为Pi＋1和Pj＋1。注意，Pi和Pj＋1、Pi＋1和Pj也分别是对踵点对，不要漏掉它们。重复这一过程，直到穿过Pi的直线倾角大于180°（想一想，为什么）时终止。由于每次旋转至少会有一条直线贴出一条新的边，旋转过程的时间复杂度为O（n）。

既然最远点对只会在对踵点对中取到，只需在上述过程中每找到一对新的对踵点对时更新答案，就可以求出最远点对，代码留给读者编写。

4.3.3　半平面交

简单地说，半平面交问题就是给出若干个半平面，求它们的公共部分。如图4-22所示就是一些半平面的交（用灰色表示），其中每个半平面用一条有向直线表示，它的左侧就是它所代表的半平面。

图　4-22

有向直线的定义如下。

在很多情况下，这个半平面交都是一个凸多边形，但也有时候会得到一个无界多边形，甚至是一条直线、线段或者是点。不管怎样，结果一定是凸的[8]。当然，半平面交也可以为空。

计算半平面交的一个方法是增量法，即初始答案为整个平面，然后逐一加入各个半平面，维护当前的半平面交。为了编程方便，我们一般用一个很大的矩形（4个半平面的交）代替“整个平面”，计算出结果以后删除这4个人工半平面。这样，每加入一个平面都相当于用一条有向直线去切割多边形。

切割的方法很简单：按照逆时针顺序考虑多边形的所有顶点，保留在直线左侧和直线上的点，而删除直线右边的点。如果有向直线和多边形相交时产生了新的点，这些点应加在新多边形中。具体来说，每考虑完一个顶点Pi，在考虑Pi＋1之前要先判断PiPi＋1是否和有向直线在PiPi＋1的内部（端点不算）相交。如果是，则还要把交点加入新多边形中。切割代码如下。

可惜，每次切割需要O（n）时间，因此上述算法的时间复杂度为O（n²），不是很优秀。有没有更快的算法呢？答案是肯定的。从学术上说，和凸包类似[9]，半平面交也可以通过排序、扫描的方法在O（nlogn）时间内解决，不同的是凸包用的是栈，而半平面交用的是双端队列。注意，按照极角排序后，每次新加入的半平面可能会让队尾的半平面变得“无用”，从而需要删除。

如图4-23所示，（a）图中新加一个半平面后，从队尾删除了两个半平面，得到（b）图。

图　4-23

注意，由于极角序是环形的，新加的半平面也可能“绕了一圈”以后让队首的半平面变得无用。

代码比凸包长，且细节更多。在多数情况下，若干半平面的交是一个凸多边形，但也有例外，比如可能得到的是一个无界的区域。解决方法和前面一样，在外面加一个坐标很大（注意不要让运算溢出）的“包围框”（由4个半平面组成），最后再把框去掉。但对于保证不会产生无界区域的情况下，就不需要加这4个特殊半平面了。

例题10　离海最远的点（Most Distant Point from the Sea, Tokyo 2007, LA 3890）

在大海的中央，有一个凸n边形的小岛。你的任务是求出岛上离海最远的点，输出它到海的距离。

【输入格式】

输入包含多组数据。每组数据第一行为整数n（3≤n≤100），即小岛的顶点数。以下n行按照逆时针顺序给出各个顶点的坐标。坐标均为不超过10000的非负整数。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出离海最远点与海的距离。你的输出和标准答案的差不应超过10⁻⁵。

【分析】

本题有很多做法，概念上最简单的一种是二分答案，然后解决一个判定问题：是否有离海距离不小于d的点？对于每条边来说，这些点形成一个半平面，如图4-24所示。

图　4-24

如果半平面交非空，就有满足题意的点存在。下面是主程序。

例题11　铁人三项（Triathlon, NEERC 2000, LA 2218）

铁人三项比赛分成连续的3段：游泳、自行车和赛跑。现在每个单项比赛的长度还没定，但已知各选手在每项比赛中的平均速度（假定该平均速度和赛程长度无关），所以你可以设计每项比赛的长度，让其中某个特定的选手获胜。你的任务是判断哪些选手有可能获得冠军（并列冠军不算）。

注意，3个单项比赛的长度均不能为0。

【输入格式】

输入包含多组数据。每组数据第一行为选手个数n（1≤n≤100），以下n行每行包含3个整数vi，ui和wi（1≤vi，ui，wi≤10000），即第i个选手在游泳、自行车和赛跑比赛中的平均速度。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，按照输入顺序给出对每个选手是否能夺冠（且不是并列）的判断，Yes表示有可能，No表示不可能。每个选手的判断单独占一行。

【分析】

设比赛总长度为1，其中游泳长度为x，自行车长度为y，赛跑长度为1－x－y，则选手i打败选手j（不能并列）的条件是

可以把它整理成Ax＋By＋C ＞ 0的形式，其中

这对应于一个半平面。这样，对于每个选手i，可以得到n－1个半平面（每个半平面代表一个选手被i打败），加上x＞0，y＞0和1－x－y＞0这3个固定约束，一共是n＋2个半平面。如果所有半平面的交非空，则有解（半平面交之中的任何一个点对应的方案都可以使选手i打败其他n－1个人，从而夺冠），否则无解。算法的总时间复杂度为O（n²logn），对于n≤100的规模绰绰有余。

注意上面的数值都很小，容易产生精度误差，在后面的代码中，我们让A，B，C同时乘以10000（根据题目的数据范围）。谨慎起见，下面的代码还特判了3个速度均小于／大于另一个选手的情况。

例题12　丛林警戒队（Jungle Outpost, NEERC 2010, LA 4992）

在丛林中有n个瞭望台，形成一个凸n多边形。这些瞭望台的保护范围就是这个凸多边形内的任意点。敌人进攻时，会炸毁一些瞭望台，使得总部暴露在那些剩下瞭望台的凸包之外，如图4-25所示。你的任务是选择一个点作为总部，使得敌人需要炸坏的瞭望台数量尽量多。

图　4-25

【输入格式】

输入包含多组数据。每组数据第一行为整数n（3≤n≤50000）。以下n行每行两个整数，即每个瞭望塔的坐标，按照顺时针顺序给出。没有3个瞭望台共线的情况。坐标均为绝对值不超过10⁶的整数。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出当总部位置最优时，敌人需要炸毁的瞭望台数目。

【分析】

如果敌人只有一颗炸弹，你会把总部建在哪里呢？对于每个点，炸掉它以后不会暴露在外面的区域是一条有向直线的“左边”。这让我们想到了什么？没错，半平面交！综合考虑所有点，可以建总部的范围就是所有这些半平面的交。

如果敌人有两颗炸弹，总部应该建在哪里呢？分析后发现，敌人最聪明的做法是炸掉两个连续的顶点，而不是分散火力（想一想，为什么）。这样，每两个连续顶点对应一个新的半平面，可以建总部的范围仍然是所有半平面的交，如图4-26所示。

图　4-26

有了上面的分析，整个问题迎刃而解：二分答案，用上述方法判断答案是否可行（也就是半平面交是否为非空）。二分需要O（logn）时间，半平面交需要O（nlogn）时间，总时间复杂度为O（n²logn）。

4.3.4　平面区域

当平面上有很多线段时，组成的图形往往不止一个多边形，而是一个平面直线图（Planar Straight-Line Graph, PSLG），它代表一个平面区域划分，其中每个区域是一个多边形，如图4-27所示。

图　4-27

如果只有点和边的信息，如何找出所有的区域呢？为方便起见，我们把每条边u－v拆成两条“半边”u→v和v→u，并且每条半边只与它左边的面相邻。比如在图4-27中，A→B的左边是灰色区域，而B→A的左边是黑色区域。接下来，我们从一条“半边”出发遍历，每次像卷包裹算法那样找一个“逆时针转得尽量多”的边作为下一条边，直到回到出发的那条半边。

程序实现上可以把边表扩大一倍，让编号为i的半边的反向边的编号为i^1。

例题13　怪物陷阱（Monster Trap, Aizu 2003, LA 2797）

给出一些线段障碍，你的任务是判断怪物能否逃到无穷远处。如图4-28所示，其中（a）图无法逃出，（b）图的可以逃出。

图　4-28

【输入格式】

输入包含多组数据。每组数据第一行为整数n（1≤n≤100），即线段条数。以下n行每行4个整数，即一条线段两端的坐标。假定线段的长度均为正，坐标绝对值不超过50。假设任意两条线段最多只有一个公共点，无三线共点。任意两个交点的距离大于10⁻⁵。输入结束标志为n＝0。

【输出格式】

对于每组数据，如果怪物成功被困住，输出yes；否则（即可以逃到无穷远处）输出no。

【分析】

本题的输入是一个PSLG，目标是判断起点和终点是否在同一个区域内，因此一个可行的方法是先找出所有区域，然后判断起点和终点分别属于哪个区域。但笔者并不推荐这样做，因为这种做法不仅麻烦，而且很容易出错。这道题看上去像个迷宫，可不可以用图论求解呢？比如，每条线段的端点看成一个点，再加上起点和无穷远点（随便取一个坐标很大的点）构图。如果两个点u和v的连线没有与其他线段规范相交，则连接一条边（表示可以从点u沿着直线直接走到点v），最后做一次BFS，看看起点和无穷远点是否连通即可。注意，线段交点并没有参与构图（想一想，为什么）。

看上去似乎合理，但其实这样做不仅是错的，而且错得很离谱。

如图4-29所示，S和A、A和T都有边直接相连，因此S和T连通。问题出在哪里？实际上，“S可以直接到达A”和“A可以直接到达T”中的A并不是同一个点。我们把迷宫墙的厚度画出来，就容易理解了，如图4-30所示。

图　4-29

图　4-30

S可以直接到达的是墙内点A，可以直接到达T的是墙外点A'，而墙内点A和墙外点A'却被墙挡住了，过不去。

解决方法很多，其中有一个容易理解，程序也很好写的方法，那就是把每条线段延长一点点，则原先仅有公共端点的两条线段变成了相交线段，如图4-31所示。

图　4-31

这样一来，就无法从S出去了。需要注意的是，还有一个小小的细节不能忽略：如果要输入的两条有公共端点的线段共线，则上述算法会出问题（想一想，为什么）。解决方案是在线段加宽后判断一下，如果加宽后的某个端点位于另一条线段的内部，则该端点不参与构图。

例题14　找边界（Find the Border, NEERC 2004, LA 3218）

一条可自交的封闭折线将平面分成了若干个区域，其中无限大的那个区域称为折线的外部区域，所有有界区域和折线本身合称内部区域（在图4-32中用阴影表示）。你的任务是求出内部区域的轮廓。

图　4-32

为了确保答案唯一，你求出的轮廓应当满足以下几个条件：不自交（但可以自己和自己接触）、相邻两点不重合、相邻两条线段不共线、当按照轮廓线顺序访问它的各个顶点时，内部区域总是在边的左侧。

【输入格式】

输入包含多组数据。每组数据第一行为折线顶点数n（3≤n≤100），接下来是n对0～100之间的整数，即折线上各顶点的坐标。所有顶点坐标均不同，且不会有顶点在另外两个顶点之间的线段上。相邻两条线段不共线。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，首先输出一行，包含一个整数m，即轮廓线上的顶点个数，接下来是各顶点的坐标。

【分析】

本题是求PSLG的外轮廓，需要借助于前面所说的“类似卷包裹”的算法。具体来说，首先找到x坐标最小的点（如果有多个，找y坐标最小的点），然后开始“卷包裹”。

首先找到初始边（如图4-33所示的粗边）。

图　4-33

然后每次都执行如下操作。

首先看看当前线段是否和其他线段相交（根据题意，一定是规范相交），如果不相交，说明可以直接走到当前线段的终点，否则走到最近的交点时就得停下来。接下来转弯并继续前进。转弯时如果有多条路可以走，选择那条右转得最厉害的线段（见图4-34）。转回原点以后，整个过程结束。

图　4-34

例题15　块和圆盘（Pieces and Discs, UVa 12296）

平面上有一个矩形，左下角在（0，0），右上角在（L，W）。接下来，画一些线段把这个矩形分成若干个块，其中每条线段的两个端点分别位于矩形两条不同的边上，如图4-35所示。

图　4-35

最后再画一些圆盘（即圆及其内部区域），你的任务是判断每个圆盘和哪些块相交（相交部分的面积必须为非0），并按照从小到大的顺序输出这些块的面积。注意，每个圆盘互不相干。

如上图，圆盘和4个块相交，这些块的面积（不是相交部分的面积）分别为0.50，10.03，10.77和18.70（已四舍五入到两位小数）。

【输入格式】

输入包含最多100组数据。每组数据第一行为4个整数n，m，L，W（1≤n，m≤20，1≤L，W≤100），其中n是线段条数，m是圆盘个数。以下n行每行4个整数x₁，y₁，x₂，y₂，表示一条连接（x₁，y₁）和（x₂，y₂）的线段。这两个端点保证分别在矩形的两条不同边上。以下m行每行3个整数x，y，R，表示一个圆心在（x，y），半径为R的圆盘。输入的任意两条线段不会重合。输入结束标志为n＝m＝L＝W＝0。

【输出格式】

对于每组数据，按照输入顺序给出与每个圆盘相交的块的面积。这些面积应四舍五入保留两位小数，且按照从小到大的顺序排好序。

【分析】

除了圆盘之外，本题的输入也是一个PSLG，因此可以按照前面叙述的算法求出各个区域。但由于本题的特殊性，不难发现把线段改成直线后答案不变，因此每个块都是凸多边形，可以用切割凸多边形的方法求解：每读入一条线段，都把它当做直线，切割所有块。这样，我们最终得到了若干凸多边形，需要分别判断是否与圆盘相交。

如何判断多边形是否和圆盘相交？显然，如果多边形的边和圆周规范相交，圆盘和多边形一定相交，但反过来却不成立——圆盘和多边形相交，多边形的边和圆周不一定规范相交。

（1）即使完全没有公共点的时候，圆盘和多边形也可以相交，原因是二者可以相互内含。因此，需要判断多边形是否有顶点在圆内，还需要判断圆心是否在多边形内。

（2）如果是非规范相交，需要分情况讨论。在图4-36（a）中，待判断的线段（用粗线表示）完全在圆外；在图4-36（b）中待判断的线段则是完全在内部。判断方法很简单，只需判断线段中点是否在圆内即可。

图　4-36

4.4　三维几何基础

我们已经在前面介绍过向量运算，其中很多内容也适合三维几何，比如点＋向量＝点；向量＋向量＝向量，“点＋点”没有定义。

直线的表示。直线仍然可以用参数方程（点和向量）来表示，并且射线和线段仍然可以看成“参数有取值范围限制”的直线，而且点到直线的投影也和二维情形一样。

平面的表示。通常用点法式（p₀，n）来描述一个平面。其中点p₀是平面上的一个点，向量n是平面的法向量。每个平面把空间分成了两个部分，我们用点法式表示其中一个半空间。具体是哪一个呢？是这个法向量所背离的那一个（即法向量指向远离半空间的方向）。既然是法向量，n就垂直于平面上的所有直线。换句话说，平面上的任意点p满足Dot（n，p－p₀）＝0。设点p的坐标为（x，y，z），p₀的坐标为（x₀，y₀，z₀），向量n的坐标表示为（A，B，C），上述等式等价于

整理得Ax＋By＋Cz－（Ax₀＋By₀＋Cz₀）＝0。如果令D＝-（Ax₀＋By₀＋Cz₀），我们就得到了平面的一般式：Ax＋By＋Cz＋D＝0。注意，当Ax＋By＋Cz＋D＞0时，上述点积大于0，即点（x，y，z）在半空间（p₀，n）外。换句话说，Ax＋By＋Cz＋D＞0表示的是一个半空间（half space）。

4.4.1　三维点积

三维点积的定义和二维非常类似，而且也能用点积计算向量的长度和夹角。

用三维点积可以解决很多基本问题。

过定点垂直于定直线的平面。平面的法向量就是这条直线，所以可以直接写出所求平面的点法式。

直线和平面的夹角、两平面的夹角、两直线的夹角。注意到与平面的夹角可以转化为与法向量的夹角，这些问题都不难解决[10]。

点到平面的距离。如图4-37所示，把向量p－p₀投影到向量n上可得：p到平面的有向距离为Dot（p－p₀，n）/Length（n）。这是一个相当简洁的结论，如果n是单位向量，甚至会更简单。

图　4-37

点在平面上的投影。有了距离，投影点本身就不难求了。设点p在平面（p₀，n）上的投影为p'，则p'－p平行于n，且p'－p＝dn，其中d就是p到平面的有向距离。

直线和平面的交点。可以简单地通过解方程得到。设平面方程为Dot（n，p－p₀）＝0，过点p₁和p₂的直线的参数方程为p＝p₁＋t（p₂－p₁），则与平面方程联立解得

其中分母为0的情况对应于直线和平面平行，或者直线在平面上（想一想，如何区分）。

顺便说一句，如果平面用一般式Ax＋By＋Cz＋D＝0，则联立解出的表达式为

4.4.2　三维叉积

三维空间里也有叉积的概念，但形式和二维叉积大不一样，它是一个向量，而不再是一个带符号的数。

虽然在学术上并不严谨，但在算法竞赛中读者可以认为叉积同时垂直于v₁和v₂，方向遵循右手定则[11]。当且仅当v₁和v₂平行时，叉积为0。

有了叉积的工具，可以解决更多的基础问题。

过不共线三点的平面。法向量为Cross（p₂－p₀，p₁－p₀），任取一个点即可得到平面的点法式。

三角形的有向面积。和二维情形一样，注意求出叉积之后要取长度。

判断点是否在三角形内。先判断点是否在三角形所在平面上，然后利用简单的面积关系即可，代码如下（假定点P在P0、P和P2确定的平面上）。

判断线段和三角形是否相交。利用上面的函数不难得到。代码如下（注意：为简单起见，我们没有考虑线段在平面上的情况）。

点到直线／线段的距离。仍然可以用面积法（注意三维叉积是向量，要用Length函数而不是fabs），代码如下。

四面体的体积。已知四边形的4个顶点A，B，C，D。根据叉积和点积的定义不难得出四面体的带符号体积为

其中AB，AC，AD呈右手系时为正。括号内的部分也称为混合积，代码如下。

多面体的体积。平面多边形的面积等于三角形的有向面积之和，空间多面体也类似。不过首先需要规定好多面体的存储方式。一种简单的表示法是点－面，即一个顶点数组V和面数组F。其中V里保存着各个顶点的空间坐标，而F数组保存着各个面的3个顶点在V数组中的索引。简单起见，假设各个面都是三角形，且3个点由右手定则确定的方向指向多边形的外部（即从外部看，3个顶点呈逆时针排列），所以这些面上3个点的排列顺序并不是任意的。

4.4.3　三维凸包

和二维情形类似，给定三维空间中的一些点，包含它们的最小凸多面体称为这些点的凸包。三维凸包的求法有很多，常用的有暴力法、卷包裹法和增量法[12]。

暴力法。枚举每3个点组成的有向三角形（实际上对应一个半空间），判断是否所有点都在这个三角形的同侧（即半空间的内部）。如果是，则这个三角形是凸包中的一个面，否则就不是。判断一个点在三角形的哪一侧需要一次叉积和一次点积（也可以理解为一次混合积），因此一共需要O（n⁴）次叉积和点积。

卷包裹法。该算法的思想是先找到一条肯定在凸包上的边PiPj（比如，投影在平面上的凸包的边），然后想象一张纸紧贴这条边向左（这里是指向量PiPj的左边）旋转，直到碰到一个点Pk，然后以PkPj和PiPk为轴继续旋转。

尽管看上去简单，卷包裹法在实现上仍有一些需要注意的地方。首先是初始边的选择。一般先把点投影到z＝0平面，求出二维凸包，然后把二维凸包上的一条边作为三维凸包卷包裹的初始边。根据二维凸包的性质，不必真的做投影和找凸包，而只需要找一个y坐标最小的点作为起点，到它极角最小的点作为第二点。看上去是不是很简单？请注意，当“y最小”和“极角最小”的选择不止一个时很容易出问题，读者不妨仔细思考。另外，在卷包裹的过程中，多点共面也是比较麻烦的问题（想一想，为什么）。总之，卷包裹的实现需要非常小心。

增量法。该算法的基本思想是把点依次加到凸包中。初始时随机选两个点p₁和p₂，然后找一个不和这两个点共线的点p₃，再找一个不和它们共面的点p₄，组成初始凸包，然后依次考虑其他点pr：如果这个点在当前凸包内，直接忽略；否则找到这个点能“看到”的所有面（如图4-38左图），删除它们，然后把阴影边界上的所有点和pr连接起来，其中每条边和pr一起构成一个三角形，如图4-38右图所示。

图　4-38

这个算法的简单实现方法是遍历所有面，判断是否可见；然后遍历所有边，判断是否在阴影边界上（即该边的两侧恰好有一个面可见）[13]。代码如下。

注意到上述代码中的注释了吗？上述代码简单而清晰，但却没有考虑特殊情况（比如凸包上多点共面）。简单起见，实践中常常把输入点进行微小扰动后再调用上述过程。比如可以使用以下代码。

注意扰动之前的点要备份。上述代码中的Face结构体是顶点下标，所以可以用扰动后的点求三维凸包，然后用这些下标引用原来（未扰动）的点。此外，还有一个很容易忽略的问题，即输入点应先判重，否则在扰动后很容易出现极小的面。这样的面很容易造成麻烦。

4.4.4　例题选讲

例题16　三维三角形（3D Triangles, UVa 11275）

给出三维空间的两个三角形，判断二者是否有公共点。若两个点距离小于0.000001，则认为它们是同一个点。三角形的内部、边和顶点都算作三角形的一部分。

【输入格式】

输入第一行为测试数据组数T（T≤1000）。每组数据包含6行共18个实数（最多包含7位小数），前3行为第一个三角形的3个顶点坐标，后3行为第二个三角形的3个顶点坐标。输入三角形不会退化。

【输出格式】

对于每组数据，如果两个三角形有公共点，则输出1，否则输出0。

【分析】

如果相交，那么必然有一个三角形的一条边经过另一个三角形的内部、边上或者顶点，因此判断方法代码如下。

主程序也是相当短的，代码如下。

例题17　Ardenia王国（Ardenia, CERC 2010, LA 4973）

给出空间中两条线段，求它们的最近距离。

【输入格式】

输入第一行为文件组数T（T≤10⁵），以下每两行为一组数据，包含6个绝对值不超过20的整数x1，y1，z1，x2，y2，z2。

【输出格式】

对于每组数据，输出两个互素的整数l和m，使得m＞0且l/m为所求距离的平方。

【分析】

首先判断两直线是否平行（或重合），如果是的话直接求。考虑4个端点到另一条线段的距离，取最小值即可。

如果不平行或重合，说明两条直线是异面直线，这时最短距离既可能是某端点到另一条线段的距离，也可能是异面直线的最短距离。

如何求异面直线的最短距离？假设两条直线分别为l₁＝（p₁，v₁）和l₂＝（p₂，v₂），那么最短距离会在某个q₁＝p₁＋sv₁和q₂＝p₂＋tv₂上取到，其中q₁和q₂分别在l₁和l₂上，且q₁q₂是这两条异面直线的公垂线。

向量q₁q₂＝q₂－q₁＝p₂－p₁＋tv₂－sv₁垂直于v₁，因此Dot（p₂－p₁＋tv₂－sv₁），v₁）＝0。根据分配率，有Dot（p₂－p₁，v₁）＋t×Dot（v₂，v₁）－s×Dot（v₁，v₁）＝0。注意这里的3个点积都是可以直接算出来的，因此实际上得到的是一个关于t和s的一次方程。根据q₁q₂垂直于v₂还可以得到一个一次方程，联立求解即可。下面的代码直接使用了经过复杂化简以后的结果，读者可以参考。

两条直线相交的情况下算出的距离为0，并且返回true。

注意，本题比较特殊，要求以分数形式输出。所以可以考虑定义一个Rat类（代表Rational）来保存和计算有理数，并且重载加法、减法和乘法，然后把本题用到的两个关键函数：点到线段距离Distance2ToSegment和异面直线的最小距离LineDistance3D改写成返回有理数的版本。代码留给读者编写。

例题18　行星（Asteroids, NEERC 2009, LA 4589）

给定两颗凸多面体行星，你的任务是求出二者重心的最近距离。两颗行星的密度都是均匀分布的，且可以任意旋转和平移。每颗行星顶点数不超过60。

【输入格式】

输入包含多组数据，每组数据分成两部分，依次描述两颗行星。每颗行星第一行为顶点数n（4≤n≤60），以下n行每行给出一个顶点坐标。这n个点保证是一个非退化凸多面体的各个顶点。

【输出格式】

对于每组数据，输出两个行星重心的最近距离。

【分析】

不难发现，最优放置方法是贴住两颗行星的某两个面，而最近距离为各自重心到这两个面的距离之和。换句话说，我们可以独立求出两颗行星的“重心到各面的最短距离”，再相加即可。

由于只给出了顶点，需要先求一次三维凸包，找到所有面，然后求出重心，最后依次计算重心到各个面的距离。如何求重心呢？因为行星是均匀的，可以先随便找一个位于行星内部的点（比如所有顶点的坐标平均数），连接该点和各个面，得到若干个三棱锥。把每个三棱锥等价成一个质点，再求这些质点的重心。质点的重心是质点坐标按照质量加权的平均数，而质量均匀的三棱锥的重心的坐标为4个顶点坐标的平均数。

例题19　压纸器（Paperweight, World Finals 2010, LA 4795）

你的公司从事压纸器的生产，压纸器由两个有公共面的四面体组成，四面体由透明玻璃构成，内部镶嵌数个彩色微粒，其中有一个微粒是一个RFID芯片。

使用时，需要把压纸器稳定的放置在一台计算机的顶部，其中紧贴计算机的那个面称为压纸器的底面。所谓“稳定”是指压纸器的重心向任意方向移动不超过0.2米时，压纸器都能回到原位。你可以认为压纸器密度均匀，并且芯片体积足够小（可以被认为是一个点）。你的任务是计算当压纸器处于稳定位置时RFID芯片离底面的最近距离和最远距离（见图4-39）。

图　4-39

【输入格式】

输入包含多组数据。每组数据仅一行，包含18个绝对值不超过1000的整数，即6个点A，B，C，D，E，F的坐标，其中ABC是公共面，D和E分别在面ABC的两侧。点F是RFID芯片的位置，F点保证严格包含在压纸器内部。输入保证四面体ABCD和ABCE体积均为正，且每个压纸器都至少有一个稳定位置。输入结束标志为单个0。

【输出格式】

对于每组数据，输出当压纸器稳定时RFID芯片到底面距离的最小值和最大值，保留5位小数。

【分析】

压纸器一共有5个顶点，底面至少有3个顶点，所以可以枚举底面上的3个顶点P₁P₂P₃，判断是否可以以该面为底面进行稳定放置。具体来说需要判断是否所有顶点都在该面的同侧，然后整个压纸器的重心是否在底面三角形的内部，且离各边的距离不超过0.2。

有一种特殊情况很容易漏掉：如果还有一个点P₄也在底面上（即四点共面），那么重心落在P₁P₂P₃P₄的凸包内[14]即可，不一定在三角形P₁P₂P₃内。

4.4.5　三维几何小结

表　4-2

4.5　小结与习题

1．基础题目

要想较好地解决算法竞赛中的几何题目，必要的平面几何、三角和解析几何知识也是不可缺少的。下面是一些偏数学，和算法关系不大的题目（如表4-3所示），供读者练习基本功。

表　4-3

三角形趣题（Triangle Fun, UVa 11437）

给定△ABC，在边BC，CA，AB上分别取点D，E，F，使得CD＝2BD，AE＝2CE，BF＝2AF。求△PQR的面积，如图4-40所示。

图　4-40

形状（Determine the Shape, UVa 11800）

给定平面上4个点，没有3点共线。你的任务是判断这4个点能组成怎样的四边形。如果能组成正方形，输出Square；如果能组成矩形，输出Rectangle。其他形状按照顺序依次为：菱形（Rhombus）、平行四边形（Parallelogram）、梯形（Trapezium）、普通四边形（Ordinary Quadrilateral）。

运动场的跑道（Athletics Track, UVa 11646）

如图4-41所示，体育场的跑道一圈是400米，其中弯道是两段半径相同的圆弧。已知矩形的长宽比例为a:b，求长和宽的具体数值。

图　4-41

地球隧道（Tunnelling the Earth, UVa 11817）

从地球上的一个点到另一个点，可以沿着球面最短路走，也可以钻隧道，走直线。这两种方法的路程之差是多少？在本题中，假定地球是正球体，半径为6371009米。起点和终点的位置均按经纬度给出，北纬和东经用正数表示，南纬和西经用负数表示。

马戏团的屋顶（Dome of Circus, NEERC 2010, LA 4986）

给定n（n≤10000）个空间中的点，你的任务是找一个底面在z＝0平面上，底面中心在（0，0，0）点的体积最小的圆锥，包含所有点（点在圆锥表面上也可以）。所有点的z坐标均为正数，且至少有一个点不在z轴上，如图4-42所示。

图　4-42

内切圆（In-Circle, UVa 11524）

如图4-43所示，△ABC的内切圆把它的三边分别划分成m₁:n₁，m₂:n₂和m₃:n₃的比例。另外已知内切圆的半径r，求△ABC的面积。

图　4-43

外切圆（Ex-Circles, UVa 11731）

如图4-44所示，已知△ABC的3条边长，求△DEF的面积，以及阴影部分的总面积。

图　4-44

最小正多边形（Smallest Regular Polygon, UVa 12300）

给定两个点A和B，求穿过这两个点的面积最小的正n边形。

交叉的梯子（Crossed Ladders, UVa 10566）

如图4-45所示，已知x，y，c，求“？”的长度。

图　4-45

圆周上的三角形（Circum Triangle, UVa 11186）

在一个圆周上有n（n≤500）个点。不难证明，其中任意3个点都不共线，因此都可以组成一个三角形。求这些三角形的面积之和。

三角形难题（Triangle Hazard, Kuala Lumpur 2008, LA 4413）

如图4-46所示，已知BC，CA，AB分别被点D，E，F分成了m₁:m₂，m₃:m₄和m₅:m₆。已知△ABC的顶点坐标很容易计算出△PRQ；但反过来呢？你的任务是根据P、Q、R三点的坐标，以及m₁～m₆的数值，计算出△ABC3个顶点A、B、C的坐标。

图　4-46

Malfatti圆（Malfatti Circles, Tokyo 2009, LA 4642）

给出一个三角形的3顶点的坐标，求3个圆，使得每个圆和三角形的两条边以及另两个圆均相切，如图4-47所示。输出这3个圆的半径。

图　4-47

2．二维几何计算

这些题目没有太多算法可言，但有很多细节需要注意，比如线段的规范相交和非规范相交的区别；圆和圆的众多位置关系、圆的公切线的各种情况等。表4-4中的几道例题堪称经典，希望读者认真学习。

表　4-4

如表4-5所示是几道巩固练习。

表　4-5

渔网（Fishnet, Hakodate 2001, LA 2402）

有一个左下角在（0，0），右上角在（1，1）的正方形，4条边上各有n个点。对于1≤i≤n，连接两条线段（ai，0）－（bi，1）和（0，ci）－（1，di），可以把正方形划分成若干部分。输入ai，bi，ci，dn的值（0＜ai，bi，ci，di＜1，序列a，b，c，d均严格递增），求划分成的最大块的面积。如图4-48所示，最大块用阴影部分标出。

图　4-48

美梦（Sweet Dream, UVa 10969）

在平面上依次放置n（n≤100）个圆（后放的覆盖先放的），如图4-49所示。按照放置顺序依次输入各个圆的圆心坐标和半径，求圆周上的可见弧长之和。

图　4-49

多边形怪兽（Fighting against a Polygonal Monster, UVa 11177）

有一个棱柱形怪物，从正面看是一个凸n（n≤50）边形。保证（0，0）在该多边形内部（不会在边界上）。

你的任务是用一根光柱打败这个怪物。从正面看，光柱是圆心在（0，0）的圆。要想打败怪物，这个圆和凸多边形的公共面积应该不小于R。为了节约能源，这个圆的半径应尽量小。

数小山（Hills, Hangzhou 2005, LA 3491）

平面上有n（n≤500）条线段，其中每条线段的端点都不会在其他线段上。你的任务是数一数有多少个“没有被其他线段切到”的三角形（即小山）。如图4-50所示，虽然有两个三角形，但其中一个被切到了，所以答案是1。

图　4-50

3．几何算法

几何算法有很多，且大都复杂，容易写错。本章只介绍最常用的几种，分别为凸包、半平面交和平面区域的相关算法。有的例题并不直接和凸包、半平面交等问题相关，甚至并不是几何问题，但可以通过问题转化和建模与这些算法巧妙的联系起来。相关题目如表4-6所示。

表　4-6

下面是习题（见表4-7）。这些题目大都比较传统，主要用来巩固对算法的理解以及代码实现。

表　4-7

围墙（Wall, NEERC 2001, LA 2453）

给出一个n（n≤1000）边形城堡，在它的外部造一个总长度尽量小的围墙，使得围墙的任何一部分离城堡的距离都不小于L，如图4-51所示。

图　4-51

艺术画廊（Art Gallery, SEERC 2002, LA 2512）

有一个n边形的艺术画廊，管理者想在里面放一个监视摄像机，能够看到整个画廊，如图4-52所示。你的任务是计算有多大面积的区域可以放这个摄像机。

图　4-52

更冷更热（Hotter Colder, UVa 10084）

“更冷更热”是一个小孩子玩的游戏，游戏玩法是这样的：甲闭上眼睛，让乙在房间里藏一个东西。睁开眼睛后，甲可以猜这个东西在哪里。第一次必须猜（0，0），以后每猜一个位置，乙根据这个位置和上一次猜的位置哪里离正确位置近做出回答。如果新猜的点比较近，回答“Hotter（更热）”；如果上次猜的点比较近，回答“Colder（更冷）”。如果二者一样近，回答“Same（相同）”。按顺序给出甲每次猜的位置和乙的回答，依次输出乙每次回答之后，所有可能位置占的总面积。

犯罪现场（Crime Scene, UVa 11726）

给定n（n≤100）个物体，每个物体都是一个圆或者k（k≤10）边形，用长度尽量小的绳子把它们包围起来。

最小包围盒（Smallest Enclosing Rectangle, UVa 12307）

平面上有n（3≤n≤100000）个点，你的任务是求出包含所有点的面积最小矩形（设面积为A），以及包含所有点的周长最小矩形（设周长为P）。输出A和P的值。

过街——极限版（Street Crossing EXTREME, UVa 11595）

平面上有n（n≤35）条直线，各代表一条街道。街道相互交叉，形成一些路段（对应于几何上的线段）。你的任务是设计一条从A到B的路线，使得穿过路段的拥挤值之和尽量小。为了安全，你的路线不能直接穿过街道的交叉点，也不能沿着街道走，而只能从路段的中间过街。

平面上还有c（c≤1000）座大楼。正常情况下，每条路段的拥挤值为1，但如果和一座大楼相邻（即可以从这座大楼出发，不经过其他路段或者交叉点到达这条路段），拥挤值需要加上该大楼的拥挤度。

战争的艺术（Art of War, CERC 2004, LA 3176）

地图上有m条线段，围成了n个封闭区域（1≤n≤600，1≤m≤4000），每个封闭区域由一条连续的边界围成，代表一个国家。在这些封闭区域外面是一个无限大的荒地。每个国家内部都有一个首都。每条线段的两侧要么是两个不同的国家，要么一侧是国家，另一侧是荒地。边界上有公共线段的两个国家被称为相邻的。

输入每个国家的首都坐标，以及每条线段的两个端点坐标，你的任务是计算出每个国家的相邻国家。输入线段不会相交，但可以有公共端点。

4．三维几何

本章仅介绍了三维几何中比较简单的内容，比如点积、叉积的基本应用和三维凸包。下面是几道经典例题，如表4-8所示。

表　4-8

相比之下，表4-9中的习题内容更加丰富，不仅包含了一些比较基础的计算题，还有具有一定难度的算法题，有些题目虽然算法不难，但对空间思维有较高的要求。

表　4-9

导弹的距离（The Deadly Olympic Returns, UVa 10794）

三维空间中有两枚沿直线匀速飞行的导弹（看作点），已知某时刻它们的位置、飞行方向和速度，求它们的最短距离（假设两枚导弹将按照原来的速度和方向一直飞行）。

星球大战（Star War, UVa 11836）

空间里有两个四面体行星，要求在二者的表面上各取一个点，使得选出的两个点距离最近。输入每个行星的4个顶点坐标（保证两个四面体不相交，且体积均不等于0），输出最短距离。

球的横切面（Inherit the Spheres, Ehime 2004, LA 3192）

空间里有n（n≤100）个球。用一个水平面从下到上扫描，可以发现截面的连通块个数会发生变化，如图4-53所示。

图　4-53

你的任务是输出每次变化的方式，1表示变大，0表示变小。比如，图4-53的变化方式为11011000。

空气动力学（Aerodynamics, NEERC 2008, LA 4371）

给出空间中的n（4≤n≤100）个点和两个整数zmin和zmax（0≤zmin≤zmax≤100），对于[zmin，zmax]内的每个整数z₀，分别输出给定点的凸包被z＝z₀所截得的面积，如图4-54所示。

图　4-54

提示：可以避开三维凸包吗？

阴影（Shade of Hallelujah Mountain, Fuzhou 2010, LA 5100）

天空中有一块大石头挡住了部分阳光。输入太阳坐标（x，y，z）和地平线的方程ax＋by＋cz＝d，你的任务是计算出无限大地平面上的阴影面积。石头保证是一个有n（4≤n≤100）个顶点的凸多面体，太阳和石头位于地平面的同侧，且严格位于石头的外面。

球和四面体（Tetrahedrons and Spheres, UVa 11887）

三维空间中有a个四面体和b个球（1≤a，b≤5），求它们并的体积。所有坐标为绝对值不超过5的整数，球的半径ri≤3。

微型光线追踪器（A Tiny Raytracer, UVa 12313）

本题的任务是实现一个小型光线追踪渲染器。场景由若干三角形网格（triangle mesh）组成，有且仅有一个点光源（point-light）。

相机模型

本题使用的相机是透视相机，位置在camera_pos，指向camera_target，up向量为camera_up，横向FOV等于f度，拍摄出来的图片是W×H像素，如图4-55所示。

图　4-55

上图中，假想在相机的正前方有一个矩形（称为图像平面），代表着场景在相机中的影像，则它的离散形式就是渲染器的输出。在本题中，所有像素都是正方形，因此图像平面的宽度与高度之比总是W:H。

从camera_pos出发指向camera_target的射线穿过图像平面的中心，而图像平面的局部y轴就是camera_up。横向FOV是指图像平面左右边界相当于相机的张角。

为了计算三维场景中一个点在最终图像中的位置，只需从相机出发引一条射线穿过该点，则该射线与图像平面的交点就是所求。如果没有交点，则该点不可见。不难证明，图像平面离相机的距离无关紧要，只要y轴方向为camera_up，从camera_pos出发指向camera_ target的射线穿过图像平面的中心即可。

光线追踪原理

对于最终图像中的每个像素，我们考虑一条射线，从眼睛（也就是相机，下同）出发，穿过像素的中心。只要跟踪这条射线，看它在场景中碰到了什么颜色的物体，根据光路可逆原理，就能知道这个像素是什么颜色的。

上述原理是高度简化的，不过足以解决本题。在最简单的情况下，所有物体既不反光也不透明，则每当射线碰到一个物体时，可以直接计算这个物体的颜色，方法是连接碰撞点和光源（本题只有一个光源），如果连线被其他物体挡住，说明这个点处于阴影中，否则用随后介绍的着色算法计算这个点的颜色。

在真实场景中，由于玻璃和水这样的物体存在，我们需要考虑光线和物体的多次碰撞，以处理反射（reflection）和折射（refraction），方法如下：如果射线碰到了一个反射性物体，则派生出一条新的反射光线，从碰撞点射出，指向碰撞表面的外部。同理，如果射线碰到了一个有一定透明度的物体，则派生出一条新的折射光线，从碰撞点射出，指向碰撞表面的内部。如果碰撞面两侧物体的折射系数（index of refraction）不同，则光线的方向将发生改变，改变方式遵守snell定律

注意，如果发生了全反射（total internal reflection），应当停止跟踪该光线，而不是派生出一条反射光线。

事实上，反射光线和折射光线本身还能继续派生出新的光线，所以我们实际上拥有一棵光线树。为了避免无穷无尽的递归下去，我们规定树的最大高度（在本题中总是等于4），这样，每个叶子要么在深度上达到了最大值，要么碰到了一个既不反射也不折射的物体（或者什么都没射到）。

递归过程伪代码如下。

注意，只要深度没有达到最大值，不管反射系数是多么小的正数，都应该跟踪反射光线；折射光线也是如此。两个颜色的加法将在随后定义。

着色

前面遗留了一个问题，就是如何计算碰撞点的颜色（即上面的get_point_color函数）。在本题中，用Lambertian着色法（也叫余弦着色法），即根据碰撞表面的法线和从碰撞点到光源的向量的点积计算亮度。当夹角增大时，亮度按照余弦函数减小。

如果点积等于0，说明两向量垂直，此时我们并不希望这个点完全呈黑色，而是要给它一点所谓的“环境光”。我们用ambient_coefficient来表示这个系数，而diffuse_coefficient＝1-ambient_coefficient表示漫反射系数。在本题中，三角形都看成是双面反光的，因此点积部分取了绝对值。object_color是物体的一个属性，即完全照明时的颜色。着色伪代码如下。

简单起见，只要连接交点和光源的线段被一个物体阻挡（即使该物体反光或者透明），就算作该交点在阴影中。这样做的确会让渲染结果不正确，但在本题中请忽略这个Bug。

在本题中，颜色用三元组（r，g，b）表示，其中实数r，g，b满足0≤r，g，b≤1。颜色加法和向量加法一样，也是每一维分别相加。不难发现，如果严格按照上面的规则编写代码，颜色加法的结果总是合法的（即相加后的结果仍满足0≤r，g，b≤1）。

注释

[1]　也可以理解成两条线段均不含端点。

[2]　这段代码的点积部分可以改成坐标比较，比较跨度较大的那一位坐标。

[3]　可以直接用来解决UVa 10674《切线》。

[4]　但并不推荐这样做。调节eps只是掩盖了问题，并没有消除。

[5]　在程序实现中，可以用圆弧中点代替整条圆弧进行判断。

[6]　事实上，也可以不重新引射线，而是通过一些条件进一步判断。不过由于转角法实在太好用了。这里不再叙述射线法的细节。

[7]　它是Graham扫描算法的变种。和原始的Graham算法相比，Andrew更快，且数值稳定性更好。

[8]　因为凸集的交是凸的。

[9]　从学术上讲，凸包的对偶问题很接近半平面交，所以二者算法很接近。

[10]　比如，两平面的夹角等于这两个平面的法线的夹角。

[11]　事实上叉积的方向和左右手系有关。如果还要更严密的话，叉积其实不是一个真正的向量，而是一个伪向量（pseudovector）。

[12]　增量算法有一种实践上非常有效的实现方法，即快速凸包，但在多数算法竞赛中，普通增量法已经足够快了。

[13]　一个较好的方法是先找到一个可见面，然后用DFS的方法找到其他可见面和阴影边界，不过一般来说不必如此。

[14]　程序上并不需要先求凸包。具体做法留给读者思考。

第5章　图论算法与模型

在《算法竞赛入门经典》中我们已经学习过基本的图论问题和算法，如无根树转有根树、表达式求值、最小生成树、最短路、最大流、最小割、最小费用流等。本章除了介绍更多图论问题和算法外，也对这些常见问题进行了更加深入的讨论，并通过更多例题展示建模和分析技巧。

5.1　基础题目选讲

本节介绍一些只用到比较基础的图论知识的题目，比如图的宽度优先遍历、欧拉回路、拓扑排序。

例题1　大火蔓延的迷宫（Fire!, UVa 11624）

你的任务是帮助Joe走出一个大火蔓延的迷宫。Joe每分钟可以走到上下左右4个方向的相邻格之一，而所有着火的格子都会往四周蔓延（即如果某个空格与着火格有公共边，则下一分钟这个空格将着火）。迷宫中有一些障碍格，Joe和火都无法进入。当Joe走到一个迷宫的边界格子时，我们认为他已经出了迷宫。

【输入格式】

输入第一行为数据组数T。每组数据第一行为两个整数R和C（1≤R，C≤1000）。以下R行每行有C个字符，即迷宫。其中“#”表示墙和障碍物，“.”表示空地，“J”是Joe的初始位置（也是一个空地），“F”是着火格。每组数据的迷宫中恰好有一个格子是“J”。

【输出格式】

对于每组数据，输出走出迷宫的最短时间（单位：分钟）。如果无法走出迷宫，则输出IMPOSSIBLE。

【分析】

如果没有火，那么本题是一个标准的迷宫问题，可以用BFS解决。加上了火，难度增加了多少呢？其实没多少。注意，火是不会自动熄灭的，因此只要某个格子在某时刻起火了，以后将一直如此。所以只需要预处理每个格子起火的时间，在BFS扩展结点的时候加一个判断，当到达新结点时该格子没着火才真的把这个新结点加到队列中。

最后需要考虑一下如何求出每个格子起火的时间，其实这也是一个最短路问题，只不过起点不是一个，而是多个（所有的初始着火点）。这只需要在初始化队列时把所有着火点都放进去即可[1]。

两个步骤的时间复杂度均为O（RC）。

例题2　独轮车（The Monocycle, UVa 10047）

独轮车是一种仅有一个轮子的特殊自行车。它的轮子被等分成5个扇形，分别涂上一种不同的颜色。现在有一个人骑车行驶在M×N的网格平面上。每个格子的大小刚好使得当车从一个格子骑到下一个格子时，轮子恰好转过一个扇形。

如图5-1所示，当轮子在1号格子的中心时，蓝色扇形的外弧线中点刚好与地面接触。当它移动到下一个格子（2号格子）的时候，白色扇形的外弧线中点与地面接触。

图　5-1

有些格子中有障碍，所以车子不能通过这些格子。骑车人从某个格子出发，希望用最短的时间移动到目标格。在任何一个格子上，他要么骑到下一个格子，要么左转或者右转90°。其中每项动作都恰好需要1秒来完成。初始时，他面朝北并且轮子的绿色扇形贴着地面。到达目标格时，也必须是绿色扇形贴着地面，但是朝向无限制，如图5-2所示。

图　5-2

【输入格式】

输入包含多组数据。每组数据的第一行包含两个整数M和N（1≤M，N≤25），表示网格的行数和列数，接下来是网格的描述，用M行长度为N的字符串来表示。字符“#”表示一个障碍方格，其他方格均可通行。骑车路线的起点用“S”表示，终点用“T”表示。输入结束标志为M＝N＝0。

【输出格式】

对于每组数据，输出测试数据编号和到达目标格的最短时间（单位：秒）。如果无法到达，输出“destination not reachable”。相邻两组数据的输出之间应有一个空行。

【分析】

本题看上去也是一个最短路问题，但似乎有一些特殊：到达同一个格子时，自行车可能处于不同的“状态”，因为朝向可能不同，接触地面的扇形颜色可能会不同。这样，我们把这两个附加因素纳入到考虑范围内，把每个（x，y，d，c）作为一个结点，表示自行车当前位置在格子（x，y），朝向为d，底面颜色为c，一共有20MN个结点。从每个结点出发最多有3条边（分别对应前进、左转和右转），因此有不超过60MN条边的情况，完全可以承受。

例题3　项链（The Necklace, UVa 10054）

有一种由彩色珠子连接成的项链。每个珠子的两半由不同颜色组成。如图5-3所示，相邻两个珠子在接触的地方颜色相同。现在有一些零碎的珠子，需要确认它们是否可以复原成完整的项链。

图　5-3

【输入格式】

输入第一行为测试数据组数T。每组数据的第一行是一个整数N（5≤N≤1000），表示珠子的个数。接下来的N行每行包含两个整数，即珠子两半的颜色。颜色用1～50的整数来表示。

【输出格式】

对于每组数据，输出测试数据编号和方案。如果无解，输出“some beads may be lost”。方案的格式和输入相同，也是一共N行，每行用两个整数描述一个珠子（从左到右顺序），其中第一个整数表示左半的颜色，第二个整数表示右半的颜色。根据题目规定，对于1≤i≤n－1，第i行的第二个数必须等于第i＋1行上的第一个数，且第N行的第二个数必须等于第一行的第一个数（因为项链是环形的）。则如果有多解，输出任意一组即可。在相邻两组输出之间应有一个空行。

【分析】

如果把珠子看成点，似乎无法把题目转化成一个经典的、可以有效解决的问题，但如果把每种颜色看成一个结点，每个珠子的两半连一条有向边，则题目转化为了欧拉回路问题，可以在线性时间内解决。通过此题，数学建模的重要性可见一斑。

例题4　猜序列（Guess, Seoul 2008, LA 4255）

对于一个序列a₁，a₂，...，an，我们可以计算出一个符号矩阵S，其中Sij为ai＋…＋aj的正负号（连加和大于0则Sij＝'＋'，小于0则Sij＝'－'，等于0则Sij＝'0'）。例如，序列-1，5，-4，2的矩阵如下。

根据序列A不难算出上述符号矩阵。你的任务是求解它的“逆问题”，即给出一个符号矩阵，找出一个对应的序列。输入保证存在一个满足条件的序列，其中每个整数的绝对值均不超过10。

【输入格式】

输入第一行为数据组数T。每组数据第一行为整数n（1≤n≤10），即序列的长度。第二行有n（n＋1）/2个字符，由符号矩阵的每一行按照从上到下的顺序连接而成。

【输出格式】

对于每组数据，在一行中输出n个绝对值不超过10的整数，即序列A。

【分析】

在前面的章节中，我们已经多次用到了“连续和转化为前缀和之差”的技巧，该技巧对本题仍然适用。设Bi＝a₁＋a₂＋…＋ai，规定B₀＝0，则矩阵中的任意一项都等价于两个Bi相减之后的正负号。例如，第x行y列的符号为正，表示ax＋…＋ay＞0，即By－Bx－1＞0，即By＞Bx－1。这样，本题就转化为已知B₀＝0，B₁，…，Bn的一些大小关系，求它们的值（只要一组解即可）。这个问题可以通过拓扑排序完成，细节留给读者思考。

5.2　深度优先遍历

在《算法竞赛入门经典》中，我们已经学过图遍历算法，包括深度优先算法（DFS）和宽度优先算法（BFS）。通过本节我们将看到，DFS可以做到的远不止那些。

这里先强调一点，本节的例题极为重要，请读者仔细阅读。

DFS框架。深度优先遍历（depth-first search，DFS）依次递归访问当前结点的所有相邻结点（adjacent vertices）。代码如下。

上面的代码使用了一个标志数组vis，初始值全为0，表示所有结点都没有被访问过。上面的PREVISIT（u）和POSTVISIT（u）允许访问结点之前和之后进行其他操作，我们很快会完善它们的细节。上面的代码使用了vector式的邻接表，其中G[u][i]表示结点u的第i个子结点[2]。

上述代码最值得注意的是，由于用到了递归，当结点特别多而且深度很大时，可能会栈溢出。一种解决方案是增加程序可用栈的大小，但在竞赛中通常不可行（选手无法修改评测时的运行环境或者编译开关）；更可靠的解决方案是把递归改成非递归，使用自己编写的栈即可[3]。

连通分量。在无向图中，如果从结点u可到结点v，那么从结点v必然也能到达结点u（只需把路径“反过来”走）；如果结点u可达结点v，而结点v又可达结点w，则结点u必然也能到达结点w（只需把两段路径拼起来）。再加上每个结点可到自身这一明显的事实，实际上证明了在无向图中，可达关系满足自反性、对称性和传递性，是一个等价关系。

从等价关系可以定义等价类。我们把相互可达的结点称为一个连通分量（connected component），则很容易用DFS在线性时间内求出任意无向图的连通分量。下面的代码为每个结点计算出了该结点所属的连通分量编号。

注意，这里有一个全局变量current_cc，表示当前连通分量的编号。如果要记录每个结点的连通分量编号，就需要在PREVISIT（u）中赋值cc[u]＝current_cc。

对于棋盘，我们常常听到“四连块”和“八连块”这样的说法。前者是指把“上下左右”作为相邻关系时，棋盘的连通分量；而后者是指把“仅有一个公共点”的格子也看做相邻时，棋盘的连通分量。

如果只需求连通分量，还可以使用第3章中介绍的并查集，不仅不会出现DFS栈溢出的问题，而且连图都不需要保存，只需按照某种顺序处理所有的边即可。

二分图判定。对于无向图G＝（V，E），如果可以把结点集分成不相交的两部分，即X和Y＝V－X，使得每条边的其中一个端点在X中，另一个端点在Y中，则称图G是二分图（bipartite graph）。二分图的另一种等价说法是，可以把每个结点着以黑色和白色之一，使得每条边的两个端点颜色不同。不难发现，非连通的图是二分图当且仅当每个连通分量都是二分图，因此我们只考虑无向连通图。

下面，我们用DFS来给任意无向图G进行黑白二着色（bicoloring[4]）。首先，总是假定结点0是黑色的。如果你找到了一种让它着白色的方案，只需把所有结点的颜色“翻转”一下，就能让结点0着黑色。

接下来，对DFS进行小小的修改，代码如下。

我们用颜色1和2分别表示黑色和白色，0表示没着色，则vis数组就没有存在的必要了。记得在调用之前把color数组初始化为0。

5.2.1　无向图的割顶和桥

对于无向图G，如果删除某个点u后，连通分量数目增加，称u为图的关节点（articulation vertex）或割顶（cut vertex）。对于连通图，割顶就是删除之后使图不再连通的点。

如何求出图的所有割顶呢？我们有两个选择。

选择一：尝试删除每个结点，然后用DFS判断连通分量是否增加，时间复杂度为O（n（n＋m）），其中n和m分别是图中的点数和边数。

选择二：深入挖掘DFS的性质，在线性时间（即O（n＋m）时间）内求出所有割顶。

毫无疑问，第二个选择比“删除－测试”的方法高效许多，并且达到了理论下界，是理想的方案。下面我们就来学习这个方法。

时间戳。本节的算法依赖于“时间戳”这个概念。反应到代码中，只需在PREVISIT（u）和POSTVISIT（u）中加如下代码，就可以为每个结点记录两个时间戳。

其中，全局的“当前时间”dfs_clock初始化为0。不难发现，当所有结点都访问过以后，dfs_clock的值将等于2m。

如图5-4所示是一个无向图和对应的DFS森林，每个结点u均标记以pre[u]和post[u]。强烈建议读者根据自己的理解模拟DFS过程，看看是否能得到相同的DFS森林。执行dfs（u）时，应当按照字母从小到大的顺序考虑u的各个子结点。

图　5-4

边分类。注意无向图的每条边（u，v）会处理两次：访问u的时候一次，访问v的时候一次。DFS森林中的边称为树边（tree edge）。第一次处理时从后代（descendant）指向祖先（ancestor）的边称为反向边（back edge）。在无向图中，除了树边之外，其他边都是反向边。有向图除了树边和反向边外还有前向边（forward edge）和交叉边（cross edge），这里不再赘述。

割顶和桥的条件。简单起见，我们只考虑连通图。在这样的情况下，DFS森林一定只有一棵树。树根是不是割顶呢？不难发现，当且仅当它有两个或更多的子结点时，它才是割顶——无向图只有树边和反向边，不存在跨越两棵子树的边。对于其他点，情况就要复杂一些。我们有下面的定理。

定理：在无向连通图G的DFS树中，非根结点u是G的割顶当且仅当u存在一个子结点v，使得v及其所有后代都没有反向边连回u的祖先（连回u不算）。

证明：如图5-5所示，考虑u的任意子结点v。如果v及其后代不能连回f，则删除u之后，f和v不再连通；反过来，如果v或它的某一个后代存在一条反向边连回f，则删除u后，以v根的整棵子树中的所有结点都可以利用这条反向边与f连通。如果所有子树中的结点都和f为连通，根据“连通”关系的传递性，整个图就是连通的。

图　5-5

方便起见，设low（u）为u及其后代所能连回的最早的祖先的pre值，则定理中的条件就可以简写成结点u存在一个子结点v，使得low（v）≥pre（u）。

作为一种特殊情况，如果v的后代只能连回v自己（即low（v）＞pre（u）），只需删除（u，v）一条边就可以让图G非连通了，满足这个条件的边称为桥（bridge）。换句话说，我们不仅知道结点u是割顶，还知道了（u，v）是桥。

注意对于每个割顶或者桥来说，上述条件可能不止成立一次，所以一般不要边判定边输出，而是用数组来记录每个结点是不是割顶，以及每条边是不是桥，最后一次性输出。

low函数的计算。现在唯一的问题是low函数本身的计算，它可以用下面的代码来递推计算。注意，代码中已经加入了割顶的判断，桥的判断留给读者。

每次递归访问结束时，该结点的low函数就被正确计算出来了。在上面的代码中，最容易写错的是，漏掉v不等于fa的判断。边（u，fa）不是反向边，而是树边（fa，u）的第二次访问。在调用这个代码之前不要忘记把所有结点的pre初始化为0，且第一次调用时，fa的值应该等于负数。

5.2.2　无向图的双连通分量

对于一个连通图，如果任意两点至少存在两条“点不重复”的路径，则说这个图是点－双连通的（一般简称双连通，biconnected）。这个要求等价于任意两条边都在同一个简单环中，即内部无割顶。

类似地，如果任意两点至少存在两条“边不重复”的路径，我们说这个图是边－双连通的（edge-biconnected）。这个要求要低一点，只需要每条边都至少在一个简单环中，即所有边都不是桥。

对于一张无向图，点－双连通的极大子图[5]称为双连通分量（Biconnected Component, BCC）或块（block）。不难发现，每条边恰好属于一个双连通分量，但不同双连通分量可能会有公共点。可以证明不同双连通分量最多只有一个公共点，且它一定是割顶。另一方面，任意割顶都是至少两个不同双连通分量的公共点。

同理，边－双连通的极大子图称为边－双连通分量（edge-biconnected component）。除了桥不属于任何边－双连通分量之外，其他每条边恰好属于一个边－双连通分量，而且把所有桥删除之后，每个连通分量对应原图中的一个边－双连通分量。

如图5-6所示有两个点－双连通分量：｛1，2，3｝和｛3，4，5｝，但只有一个边－双连通分量：｛1，2，3，4，5｝。

图　5-6

计算点－双连通分量一般用如下算法[6]。该算法用一个栈S来保留在当前BCC中的边（注意不是点而是边），细节请看代码。

边－双连通分量可以用更简单的方法求出，分两个步骤，先做一次dfs标记出所有的桥，然后再做一次dfs找出边－双连通分量。因为边－双连通分量是没有公共结点的，所以只要在第二次dfs的时候保证不经过桥即可。

例题5　圆桌骑士（Knights of the Round Table, LA 3523）

有n个骑士经常举行圆桌会议，商讨大事。每次圆桌会议至少应有3个骑士参加，且相互憎恨的骑士不能坐在圆桌旁的相邻位置。如果发生意见分歧，则需要举手表决，因此参加会议的骑士数目必须是奇数，以防止赞同和反对票一样多。知道哪些骑士相互憎恨之后，你的任务是统计有多少个骑士不可能参加任何一个会议。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n≤1000，1≤m≤10⁶）。以下m行每行包含两个整数k₁和k₂（1≤k₁，k₂≤n），表示骑士k₁和骑士k₂相互憎恨。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出一行，即无法参加任何会议的骑士个数。

【分析】

以骑士为结点建立无向图G。如果两个骑士可以相邻（即他们并不相互憎恨），在他们之间连一条无向边，则题目转化为求不在任何一个简单奇圈[7]上的结点个数。如果图G不连通，应对每个连通分量分别求解。下面假设图G连通。

简单圈上的所有结点必然属于同一个双连通分量，因此需要先找出所有双连通分量。二分图是没有奇圈的，因此我们只需要关注那些不是二分图的双连通分量。虽然这些双连通分量一定含有奇圈，但是不是其中的每个结点都在奇圈上呢？换句话说，如果结点v所属的某一个双连通分量B（因为v可能属于多个双连通分量）不是二分图，v是否一定属于一个奇圈呢？

问题在于，尽管B不是二分图意味着它一定包含一个奇圈C，这个C可能并不包含v。我们得想办法让v参与进来。

如图5-7所示，根据连通性，从v一定可以到达C中的某个结点u₁；根据双连通性，C中还应存在另一个结点u₂，使得从v出发有两条不相交路径（除起点外无公共结点），分别到u₁和u₂。由于在C中，从u₁到u₂的两条路的长度一奇一偶，总能构造出一条经过v的奇圈。

图　5-7

这样，主算法就很清楚了：对于每个连通分量的每个双连通分量B，若它不是二分图，给B中所有结点标记为“在奇圈上”。注意，由于每个割顶属于多个双连通分量，它可能会被标记多次。算法的时空复杂度都是线性的。核心代码如下。

例题6　井下矿工（Mining Your Own Business, World Finals 2011, LA 5135）

有一座地下的稀有金属矿由n条隧道和一些连接点组成，其中每条隧道连接两个连接点。任意两个连接点之间最多只有一条隧道。为了降低矿工的危险，你的任务是在一些连接点处安装太平井和相应的逃生装置，使得不管哪个连接点倒塌，不在此连接点的所有矿工都能到达太平井逃生（假定除了倒塌的连接点不能通行外，其他隧道和连接点完好无损）。为了节约成本，你应当在尽量少的连接点安装太平井。还需要计算出当太平井的数目最小时的安装方案总数。

【输入格式】

输入包含多组数据。每组数据第一行为隧道的条数n（n≤50000），以下n行每行两个整数，即一条隧道两端的连接点编号（所有连接点从1开始编号）。每组数据的所有连接点保证连通。

【输出格式】

对于每组数据，输出两个整数，即最少需要安装的太平井数目以及对应的方案总数。方案总数保证在64位带符号整数的范围内。

【分析】

本题的模型是：在一个无向图上选择尽量少的点涂黑（对应太平井），使得任意删除一个点后，每个连通分量至少有一个黑点。不难发现，把割顶涂黑是不划算的，而且在同一个点－双连通分量中涂两个黑点也是不划算的。进一步分析发现，当一个点－双连通分量只有一个割顶时才需要涂，而且是任选一个非割顶涂黑即可。两个问题同时解决。

一个特殊情况是整个图没有割顶。此时需要涂两个点，方案总数是V（V－1）/2，其中V是连接点个数。在求出BCC之后的核心代码如下。

本题还有一种不用求BCC的简单做法，即先找出割顶，然后从非割顶的结点开始DFS，在DFS过程中不许经过割顶。细节留给读者思考。

5.2.3　有向图的强连通分量

和无向图的连通分量类似，有向图中有“强连通分量”的说法。“相互可达”的关系在有向图中也是等价关系。每一个集合称为有向图的一个强连通分量（Strongly Connected Componenet, SCC）[8]。如果把一个集合看成一个点，那么所有SCC构成了一个SCC图。这个SCC图不会存在有向环，因此是一个DAG。

如何求出有向图的各个强连通分量呢？我们再次借助DFS。以图5-8（a）为例，如果从I开始DFS，将得到只包含｛G，H，I，J，K，L｝的一棵DFS树，然后从C出发，得到｛C，F｝，再从D出发得到｛D｝，以此类推，每次得到一个SCC，如图5-8（b）所示。

图　5-8

遗憾的是，并不是每个顺序都是“好用”的。如果一开始就不幸选择从A开始进行遍历，这棵DFS树将包含整个图。也就是说，这次不明智的DFS把所有SCC混在了一起，什么也没得到。很明显，我们希望按SCC图拓扑顺序的逆序进行遍历。这样才能每次DFS只得到一个SCC，而不会把两个或多个SCC混在一起。

Kosaraju算法便是基于这个思想的[9]，代码如下（假设G的转置事先计算好并保存在G2中）。

算法的正确性证明略，有兴趣的读者可以参考相关书籍。

最后，我们来学习SCC的Tarjan算法。它的时间复杂度也是线性的，不需要计算图的转置，并且比Kosaraju算法常数更小。该算法由Tarjan于1972年提出，是SCC的第一个线性算法。Tarjan算法仍然借助DFS，但它并不是靠遍历顺序来把不同SCC分离到不同的DFS树中，而是允许多个SCC并存于同一棵DFS树中，然后用某种手段把它们分开。

考虑强连通分量C，设其中第一个被发现的点为x，则C中其他点都是x的后代。我们希望在x访问完成时立刻输出C。这样，就可以在同一棵DFS树中区分开所有SCC了。因此问题的关键，是判断一个点是否为一个SCC中最先被发现的点。

如图5-9所示。假设我们正在判断u是否为某SCC的第一个被发现结点。如果我们发现从u的子结点出发可以到达u的祖先w，显然u、v、w在同一个SCC中，因此u不是该SCC中第一个被发现的结点（见图5-9（a））；另一方面，如果从v发现最多只能到u，那么u是该SCC中第一个被发现的结点（见图5-（9b））。这样，问题转化为求一个点u最远能到达的祖先的d值。注意，这里的“到达”只能通过当前SCC中的点，而不能通过已经确定SCC编号的其他点。图中虚线表示一条或多条边。

图　5-9

很像无向图DFS中的low函数是么？没错，我们可以类似的定义lowlink（u）为u及其后代能追溯到的最早（最先被发现）祖先点v的pre（v）值，如此便可以在计算lowlink函数的同时完成SCC计算，代码如下。

程序用一个附加栈S保存当前SCC中的结点（注意这些结点形成一棵子树而不一定是一个链），scc_cnt为SCC计数器，sccno[i]为i所在的SCC编号。

例题7　等价性证明（Proving Equivalences, NWERC 2008, LA 4287）

在数学中，我们常常需要完成若干个命题的等价性证明。比如，有4个命题a，b，c，d，我们证明a↔b，然后b↔c，最后c↔d。注意每次证明都是双向的，因此一共完成了6次推导。另一种方法是证明a→b，然后b→c，接着c→d，最后d→a，只需4次。现在你的任务是证明n个命题全部等价，且你的朋友已经为你做出了m次推导（已知每次推导的内容），你至少还需要做几次推导才能完成整个证明？

【输入格式】

输入第一行为数据组数T（T≤100）。每组数据第一行为两个整数n和m（1≤n≤20000，0≤m≤50000），即命题的个数和已完成的推导个数。以下m行每行包含两个整数s₁和s₂（1≤s₁，s₂≤n，s₁≠s₂），表示已证明s₁→s₂。

【输出格式】

对于每组数据，输出还需要完成的推导数目的最小值。

【分析】

如果把命题看做结点，推导看做有向边，则本题就是给出n个结点m条边的有向图，要求填加尽量少的边，使得新图强连通。算法如下。

首先找出强连通分量，然后把每个强连通分量缩成一个点，得到一个DAG。接下来，设有a个结点（别忘了，这里的每个结点对应于原图的一个强连通分量）的入度为0，b个结点的出度为0，则max｛a，b｝就是答案。注意特殊情况：当原图已经强连通时，答案是0而不是1。证明部分留给读者完成。

例题8　最大团（The Largest Clique, UVa 11324）

给一张有向图G，求一个结点数最大的结点集，使得该结点集中任意两个结点u和v满足：要么u可以到达v，要么v可以达到u（u和v相互可达也可以）。

【输入格式】

输入第一行为测试数据组数T。每组数据第一行为结点数n和边数m（0≤n≤1000，0≤m＜＝50000）。以下m行每行两个整数u和v，表示一条有向边u→v。结点编号1～n。

【输出格式】

对于每组数据，输出最大结点集的结点个数。

【分析】

不难发现，在最优方案中，同一个强连通分量中的点要么都选，要么都不选。把强连通分量收缩点后得到SCC图，让每个SCC结点的权等于它的结点数，则题目转化为求SCC图上权最大的路径。由于SCC图是一个DAG，可以用动态规划求解。

5.2.4　2-SAT问题

2-SAT问题是这样的：有n个布尔变量xi，另有m个需要满足的条件，每个条件的形式都是“xi为真／假或者xj为真／假”。比如：“x₁为真或者x₃为假”、“x₇为假或者x₂为假”都是合法的条件。注意这里的“或”是指两个条件至少有一个是正确的，比如x₁和x₃一共有3种组合满足“x₁为真或者x₃为假”，即x₁真x₃真、x₁真x₃假、x₁假x₃假。2-SAT问题的目标是给每个变量赋值，使得所有条件得到满足。

2-SAT的解法有多种不同的叙述方式，这里采用一种比较容易理解，且效率也不错的方式。构造一张有向图G，其中每个变量xi拆成两个结点2i和2i＋1，分别表示xi为真和xi为假。最后要为每个变量选其中的一个结点标记。比如，若标记了结点2i，表示xi为真；如果标记了2i＋1，表示xi为假。

对于“xi为真或者xj为真”这样的条件，我们连一条有向边2i＋1→2j，表示如果标记结点2i＋1那么也必须标记结点j（因为如果xi为假，则xj必须为真才能使条件成立）。这条有向边相当于“推导出”的意思。同理，还需要连一条有向边2j＋1→2i。对于其他情况，也可以类似连边。换句话说，每个条件对应两条“对称”的边[10]。

接下来逐一考虑每个没有赋值的变量，设为xi。我们先假定它为真，然后标记结点2i，并且沿着有向边标记所有能标记的结点。如果标记过程中发现某个变量对应的两个结点都被标记，则“xi为真”这个假定不成立，需要改成“xi为假”，然后重新标记。注意，这个算法没有回溯过程。如果当前考虑的变量不管赋值为真还是假都会引起矛盾，可以证明整个2-SAT问题无解（即使调整以前赋值的其他变量也没用）。

例题9　飞机调度（Now or later, LA 3211）

有n架飞机需要着陆。每架飞机都可以选择“早着陆”和“晚着陆”两种方式之一，且必须选择一种。第i架飞机的早着陆时间为Ei，晚着陆时间为Li，不得在其他时间着陆。你的任务是为这些飞机安排着陆方式，使得整个着陆计划尽量安全。换句话说，如果把所有飞机的实际着陆时间按照从早到晚的顺序排列，相邻两个着陆时间间隔的最小值（称为安全间隔）应尽量大。

【输入格式】

输入包含若干组数据。每组数据第一行为飞机的数目n（2≤n≤2000）。以下n行每行两个整数，即早着陆时间和晚着陆时间。所有时间t满足0≤t≤10⁷。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出安全间隔的最大值。

【分析】

“最小值尽量大”的典型处理方法是二分查找最终答案P。这样，原来的问题转化为了判定问题“是否存在一个调度方案，使得相邻两个着陆时间差总是不小于P”，而这个问题可以进一步转化为：任意两个着陆时间差总是不小于P。令布尔变量xi表示第i架飞机是否早着陆，则唯一的限制就是“时间差小于P的两个着陆时间不能同时满足”。例如，若Ei和Lj的时间差小于P，则不能同时满足xi＝true和xj＝false。可以用一个子句来表达这样的限制。每一组不能同时满足的着陆时间对应于一个子句，则整个约束条件对应于一个alt2-SAT问题的实例，包含n个变量和不超过n（n－1）/2个子句。

考虑到还要在所有O（n²）种可能的答案中二分查找，总时间复杂度为O（n²logn）。考虑到时间范围比较大，也可以直接二分时间的数值，时间复杂度为O（n²logT），其中T为所有时间的最大值。

例题10　宇航员分组（Astronauts, LA 3713）

有A、B、C3个任务要分配给n个宇航员，其中每个宇航员恰好要分配一个任务。设所有n个宇航员的平均年龄为x，只有年龄大于或等于x的宇航员才能分配任务A；只有年龄严格小于x的宇航员才能分配任务B，而任务C没有限制。有m对宇航员相互讨厌，因此不能分配到同一任务。编程找出一个满足上述所有要求的任务分配方案。

【输入格式】

输入包含若干组数据。每组数据第一行为两个整数n和m（1≤n≤100000，1≤m≤100000）。以下n行为各飞行员的年龄（年龄为0～200的整数）。以下m行每行包含两个整数i和j（1≤i，j≤n），表示宇航员i和宇航员j相互讨厌。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出n行，即每个宇航员分配到的任务（用大写字母A，B，C表示）。如果无解，输出“No solution.”。

【分析】

不难发现每个宇航员只有两种选择，因此可以用布尔变量xi表示第i个宇航员的分配方案。年龄大于或等于x的宇航员要么做任务A（xi＝true），要么做任务C（xi＝false），而年龄严格小于x的宇航员要么做任务B（xi＝true），要么做任务C（xi＝false）。

下面考虑一对相互讨厌的宇航员i和j。如果他们属于同一种类型，那么xi和xj必须不相同。这个条件看起来简单，但却无法用一个子句表达，需要用两个，即alt前者表示二者至少有一个true，后者表示二者至少有一个false，因此不可能同时为true或同时为false。如果属于不同类型，那么唯一的冲突可能就是xi＝xj＝false，用子句alt可以排除这种情况。

每对冲突的宇航员对应不超过两个子句，因此我们得到了一个包含n个变量、不超过2m个子句的2-SAT问题实例。

5.3　最短路问题

在《算法竞赛入门经典》中我们曾经学过最短路问题及其经典算法——Dijkstra、Bellman-Ford和Floyd。请读者在继续阅读之前确保自己已熟练掌握了这3种算法。遗憾的是，限于篇幅，《算法竞赛入门经典》里没有相关例题，而形形色色的题目恰恰是图论中最优美、最吸引人的东西。本节在一定程度上弥补了这一缺憾，请读者细细品味。

5.3.1　再谈Dijkstra算法

单源最短路。在很多情况下，我们使用Dijkstra算法只是为了计算两点之间的最短路[11]，但Dijkstra的威力并不止这些，它可以计算从起点出发到每个点的最短路，即单源最短路（Single-Source Shortest Paths, SSSP）。这一特点使得Dijkstra常常用来进行其他算法的预处理。为了更好地和图论其他算法统一，从本章开始，本书使用结构体来保存加权图中的有向边[12]（也称弧），方法如下。

这里的dist是距离（distance）的缩写。这里的“距离”是一个抽象概念，如果要求的是“最短总时间”，那么这个“距离”就是指时间。在后面的小节中，我们还会在这个结构体里添加更多的内容，比如网络流中的容量、费用等。但不管怎样，图的存储结构都是类似的。下面是Dijkstra算法的完整代码。

例题11　机场快线（Airport Express, UVa 11374）

在Iokh市中，机场快线是市民从市内去机场的首选交通工具。机场快线分为经济线和商业线两种，线路、速度和价钱都不同。你有一张商业线车票，可以坐一站商业线，而其他时候只能乘坐经济线。假设换乘时间忽略不计，你的任务是找一条去机场最快的线路。

【输入格式】

输入包含多组数据。每组数据第一行为3个整数N，S和E（2≤N≤500，1≤S，E≤100），即机场快线中的车站总数，起点和终点（即机场所在站）编号。下一行包含一个整数M（1≤M≤1000），即经济线的路段条数。以下M行每行3个整数X，Y，Z（1＜＝X，Y＜＝N，1≤Z≤100），表示可以乘坐经济线在车站X和车站Y之间往返，其中单程需要Z分钟。下一行为商业线的路段条数K（1≤K≤1000），以下K行是这些路段的描述，格式同经济线。所有路段都是双向的，但有可能必须使用商业车票才能到达机场。保证最优解唯一。

【输出格式】

对于每组数据，输出3行。第一行按访问顺序给出经过的各个车站（包括起点和终点），第二行是换乘商业线的车站编号（如果没用商业线车票，输出Ticket Not Used），第三行是总时间。

【分析】

因为商业线只能坐一站，所以可以枚举坐的是哪一站，比较所有可能性下的最优解。如果可以在O（1）时间内计算出每种方案对应的最优方案，整个问题就可以在O（K）时间内得到解决。

假设我们用商业线车票从车站a坐到车站b，则从起点到a、从a到终点这两部分路线对于“经济线网络”来说都应该是最短路。换句话说，我们只需要从起点开始、到终点结束做两次单源最短路，记录下从起点到每个点x的最短时间f（x）和从每个点x到终点的最短时间g（x），则总时间为f（a）＋T（a，b）＋g（b），其中T（a，b）为从a坐一站商业线到达b的时间。

算上预处理时间O（mlogn），本算法的总时间复杂度为O（mlogn＋k）。

路径统计。用Dijkstra还可以枚举两点之间的所有最短路，以及统计最短路的条数。枚举最短路的方法是这样的：先求出所有点到目标点的最短路长度d[i]，然后从起点开始出发行走，但只沿着d[i]＝d[j]＋w（i，j）的边走。不难证明，如果一直顺着这样的边走，走到目标点时，走出的一定是最短路；反过来，只要有一次走的不是这样的边，走到目标点时，走出的一定不是最短路。有了这样的结论，计数也不是难事，令f[i]表示从i到目标点的最短路的条数，则f[i]＝sum｛f[j]｜f[i]＝d[j]＋w（i，j）｝，边界条件为终点的f值等于1。

例题12　林中漫步（A Walk through the Forest, UVa 10917）

Jimmy下班需要穿过一个森林。劳累一天后在森林中漫步是件非常惬意的事，所以他打算每天沿着一条不同的路径回家，欣赏不同的风景。但他也不想太晚回家，因此他不打算走“回头路”。换句话说，他只沿着满足如下条件的道路（A，B）走：存在一条从B出发回家的路径，比所有从A出发回家的路径都短。你的任务是计算一共有多少条不同的回家路径。

【输入格式】

输入包含多组数据。每组数据第一行为n，m（1≤n≤1000），即交叉点的数目和道路的数目。交叉点编号为1～n，公司的编号为1，家的编号为2。以下m行每行3个整数a，b，d，表示有一条连接a和b的双向道路，长度为d（1≤d≤10⁶）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出路径的条数。输入保证答案不超过2³¹－1。

【分析】

首先求出从每个点u回家的的最短路长度d[u]，则题目中的条件“存在一条从B出发回家的路径，比所有从A出发回家的路径都短”实际上是指d[B]＜d[A]。这样，我们创建一个新的图，当且仅当d[B]＜d[A]时加入有向边A→B，则题目的目标就是求出起点到终点的路径条数。因为这个图是DAG，可以用动态规划求解。

最短路树。用Dijkstra算法可以求出单源最短路树，方法是在发现d[i]＋w（i，j）＜d[j]时除了更新d[j]之外还要设置p[i]＝j。这样，把p看成是父指针，则所有点形成了一棵树[13]。这样，要从起点出发沿最短路走到任意其他点，只需要顺着树上的边走即可。前面的Dijkstra算法的代码已经求出了p数组。

例题13　战争和物流（Warfare And Logistics, LA 4080）

给出一个n个结点m条边的无向图，每条边上有一个正权。令c等于每对结点的最短路长度之和。例如n＝3时，c＝d（1，1）＋d（1，2）＋d（1，3）＋d（2，1）＋d（2，2）＋d（2，3）＋d（3，1）＋d（3，2）＋d（3，3）。要求删除一条边后使得新的c值c'最大。不连通的两点的最短路长度视为L。

【输入格式】

输入包含多组数据，每组数据第一行为3个整数n，m，L（1＜n≤100，1≤m≤1000，1≤L≤10⁸）。以下m行每行包含3个整数a，b，s，表示结点a和结点b有一条长度为s的双向道路相连。

【输出格式】

对于每组数据，输出c的原始值和删除一条边后的最大值c'。

【分析】

如果用floyd算法计算c，每尝试着删除一条边都要重新计算一次，时间复杂度为O（n³m），很难承受。如果用n次Dijkstra计算单源最短路，时间复杂度为O（nm²logn）。虽然看上去比O（n³m）略好，但由于floyd算法的常数很小，实际运行时间差不多。这时候，最短路树派上用场了。因为在源点确定的情况下，只要最短路树不被破坏，起点到所有点的距离都不会发生改变。换句话说，只有删除最短路树上的n－1条边，最短路树才需要重新计算。这样，对于每个源点，最多只需要求n次而不是m次单源最短路，时间复杂度降为O（n²mlogn），可以承受。另外，本题有一个不容易发现的“陷阱”：如果有重边，删除一条边时，应该用第二短的边代替。

例题14　过路费（加强版）（Toll! Revisited, UVa 10537）

运送货物需要缴纳过路费。进入一个村庄需要缴纳1个单位的货物，而进入一个城镇时，每20个单位的货物中就要上缴1个单位（比如，携带70个单位货物进入城镇，需要缴纳4个单位）。如图5-10所示，由于你必须途径一个城镇和两个村庄才能到达目的地，为了运送66把勺子，你必须携带76把勺子出发。

图　5-10

当起点与终点固定时，到达目的地的路线往往不唯一。如图5-11所示，运送39把勺子的最佳路线是A→b→c→X，而运送10把勺子的最佳路线是A→D→X。这里，大写字母表示城镇，小写字母表示村庄。

图　5-11

你的任务是找一条缴纳过路费最少的路线。

【输入格式】

输入包含多组数据。每组数据第一行为整数n，即地图上道路的条数（n≤0）。以下n行每行包含两个字母，即每条道路两端的村庄或城镇编号。大写字母表示城镇，小写字母表示村庄。道路都是双向的。接下来的一行包含一个整数p（0＜p≤10⁹）和两个字母，即运送的货物数量、起点和终点。输入保证货物可以从起点运送到终点。输入结束标志为n=-1。

【输出格式】

对于每组数据，输出3行，分别为测试数据编号、需要的货物数量的最小值，以及对应的路线。如果有多条路线，输出字典序最小的。

【分析】

尽管和普通的最短路不尽相同，本题仍然可以用Dijkstra算法的思路解决，所不同的是需进行逆推而非顺推。令d[i]表示进入结点i之后，至少要有d[i]单位的货物，到达目的地时货物数量才足够，则每次选择一个d[i]最小的未标号结点，更新它的所有前驱结点的d值即可算出所有的d值，然后在输出路径时根据d值可以直接构造字典序最小解。需要注意的是，d[i]可能会超过无符号32位整数的范围，需要用long long。

5.3.2　再谈Bellman-Ford算法

Bellman-Ford算法的一个重要应用是判负圈。在迭代n－1次后如果还可以进行松弛操作，说明一定存在负圈。如果采用队列实现，那么当某个结点入队了n次时可以判断出存在负圈，代码如下。

例题15　在环中（Going in Cycle!!, UVa 11090）

给定一个n个点m条边的加权有向图，求平均权值最小的回路。

【输入格式】

输入第一行为数据组数T。每组数据第一行为图的点数n和边数m（n≤50）。以下m行每行3个整数u，v，w，表示有一条从u到v的有向边，权值为w。输入没有自环。

【输出格式】

对于每组数据，输出最小平均值。如果无解，输出“No cycle found.”。

【分析】

使用二分法求解。对于一个猜测值mid，只需要判断是否存在平均值小于mid的回路。如何判断呢？假设存在一个包含k条边的回路，回路上各条边的权值为w₁，w₂，…，wk，那么平均值小于mid意味着w₁＋w₂＋…＋wk＜k*mid，即：

换句话说，只要把每条边（a，b）的权w（a，b）变成w（a，b）-mid，再判断新图中是否有负权回路即可。代码如下[14]。

本题还有一个称为Karp算法的专门算法，时间复杂度仅为O（mn），但只适用于强连通图，在应用到本题时需要先找出强连通分量，有兴趣的读者可以一试。

例题16　Halum操作（Halum, UVa 11478）

给定一个有向图，每条边都有一个权值。每次你可以选择一个结点v和一个整数d，把所有以v为终点的边的权值减小d，把所有以v为起点的边的权值增加d，最后要让所有边权的最小值非负且尽量大。

【输入格式】

输入包含若干组数据。每组数据第一行为两个整数n和m（n≤500，m≤2700），即点和边的个数。以下m行每行包含3个整数u，v，d，即有一条以u为起点v为终点的边，权值为d（1≤u，v≤n，-10000≤d≤10000）。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出边权最小值的最大值。如果无法让所有边权都非负，输出“No Solution”；如果边权最小值可以任意大，输出“Infinite”。

【分析】

注意，不同的操作互不影响，因此可以按任意顺序实施这些操作。另外，对于同一个结点的多次操作也可以合并，因此可以令sum（u）为作用于结点u之上的所有d之和。这样，本题的目标就是确定所有的sum（u），使得操作之后所有边权的最小值尽量大。

“最小值最大”又让我们想到了二分答案。二分答案x之后，问题转化为是否可以让操作完毕后每条边的权值均不小于x。对于边a→b，不难发现操作完毕后它的权值为w（a，b）＋sum（a）－sum（b），因此每条边a→b都可以列出一个不等式w（a，b）＋sum（a）－sum（b）≥x，移项得sum（b）－sum（a）≤w（a，b）－x。这样，我们实际上得到一个差分约束系统（system of difference constraints）。

所谓差分约束系统是指一个不等式组，每个不等式形如xj－xi≤bk，这里的bk是一些事先已知的常数。注意到这个不等式类似于最短路中的不等式d[v]≤d[u]＋w（u，v），我们可以用最短路算法求解：对于约束条件xj－xi≤bk，新建一条边i→j，权值为bk，再加一个源点s，从s出发与所有其他点相连，权值为0。在这个图上运行Bellman-Ford算法，则源点s到每个点i的距离就是xi的值。如果Bellman-Ford算法失败，即图中有负权环，则差分约束系统无解（想一想，为什么）。

考虑下面的不等式组（注意存在冗余不等式）：

对应的图如图5-12所示。

图　5-12

注意，在用Bellman-ford算法判断负圈时，每条边的权值都减少了x，是不是和例题15很像？本题和“平均值最大回路”问题是否有联系呢？这个问题留给读者思考。

5.3.3　例题选讲

例题17　蒸汽式压路机（Steam Roller, LA 4128）

有一个r条横线和c条竖线组成的网格，你的任务是开着一辆蒸汽式压路机，用最短时间从起始点（r1，c1）出发，最后停到目的地（r2，c2）。其中一些线段上有权值，代表全速通过需要的时间。权值为0的边不能通过。

由于蒸汽式压路机的惯性较大，对于任意一条边，如果在进入这条边之前刚转弯，或者离开这条边以后立即需要转弯，则实际时间为理想值的两倍。同样的道理，整条路线的起点和终点所在的边也需要两倍时间。注意，“时间加倍”规则不可叠加。比如，若离开整条路线的第一条边之后立即转弯，第一条边所花的时间仍是理想值的两倍而不是4倍。

如图5-13所示。

图　5-13

沿着理想值为9的边走，总时间为18＋18＋18＋18＋18＋18＝108，而沿着理想值为10的边走，总时间为20＋10＋20＋20＋10＋20＝100。

【输入格式】

输入包含多组数据。每组数据第一行包含6个整数R，C，r1，c1，r2，c2（1≤r1，r2≤R≤100，1≤c1，c2≤C≤100）。接下来有2RC-R-C个整数，从上到下从左到右给出各条边的权值，0表示此路不通，在其他情况下，权值为不超过10000的正整数，代表全速通过的时间。输入结束标志为R＝C＝r1＝c1＝r2＝c2＝0。

【输出格式】

对于每组数据，输出最短时间。如果无法到达，则输出“Impossible”。

【分析】

把每个点（r，c）拆成8个点（r，c，dir，doubled），分别表示上一步从上下左右的哪个方向（dir）移动到这个点，以及移动到这个点的这条边的权值是否已加倍（double）。注意，我们规定总是在“已确定某个权值会加倍”的时刻立即给权值加倍，因此在如图5-14（a）所示的情况下，边（u，v）未加倍，但在图5-14（b）中会加倍。灰色的结点v表示当前点，u是上一个经过的点，下同。

图　5-14

在构图时，每个点最多有4个后继点（分别对应上、下、左、右4个方向）。需要小心考虑当前的方向与上一步方向是否一致，以及当前点的权值是否已经加倍来计算后继结点。如图5-15所示是3种典型的情况。

图　5-15

在图5-15（a）中，需要再累加一次（u，v）的权值；图5-15（b）不需要累加权值，因为（u，v）是全速通过；而在图5-15（c）的情况下也不需要累加权值，因为已经加过一次了。

接下来只需对这8rc个点求最短路。需要注意的是，本题有两个不易想到的细节。

（1）可能先往左移动若干步后又往右移动。因为权值加倍的限制，这样可能比直接往右移动的权值之和要小，上下方向也是类似的。这意味着我们要老老实实地构图，不要想当然地删除一些看上去没用的边。

（2）即使走到了终点（r2，c2），路线也不一定结束，因为有可能在走到（r2，c2）时再往前移动一格然后回退一格，使得走到（r2，c2）的那条边不用加倍而权值之和更小。这意味着我们必须等（r2，c2）的8个分裂点全部标号完毕之后才能终止程序。

还有另外一种构图方法。将一个点拆成5个点（r，c，dir），代表到达（r，c），并且正在沿着方向dir走。增加一个新的方向——静止，然后规定不能直接改变方向，只能先改成静止，再改成新的方向。这样一来，“动点”有“停下来”和“继续走”两种决策，而“静点”只有一种决策，即“开始走”。上述3种决策中，当且仅当“继续走”时权值不需要加倍，而开始和停止时权值需要加倍。

这样就可以了吗？很可惜，漏掉了一种情况，那就是“开始走了一条边以后停下来”。如果用刚才的模型，这条边会加倍两次，不符合题意。解决方法是为“静点”增加一种决策，即“走一条边后立即停止”。

例题18　低价空中旅行（Low Cost Air Travel, World Finals 2006, LA 3561）

很多航空公司都会出售一种联票，要求从头坐，上飞机时上缴机票，可以在中途任何一站下飞机。比如，假设你有一张“城市1→城市2→城市3”的联票，你不能用来只从城市2飞到城市3（因为必须从头坐），也不能先从城市1飞到城市2，再用其他票飞到其他城市玩，回到城市2后再用原来的机票飞到城市3（因为机票已经上缴）。

这里有一个例子。假设有3种票，每种票的情况如下所示。

❑　票1：城市1→城市3→城市4，票价225美元

❑　票2：城市1→城市2，票价200美元

❑　票3：城市2→城市3，票价50美元

你想从城市1飞到城市3，有两种方法可以选择。买票1，只飞第一段；买票2和3，通过城市2中转。显然，第一种方法比较省钱，虽然浪费了一段。

给出票的信息，以及一个或多个行程单，你的任务是买尽量少的票（同一种票可以买多张），使得总花费最小。输入保证行程总是可行的。行程单上的城市必须按顺序到达，但中间可以经过一些辅助城市。

【输入格式】

输入包含多组数据。每组数据第一行为一个整数NT，即联票的种类数。以下NT行每行为一个联票描述，其中第一个整数为票的价格，然后是联票上城市的数目以及这些城市的整数编号（按顺序给出）。接下来为一个整数NI，即需要计算最小花费的行程单数目。以下NI行每行为一个行程单，其中第一个整数为行程单上的城市数目（包括起始城市），以及这些城市的编号（按顺序给出）。输入保证每组数据最多包含20种联票和20个行程单，每张票或者行程单上有至少2个，最多10个城市。票价不超过$10000。联票或者行程单上的相邻城市保证不同。票和行程单都从1开始编号。输入结束标志为NT＝0。

【输出格式】

对于每组数据的每张行程单，输出最小花费和对应的方案（见样例输出）。输出保证唯一。

【分析】

要依次经过行程单上的各个城市，那么是不是可以先求出每两个相邻城市的最小花费，再加起来呢？不可以，因为不一定每两个相邻城市都是用一张或多张完整的票。比如行程单上是城市1→城市2→城市3，只有一张票：城市1→城市2→城市3。在这样的情况下，单独从城市2到城市3是无解的。

正确的方法是把“目前已经经过了行程单上的几个城市”i（不超过10）作为状态的一部分，再加上当前城市编号j（注意，最多可能有400个城市），以合起来的（i，j）为结点构图。构图方法是考虑每张机票的每种用法（飞前面几段），计算出新的状态。

比如，有一张票FABCAD，行程单为BA，则以（0，F）为起点有5条边，分别指向（0，A），（1，B），（1，C），（2，A）和（2，D）。

还有几点需要注意：本题最坏情况下可能会用200张票，金额超过100万美元。另外，虽然题目说答案唯一，但这并不代表图上的最短路是唯一的，因为就算是以相同的顺序使用相同的票，每张票使用的部分也可以不同。

例题19　动物园大逃亡（Animal Run, Beijing 2006, LA 3661）

由于控制程序出了bug，动物园的笼子无缘无故被打开，所有动物展开了一次大逃亡。整个城市是一个网格，另外每个单位方格都有一条从左上到右下的对角线，其中动物园在左上角，动物们的目的地是右下角。所有道路（即网格的边和对角线）都是双向的。

每条道路上都有一个正整数权值，代表拦截这条边所需要的工作人员数，如图5-16所示。你的任务是派尽量少的工作人员，使动物无法从动物园走到目的地（动物只能经过没有被拦截的边）。

图　5-16

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（3≤n，m≤1000），即网格的行数和列数。以下n行每行m－1个整数，即拦截每条横边所需的人数。以下n－1行每行有m个整数，即拦截每条竖边所需的人数。以下n－1行每行m－1个整数，表示拦截每条对角线边所需的人数。上述边的排列顺序为从上到下从左到右。所有权值均为不超过10⁶的正整数。输入结束标志为n＝m＝0。输入文件大小约为16MB。

【输出格式】

对于每组数据，输出需要派遣的工作人员总数。

【分析】

如果把网格的交叉点看做结点，横竖线和斜线看成边，则本题转化为经典的最小割问题。很可惜，由于数据规模太大，常用的最大流算法对于本题而言实在是太慢了。怎么办呢？注意到本题的图是一张平面图，“左上角无法走到右下角”实际上等价于“障碍物从左／下连到右／上”，如图5-17所示（为了使图清晰，斜线已略去）。

图　5-17

无论是上述哪种情况，中间的线都是不能断的，否则无法成功拦截；另一方面，任何多余的边都是不需要拦截的（因为“需要拦截的人数”是非负的），因此问题转化为从左／下边界到右／上边界的最短路问题。

把每条道路看做一个结点，每个三角形的3条道路两两直接相连，于是得到了一个结点和边均有O（nm）个的点带权图。初始化时，令左边界与下边界的所有道路的d值为0，求一次Dijkstra之后，找出右边界与上边界上所有道路的d值的最小值即可。

5.4　生成树相关问题

《算法竞赛入门经典》中已经给出了最小生成树的Kruskal算法。为了更好地理解最小生成树问题，下面不加证明地给出两个性质。

（1）切割性质。假定所有边权均不相同。设S为既非空集也非全集的V的子集，边e是满足一个端点在S内，另一个端点不在S内的所有边中权值最小的一个，则图G的所有生成树均包含e。

（2）回路性质。假定所有边权值均不相同。设C是图G中的任意回路，边e是C上权值最大的边，则图G的所有生成树均不包含e。下面将介绍生成树的相关问题。

增量最小生成树。从包含n个点的空图开始，依次加入m条带权边。每加入一条边，输出当前图中的最小生成树权值（如果当前图不连通，输出无解）。

解：如果每次重新求解完整的最小生成树问题，总时间复杂度高达O（m²logn）。有没有高效的方法呢？答案是肯定的。根据回路性质，可以得到如下的改进算法：每次求出新的最小生成树后，把其他所有边删除。由于每次只需计算一个n条边（原生成树有n－1条，新加入一条）的图的最小生成树，Kruskal算法的时间复杂度降为O（nlogn），总时间为O（nmlogn）。

这个算法还可以进一步改进为，加入一条边e＝（u，v）后，图中恰好包含一个环。根据回路性质，删除该回路上权值最大的边即可，因此只需在加边之前的MST上找到u到v的唯一路径上权值最大的边，再和e比较，删除权值较大的一条。由于路径唯一，可以用DFS或者BFS找到这条u到v的路径，总时间复杂度为O（nm）。

最小瓶颈生成树。给出加权无向图，求一棵生成树，使得最大边权值尽量小。

解：由于只关心最大边权值，我们可以从一个空图开始，按照权值从小到大的顺序依次加入各条边，则图第一次连通时，该图的最小生成树就是原图的最小瓶颈生成树。细心的读者可能看出了，这个过程不就是Kruskal算法么？没错，原图的最小生成树就是一棵最小瓶颈生成树[15]。

最小瓶颈路。给定加权无向图的两个结点u和v，求出从u到v的一条路径，使得路径上的最长边尽量短。

解：这个问题可以用二分法与BFS法解决，但我们有更好的算法。先求出这个图的最小生成树，则起点和终点在树上的唯一路径就是我们所要找的路径，这条路径上的最长边就是问题的答案。证明方法和上面几乎相同，读者不妨一试。

每对结点间的最小瓶颈路。给出加权无向图，求每两个结点u和v之间的最小瓶颈路的最大边长f（u，v）。

解：有了前面的经验，不难想到先求出最小生成树。接下来，用DFS把最小生成树变成有根树，同时计算f（u，v），当新访问一个结点u时，考虑所有已经访问过的老结点x，更新f（x，u）＝max（f（x，v），w（u，v）），其中v是u的父结点。每个f（u，v）只需常数时间计算，因此时间复杂度为O（n²）。

次小生成树。把所有生成树按照权值之和从大到小的顺序排列，求排在第二位的生成树。注意，如果最小生成树不唯一，次小生成树的权值和最小生成树相同。

解：次小生成树不会和最小生成树完全相同，因此可以枚举最小生成树中不在次小生成树中出现的边。注意最小生成树只有n－1条边，所以只需枚举n－1次。每次在剩下的边里求一次最小生成树，则这n－1棵“缺一条边的图”的最小生成树中权最小的就是原图的最小生成树，时间复杂度为O（nmα（n，m））（注意边只需排序一次）。

还有一种更好的方法：枚举要加入哪条新边。在最小生成树上加一条边u－v之后，图上会出现一条回路，因此删除的边必须在最小生成树上u到v的路径上，而且是这条路径上的最长边。可以证明，次小生成树一定可以由最小生成树加一条边再删一条边[16]得到，因此只需按照“每对结点之间的最小瓶颈路”的方法求出每对结点u和v在最小生成树中唯一路径的最大边权maxcost[u][v]，则剩下部分只需要O（m）时间（枚举所有m－n＋1条边进行交换，每次花O（1）时间求出新生成树的权值）。总时间复杂度为O（n²）。

最小有向生成树。给定一个有向带权图G和其中一个结点u，找出一个以u为根结点，权和最小的有向生成树。有向生成树（directed spanning tree）也叫树形图（arborescence），是指一个类似树的有向图，满足以下条件。

❑　恰好有一个入度为0的点，称为根结点。

❑　其他结点的入度均为1。

❑　可以从根结点到达所有其他结点。

不难发现，如果树型图的结点数为n，它的边数一定为n－1，且树型图中不存在有向环。

解：固定根的最小树形图可以用朱－刘算法解决。首先是预处理，删除自环[17]并判断根结点是否可以到达其他结点。如果不是，输出无解并终止程序。

接下来是算法的主过程。首先，给所有非根结点选择一条权最小的入边。如果选出来的n－1条边不构成圈，则可以证明这些边就形成了一个最小树形图，否则把每个圈各收缩成一个点，继续上述过程。

缩圈之后，圈上所有边都消失了，因此在最终答案里需要累加上这些边权之和。但这样做有个问题：假设在算法的某次迭代中，把圈C收缩为了人工结点v，则在下一次迭代中，给v选择的入弧将与v在圈C中的入弧发生冲突。如图5-18所示，X在圈中已经有了入弧Y→X；如果收缩之后又选了一个入弧Z→X，必须把弧Y→X从最小树型图中删掉。这等价于把弧Z→X的权值减小了Y→X的权值。

图　5-18

例题20　秦始皇修路（Qin Shi Huang's National Road System, Beijing 2011, LA 5713）

秦朝有n个城市，需要修建一些道路使得任意两个城市之间都可以连通。道士徐福声称他可以用法术修路，不花钱，也不用劳动力，但只能修一条路，因此需要慎重选择用法术修哪一条路。秦始皇不仅希望其他道路的总长度B尽量短（这样可以节省劳动力），还希望法术连接的两个城市的人口之和A尽量大，因此下令寻找一个使A/B最大的方案。你的任务是找出这个方案。

任意两个城市之间都可以修路，长度为两个城市之间的欧几里得距离。

【输入格式】

输入第一行为数据组数t（t≤10）。每组数据第一行为城市数目n（3≤n≤1000），以下n行每行3个整数X，Y，P，即一个城市坐标在（X，Y），人口为P（0≤X，Y≤1000，1≤P≤100000）。输入保证所有城市位置均不同。

【输出格式】

对于每组数据，输出A/B的最大值，四舍五入保留两位小数。

【分析】

本题和前面讲过的《机场快线》一题有些类似。法术只能修一条路，不妨枚举它两端的城市u和v。如果可以在O（1）时间内算出“在原图中删除边u－v后的最小生成树权值”，就可以在O（n²）时间内解决本题了。

这个问题看上去和次小生成树有些相像？是的，本题也可以采取相同的方法，先求出最小生成树，在枚举边u－v后删除最小生成树中u和v之间唯一路径上的最大权maxcost[u][v]。只需在预处理时用O（n²）时间算出maxcost数组，问题就得到了解决。

例题21　邦德（Bond, UVa 11354）

有n座城市通过m条双向道路相连，每条道路都有一个危险系数。你的任务是回答若干个询问，每个询问包含一个起点s和一个终点t，要求找到一条从s到t的路，使得途径所有边的最大危险系数最小。

【输入格式】

输入包含最多5组数据。每组数据第一行为n和m（2≤n≤50000，1≤m≤100000）。以下m行每行包含3个整数x，y，d（1≤x，y≤n，0≤d≤10⁹），即城市x和城市y之间有一条危险系数为d的道路（城市编号为1～n）。下一行包含一个整数Q（1≤Q≤50000），以下Q行每行包含两个整数s，t（1≤s，t≤n，s!＝t），即询问的起点和终点。

【输出格式】

对于每个询问，输出最优路线上所有边的危险系数的最大值。不同数据之间输出一个空行。

【分析】

本题也是要求瓶颈路，但需要快速回答很多查询。这很像前面数据结构部分的题目，因此也可以考虑先做预处理，把信息组织成某种易于查询的结构。

首先求出最小生成树，并把它改写成有根树，让fa[i]和cost[i]分别表示结点i的父亲编号和它与父亲之间的边权，L[i]表示结点i的深度（根结点深度为0，根结点的子结点深度为1，依此类推）。前面已经介绍过将无根树转化为有根树的方法，这里略去。

接下来，通过预处理计算出数组anc和maxcost数组，其中anc[i][j]表示结点i的第2j级祖先编号（anc[i][0]就是父亲fa[i]，anc[i][j]=-1表示该祖先不存在），maxcost[i][j]表示结点i和它的2j级祖先之间的路径上的最大权值。预处理代码如下。

有了这些预处理，我们就可以编写查询处理过程了。假定查询的两个结点为p和q，并且p比q深（如果不是的话，交换），则可以先把p提升到和q处于同一级的位置，然后利用类似二进制展开的方法不断把p和q同时往上“提”（要保证二者深度始终相等），同时更新最大边权。代码如下。

上述代码也能求出p和q的最近公共祖先（见注释）。这样，就在预处理O（nlogn），查询O（logn）的时间内解决了本题。

例题22　比赛网络（Stream My Contest, UVa 11865）

你需要花费不超过cost元来搭建一个比赛网络。网络中有n台机器，编号为0～n－1，其中机器0为服务器，其他机器为客户机。一共有m条可以使用的网线，其中第i条网线的发送端是机器ui，接受端是机器vi（数据只能从机器ui单向传输到机器vi），带宽是bi Kbps，费用是ci元。每台客户机应当恰好从一台机器接受数据（即恰好有一条网线的接收端是该机器），而服务器不应从任何机器接收数据。你的任务是最大化网络中的最小带宽。

【输入格式】

输入第一行为数据组数T（T≤50）。每组数据第一行为3个整数n，m，cost（1≤n≤60，1≤m≤10000，1≤cost≤10⁹）。以下m行每行用4个整数u，v，b，c（0≤u，v＜n，1≤b，c≤10⁶，u!＝v）描述一条网线。

【输出格式】

对于每组数据，输出最小带宽的最大值。如果无法搭建网络，输出“streaming not possible.”（不含引号）。

【分析】

如果已知最小带宽，则问题转化为：如果禁用小于此带宽的网线，是否可以成功搭建网络？利用最小树形图，不难作出判断。只需求出从0出发的最小树形图，判断权值和是否超过cost即可。由于最小带宽越小越容易有解，所以只需二分这个最小带宽。注意在求最小树形图之前不要忘记前面提到的两个预处理。

5.5　二分图匹配

本节介绍二分图匹配。虽然很多问题都可以借助最大流或者最小费用流算法解决，但这里介绍的方法更简单，速度也更快。和前面的内容一样，这些算法思想本身也是很有启发性的，请不要简单地把它们看做黑盒算法，只知其然而不知其所以然。

5.5.1　二分图最大匹配

在图论中，匹配是指两两没有公共点的边集。二分图最大匹配问题是这样的：给出一个二分图，找一个边数最大的匹配，即选尽量多的边，使得任意两条选中的边均没有公共点。如图5-19所示就是一个最大匹配。注意，如果所有点都是匹配点（匹配中的某一条边的端点），则称这个匹配是完美匹配（perfect matching）。

为方便叙述，在本书中我们总是把二分图的两个结集称为X和Y，有时也称左边和右边，其中左边是X集，右边是Y集，如图5-19所示。

图　5-19

这个问题可以用后面即将介绍的网络流解决，但用增广路算法更加简洁。

增广路定理。为了叙述增广路算法，我们首先学习增广路定理。为方便叙述，我们用未盖点来表示不与任何匹配边邻接的点，其他点为匹配点，即恰好和一条匹配边邻接的点。从未盖点出发，依次经过非匹配边、匹配边、非匹配边、匹配边……所得到的路径称为交替路。注意，如果交替路的终点是一个未盖点，则称这条交替路为一条增广路。在增广路中，非匹配边比匹配边多一条。

增广路的作用是改进匹配。假设我们已经找到一个匹配，如何判断它是不是最大匹配呢？看增广路。如果有一条增广路，那么把此路上的匹配边和非匹配边互换，得到的匹配比刚才多一条边。反过来，如果找不到增广路，则当前匹配就是最大匹配（证明略）。这就是增广路定理，即一个匹配是最大匹配的充分必要条件是不存在增广路。注意，这个充要条件适合于任意图，不仅仅是二分图。

有个很有意思的游戏可以加深对增广路算法的理解。这是一个无向图上的游戏，Alice和Bob轮流操作，Alice先走。第一次可以任选一个点放一枚棋子，以后每次把棋子移动到一个相邻点上，并把棋子原先所在的点删除，谁不能移动就算输。若双方都采取最优策略，谁将取胜？

解：如果有完美匹配，则Alice输，因为Bob只需沿着匹配边走即可。否则Alice赢，因为任意求一个最大匹配，Alice把棋子放在任一个未盖点上，Bob都只能把它移动到已盖点上（否则得到增广路）。Alice沿着匹配边移动，下一步Bob又只能把它移到另一个已盖点上，只要Bob能移动，Alice就能移动。

增广路算法。根据增广路定理，最大匹配可以通过反复找增广路来求解。如何找增广路呢？根据定义，首先需要选一个未盖点u作为起点。不失一般性，设这个u是X结点。接下来，需要选一个从u出发的非匹配边（u，v），到达Y结点v。如果v是未盖点，说明我们成功地找到了一条增广路，否则说明v是匹配点，下一步得走匹配边。因为一个匹配点恰好和一条匹配边邻接，这一步没得选择。设匹配点v邻接的匹配边的另一个端为left[v]，则可以理解为从u直接走到了left[v]，而这个left[v]也是一个X结点。如果始终没有找到未盖点，最后会扩展出一棵所谓的匈牙利树。

这样，我们得到了一个算法，即每次选一个未盖点u进行DFS[18]。注意，如果找不到以u开头的增广路，则换一个未盖点进行DFS，且以后再也不从u出发找增广路。换句话说，如果以后存在一个从u出发的增广路，那么现在就找得到。证明并不复杂，留给读者思考。

5.5.2　二分图最佳完美匹配

假定有一个完全二分图G，每条边有一个权值（可以是负数）。如何求出权值和最大的完美匹配？Kuhn-Munkres算法（KM算法，也称为匈牙利算法）可以解决这个问题。为了学习这个算法，我们先来看两个概念。

可行顶标是一个结点函数l，使得对于任意弧（x，y），有l（x）＋l（y）≥w（x，y）。相等子图是G的生成子图，包含所有点，但只包含满足l（x）＋l（y）＝w（x，y）的所有边（x，y）。关于这两个概念，有一个极为重要的定理，即如果相等子图有完美匹配，则该匹配是原图的最大权匹配。

这个定理看上去很高深，其实很容易证明。设M*是相等子图的完美匹配，根据定义有M*的权和等于所有点的顶标之和。另一方面，任取G的一个完美匹配M，由于M中的边只满足不等式w（x，y）≤l（x）＋l（y），M的权和不超过所有顶标之和，也就是M*的权和。

这样看来，问题的关键就是寻找好的可行顶标，使相等子图有完美匹配。

算法思路是这样的：任意构造一个可行顶标（比如，Y结点顶标为0，X结点的顶标为它出发所有边的最大权值），然后求相等子图的最大匹配，如果存在完美匹配，算法终止，否则修改顶标使得相等子图的边变多，有更大机会存在完美匹配。

如何修改顶标呢？我们仍然从匈牙利树入手。设匈牙利树中的X结点集为S，Y结点集为T，则S到T'没有边（否则匈牙利树可以继续生长）；S'到T的边都是非匹配边（想一想，为什么）；如果把S中所有点的顶标同时减少一个相同整数a，则S到T'中可能会有新边进入相等子图。为了保证S－T的匹配边不离开相等子图，还要把T中所有点的顶标同时增加a。如图5-20所示。

图　5-20

不难发现，应取a＝min｛l（x）＋l（y）－w（x，y）｜x in S，y in T'｝，因为如果S中每个顶标的实际减小值比这个值小，则不会有新边进入；如果比这个值大则顶标将变得不可行。

设边（x，y）进入相同子图，有以下两种情况。

情况一：y是未盖点，则找到增广路。

情况二：y和S中的点z匹配，则把z和y分别加入S和T中。

不难发现，每次修改顶标要么找到增广路，要么使匈牙利树增加两个结点，因此一共需要O（n²）次修改顶标操作。这样，问题关键就在于快速修改顶标。

最朴素的做法，是枚举S和T中的每个元素，根据定义计算最小值，每次修改的时间为O（n²），总时间O（n⁴）。第二种方法是给T中的每个结点y定义松弛量

则a的计算公式变为a＝min｛slack（y）｜y in T'｝。

每次增广后，需要用O（n²）时间计算所有点的初始slack。由于每次扩展匈牙利树时所有S－T弧的增量相同，因此修改每个slack值只需要常数时间，而计算所有slack值需要O（n）时间。这样，每次增广后最多修改n次顶标，因此每次增广后修改顶标总时间降为O（n²），总时间为O（n³）[19]。

下面给出O（n⁴）时间复杂度的实现，它清晰易懂，且实际效果并没有理论上那么糟糕。改写成O（n³）的代码留给读者练习。

例题23　蚂蚁（Ants, NEERC 2008, LA4043）

给出n个白点和n个黑点的坐标，要求用n条不相交的线段把它们连接起来，其中每条线段恰好连接一个白点和一个黑点，每个点恰好连接到一条线段，如图5-21所示。

图　5-21

【输入格式】

输入包含多组数据。每组数据第一行为整数n（1≤n≤100），以下n行每行包含两个整数，即各白点坐标；以下n行每行包含两个整数，即各黑点坐标。坐标均为绝对值不超过10000的整数。输入保证没有两个坐标相同的点，且任意3点不共线。所有白点和黑点均按照输入顺序编号为1～n。

【输出格式】

对于每组数据，输出n行，其中第i行为第i个白点所连接的黑点编号。

【分析】

因为结点有黑白两色，我们不难想到构造一个二分图，其中每个白点对应一个X结点，每个点对应一个Y结点，每个黑点和每个白点相连，权值等于二者的欧几里得距离。建模之后，最佳完美匹配就是问题的解。为什么呢？假设在最佳完美匹配中有两条线段a₁－b₁与a₂－b₂相交，那么dist（a₁，b₁）＋dist（a₂，b₂）一定大于dist（a₁，b₂）＋dist（a₂，b₁），因此如果把这两条线段改成a₁－b₂和a₂－b₁后总长度会变少，与“最佳”二字矛盾。换句话说，最佳匹配中不会出现线段相交的情况。这样，我们只需直接套用KM算法即可解决本题，非常方便[20]。

例题24　少林决胜（Golden Tiger Claw, UVa 11383）

给定一个N×N矩阵，每个格子里都有一个正整数w（i，j）。你的任务是给每行确定一个整数row（i），每列也确定一个整数col（i），使得对于任意格子（i，j），w（i，j）≤row（i）＋col（j）。所有row（i）和col（i）之和应尽量小。

【输入格式】

输入包含最多15组数据。每组第一行为正整数N（N≤500），以下N行每行N个整数，即各个格子的整数值（均为不超过100的正整数）。输入结束标志为文件结束符。

【输出格式】

对于每组数据，输出3行，第一行为从上到下各行的row（i）；第二行为从左到右各列的col（i），第三行为所有row（i）和col（i）的总和的最小值。如果有多种方案，任意输出一种即可。

【分析】

本题看上去和最佳匹配没有任何关系，事实上也确实没有关系。那为什么本题还会出现在这个小节中呢？因为虽然它和最佳匹配这个模型无关，却是KM算法的一个副产品。还记得等式l（x）＋l（y）≥w（x，y）吗？算法结束后，所有顶标之和是最小的。

例题25　固定分区内存管理（Fixed Partition Memory Management, World Finals 2001, LA 2238）

早期的多程序操作系统常把所有的可用内存划分成一些大小固定的区域，不同的区域一般大小不同，而所有区域的大小之和为可用内存的大小。给定一些程序，操作系统需要给每个程序分配一个区域，使得它们可以同时执行。可是每个程序的运行时间可能和它所占有的内存区域大小有关，因此调度并不容易。

编程计算最优的内存分配策略，即给定m个区域的大小和n个程序在各种内存环境下的运行时间，找出一个调度方案，使得平均回转时间（即平均结束时刻）尽量小。具体来说，你需要给每个程序分配一个区域，使得没有两个程序在同一时间运行在同一个内存区域中，而所有程序分配的区域大小都不小于该程序的最低内存需求。程序对内存的需求不会超过最大内存块的大小。

【输入格式】

输入包含多组数据。每组数据第一行为正整数m和n，即区域的个数和程序的个数（1≤m≤10，1≤n≤50）。下一行包含m个正整数，即各内存区域的大小。以下n行描述每个程序在各种内存大小中的执行时间，其中第一个整数为情况总数k（k≤10），然后是k对整数s₁，t₁，s₂，t₂，...，sk，tk，满足si＜si＋1。当内存不足s₁时程序无法运行；当内存大小s满足si≤s＜si＋1时运行时间为ti；如果内存至少为sk，运行时间为tk。输入结束标志为m＝n＝0。

【输出格式】

对于每组数据，输出最小平均回转时间和调度方案。格式参考样例输出。如果有多组解，任意输出一组即可。

【分析】

先来看一个内存区域的情况。假设在这个内存区域按顺序执行的k个程序的运行时间分别为t₁，t₂，t₃，…，tk，那么第i个程序的回转时间为ri＝t₁＋t₂＋…＋ti，所有程序的回转时间之和等于r＝kt₁＋（k－1）t₂＋（k－2）t₃＋…＋2tk－1＋tk。换句话说，如果程序i是内存区域j的倒数第p个执行的程序，则它对于总回转时间的“贡献值”为pTi，j，其中Ti，j为程序i在内存区域j中的运行时间。

这样一来，算法就比较明显了。构造二分图G，X结点为n个程序，Y结点为n×m个“位置”，其中位置（j，p）表示第j个内存区域的倒数第p个执行的程序。每个X结点i和Y结点（j，p）连有一条权为pTi，j的边，然后求最小权匹配即可。注意，并不是每个匹配都对应一个合法方案（比如，一个区域不能只有倒数第一个程序而没有倒数第二个程序），但最佳匹配一定对应一个合法方案（想一想，为什么）。

5.5.3　稳定婚姻问题

例题26　女士的选择（Ladies' Choice, SWERC 2007, LA 3989）

在一个盛大的校园舞会上有n位男生和n位女生，每人都对每个异性有一个排序，代表对他们的喜欢程度。你的任务是将男生和女生一一配对（每人恰好有一个舞伴），使得男生u和女生v不存在以下情况：（1）男生u和女生v不是舞伴；（2）他们喜欢对方的程度都大于喜欢各自当前舞伴的程度。如果出现了（2）中的情况，他们可能会擅自抛下自己的舞伴，另外组成一对。

你的任务是对于每个女生，在所有可能和她跳舞的男生中，找出她最喜欢的那个[21]。

【输入格式】

输入第一行为数据组数T。每组数据第一行为整数n（1≤n≤1000），以下n行每行包含n个1～n的整数，表示每个女生对所有男生的排序（例如，第一个整数就是该女生最喜欢的男生的编号）。以下n行为男生对女生的排序，格式同上。

【输出格式】

对于每组数据，输出n行，即每个女生在最好情况下的舞伴编号。相邻数据之间应输出一个空行。

【分析】

本题就是著名的稳定婚姻问题（stable marriage problem），只是把“结婚”和“配偶”换成了“跳舞”和“舞伴”。下面用原题中的术语来介绍这个问题的Gale-Shapley算法（Gale-Shapley Algorithm）。这个算法也称求婚-拒绝算法（Propose-and-reject algorithm），因为算法过程就是男士不停地求婚，女士不停地拒绝。

算法的详细过程是，在每一轮中，每个尚未订婚的男士在他还没有求过婚的女士中选一个自己最喜欢的求婚（不管她有没有订婚）。然后每个女士在向她求婚的人之中选择她最喜欢的一个订婚，并且拒绝其他人。注意，这些向她求婚的人中包含她的未婚夫，因此她可以选择另一个自己更喜欢的人订婚，而抛弃自己的现任未婚夫。

当所有人都订婚时，算法结束。接下来他们就可以挑个良辰吉日，举行集体婚礼。不过，结局一定是这样吗？有没有可能出现某个人会孤独终老的情况？幸运的是，这种情况不会发生，因为每个女士一旦订婚，将一直处于订婚状态，因此如果有女士始终没有订婚，只有一种可能，就是从来没有人向她求过婚。但这是不可能的，因为如果存在一个找不到伴侣的男士，在放弃之前一定会向所有女士求婚。

接下来让我们证明算法得到的方案是稳定的。考虑任意男士A和女士B，假设算法结束后A和C订婚，B和D订婚，是否有可能B（和喜欢D相比）更喜欢A，而A（和喜欢C相比）更喜欢B？如果A更喜欢B，那么在算法结束之前A一定已经向B求过婚了，并且B拒绝了她。注意到女士每次换未婚夫时，一定是更喜欢新的那个，所以B不可能认为她曾拒绝过的A比她的最终伴侣D更好。

这个算法看上去对男士们有些残酷，但结果却出人意料。所有男士都能娶到自己有可能娶到的最好的妻子，而女士却相反——只能嫁给自己有可能嫁到的最差的丈夫。这其实不难理解：男士按照自己喜欢的顺序来求婚，因此他在找到最终伴侣之前，已经确认了他更喜欢的女士不那么喜欢自己，可以死心了；但另一方面，女士是被动的，就算她已经拒绝了很多人，自己真正喜欢的那些人却有可能从来没有向她求过婚（因为她很喜欢的人，未必很喜欢她）。限于篇幅，这里不给出严密的证明，有兴趣的读者可以查阅相关资料。代码如下。

注意，上面的代码并没有像上面叙述的那样一轮一轮地进行（因为算法的证明中没有用到这一点），而是每次直接选一个光棍进行求婚，并且根据情况让求婚对象接受或者拒绝他即可。因为每个男士最多考虑每个女士各一次，而每次考虑的时间复杂度均为O（1），因此算法的总时间复杂度为O（n²），这已经是输入下限了。

5.5.4　常见模型

二分图匹配有一些直接应用，现仅举几例。

例题27　我是SAM（SAM I AM, UVa 11419）

给出一个R×C大小的网格，网格上面放了一些目标。可以在网格外发射子弹，子弹会沿着垂直或者水平方向飞行，并且打掉飞行路径上的所有目标，如图5-22所示。你的任务是计算出最少需要多少子弹，各从哪些位置发射，才能把所有目标全部打掉。

图　5-22

【输入格式】

输入包含多组数据。每组数据第一行为3个整数R，C和N（1≤R，C≤1000，1≤N≤10⁶），即网格的行数、列数和目标个数。以下N行每行包含两个整数ri和ci（1≤ri≤R，1≤ci≤C）即每个目标所在的行列编号。各行从上到下编号为1～R，各列从左到右编号为1～C。输入结束标志为R＝C＝N＝0。

【输出格式】

对于每组数据，输出一行。其中第一个整数表示最少需要的子弹数目，接下来是这些子弹的发射位置。用rx表示第x行，cx表示第x列。

【分析】

本题的模型是二分图最小覆盖，即选择尽量少的点，使得每条边至少有一个端点被选中。可以证明，最小覆盖数等于最大匹配数。

建模方法为，将每一行看作一个X结点，每一列看做一个Y结点，每个目标对应一条边。这样，子弹打掉所有目标意味着每条边至少有一个结点被选中。

如何构造解呢？我们需要借助于匈牙利树。从X中的所有未盖点出发扩展匈牙利树，标记树中的所有点，则X中的未标记点和Y中的已标记点组成了所求的最小覆盖。证明留给读者思考[22]。

例题28　保守的老师（Guardian of Decency, NWERC 2005, LA3415）

Frank是一个思想有些保守的高中老师。有一次，他需要带一些学生出去旅行，但又怕其中一些学生在旅途中萌生爱意。为了降低这种事情发生的概率，他决定确保带出去的任意两个学生至少要满足下面4条中的一条。

（1）身高至少相差40厘米。

（2）性别相同。

（3）最喜欢的音乐属于不同类型。

（4）最喜欢的体育比赛相同（他们很有可能是不同队伍的球迷，这样他们就可能聊得不愉快）。

你的任务是帮Frank挑选尽量多的学生，使得任意两个学生至少满足上述条件中的一条。

【输入格式】

输入第一行为测试数据组数T（T≤100）。每组数据第一行为学生总数n（n≤500）。以下n行每行描述一个学生，先是一个整数，表示身高（单位：厘米），然后是一个字符，表示性别（M为男性，F为女性），然后是一个字符串，即喜欢的音乐风格，最后是一个字符串，即最喜欢的体育比赛名称。上述字符串不超过100个字符，且不含空白符。

【输出格式】

对于每组数据，输出可以参加旅行的学生数目的最大值。

【分析】

本题的模型是二分图的最大独立集，即选择尽量多的结点，使得任意两个结点不相邻（即任意一条边的两个端点不会同时被选中）。最大独立集与最小覆盖是互补的，因此答案就是结点总数减去最大匹配数。

什么叫“互补”呢？就是说，把最小覆盖中的“已选点”和“未选点”互换，就可以得到最大独立集。为什么二者互补呢？让我们来看看二者的条件。

覆盖集：对于每条边，至少有一个点要被选中。

独立集：对于每条边，至少有一个点不被选中。

这样，每个覆盖集都和一个唯一的独立集互补，而每个独立集也都和一个唯一的覆盖集互补，因此最小覆盖集与最大独立集互补。

本题的建模方法为，将每个人看做一个结点，如果两个人4个条件都不满足，就意味着他们不能同时被选择，连一条无向边。这样，问题就转化为求这个图的最大独立集。因为每个人不是男生就是女生，所以这个图是二分图。

例题29　出租车（Taxi Cab Scheme, NWERC 2004, LA 3126）

你在一座城市里负责一个大型活动的接待工作。明天将有m位客人从城市的不同位置出发，到达他们各自的目的地。已知每人的出发时间、出发地点和目的地，你的任务是用尽量少的出租车送他们，使得每次出租车接客人时，至少能提前一分钟到达他所在的位置。注意，为了满足这一条件，要么这位客人是这辆出租车接送的第一个人，要么在接送完上一个客人后，有足够的时间从上一个目的地开到这里。

为简单起见，假定城区是网格型的，地址用坐标（x，y）表示。出租车从（x₁，y₁）处到（x₂，y₂）处需要行驶｜x₁－x₂｜＋｜y₁－y₂｜分钟。

【输入格式】

输入第一行为测试数据组数T。每组数据第一行为客人数目m（0≤m＜500）。以下m行每行描述一个客人的行程，先是出发时间（格式为hh:mm，范围从00:00—23:59），然后是起点坐标和终点坐标。坐标均为小于200的非负整数。

【输出格式】

对于每组数据，输出一行，即最少需要的出租车数目。

【分析】

本题的模型是DAG的最小路径覆盖。所谓最小路径覆盖，就是在图中找尽量少的路径，使得每个结点恰好在一条路径上（换句话说，不同的路径不能有公共点）。注意，单独的结点也可以作为一条路径。

在本题中，“时间”是一个天然的序，因此可以构图如下：每个客人是一个结点，如果同一个出租车在接完客人u以后来得及接客人v，连边u→v。不难发现，这个图是一个DAG，并且它的最小路径覆盖就是本题的答案。

DAG最小路径覆盖的解法如下：把所有结点i拆为X结点i和Y结点i'，如果图G中存在有向边i→j，则在二分图中引入边i→j'。设二分图的最大匹配数为m，则结果就是n－m。为什么呢？因为匹配和路径覆盖是一一对应的。对于路径覆盖中的每条简单路径，除了最后一个“结尾结点”之外都有唯一的后继和它对应（即匹配结点），因此匹配数就是非结尾结点的个数。当匹配数达到最大时，非结尾结点的个数也将达到最大。此时，结尾结点的个数最少，即路径条数最少。

需要注意的是，本算法也适用于带权的DAG，但不适用于非DAG的有向图（即有环的有向图）。具体原因留给读者思考。

5.6　网络流问题

在《算法竞赛入门经典》中，我们已经学习过网络流的基本算法。那些算法很经典，但效率不够高，无法满足算法竞赛的需求。本节补充一些相对较快的网络流算法，并且用大量的实例锻炼读者的建模和算法设计能力。请读者把主要精力放在思维锻炼和题目的分析上，不要过于执著于那些经典算法的细节优化[23]。本节中给出的代码在大多数比赛中已经完全够用了。

5.6.1　最短增广路算法

《算法竞赛入门经典》中已经介绍过一些基本算法，其中最快的当属“最短增广路算法”，即每次沿着最短增广路（即边数最少的增广路）进行增广。可以证明，这样最多需要O（nm）次增广，因此算法的时间复杂度取决于找最短增广路的效率。在《算法竞赛入门经典》中，直接用BFS进行查找，每次需要O（m）时间，总时间复杂度高达O（nm²），还有很大提升空间。

数据结构。和前面的Dijkstra算法、Bellman-Ford算法类似，下面要介绍的算法均使用如下数据结构来表示一条弧。

这代表一条从from到to的容量为cap，流量为flow的弧。当且仅当flow ＜ cap的时候，该弧存在于残量网络中。注意，也可以只用一个变量residual表示残量[24]，当cap＝0的时候，意味着此边是反向弧，此时flow≤0。

下面是插入弧的过程。注意，原图中的一条弧对应于两个Edge结构体，一个是这条弧本身，另一个是它的反向弧。根据插入顺序不难看出，edges[0]和edges[1]互为反向弧，edges[2]和edges[3]互为反向弧，依次类推，一般地，edges[e]和edges[e^1]互为反向弧。

这个数据结构兼顾了效率、代码清晰度和调试难度，虽然效率不及一些其他写法，但对于正常比赛题目来说已经足够快。它的最大好处是同时适用于稠密图和稀疏图，而且支持重边[25]。

Dinic算法。我们要介绍的第一种算法称为Dinic算法。从宏观上讲，该算法就是不停地用BFS构造层次图，然后用阻塞流来增广。“层次图”和“阻塞流”是Dinic算法的关键字。

什么叫层次图呢？假设在残量网络中，起点到结点u的距离为dist（u），我们把dist（u）看做结点u的“层次”。只保留每个点出发到下一个层次的弧[26]，得到的图就叫做层次图。层次图上的任意路径都是“起点→层次1→层次2→层次3→…”这样的顺序。不难发现，每条这样的路径都是s－t最短路。

阻塞流听起来比较玄乎，但实际上就是指不考虑反向弧时的“极大流”。看不懂这句话也没关系，对应到程序里就是从起点开始在层次图中DFS，每找到一条路就增广。

这里有一个层次图的例子，如图5-23所示。

图　5-23

3次增广过程及最终结果如图5-24所示。

图　5-24

这3次增广后层次图中不存在s－t路径，合称阻塞流。

最多计算n－1次阻塞流（因为每次沿阻塞流增广后，最大距离至少会增加1），而每次阻塞流的计算时间均不超过O（mn），因此总时间复杂度为O（n²m）。实际上Dinic算法比这个理论界要好得多。如果所有容量均为1，可以证明Dinic算法的时间复杂度为O（min（n2/3，m1/2）m）；对于二分图最大匹配这样的特殊图，可以证明Dinic算法的时间复杂度为O（n1/2m）[27]。

下面给出一个参考实现。为了使思路更清晰，代码采用递归DFS找增广路，如果效率要求很高，可以改成迭代实现[28]。

BFS并没有什么特别之处，唯一需要注意的是只能考虑残量网络中的弧。代码如下。

DFS过程除了当前结点x外，还需要传入一个表示“目前为止所有弧的最小残量”的a，当x为汇点或者a＝0时终止DFS过程[29]，否则多路增广。这里还有一个重要的优化：保存每个结点x正在考虑的弧cur[x]，以避免重复计算（想一想，为什么）。代码如下。

最后是主过程。不停地用BFS构造分层网络，然后用DFS沿阻塞流增广。代码如下。

ISAP算法。第二种算法没有正式的名称，首次出现于Ahuja和Orlin的经典教材《Network Flows: Theory, Algorithms and Applications》中，作者称它是一种“改进版的SAP（Improved SAP, ISAP）”。

算法基于这样一个事实：每次增广之后，任意结点到汇点（在残量网络中）的最短距离都不会减小。这样，我们可以用一个函数d（i）表示残量网络中结点i到汇点的距离的下界，然后在增广过程中不断修正这个下界（而不是像dinic算法那样多次增广以后才重建层次图），则增广的时候和Dinic类似，只允许沿着d（i）＝d（j）＋1的弧（i，j）走。

严格地说，算法中的d函数是满足如下两个条件的非负函数，即d（t）＝0；对于残量网络中的任意弧（i，j），d（i）≤d（j）＋1。不难证明，只要满足这两个条件，d（i）就是i～t距离的下界。而且当d（s）≥n时，残量网络中不存在s－t路。

和Dinic算法类似，找增广的过程就是从s开始沿着“允许弧”（即在残量网络中的，满足d[i]＝d[j]＋1的弧i→j）往前走（ISAP算法中叫Advance）。如果走不动了怎么办？在Dinic算法中，直接“往回走一步”即可，因为如果找不到增广路，会重新构造层次图；但在ISAP中，并没有一个“一次性修改所有距离标号”的过程，只能边增广边修改。具体来说，在从结点i往回走的时候，把d（i）修改为min｛d（j）｜（i，j）是残量网络中的弧｝＋1（ISAP算法叫Retreat）即可。注意，如果残量网络中从i出发没有弧，则设d（i）＝n。

ISAP算法看上去不难理解，但是实现起来却有诸多细节。首先，我们需要使用一种“当前弧”的数据结构加速允许弧的查找，其次，还需要一个num数组维护每个距离标号下的结点编号。当把一个结点的距离标号从x改成y的时候，把num[x]减1，num[y]加1，然后检查num[x]是否为0。如果是0的话，说明s－t不连通，算法终止（想一想，为什么）。这就是所谓的gap优化。最后，初始距离标号可以统一设为0，也可以用逆向BFS找，单次运行时效率相差不大，但如果是多次求解小规模网络流，加上BFS以后速度往往会有明显提升。

数据结构方面，只多了如下两个数组。

BFS过程改成了逆向，留给读者编写。增广过程分为两步，先从汇点开始逆推，计算出可改进量a，然后再逆推一次，进行增广。

最后是主过程。根据情况前进或者后退，走到汇点时增广。

5.6.2　最小费用最大流算法

在《算法竞赛入门经典》中我们已经学习过最小增广路算法，但实现时使用的邻接矩阵，无法支持多重边。下面我们仍然实现这个算法，但采用的是前述数据结构，代码如下。

下面是沿最短路增广的过程。

主过程很好编写，代码如下。

上述过程求出的是最小费用最大流。如果是要固定流量k，可以在增广的时候检查一下，在flow＋a≥k的时候只增广k-flow单位的流量，然后终止程序。

需要注意的是，网络中可以有负权边，但不能有负权圈，否则连续最短路算法的“数学归纳法基础[30]”将失效，算法会计算出错误的结果。

5.6.3　建模与模型变换

在算法竞赛中，解决和网络流相关的问题往往重在建模。下面给出一些经典问题及其建模方法，以帮助读者理解和应用。

多源多汇问题。源有多个，汇也有多个，流可以从任意一个源流出，最终可以流向任意一个汇，总流量等于所有源流出的总流量，也等于流进所有汇的总流量。

解：加一个超级源s'和超级汇t'，然后从s'向每个源引一条有向弧，容量为无穷大，每个汇向t'引一条弧，容量为无穷大即可，如图5-25所示。

图　5-25

结点容量。每个结点都有一个允许通过的最大流量，称为结点容量。

解：把每个原始结点u分裂成u₁和u₂两个结点[31]，中间连一条有向弧，容量等于u的结点容量。原先到达u的弧改成到达u₁；而原先从u出发的弧改成从u₂出发，如图5-26所示。

图　5-26

无源无汇有容量下界网络的可行流。这个问题比较特殊。不仅无源无汇，而且每条弧除了有容量上界c之外，还有一个容量下界b。既然无源无汇，所有结点都应满足“入流＝出流”这个流量平衡条件。由于下界的存在，仅仅是求出满足这些流量平衡条件的可行流就已经有难度了[32]。

解：建立附加源s和汇t，然后对弧进行改造：首先添加弧t→s并设容量为无穷大，然后把每条下界为b弧拆成3条，如图5-27（a）所示，然后合并，如图5-27（b）所示。

图　5-27

最后，求改造后的网络的s－t最大流即可。当且仅当所有附加弧满载时原网络有可行流。

有容量下界网络的s－t最大／最小流。容量同时有上下界，且源点s和汇点t各有一个，求s到t的最大流和最小流[33]。

解：先用上题的解法求出可行流，然后用传统的s－t增广路算法即可得到最大流。把t看成源点，s看成汇点后求出的t－s最大流就是s－t最小流。注意，原先每条弧u→v的反向弧容量为0，而在有容量下界的情形中，反向弧的容量应该等于容量下界。

费用与流量平方成正比的最小流。容量c均为整数，并且每条弧还有一个费用系数a，表示该弧流量为x时费用为ax²，如何求最小费用最大流？

解：用拆边法。如图5-28所示，一个费用系数为a，容量为5的边被拆成5条容量为1，费用各异的弧。

图　5-28

因为求的是最小费用流，如果这条弧的流量为1，走的肯定是cost＝1的那条弧；如果流量为2，肯定走的cost＝1和cost＝3那两条；如果流量为3，走的肯定是cost＝1，3，5那3条……不难验证，不管流量是1～5之间的哪一个，左右两个图都是等价的。这样，问题就转化为普通的最小费用最大流问题。

需要注意的是，因为有重边，普通的邻接矩阵无法满足要求。要么采用前面给出的代码，要么采用把邻接矩阵加一维，表示某两点间的第几条弧。

流量不固定的s－t最小费用流。如果网络中的费用有正有负，如何求s－t最小费用流？注意，这里的流量并不固定。

解：如果费用都是正的，最小费用流显然是零流；但由于负费用的存在，最短增广路的权值可能是负的，这样增广后会得到更小的费用；但随着增广的进行，增广路权值逐渐增大，最后变成正数，此时应停止增广。换句话说，最小费用随着流量增大先减小，后增大，成下凸函数。前面说过，下凸函数求最小值一般使用三分法，但这里可不用这么麻烦，只需在最短增广路费用为正时停止增广即可，三分反而比较慢（想一想，为什么）。需要注意的是，如果一开始不仅有负费用弧，还有负费用圈，必须先用消圈法消去负圈，否则最短增广路算法的前提不成立。当然，如果网络是无环的，则无此问题。

下面通过一些例子说明一些建模技巧。

二分图带权最大独立集。给出一个二分图，每个结点上有一个正权值。要求选出一些点，使得这些点之间没有边相连，且权值和最大。

解：在二分图的基础上添加源点S和汇点T，然后从S向所有X集合中的点连一条边，所有Y集合中的点向T连一条边，流量均为该点的权值。X结点与Y结点之间的边的容量均为无穷大。这样，对于该图中的任意一个割，将割中的边对应的结点删掉就是一个符合要求的解，权和为所有权和减去割的容量。因此，只需要求出最小割，就能求出最大权和。

公平分配问题[34]。把m个任务分配给n个处理器。其中每个任务有两个候选处理器，可任选一个分配。要求所有处理器中，任务数最多的那个处理器所分配的任务数尽量少。不同任务的候选处理器集｛p₁，p₂｝保证不同。

解：本题有一个比较明显的二分图模型，即X结点是任务，Y结点是处理器。二分答案x，然后构图，首先从源点S出发向所有的任务结点引一条边，容量等于1，然后从每个任务结点出发引两条边，分别到达它所能分配到的两个处理器结点，容量为1，最后从每个处理器结点出发引一条边到汇点T，容量为x，表示选择该处理器的任务数不能超过x。这样，网络中的每个单位流量都是从S流到一个任务结点，再到处理器结点，最后到汇点T。只有当网络的总流量等于m时才意味着所有任务都选择了一个处理器。这样，我们通过O（logm）次最大流便算出了答案。

区间k覆盖问题。数轴上有一些带权值的左闭右开区间，选出权和尽量大的一些区间，使得任意一个数最多被k个区间覆盖。

解：本题可以用最小费用流解决，构图方法是把每个数作为一个结点，然后对于权值为w的区间[u，v），加边u→v，容量为1，费用为-w。再对所有相邻的点加边i→i＋1，容量为k，费用为0。最后，求最左点到最右点的最小费用最大流即可，其中每个流量对应一组互不相交的区间。如果数值范围太大，可以先进行离散化。

最大闭合子图。给定带权图G（权值可正可负），求一个权和最大的点集，使得起点在该点集中的任意弧，终点也在该点集中。

解：新增附加源s和附加汇t，从s向所有正权点引一条边，容量为权值；从所有负权边向汇点引一条边，容量为权值的相反数。求出最小割以后，S－｛s｝就是最大闭合子图。

最大密度子图。给出一个无向图，找一个点集，使得这些点之间的边数除以点数的值（称为子图的密度）最大。

解：如果两个端点都选了，就必然要选边，这就是一种推导。如果把每个点和每条边都看成新图中的结点，是否可以把问题转化为最大闭合子图呢？请读者继续思考。

5.6.4　例题选讲

例题30　网络扩容（Frequency Hopping, UVa 11248）

给定一个有向网络，每条边均有一个容量。问是否存在一个从点1到点N，流量为C的流。如果不存在，是否可以恰好修改一条弧的容量，使得存在这样的流？

【输入格式】

输入包含多组数据。每组数据第一行为3个整数N，E，C（1≤N≤100，E≤10000，C≤2000000000），即网络的点数、边数和需要的流量。以下E行每行为3个整数u，v，cap，即点u到点v的容量为cap（1≤u，v≤N，1≤cap≤5000）。输入结束标志为N＝E＝C＝0。

【输出格式】

对于每组数据，如果流量已经存在，输出possible；如果目前不存在，但可以通过修改恰好一条弧的容量得到，则输出“possible option:”和这些弧的列表（按照起点从小到大排序，起点相同时按照终点从小到大排序）；否则输出not possible。

【分析】

先求一次最大流，如果流量至少为C，则直接输出possible，否则需要修改的弧一定是最小割里的弧。依次把这些弧的容量增加到C，然后再求最大流，看最大流量是否至少为C即可。

很可惜，这样写出来的程序会超时，还需要加两个重要优化。第一个优化是求完最大流后把流量留着，以后每次在它的基础上增广，第二个优化是每次没必要求出最大流，增广到流量至少为C时就停下来。

例题31　运送超级计算机（Bring Them There, NEERC 2003, LA 2957）

宇宙中有n个星球，你的任务是用最短的时间把k个超级计算机从星球S运送到星球T。每个超级计算机需要一整艘飞船来运输。行星之间有m条双向隧道，每条隧道需要一天时间来通过，且不能有两艘飞船同时使用同一条隧道。隧道不会连接两个相同的行星，且每一对行星之间最多只有一条隧道。

【输入格式】

输入包含多组数据。每组数据第一行包含5个正整数n，m，k，S，T（2≤n≤50，1≤m≤200，1≤k≤50，1≤S，T≤N，S≠T）。以下m行每行包含两个不同的整数u和v，表示行星u和v之间有一条隧道。注意，隧道是双向的，但每一天只有一艘飞船能穿过一条隧道。另外，两艘飞船不能同时沿着相反的方向穿过同一个隧道。

【输出格式】

输出第一行为最少需要的天数L。以下L行每行描述一条的行动。每行第一个整数为Ci，即当天移动的飞船数。接下来有Ci对整数（A，B），表示有一艘飞船从行星A出发到达行星B。输入保证有解。

【分析】

首先假定答案是T，如何判断是否可以成功地运送所有k台超级计算机呢？构图如下。把原图中的每个点u拆成T＋1个，分别为u₀，u₁，...，uT，其中u₀是初始状态的结点u，而ui表示经过i天之后的结点u。对于原图中的相邻结点a和b，在新图中添加一条从ai到bi＋1的边，容量为1，再添加一条bi到ai＋1的边，容量也为1。对于原图中的每个结点u，添加边ui→ui＋1，容量为无穷大，表示飞船总是可以原地不动。在这个图中求最大流，判断流量是否至少为k即可。

不难想到二分答案，然后用上面的方法判定，但其实有更好的方法。可逐步增大天数T，但每次不要重新求最大流，而是直接新加一层结点，在上次求出的最大流基础上继续增广，直到流量达到k。

最后还有一个细节需要注意。如果在某时刻ai→bi＋1和bi→ai＋1同时有流量，是不符合题意的。这个问题并不难解决，只需相互抵消，仅留一个方向的流量即可。

例题32　足球联赛（The K-league, Taejon 2002, LA 2531）

有n支队伍进行比赛，每支队伍需要打的比赛数目相同。每场比赛恰好一支队伍胜，另一支败。给出每支队伍目前胜的场数和败的场数，以及每两个队伍还剩下的比赛场数，确定所有可能得冠军的球队（获胜场数最多的得冠军，可以并列）。

【输入格式】

输入第一行为数据组数T。每组数据第一行为队伍数n（1≤n≤25），第二行为2n个非负整数w₁，d₁，w₂，d₂，…，wn，dn（1≤wi，di≤100），其中wi为队伍i已经取胜的场数和输掉的场数。第三行包含n²个不超过100的非负整数a1，1，a1，2，…，a1，n，a2，1，…，an，n，其中ai，j表示队伍i和队伍j之间还需要打的场数。输入保证ai，j＝aj，i，且ai，i＝0。

【输出格式】

对于每组数据，输出一行，按照从小到大顺序给出所有可能获得冠军的队伍编号。

【分析】

依次判断每支队伍i是否可能成为冠军。首先，让i在所有剩下的比赛中获胜，计算出i获胜的总场数total，然后判断其他队伍是否可以相互制约，使得每支队伍的获胜场数均不超过total。如何判断呢？注意到任意两支队伍u和v之间的比赛只有u胜或者v胜两种可能性，可以把每场比赛看成一个“任务”，每支队伍看成“处理器”，则会得到一个和“公平分配问题”几乎一样的模型。

具体来说，对每两支队伍（u，v）构造一个X结点，从S引一条弧过来，容量为这两支队伍还需要比赛的场数；对每支队伍u构造一个Y结点，引一条弧到T，容量为total-win（u），其中win（u）表示队伍u已经获胜的场数。最后，每个结点（u，v）到u和v各连一条弧，容量为无穷大（想一想，为什么）。这样，当且仅当当前图的最大流使从S出发的所有弧满载时，当前判断的队伍可以得冠军。

例题33　收集者的难题（Collector's Problem, UVa 10779）

Bob和他的朋友从糖果包装里收集贴纸。这些朋友每人手里都有一些（可能有重复的）贴纸，并且只跟别人交换他所没有的贴纸。贴纸总是一对一交换。

Bob比这些朋友更聪明，因为他意识到只跟别人交换自己没有的贴纸并不总是最优的。在某些情况下，换来一张重复贴纸更划算。

假设Bob的朋友只和Bob交换（他们之间不交换），并且这些朋友只会出让手里的重复贴纸来交换他们没有的不同贴纸。你的任务是帮助Bob算出他最终可以得到的不同贴纸的最大数量。

【输入格式】

输入第一行为数据组数T（T≤20）。每组数据第一行包含两个整数n和m（2≤n≤10，5≤m≤25）。n表示总人数（包括Bob），m是贴纸的种数。接下来的n行描述了每个人收藏的贴纸，其中第一行描述的是Bob。第i行的开头是一个整数ki（ki≤50），表示第i个人有多少张贴纸，接下来有ki个1～m的整数，表示这些贴纸的种类。

【输出格式】

对于每组数据，输出Bob最终能得到多少种贴纸。

【样例输入】

2

2 5

6 1 1 1 1 1 1

3 1 2 2

3 5

4 1 2 1 1

3 2 2 2

5 1 3 4 4 3

【样例输出】

Case #1: 1

Case #2: 3

【样例解释】

在第一组数据中，不可能发生任何交换，所以Bob只能得到1号贴纸。在第二组数据中，Bob可以出让一个1号贴纸并且跟第2个人换回一个2号贴纸，然后用这个贴纸跟第3个人换回一个3号或者4号贴纸。最终他拥有的贴纸种类集合是｛1，2，3｝或者｛1，2，4｝。

【分析】

我们用n－1个点表示每个除Bob之外的人，再用m个点表示每个物品，同时添加一个源s和汇t。从s向每个物品连边，容量为Bob拥有该物品的数量。如果Bob以外的某个人i拥有至少两个物品j，就从i向物品j连边，容量为i所拥有的物品j的数量减1（因为i自己要留一个）；反之，如果i没有物品j，从物品j向i连边，容量为1，表示i最多接受1个物品j。最后从每个物品向t连边，容量为1。不难证明，这张图上最大流的流量就是Bob所能拥有的物品个数。样例2对应的网络，如图5-29所示。

图　5-29

上图还描述了3个单位流量的走向。首先是一个物品1，先给B，换得2，然后给C，换得3。第二个单位流量和第三个单位流量没有做任何交换，只是把本来就拥有的物品1和物品2留到了最后。

例题34　生产销售规划（Acme Corporation, UVa 11613）

Acme公司生产一种X元素。给出该元素在未来M个月中每个月的单位售价、最大产量、生产成本、最大销售量，以及最大存储时间（过期报废，不过可以存储任意多的量）。你的任务是计算出公司能够赚到的最大利润。

【输入格式】

输入第一行为数据组数T（T≤100）。每组数据第一行为两个整数M和I（M≤100，0≤I≤10⁶），表示要考虑的月数，以及每个单位的X元素存放一个月的代价。接下来的M行描述了每个月的参数，其中第i行包含5个整数mi，ni，pi，si，Ei（0≤mi，ni，pi，si≤10⁶，0≤Ei≤M），其中mi是第i月的每单元生产成本，ni是最大产量，pi是销售单价，si是当月最大销售量，Ei是这个月生产的X元素能够储存的最大时间。比如说对于第1个月，E₁＝3，这个月生产的元素就只能在第1～4个月销售，到了第5个月就不能卖了。

【输出格式】

对于每组数据，输出前M个月能够取得的最大利润。注意，只能考虑前M个月的销售。如果有任何元素的储存超过这个期限，无论卖不卖都得忽略。

【样例输入】

2 2

2 10 3 20 2

10 100 7 5 2

【样例输出】

Case 1: 20

【分析】

给每个月建立两个点i和i'，再建立源点s和汇点t。源点i向每个点i连一条边，流量表示第i个月的产量；每个点i'到汇点连一条边，表示第i个月的销量。每个点i向点i'，（i＋1）'，（i＋2）’…连一条边，表示存储0，1，2…个月以后卖掉。各个弧的容量和费用比较直观，不再赘述。

读者可能会问，为什么要用两个点表示一个月呢？一个点不行么？如果只有一个点的话，流量可能无法表示一个合法的方案。比如，本来我们希望每个月的产品最多存放一个月，即连边i→i＋1，但实际上可以通过i→i＋1→i＋2…的方式连续存储，违反了题意。

样例对应的网络如图5-30所示。图中（cap，cost）表示一条容量为cap，单位费用为cost的弧。

图　5-30

前面说过，求不固定流量的最小费用流可以直接做，当s－t增广路长度大于0时停止增广。样例前两次增广如图5-31所示。

图　5-31

再增广的话就必须走cost＝10的那条弧了，s－t最短路长度大于0，停止增广。注意必须用Bellman-ford求最短路，因为有初始负费用。

5.7　小结与习题

基础知识和算法。下面是一些比较基础的题目，作为知识回顾，如表5-1所示。

表　5-1

1．DFS及其应用

DFS可以用来计算连通分量、双连通分量、求割顶和桥、进行二分图判定、求SCC及SCC图，还可以求解2-SAT。下面的例题（见表5-2）覆盖了这些常见应用。

表　5-2

下面是习题，如表5-3所示，其中有些题目偏难，初学者不必强求。

表　5-3

爪分解（Claw Decomposition, UVa 11396）

给出一个n个结点（n≤300）的简单无向图，每个点的度数均为3。你的任务是判断能否把它分解成若干个爪（即K1，3，如图5-32所示）。在你的分解方案中，每条边必须恰好属于一个爪，但同一个结点可以出现在多个爪里。

图　5-32

鸽子和炸弹（Doves and Bombs, UVa 10765）

给定一个n个点的连通的无向图，一个点的“鸽子值”定义为将它从图中删去后连通块的个数。求每个点的“鸽子值”。

婚宴（Wedding, UVa 11294）

有n（n≤30）对夫妻参加一个婚宴。所有人都坐在一个长长的餐桌左侧或者右侧，新郎和新娘面对面坐在桌子的两侧。由于新娘的头饰很复杂，她无法看到和她坐在餐桌同一侧的其他人，但能看到餐桌另一侧的所有人。任意一对夫妻不能坐在桌子的同侧，另外还有m对人吵过架，而新娘不希望同时看到两个吵过架的人。你的任务是安排每个人坐在餐桌的左侧或右侧，使得上述条件全部满足。

投票（The Minister's Major Mess, World Finals 2009, LA 4452）

n个人对m个方案投票。每个人最多只能对其中的4个方案投票（其他相当于弃权票），每一票要么支持要么反对。问是否存在一个最终决定（对每个方案要么采用要么否定），使得每个投票人都有超过一半的建议被采纳。在所有可能的最终决定中，哪些方案的态度是确定的？

无向仙人掌（Cactus, NEERC 2005, LA 3514）

仙人掌被定义为每个点最多在一个简单回路上的连通无向图。所有简单回路，是指结点不重复经过的环，如图5-32所示。

给出一个有n（n≤20000）个结点，m（m≤1000）条边的无向图，你的任务是计算一个无向图的“仙人掌度”，即它有多少生成子图（包括自身）也是仙人掌。如果原图不是仙人掌，输出0。比如，图5-32（a）的仙人掌度为35，图5-32（b）为0（因为边2-3在两个简单回路上），图5-32（c）为0（因为不连通）。

道路修建（RevolC FaeLoN, UVa 10972）

题目大意是给定一个无向图，要求把所有无向边改成有向边，并且添加最少的有向边，使得新的有向图强连通。

有绳电话（String Phone, Daejon 2010, LA 4849）

平面网格上有n（n≤3000）个单元格，各代表一个重要的建筑物。为了保证建筑物的安全，警察署给每个建筑物派了一名警察，并配发了一些有绳电话以供联络。顾名思义，有绳电话是指长度固定的电话，电话两端的距离必须保持不变。在本题中，坐标（x1，y1）和（x2，y2）之间的距离为｜x1－x2｜＋｜y1－y2｜。我们将以无向加权图的形式给出哪些警察之间会使用有绳电话，以及每根绳子的长度，如图5-33（a）所示。这个图保证是连通的。

图　5-33

现在已经确定每名警察所巡逻的建筑物，你的任务是确定每个警察守在建筑物4个顶点中的哪一个，使得所有有绳电话都能正常使用，如图5-33（b）所示。

有歧义的编码（General Sultan, UVa 11604）

给一个包含n（n≤100）个符号的二进制编码方式，是否存在一个二进制序列，存在至少两种解码方法。比如｛a＝01，b＝001，c＝01001｝是有歧义的，因为01001可以解码为a＋b或者c。每个编码由不超过20个0或1组成。

2．最短路及其应用

最短路问题看上去简单，其实有很多可以挖掘的东西。如表5-4所示的例题就覆盖了很多与最短路相关的算法和技巧，堪称经典。

表　5-4

下面是习题（见表5-5）。和前面一样，部分题目颇有难度，初学者不必强求。

表　5-5

续表

倒水（Fill, UVa 10603）

有3个没有刻度的水壶，容量分别为a，b和c（单位为升，都是不大于200的正整数）。初始时前两个水壶是空的，而第三个装满了水。每次可以从一个水壶往另一个水壶里倒水，直到一个水壶倒空或者另一个水壶倒满。为了使某个水壶恰好有d升水，至少要倒多少升的水？如果无解，则找一个小于且最接近d的d'代替d。

超级马里奥大冒险（Adventure of Super Mario, UVa 10269）

再次成功营救出桃子公主之后，马里奥需要从Bowser魔王的城堡返回他的家。马里奥世界里一共有A个村庄和B个城堡（1≤A，B≤50），其中村庄编号为1～A，城堡编号为A＋1～A＋B。马里奥住在村庄1，而Bowser住在城堡A＋B。

马里奥能以1千米／小时的速度沿着村庄和城堡之间的双向道路行走，也可以用魔法鞋加速。魔法鞋可以让马里奥瞬间移动不超过L（L≤50）千米，但由于城堡中有很多陷阱，稍不留神就会中圈套，因此马里奥从不使用魔法鞋穿过城堡。另外，使用魔法鞋的起点和终点都只能是城堡或者村庄，且最多只能使用K（K≤10）次魔法鞋。

你的任务是计算马里奥回家的最短时间。输入需保证马里奥一定能回家。

加满油（Full Tank? UVa 11367）

n（n≤1000）个城市之间有m（m≤10000）条道路。给出起点s和终点t，以及汽车的油箱容量c，求从s到t的最便宜路径。假定初始时油箱是空的，第i个城市的油价为pi。需要回答q（q≤100）组询问。

飞到弗雷德里顿（Flying to Fredericton, UVa 11280）

给出n（2≤n≤100）个城市之间的m（0≤m≤1000）条航线以及对应的机票价格，要求回答一些询问，每个询问是给出最大停留次数s，求从Calgary（起点城市）到Fredericton（终点城市），中途停留次数不超过s的最便宜的行程。对于每条航线，保证该航线起点到Calgary的距离小于航线终点到Calgary的距离。

程序分析（Tomato Automata, SEERC 2005, LA3310）

有n行程序，每行程序为一个命令，共有5种命令。

❑　Ifgo x：任意选择跳至第x行或下一行。

❑　Jump x：跳至第x行。

❑　Pass：跳到下一行。

❑　Loop s，x：从第s行到当前行，循环x次。

❑　Die：终止程序的运行。

要求循环只能严格嵌套，不能从循环外直接跳至某个循环内部，也不能从循环内部直接跳出循环外部。当前在第一行，问最多可以运行多少行的程序或者如果程序可以死循环，输出infinity。x≤100000。

3．最小生成树

相比之下，最小生成树的题目大都比较传统，有规律可循。表5-6中的例题已经覆盖了大多数算法和技巧，不过还有一些较为偏门的变形没有涉及，有兴趣的读者可以在附录A中介绍的两场图论比赛问题中寻找更多的题目。

表　5-6

下面是习题，如表5-7所示。和例题一样，这些题目相对传统，但也需要小心。

表　5-7

生成树的苗条度（Slim Span, Japan 2007, LA 3887）

给出一个n（2≤n≤100）个结点的无向图，找一棵苗条度（最大边减最小边的值）最小的生成树。图中不含自环和重边。

ACM竞赛和停电（ACM Contest and Blackout, UVa 10600）

为了举办ACM竞赛，市长决定给所有的n（3≤n≤100）所学校提供可靠的电力供应。当且仅当一个学校直接连到电站，或者连到另一个有可靠供应的学校时，才有可靠供应。现在给出在不同的学校之间布线成本，找出最便宜的两种连线方案。一个方案的成本等于所有学校之间连线的成本的总和。

广播消息（Teen Girl Squad, UVa 11183）

你有一个好消息要告诉你的n－1（n≤1000）个好朋友，不过你们不在同一个地方，因此只能靠打电话。给出m（0≤m≤40000）个三元组（u，v，w）表示u打电话给v要花w分钱（注意，这不代表v可以给u打电话），你的任务是用最少的总费用把这个好消息通知到所有人。除了给出的三元组外，其他的通话方式都是不允许的。

沙漠探险（Traval in Desert, UVa 10816）

你想在沙漠中探险。由于沙漠非常热，你希望尽可能地降低旅途中的最高温度。沙漠中有一些绿洲，可以用来休息，而绿洲之间的道路则是温度不低的沙漠。你的任务是选择一条从起点到终点（起点和终点均为绿洲）的路线，使得途经道路的最高温度尽量低。如果有多个路线满足此条件，则选择长度最短的那一条。

北极通信网络（Arctic Network, UVa 10369）

北极某区域共有P座村庄，每座村庄均有自己的平面坐标。现决定在村庄之间建立通信网络，其中通信工具可以是无线电收发机，也可以是卫星设备。你的任务是让任意两座村庄之间都可以直接或间接地通信。

卫星设备一共有S台，拥有卫星设备的两座村庄无论相距多远都可以直接通信；其他每个村庄都可以配备一台无线电收发机，但必须具有相同的参数d。只有欧几里得距离不超过d的村庄才能用无线电收发机直接通信。由于d值越大的无线电收发机越贵，你需要合理分配这S台卫星设备，才能让其他村庄的无线电收发机的d值最小。

旅游路线（Tour Belt, Daejon 2010, LA 4848）

给出一个有n个结点m条边的加权无向图G（2≤n≤5000，1≤m≤n（n－1）/2），满足如下条件的结点集B（2≤｜B｜≤n）称为候选子图。B内的结点在G中连通，且B的任何一条内弧的权值严格大于任何一条边界弧的权值。这里的内弧是指两个端点都在B中的弧，边界弧是指恰好有一个端点在B中的弧。你的任务是求出所有候选子图的结点数之和。

如图5-34所示，图（a）有3个候选子图｛1，2｝，｛3，4｝和｛1，2，3，4｝，结点数之和为8。图（b）有6个候选子图，分别为｛1，2｝，｛3，4｝，｛5，6｝，｛7，8｝，｛3，4，5，6｝和｛1，2，3，4，5，6，7，8｝，结点数之和为20。注意｛1，2，7，8｝不是候选子图，因为这些结点在G中不连通（｛1，2｝和｛7，8｝之间没有边相连）。

图　5-34

4．二分图匹配

涉及二分图匹配的经典问题有不少，很能启发思维。表5-8中的例题包括了二分图最大基数匹配、最佳匹配、稳定婚姻问题、二分图的最小覆盖和最大独立集、DAG的最小路径覆盖等问题。

表　5-8

续表

下面是习题，如表5-9所示，其中个别题目有难度。

表　5-9

最优巴士线路设计（Optimal Bus Route Design, Taipei 2005, LA 3353）

给出一个n（n≤100）个点的有向带权图，找若干个圈，使得每个结点恰好属于一个圈。要求总长度尽量小。注意即使有向边（u，v）和（v，u）都存在，它们的长度也不一定相同。

车（Rooks, UVa 10615）

给出一个N×N（N≤100）的棋盘，上面放了一些车。现在要用尽量少的颜色对这些车进行着色，使得同一行或同一列的任意两个车的颜色都不同。

猫和狗（Cat vs. Dog, NWERC 2008, LA4288）

一群猫和一群狗参加选美。观众要么是猫的爱好者，要么是狗的爱好者。每一个猫的爱好者会特别喜欢某一只猫（每个人喜欢的猫不同）而且特别讨厌某只狗；每一个狗的爱好者会特别喜欢某一只狗而且特别讨厌某只猫。现有猫的爱好者s人，狗的爱好者t人，猫m只，狗n只。组委会要淘汰一些猫和狗，剩下的都能进下一轮。问怎样安排，才能使尽可能多的人的两个愿望都满足（即喜欢的晋级，讨厌的淘汰）？

长城游戏（The Great Wall Game, World Finals 2005, LA 3276）

在一个n×n的棋盘上有n个棋子，要求通过移动棋子使棋子的排布满足以下情况之一：呈横行排列；呈纵行排列；呈对角线排列（有两条）。棋子移动一个单元格的费用为1，总费用为所有棋子的移动费用之和。求最小费用。

国王的要求（King's Quest, NEERC 2003, LA 2966）

从前有一个国王，他有n（n≤2000）个儿子。皇宫里还有n个美丽的女孩，并且国王知道每一个王子喜欢的女孩都是谁。王子们都年轻强壮并且富有智慧，所以一个王子喜欢很多女孩也是很正常的。

国王找到巫师，告诉巫师每个王子所喜欢的女孩们，并且要求巫师找到一种方案，使每个王子都娶到一个自己喜欢的女孩。当然，一个女孩只能嫁给一个王子。

巫师提交了一种方案，国王看了之后说：“我挺喜欢你的方案，但我不是十分满意。我还想知道每一个王子能娶的所有女孩，当然，他娶了某个女孩之后，剩下的其他王子还是可以找到自己喜欢的人作为自己的伴侣。”

输入n个王子各自喜欢的女孩的列表，以及巫师提交的方案（即每个王子的新娘），你的任务是计算出每个王子有可能娶到的女孩的列表。

5．网络流

网络流是本章中建模技巧最多、经典算法最复杂的算法。寥寥几道例题不足以展示出网络流的全部建模技巧，请读者在表5-10所示的例题和附录A中寻找更多的题目。

表　5-10

下面是习题，如表5-11所示。

表　5-11

最短往返路（Dijkstra, Dijkstra, UVa 10806）

给出一个带有n（n≤100）个结点的带权无向图，找到从起点S到终点T的最短往返路径，不能重复走过同一条边。输入图中不含重边。

号码簿分组（Jamie's Contact Group, Shanghai 2004, LA 3268）

有n（n≤1000）个人和m（m≤500）个组。一个人可能属于很多组。现在请你从某些组中去掉几个人，使得每个人只属于一个组，并使得人数最多的组中人员数目为最小值。

峨眉山猴子（Monkeys in the Emei Mountain, UVa 11167）

雪雪是一只猴子。它在每天的2:00—9:00之间非常渴，所以在这个期间它必须喝掉2个单位的水。它可以多次喝水，只要它喝水的总量是2。它从来不多喝，在1小时内它只能喝1个单位的水。所以它喝水的时间段可能是2:00—4:00，或者3:00—5:00，或者7:00—9:00。甚至喝两次，第一次2:00—3:00，第二次8:00—9:00。但是它不能在1:00—3:00喝水，因为在1:00时它不渴，也不能在8:00—10:00喝水，因为9:00必须结束。

一共有n（n≤100）只这样的猴子。我们用一个（v，a，b）（0≤v，a，b≤50000，a＜b，v≤b－a）来描述一个在时间a～b之间口渴，并且必须在这个期间喝够v个单位水的猴子。所有猴子喝水的速度都是1小时喝1个单位的水。

现在的问题是只有一个地方可以让m只猴子同时喝水，需要作出一个能满足所有猴子的喝水需要的安排。多解时任意输出一组即可。

矩阵解码（Matrix Decompressing, UVa 11082）

对于一个R行C列的正整数矩阵（1≤R，C≤20），设A[i]为前i行所有元素之和，B[i]为前i列所有元素之和。已知道R，C和数组A和B，找一个满足条件的矩阵。矩阵中的元素必须是1～20之间的正整数。输入保证有解。

航程规划（Objective:Berlin, SWERC 2004, LA 3645）

有n个城市和m条航线。给出每条航线的出发地、目的地、座位数、起飞时间和到达时间（用HHMM方式表示），再给出城市A和B，以及最晚到达B的时间（用HHMM方式表示），求一天之内最多能有多少人从A飞到B（可以通过其他城市中转）。假设上下飞机的时间忽略不计）。

滑雪场地检查（Inspection, NEERC 2009, LA 4597）

你带领一支团队检查一所新建的滑雪场地。滑雪场地可以用一个有向无环图表示，其中图的结点代表滑坡之间的交叉点，边代表滑坡，方向总是从高到低（否则就没有办法借助重力滑雪了）。

你的团队必须检查每一个滑坡。由于电梯还没有正式启用，你必须用直升机完成任务。每次使用直升机可以在滑雪场地的某个交叉点放置一人，让他顺着滑坡往下滑，沿途检查他所滑过的所有滑坡。每个滑坡可以被检查多次，但必须至少被检查一次。你现在要计算至少用几次直升机才能完成所有滑坡的检查工作。

企鹅的游行（March of the Penguins, NWERC 2007, LA 3972）

网格上有n（n≤100）片荷叶，初始时第i片荷叶上有ni只企鹅（0≤ni≤10）。由于承受力有限，第i片荷叶最多只能承受mi（1≤mi≤200）只企鹅从上面跳走。一只企鹅每次最多跳D（D≤10⁵）单位距离。要求所有企鹅在同一片荷叶上集合。问哪些荷叶可以成为这个汇合地点。

寡头的竞争（Duopoly, Hangzhou 2005, LA 3487）

T公司和M公司想向政府申请一些资源的使用权。每一项申请包含一个资源列表和该公司愿意支付的金额。如果该申请得到批准，该公司将得到列表中所有资源的使用权。政府只能完整地批准或拒绝一个申请，不能只批准申请中的部分资源，也不能将一个资源的使用权同时批给两个公司。同一个公司的两项申请中保证不包含相同的资源。你的任务是帮助政府决定应当批准哪些申请，使得政府收益最大化，即被批准的那些申请的金额之和最大。

每个公司申请数目n不超过3000，每项申请愿意支付的金额为不超过1000的正整数，每项申请涉及的资源数为1～32的整数。

粉刷道路（Paint the Roads, Dhaka 2006, LA 2197）

某国有n个城市，用m条单向道路相连。你的任务是粉刷其中一些道路，使得被粉刷的道路组成一些没有公共边的回路，且每个城市恰好在其中的k条回路上。被粉刷的所有道路的总长度应尽量小。

音乐厅调度（Concert Hall Schedule, Aizu 2003, LA 2796）

一个著名的音乐厅因为财务状况恶化快要破产，你临危受命，试图通过管理的手段来拯救它，方法之一就是优化演出安排，即聪明地决定接受或拒绝哪些乐团的演出申请，使得音乐厅的收益最大化。该音乐厅有两个完全相同的房间，因此各乐团在申请演出的时候并不会指定房间，你只需要随便分配一个即可。每个演出都会持续若干天，每个房间每天只能举行一场演出。申请数目n为不超过1000的正整数，每个申请用3个整数i，j，w描述，表示从第i天演到第j天，愿意支付w元。

货物运输（Transportation, Harbin 2010, LA5095）

某国有n（n≤100）座城市，由m（m≤5000）条单向道路相连。你希望从城市1运送k（0＜＝k＜＝100）单位货物到城市n，这些道路并不安全，有很多强盗，所以你决定雇保镖来保护你。每条道路都有一个危险系数ai（0＜ai≤100），如果你带着x个单位的货物通过，需要给保镖aix²元钱才能保证你的安全（这是合理的，因为带在身边的货物越多越不安全）。另外，每条路还有一个容量限制Ci（Ci≤5），表示最多只能带Ci个单位的货物通过。注意，货物不能拆开，因此在通过每条路时，身上的货物数量必须是整数。

芯片难题（Chips Challenge, World Finals 2011, LA Transportation, Harbin 2010, LA 5131）

在一个N×N（N≤40）网格里放部件。其中一些格子已经放了部件（用C表示），还要放一些（用W表示），使得第x行的总部件个数（C和W之后）等于第x列。有些格子不能放（用／表示）。为了保证散热，任意行／列的部件（包括C和W）不能超过总部件数的A/B。

注释

[1]　这是一个通用的思路——当有多个源点的时候，增加一个“超级源”。这个技巧在网络流中也很常见。

[2]　我们很容易把数据结构改成邻接矩阵，细节留给读者思考。

[3]　多数情况并不存在此问题。建议先编写递归版，实测后发现确实会栈溢出时再改成非递归代码。

[4]　很多选手更喜欢用BFS进行二着色。和DFS相比，BFS的好处是不会发生栈溢出。

[5]　请注意“极大”二字。

[6]　这个算法是Tarjan提出的，但由于他提出的算法太多了，请读者不要把它和其他Tarjan算法混淆。

[7]　奇圈是指包含奇数个结点的圈（回路）。这里强调“简单”二字，是因为同一个骑士不能在圆桌上出现两次。

[8]　每个强连通分量都是原图的一个极大强连通子图。

[9]　思想如此，但细节并不相同。在学习kosaraju算法时，请读者以代码为准。

[10]　但有时会出现已知某个变量为某个取值的情况，导致整个图不对称。

[11]　如果不熟悉Dijkstra算法，请参阅《算法竞赛入门经典》的相关章节。

[12]　无向边往往需要拆成两条有向边，但也不一定（比如最小生成树的Kruskal算法）。

[13]　因为连通且有n－1条边（注意起点的p值等于它自己，其他每个点u对应一条边p[u]→u。

[14]　别忘了把Bellman-Ford代码中的dist和d数组改成double类型的。

[15]　但并不是每棵最小瓶颈生成树都是最小生成树。这一点请读者注意。

[16]　称为“边交换”。

[17]　即起点和终点相同的有向边。它们显然不在最小树型图中。

[18]　从上述分析可知，BFS也是可以的，但DFS更容易编写。另外，为了加快速度，实践中一般会先执行某种贪心算法预先求出一个匹配，然后在此基础上增广，而不是从空匹配开始增广。

[19]　这个改进方法由Edmonds和Karp提出。

[20]　另外，本题还有一个分治算法，在《算法竞赛入门经典》中已经简要的介绍过，有兴趣的读者可以实现一下。

[21]　原题不严密。这里在不影响结果的前提下对题目做了修改。

[22]　提示：如果用匈牙利树思考起来比较困难，可以用最小切割最大流定理。

[23]　这个建议同样适用于其他知识点。

[24]　很多人直接用变量cap表示残量。

[25]　我们很快就会看到，重边在最小费用流里是不能避免的。

[26]　也就是说，只保留满足dist（u）＋1＝dist（v）的边（u，v）。

[27]　事实上，这个界限满足所有“单位网络”，即除了源点和汇点之外，每个点要么只有一条入弧，且容量为1，要么只有一条出弧，且容量为1，其他弧的容量为任意整数。

[28]　一般来说不必如此。如果效率要求很高，推荐使用后面介绍的ISAP或者预流推进算法。Dinic算法最大的优点就是概念简单，且速度也不错。

[29]　实践中，如果不在a=＝0时及时终止，整个程序的效率往往会大打折扣，原因请读者思考。

[30]　连续最短路算法的正确性依赖于消圈定理。根据数学归纳法，这需要确保两件事：初始时没有负圈；增广过程中也不引入负圈。

[31]　图5-26中u₁＝u，u₂＝u＋n，但实际上编号是无关紧要的。

[32]　相比之下，原始最大流问题中的容量下界全等于0，因此零流就是可行流。

[33]　如果没有下界，最小流就是零流，没什么好求的。

[34]　本题可以提交，题目名为Fair Share （LA 3231）。

第6章　更多算法专题

我们在前5章中已经学到了很多经典算法和数据结构，但限于篇幅，还有很多专题尚未讨论。本章选取了算法竞赛中一些相对常用的专题进行了分析讲解，对前5章的内容是一个重要的补充。

6.1　轮廓线动态规划

本节介绍一类特殊的问题，需要用一种称为“轮廓线动态规划”的方法进行求解。这类问题的共同特点是：在一个比较“窄”（行数少或者列数少）的棋盘上进行复杂操作。如果采用传统方法（以整行整列为状态）进行动态规划，将无法进行状态转移，因此只能把参差不齐的“轮廓线”（每道题目都不一样，详见例题）也作为状态的一部分。尽管轮廓线的形态复杂，但由于棋盘比较窄，状态总数仍然可以控制在可以接受的范围内。

本节的基础模型是在“递推关系”部分中学到的多段图路径问题，因此在这里以计数问题为例稍费笔墨把它回顾一下。如图6-1所示，从左到右有n列结点，每列称为一个“阶段”，每个阶段的结点只会往下一个阶段的结点连有向边（每个结点出发可以连多条边），求从阶段1到阶段n每个结点的路径条数（起点可以任意，只要是从阶段1开始即可）。

图　6-1

设d[i][j]为从阶段1到结点（i，j）的路径条数，则可以写出如下伪代码（已用滚动数组）。

这里的cur就是“正在计算”的那一个阶段。最后d[cur]为阶段n各结点的值。不难发现，这些阶段的名字和编号并不重要，只需要从小到大枚举这些阶段即可。

本节各例题都可以看成多段图的问题，其中例题1和例题3是计数问题，例题2是优化问题（需要修改上述伪代码中的初始化代码和更新代码）。

例题1　铺放骨牌（Tiling Dominoes, UVa 11270）

用1×2骨牌覆盖n×m棋盘，有多少种方法？如图6-2所示为一种铺放方法。

图　6-2

【输入格式】

输入包含多组数据。每组数据占一行，包含两个正整数n和m（nm≤100）。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出总数。

【分析】

本题也许是最为经典的轮廓线动态规划问题了，请读者在继续阅读之前先尝试着用整行整列为状态（比如，用d（i）表示前i行的铺法总数）设计动态规划算法，以获得“轮廓线需要纳入到状态表示中”这一关键点的直观感受。

题目规定mn≤100，因此m和n至少有一个数不超过10。为了简单起见，我们规定m≤n（当m＞n的时候，交换m和n即可，结果不变），因此m≤10，符合前面说的“窄棋盘”的条件。

对于本题来说，我们按照从上到下、从左到右的顺序把棋盘划分成若干个阶段，每个阶段有2m个结点，其中每个结点用一个m位二进制整数表示。比特1表示已覆盖，0表示没有覆盖，如图6-3所示。

图　6-3

阶段决策是“以当前格子为右下角，要不要放骨牌以及放哪种骨牌”。答案有3种：不放骨牌、放竖的骨牌、放横的骨牌。

情况一：不放。如果k₄＝0，并不能转移到任何合法状态，因为棋盘不可能铺满。只有当k₄＝1的时候，转移到二进制为k₃k₂k₁k₀0的状态。

情况二：往上放。只有当k₄＝0并且i不是最上面一行的时候，这个骨牌才放得下。转移到k₃k₂k₁k₀1的状态。

情况三：往左放。当k₀＝0且j不是最左列的时候才能放下，而且还要当k₄＝1的时候才能转移到合法的状态。新状态为k₃k₂k₁11。

不难发现，时间复杂度为O（2m×nm），其中m是行数和列数的较小值，n是另外一个值。

下面考虑程序实现。我们把各行编号为0～n，各列编号为0～m，然后初始状态d[0][2m－1]＝1，其他d[0][j]无所谓（想一想，为什么），最终答案为d[cur][2m－1]。

状态转移用位运算写起来比较方便。设旧状态为k，什么都不放的话，新状态为k左移一位的结果，即k＜＜1。为了清晰起见，这里用LEFT（k）表示。转移的必要条件是LEFT（k）的第m位为1，即k的第m－1位为1。为了清晰起见，我们用TEST（k，b）表示k的第b位是否为1，用CLEAR（k，b）表示k把第b位清0后的数。这样，我们可以写出如下的代码。

不放：代码如下。

上放：需要判断k的第m－1位是否为1，转移时先左移，然后打开第0位和第m位，代码如下。

左放：需要判断k的第0位是否为1，转移时先左移，然后打开第0位和第1位，代码如下。

如果对位运算很熟悉，可以直接把上述代码写成位运算的形式。完整代码如下。

例题2　连线（Manhattan Wiring, Japan 2006, LA 3620）

n×m网格里有空格和障碍，还有两个2和两个3。要求把这两个2和两个3各用一条折线连起来，使得总长度尽量小（线必须穿过格子的中心，每个单位正方形的边长为1）。

限制条件如下：障碍格里不能有线，而每个空格里最多只能有一条线。由此可知，两条折线不能相交，每条折线不能自交。

如图6-4所示，折线总长度为18（别忘了两个2和两个3所在的4个格子中各有一条长度为0.5的线）。

图　6-4

【输入格式】

输入包含多组数据。每组数据的第一行为正整数n和m（1≤n，m≤9），以下n行每行为m个整数，描述该网格。0表示空格，1表示障碍，2表示写有“2”的格子，3表示写有“3”的格子。

【输出格式】

对于每组数据，输出两条折线的最小总长度。如果无解，输出0。

【分析】

本题看似是图论问题，却很难用我们学过的图论工具进行解决。比如，若先用最短路线连接两个数字2，也许会迫使两个数字3用一条特别长的路线连接（答案错）。如果枚举两个数字2之间的所有路径，再分别求两个数字3的最短路，则虽然正确性得到了保证，但运行时间可能会很长（超时）。

我们沿用例题1的思路，把本题看成是一个多阶段的决策问题，其中“决策”是指“搭积木”。一共有11种积木，如图6-5所示。

图　6-5

拼的规则很简单：两块积木在接触的地方或者都有线，或者都没线。如图6-6所示，左边两个是非法的，而右边两个是合法的。

图　6-6

这样，每个状态仍用当前格子和“轮廓线”来表示。如何表示轮廓线呢？我们可以用“每个位置是否有线”来描述一条轮廓线，但为了避免把“2线”（连接两个2的线）和“3线”（连接两个3的线）搞混，我们还需要给线加一个标记。这样，轮廓线上的每个位置有3种可能：0（无线）、1（2线）、2（3线）。如图6-7所示。

图　6-7

这样，我们可以很自然地用三进制来表示轮廓线状态[1]，因此状态总数是O（nm3n＋1）的。每个状态最多有11种转移，因此总时间复杂度为O（nm3n＋1）。算法梗概并不难理解，但状态转移却有些复杂，强烈建议读者编写程序。

例题3　黑和白（Black and White, UVa 10572）

在一个m行n列的网格中已经有一些格子涂上了黑色或者白色。你的任务是把其他格子也涂上黑色或者白色，使得任意2×2子网格不会全黑或者全白，且所有黑格四连通[2]，所有白格也四连通。

比如，在如图6-8所示的4幅图中，第一幅中黑格不连通，第三幅中存在2×2的全黑子网格，其余两幅图合法。

图　6-8

输出方案总数和其中一组方案。

【输入格式】

输入的第一行为数据组数T（T≤100）。每组数据的第一行为两个整数m和n（2≤n，m≤8）。以下m行每行包含n个字符，即网格本身。其中“#”表示黑格，“o”表示白格，“.”表示尚未涂色的格子。

【输出格式】

对于每组数据，第一行输出方案总数，接下来是任意一组方案（如果有的话）。

【分析】

本题和例题1、例题2有些相似，也可用基于轮廓线的动态规划算法解决，但问题在于：“连通”应如何表示呢？我们可以给每个连通分量编号，像图6-9（a）那样。

图　6-9

目前一共有5个连通分量，编号为0～4，则轮廓线上各个格子的连通分量如图6-9（b）所示[3]。注意到连通分量的编号方式不唯一，为了避免重复，我们采用最小表示法，即所有等价的表示法中字典序最小的一个。

记录了轮廓线上各个格子的颜色和连通分量编号后，就可以开始设计状态转移方程了。本题的状态转移比较复杂，总的来说就是尝试两种决策（涂黑、涂白），判断转移时候合法。具体规则如下。

（1）首先，如果涂色方案和输入冲突，不合法。

（2）其次，如果涂色方案导致了2×2同色子网格，不合法。这也解释了轮廓线上为什么待涂色格子的左边一列要有两个格子——如果只存在一个格子的话，无法进行这个判断。

（3）现在，涂色方案通过了初步测试，把原状态S复制成新状态T，然后把T中新格子的连通分量编号设为S的最大连通分量编号加1（代表这是一个新的连通分量）；如果新格子的颜色和它上方格子的颜色相同，则改成那个格子的颜色；设这一步结束后，T中新格子的连通分量编号为c。

（4）再接下来尝试合并连通分量。如果新格子和它左边格子的颜色也相同，则把T中所有编号为c的连通分量都改成c'，其中c'是新格子左边格子在T中的连通分量编号。

（5）最后一步判断最为关键，也最容易想错。注意到新格子上方的那个格子很快就要退出轮廓线了，我们需要确保它所在的连通分量不能消失（否则就无法和目前尚未出现的格子连通了）。但这里有一个特殊情况：如果剩下的格子全部涂上另外一种颜色，则这个连通分量消失了也没关系。请读者思考如何区分这两种情况（提示：可以考虑修改状态表示）。

强烈建议读者编写程序，因为这既是一个不错的思维练习，也是一个不错的编程练习。此外，本题还可以用一种称为“广义括号法”的方法表示连通性信息，往往比最小表示法效率更高，但代码相对容易写错，有兴趣的读者可以自行查找相关资料。

6.2　嵌套和分块数据结构

在第3章中，我们学到了不少实用的数据结构，如优先队列、并查集、Fenwick树、线段树、Treap和伸展树。特别是后三者，灵活的信息维护机制使得它们可以高效解决很多问题。与这些经典数据结构不同的是，本节介绍的是两种“非主流”的数据结构设计方法：嵌套和分块。它们通常不出现在教科书中，但思想精巧，实用性高。

数据结构的嵌套。嵌套的思想是把一个数据结构的基本单元进一步细化，用所谓的“子数据结构”来表示，比如所谓的“线段树套Treap”，就是用Treap来表示线段树中的结点，即线段树中每个结点保存着一棵Treap[4]，而不是一个仅仅写着几个附加信息的圆圈。作为一种特殊情况，“二维线段树”是指一棵线段树的每个结点都保存着一棵线段树[5]。听起来很玄乎？下面来看一道需要二维线段树的例题。

例题4　人口普查（Census, UVa 11297）

你的任务是维护一个n行m列的数字矩阵，要求支持以下两种操作。

❑　qx₁y₁x₂y₂：查询所有满足x₁≤x≤x₂，y₁≤y≤y₂的格子（x，y）的最大值和最小值。

❑　cxyv：把格子（x，y）的值修改成v。

【输入格式】

输入仅包含一组数据。第一行为两个整数n和m（1≤n，m≤500）；以下n行每行m个整数，即矩阵中的元素；下一行为整数Q（Q≤40000），即查询的个数；以下Q行每行一个查询。

【输出格式】

对于每个q查询，依次输出最大值和最小值。

【分析】

本题是动态RMQ的二维情况，因此可以用类比的方式构造二维线段树。具体来说，先对x坐标构造一棵线段树（简称x树），其中每个结点是一棵y方向的线段树（简称y树），然后用Max[xo][yo]和Min[xo][yo]分别表示编号为xo的这棵y树中编号为yo的这棵子树所代表的所有元素的最大值和最小值。

查询过程（query2D函数）相对简单，包括以下两种情况。

❑　查询x树叶结点时直接查询该结点对应的y树（调用query1D函数）。

❑　查询x树非叶结点时分别查询两棵子树（递归调用query2D函数）。

因为涉及O（logn）个x结点，其中每个结点对应的y树涉及不超过O（logn）个结点，因此时间复杂度为O（log²n）。

修改过程（modify2D函数）稍微复杂一点，包括以下两种情况。

❑　修改x树叶结点时直接修改该结点对应的y树（调用modify1D函数）。

❑　修改x树的非叶结点分为两个步骤，第一步是递归修改x树上的子树，第二步是修改此结点对应的y树（调用modify1D函数）[6]。

一共有O（logn）棵y树需要修改，而每棵y树最多需要O（logn）个结点，一共要修改O（log²n）个结点。修改单个结点只需O（1）时间，因此总时间复杂度为O（log²n）。

上述叙述过于抽象，因此强烈建议读者认真阅读下面的代码。

为了让代码和第3章中的线段树在外观上尽量保持一致，我们用到了很多临时成员变量。比如，查询参数x1，y1，x2，y2可以被query1D和query2D所公用，xo用来给query1D和modify1D提供“当前主线段树结点编号”，而isleaf表明正在修改的y树是不是主线段树的叶子（这会影响到结点修改方式）。

上述代码的速度已经足够快，但还有优化的余地：编写一个build函数，递归构造出初始线段树，而不是像上面的代码那样连续执行nm次modify操作。有了上述代码作为参考，相信读者可以独立完成这个build函数，这里不再赘述。

例题5　“动态”逆序对（"Dynamic" Inversion, UVa 11990）

给一个1～n的排列A，要求按照某种顺序删除一些数（其他数顺序不变），输出每次删除之前逆序对的数目。所谓逆序对数，就是满足i＜j且A[i]＞A[j]的有序对（i，j）的数目。

【输入格式】

输入包含多组数据，每组数据的第一行为两个整数n和m（1≤n≤200000，1≤m≤100000）；接下来的n行表示初始排列；接下来的m行按顺序给出删除的整数，每个整数保证不会删除两次。输入结束标志为文件结束符（EOF）。输入文件大小不超过5MB。

【输出格式】

对于每次删除，输出删除之前的逆序对数。

【分析】

首先用O（nlogn）时间求出初始排列的逆序对数[7]，则每次需要求出被删除的那个数左边有多少个数比它大，右边有多少个数比它小。根据对称性，我们只考虑如何求出“左边有多少个数比它大”即可。

令larger（p，v）表示Ap＞v是否成立（1表示成立，0表示不成立），则“前k个数有多少个比v大”等于larger（1，v）＋larger（2，v）＋larger（3，v）＋…＋larger（k，v）。

注意，这是一个前缀查询[8]，因此可以用Fenwick树维护。回忆第3章中对Fenwick树的C数组的定义

对应到本题中，Ci的值实际上等于“Ai－lowbit（i）＋1，…，Ai中有多少个元素比v大”。考虑到本题还有删除操作，可以用第3章介绍过的名次树来实现这些Ci。这样，我们得到了一个“Fenwick树套名次树”的特殊数据结构，在O（log²n）时间内解决了本题。

具体来说，对于每个i，我们用Ai－lowbit（i）＋1，…，Ai这些数构造第i棵名次树，支持两个操作：删除元素和统计树里有多少个元素小于某个k。当删除第i个位置的数v时，按照Fenwick树算法计算Q（i－1）＋…＋Q（i－lowbit（i－1））…这里的Q（i）就是查找第i棵BST中大于v的个数。

这里的名次树并不需要用Treap或者伸展树实现，因为它只有删除没有插入和修改。如果给每个结点添加一个“是否已删除”的标记，就可以在不改变树结构的情况下支持删除操作。换句话说，只要在预处理时把每棵名次树都建成完全平衡的二叉树，则两个操作的时间复杂度总是对数级别的。

另外，本题题目中的Dynamic加了引号，暗示着本题并不一定要写动态的在线算法。事实上，借助于分治法和Fenwick树，本题可以用更短的代码在更短的运行时间内解决问题，读者不妨一试。

数据结构的分块。这是另一种常见的数据结构设计思路，多用于线性结构的分块。分块的细节很多且并不唯一，下面仅举一例，建议有兴趣的读者完成本章后面的习题。

例题6　数组变换（Array Transformation, UVa 12003）

输入一个数组A[1，…，n]和m条指令，你的任务是对数组进行变换，输出最终结果。每条指令形如（L，R，v，p），表示先统计出A[L]，A[L＋1]，…，A[R]中严格小于v的元素个数k，然后把A[p]修改成uk/（R－L＋1）。这里的除法为整数除法（即忽略小数部分）。

【输入格式】

输入的第一行为3个整数n，m，u（1≤n≤300000，1≤m≤50000，1≤u≤10⁹）。以下n行为数组A[i]（1≤A[i]≤u）；再以下m行每行为4个整数L，R，v，p（1≤L≤R≤n，1≤v≤u，1≤p≤n）。

【输出格式】

输出n行，每行为一个整数，即变换后的最终数组。

【分析】

设A[L]，A[L＋1]，…，A[R]中严格小于v的元素个数为count（L，R，v），我们可以把它转化为前缀和的减法：count（L，R，v）＝prefix_count（R，v）－prefix_count（L－1，v），其中prefix_count（n，v）表示前n个数中严格小于v的元素个数。这样，我们可以沿用上题的思路，用嵌套数据结构来解决问题，只是要把删除操作改成修改操作。这样一来，静态BST必须换成动态BST，算法的时间效率降低了，编程复杂度却提高了。

有没有编程简单、速度又快的方法呢？有，那就是分块法。预设一个整数值SIZE，然后每SIZE个元素分成一块，分别排好序，则查询（L，R，v，p）的执行可以分成两步。

第一步，先找出L和R所在块，逐一比较出有多少个元素比v小，然后对于中间的那些块直接用二分查找，相加后得到k。

第二步，在p所在块中找到修改前的A[p]，改成uk/（R－L＋1），然后不断交换相邻元素，直到该块重新排好序。

如果SIZE太大，第二步会比较慢；如果SIZE太小，第一步会比较慢（因为块变多了）。根据前面的经验，SIZE设为n的算术平方根附近会比较快，也可以实验得出比较好的SIZE值。代码如下。

分块法非常实用。事实上，很多用伸展树或者树套树才能解决的问题都可以用分块法解决，比如，第3章中的《排列变换》和《魔法珠宝》，以及例题5《“动态”逆序对》，有兴趣的读者不妨一试。当然，尽管可以解决上述问题，分块法并不一定是最理想的解决方案，关键还是要具体问题具体分析。

6.3　暴力法专题

6.3.1　路径寻找问题

在《算法竞赛入门经典》中，我们曾经介绍过“8数码”问题。还有很多这样的问题，它们的共同特点是：把一个初始状态一步一步转变为终止状态，要求步数最小，或者费用最小（可以定义每次状态转移的费用）。在人工智能中，称这样的问题为路径寻找问题（Path-Finding Problems），属于单智能体搜索（single-agent search）的范畴[9]。

聪明的读者一定注意到了，状态是这类问题的关键词。还想到了什么？没错，它也是动态规划的关键词，我们甚至还在最短路问题中也不止一次提到过它。

这并不是偶然。状态及其错综复杂的关系构成了一张有向图。如果它是一个DAG（有向无环图），可以使用动态规划，否则只能使用图论中的最短路算法，如Dijkstra算法。但有一点需要注意，状态图有隐式和显式之分。很多题目有类似这样的输入格式：“输入第一行为整数n和m，代表结点数和边数，其中结点编号为1～n…”这就是典型的显式图，因为所有结点和边在一开始就给出了；还有很多问题却不是这样，比如下面的例题7，在这样一个智力游戏中，只有初始状态是事先已知的，还有哪些状态可以到达？要试一试才能知道。

一般而言，适合路径寻找问题的算法有4个：广度优先搜索（BFS）、迭代加深搜索（IDDFS）、A*搜索和IDA*搜索[10]，其中前两个是盲目搜索算法，后两个是启发式搜索。限于篇幅，这里只介绍一个问题，不过它足够经典，请读者认真阅读。如果对BFS、结点查找表和h函数的作用还不太清楚，请阅读《算法竞赛入门经典》并参考相关资料。

例题7　冰人（Iceman, Xi'an 2006, LA 2417）[11]

在寒冰王国的一个房间里有一个冰人，你的任务是让他到达一个给定的目的地。这个房间是一个n×m网格，各行从上到下编号为1～m，各列从左到右编号为1～n。每格要么为空，要么是冰，要么是石头。石头始终不变，而冰可以由魔法创造或者销毁。空格用“.”表示，石头用“X”表示，而冰有4种，我们稍后介绍。房间的第一行、最后一行、第一列和最后一列都是石头。冰人的初始位置总是一个空格，用“@”表示。它的目的地也是一个空格，它的正下方总是一块石头，用“#”表示。虽然冰人看上去比较大，但他总是恰好占据一个空格，如图6-10所示。

图　6-10

冰人有4种移动方式：左移（L），右移（R），左魔法（＜），右魔法（＞）。假设冰人在格子（r，c），则左魔法（＜）作用于他的左下方，即（r＋1，c－1）格子。如果该格子是石头，则魔法无效；如果格子是空格，则变为冰；如果是冰，则变为空格。有4种类型的冰：双端自由的冰（O），左端自由的冰（[），右端自由的冰（]）和无自由端的冰（=），其中“自由”的意思是不与该方向的相邻冰或者石头粘在一起。当魔法把空格变成冰以后，如果它的左边（右边）相邻格是冰或者石头，那么这块新创造出来的冰会和左边（右边）的相邻冰或者石头粘在一起。“粘”的作用是相互的，因此同一行的多块冰或者石头可以粘成一个整体。上述连接只能通过魔法，而不能通过推移（见后）创建，且只有水平连接，没有垂直连接。我们把连接在一起且最左、最右端都不是石头的整体称为冰棒（特别地，双端自由冰是长度为1的冰棒）。当冰棒中每个格子的正下方均为空时该冰棒将整体下落。但如果一块冰直接或者间接的与一块石头粘在一起，它是不会下落的（石头不会动）。当魔法作用在冰上并销毁它时，它左右的连接（如果有的话）都将随之被销毁。右魔法“＞”与左魔法对称，它作用在（r＋1，c＋1）格子上。

左移（L）比较复杂。仍然假设冰人在（r，c），如果（r，c－1）为空，则冰人移动到此方格。如果此时他脚下的方格（r＋1，c－1）为空，则冰人垂直下落，直到脚下是冰或者石头为止，且下落过程中不能进行操作；如果（r，c－1）是石头，则他会试着往上爬。如果（r－1，c－1）和（r－1，c）均为空，则他将到达（r－1，c－1），否则留在（r，c）；如果（r，c－1）是冰，则当且仅当（r，c－2）也为空时，该自由冰被推向左方，并将一直向左运动，直到被冰或石头挡住，或者正下方为空（此时冰块按照前面叙述的规则下落，且落地后不再继续往左移动）。注意，冰块被推走后，它上方的自由冰棒将会落下。每次操作后，冰人总是等所有冰块都不再移动后才能继续操作。如果（r，c－1）是冰但不能被推走，则把它当作石头一样处理（可以尝试着爬上去）。

编程用最少步数把冰人移动到目标格中。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（1≤n，m≤10）；以下n行每行包含m个字符，即游戏地图。每个字符都是'．'，'X'，'@'，'O'，'［'，'］'，'='之一。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出步数最小的解。输入保证最优解不超过15步，且最优解唯一。

【分析】

本题就是一道路径寻找问题：从输入的初始状态到“冰人在目标格中”的任意一个状态。由于状态非常直观，我们不妨沿用输入格式，把每行字符串按照从上到下的顺序拼接成一个长度为n×m的字符串。

由于已知解的长度上限，下面的代码直接把启发信息（h函数）用到了BFS中进行最优性剪枝。如果阅读下面的代码有困难，请参考《算法竞赛入门经典》中的算法框架（注意，查找表的作用包含在了sol中）。

不难发现，程序的核心在于上面的expand函数，它负责计算对某个状态进行某个特定操作后生成的子状态，其代码如下。

上述代码中用到了一个重要函数fall，它的作用是让状态中悬空的冰块和冰人落地，代码如下。

expand函数中还有一个重要的函数h，计算状态的“启发函数”。具体来说，任意状态s至少还需要h（s）次操作才能到达目标位置[12]，代码如下。

上述代码中还有几个数组没有交待：icy[c]表示字符c是冰，即'O'，'［'，'］'和'='4种，link_l[c]和link_r[c]分别表示字符c往左、右连接后的新字符，如link['O']=']'；clear_l[c]和clear_r[c]分别表示字符c拆除左、右连接后的新字符，如clear_r['［'］='O'。这些数组都是常量，在init（）函数中初始化，这里不再赘述。

6.3.2　对抗搜索

博弈是人工智能研究的起源和动力之一。我们曾在组合游戏部分中介绍过对“必胜状态”和“必败状态”的概念，并且得出结论：当且仅当后继状态中至少有一个必败状态时，本状态是必胜状态。遗憾的是，这个方法有两个缺陷。首先，根据上述定义，必须穷举从初始状态开始能到达的所有状态才能判断出初始状态是必胜还是必败。如果状态数过多（比如国际象棋或者围棋），计算出整棵“解答树”几乎是不可能的。其次，即使能算出结果，也只能得到胜负关系，无法区分出“大胜”和“险胜”，也无法区分出“惨败”和“惜败”。

本节的内容正是上述概念和算法的推广，它的适用范围更广，包括各种完全信息、无概率因素、轮流操作的双人零和（即双方利益完全对立）博弈。二人麻将不在本节的讨论范围内，因为它不是完全信息的（不能看到对方的牌）；石头剪刀布也不在本节的讨论范围内，因为不是轮流操作，而是同时操作；大富翁也不在讨论范围内，因为有概率因素（需要扔骰子）。

既然双方完全对立，我们可以把自己喜欢的那一方称为MAX游戏者，另一方称为MIN游戏者，然后画出一棵所谓的博弈树，如图6-11所示。

图　6-11

这棵树是如何得来的呢？首先，画出整棵树，即不断扩展所有非终局状态，使得树上的所有叶子对应所有可能的终局。接下来，给每个终局打个分。分数越高，表示对MAX游戏者越有利。由于二者利益完全对立，分数越高对MIN游戏者越不利。

接下来，我们自底向上一层一层递推每个非终局的分数。MAX结点的分数等于它所有子局面分数的最大值（因为MAX游戏者会选择一个最有利于自己的操作），MIN结点的分数等于它所有子局面分数的最小值（因为MIN游戏者会选择一个最不利于对手的操作）。

最后一步是找出最优策略并自顶向下走即可。不难发现，沿途经过的所有局面的分数都相同。

在实际系统中，由于完整的博弈树往往很大，我们无法先扩展出整棵树，而是当树的高度达到预设的最大深度后采取主观估价的方式给结点打分，而不是继续扩展。这样做虽然无法保证找到全局最优策略，但至少已经是“多考虑了好几步”之后得出的策略。

上述过程就是著名的极大极小搜索算法（minimax search）。为了发挥它的最大威力，我们还需要请出它的最佳拍档——alpha-beta剪枝。它的基本思想是这样的：每个MAX结点设置一个目前已知的下界alpha，每个MIN结点设置一个目前已知的上界beta。当计算一个MIN结点时，如果它的beta值小于等于其父结点（一定是MAX结点）的alpha值，则可以立即停止此结点的计算（alpha剪枝）；当计算一个MAX结点的时候，如果它的alpha值大于等于其父结点（一定是MIN结点）的beta值，也可以立即停止此结点的计算（beta剪枝），如图6-12所示。

图　6-12

在图6-12中，子结点的扩展从左到右进行[13]，因此当刚遍历完B的第一棵子树时，B的alpha值等于5，而A的beta值等于3，因此B的第二棵子树被beta剪枝。当计算完D的第一棵子树时，D的beta值为0，而C的alpha值为3，因此D的第二棵子树被alpha剪枝。同理，E的第二棵子树叶会被alpha剪枝。

下面是用了alpha-beta剪枝的minimax搜索框架，读者可以领会其思想，根据情况修改，使代码为已所用。

其中，MAX游戏者编号为0，MIN游戏者编号为1。初始调用为alphabeta（initial, player, -INF, +INF），其中player是第一个游戏者。

例题8　纸牌房屋（House of Cards, World Finals 2009, LA 4451）

有一副牌，共有红黑两种花色，其中每种花色有M张牌。把这2M张牌按照某种顺序排好，取出前8张牌拼成如图6-13所示的“房子”状（有4个峰和3个谷），剩下的牌正面朝上排列好放在桌面上，这样两个游戏者都能知道接下来的每轮各是什么牌。

图　6-13

Axel和Birgit两人进行游戏，其中Axel拥有红色，Birgit拥有黑色。左数第一张牌的颜色对应的游戏者为先手。Birgit为先手，因为第一张牌6B是黑色的。

游戏者轮流操作。每次操作都取出剩下的左数第一张牌，然后进行以下3种操作之一。

❑　拿在手里（成为“拿着的牌”）。

❑　在两个相邻峰之间搭一张牌（使用拿着的牌或者新取的牌），作为一层楼面。剩下的一张（如果有的话）拿在手里。

❑　将两张牌搭在一张楼面牌上，形成一个新的峰（其中一张牌一定是拿着的牌）。

注意，不是所有情况下这3种操作都是可行的。游戏规定手里最多只能拿一张牌，因此第一种操作可行的前提是手里没有牌。

每当组成一个新的三角形（加楼面牌时会形成一个向下的三角形，加峰的时候会形成一个向上的三角形）时，计分如下：首先数一数组成三角形的3张牌中哪种花色较多，然后给该游戏者加分，分值为这3张牌的点数之和。

在上面的例子中，如果Birgit在中间的谷上放一张（11R）作为楼面牌，她将得到14分；如果她把这张牌放在左边的谷上，Axel将得到19分；如果放到右边的谷上，Axel将得到29分。

当桌面上没有正面朝上的游戏牌时游戏结束。如果一个游戏者手里拿着牌，那么还将对他的得分进行最后的调整：如果此牌的花色和自己拥有的颜色相同，加分；否则减分，加减的分值均为该牌的点数。

假定两位游戏者都足够聪明，你的任务是计算某个游戏者最多可以比对手高多少分。

【输入格式】

输入包含多组数据。每组数据的第一行为一个游戏者名字；第二行是整数M（5≤M≤13）；第三行按顺序给出2M张牌，其中每种花色和点数的组合恰好出现一次。红色牌用1R，2R，…，MR表示，黑色牌用1B，2B，…，MB表示。输入结束标志为字符串END。

【输出格式】

对于每组数据，输出给定游戏者的分数与对手相差的最大值。如果两人平手，输出“Axel and Birgit tie”。

【分析】

本题是标准的对抗搜索问题。前面已经给出了框架，因此只需设计状态即可。不难发现，最多能放下26张牌（见图6-14），因此题目中M≤13这个条件是合理的。

图　6-14

在任意时刻，被“压在底下”的牌都不会影响后面的游戏，因此不应纳入到状态表示中。比如，如图6-15所示，只有7张牌是有用的。为了避免不必要的元素移动，我们用两张相等的牌表示一张楼面牌，这样任意时刻都有8张牌是有用的。

图　6-15

我们用card[i]和type[i]表示第i张牌的数值（正数表示红色，负数表示黑色）和类型（UP、FLOOR或者DOWN），hold[p]表示游戏者p手里的牌（p＝0表示Axel，p＝0表示Birgit，hold[p]＝0表示手里没牌），pos表示下一张需要拿的牌的编号，score是当前得分。状态定义如下。

状态扩展比较麻烦。为简单起见，我们先编写一个“复制结点”的过程，它只是简单地复制数据，并且给pos加1。其他修改留在状态扩展中编写。

请读者再次阅读题目，然后一口气看完下面的状态扩展代码。

虽然这里给出了完整代码，但仍建议大家独立实现本题（或者在本章习题中挑一个感兴趣的对抗搜索项目）。上面的alpha-beta代码框架虽然方便（只需编写状态定义和扩展函数），但时间和空间上会有一些不必要的开销（需要先显式生成并保存一系列的子状态）。有趣的是，有些语言可以在保持代码整洁的前提下用惰性序列来避免这些开销，有兴趣的读者可以阅读附录中的介绍。

6.3.3　精确覆盖问题和DLX算法

看到这个标题，读者也许会奇怪：精确覆盖问题和DLX算法有什么特别之处？为什么要花一小节的篇幅单独介绍它？答案是为了解决数独问题。后面我们即将看到，数独是精确覆盖问题的一个特例。事实上，精确覆盖问题的DLX算法是目前已知的求解数独（特别是16×16的版本）问题最有效的方法之一。如果你对用计算机求解数独感兴趣，请仔细阅读本节。

精确覆盖问题（Exact Cover Problem）。有一些由整数1～n组成的集合S₁，S₂，S₃，…，Sr，要求选择若干个集合Si，使得1～n的每个整数恰好在一个集合中出现。比如，n＝7，S₁＝｛1，4，7｝，S₂＝｛1，4｝，S₃＝｛4，5，7｝，S₄＝｛3，5，6｝，S₅＝｛2，3，6，7｝，S₆＝｛2，7｝，则一个精确覆盖为S₂，S₄，S₆，因为｛1，4｝，｛3，5，6｝，｛2，7｝无重复、无遗漏地包含了1～7的所有整数。

我们可以用一个r行n列的01矩阵来表示一个精确覆盖问题，其中第i行第j个元素表示Si是否包含元素j（1表示包含，0表示不包含）。而n＝7，S₁＝｛1，4，7｝，S₂＝｛1，4｝，S₃＝｛4，5，7｝，S₄＝｛3，5，6｝，S₅＝｛2，3，6，7｝，S₆＝｛2，7｝，可以表示为一个6行7列的矩阵。

算法X（Algorithm X）。和普通的回溯法一样，我们可以编写一个递归过程求解精确覆盖问题。每次选择一个没有被覆盖的元素，然后选一个包含它的集合进行覆盖。对应到矩阵中，如果我们删除所有已经选择的行和已被覆盖的列，则这个递归过程可以描述为：每次选择一个没有被删除的列，然后枚举该列为1的所有行，尝试删除该行，递归搜索后恢复该行。删除行时，除了需标记“此行已删除”之外，同时还需要把该行中值为1的列删除，恢复时也同样。

舞蹈链（Dancing Links）。删除列并不是一个简单的操作，因为还需要把覆盖它的行也一并删除掉（因为每列只能被一行所覆盖）。为了提高效率，我们需要一种能高效支持上述操作的数据结构，它就是Knuth的舞蹈链（Dancing Links）。使用了舞蹈链的算法X通常称为DLX算法。

舞蹈链是一个4个方向的循环链表结构，每个普通结点对应矩阵中的一个1，另外还有n＋1个虚拟结点，其中每列最上方有一个虚拟结点，而所有虚拟结点的最前面有一个头结点h[14]，如图6-16所示。每个结点记录5个指针：L、R、U、D和C。

图　6-16

L和R：表示该结点所在行的左边相邻结点和右边相邻结点（由于是循环链表，每一行最左边结点的L指针指向这一行的最后一个结点；最右边结点的R指针指向这一行的第一个结点）。

U和D：表示该结点所在列的上边相邻结点和下方相邻结点（每一列的最上面虚拟结点的U指针指向这一列的最下方结点，最下方结点的D指针指向最上方的结点）。

C：指向该结点所在列虚拟结点[15]。

另外，每一列还维护一个S值，表示这一列中的普通结点个数（不包括虚拟结点），这样每次可以找一个S值最小的未删除列，进一步加快求解速度。

有了舞蹈链之后，可以高效地实现删除列和恢复列的过程。虽然代码很简单，原理却不太容易叙述清楚，这里略去[16]。请读者在阅读代码时注意恢复时遍历链表的顺序和删除时相反。

例题9　数独（Sudoku, SEERC 2006, LA 2659）

求解16行16列数独问题的一组解：给定一个16×16的字母方阵，要求将所有的空格子填上A～P中的一个字符，并满足以下条件。

（1）每行A～P恰好各出现一次。

（2）每列A～P恰好各出现一次。

（3）粗线分隔出的每个4×4子方阵（一共有4×4个）中，A～P恰好各出现一次。

该数独问题及其解如图6-17所示。

图　6-17

【输入格式】

输入包含多组数据。每组数据包含16行，每行包含16个字符。空格用连字符“－”表示。相邻两组数据之间有一个空行。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出填好的字母方阵。相邻两组数据之间输出一个空行。

【分析】

为了套用DLX算法框架，首先需要搞清楚行、列的含义，以及“1”在哪些格子中。

（1）行代表着决策（因为要“选”一些行）。每个决策可以用一个三元组（r，c，v）表示，意思是将（r，c）这个格子填上字母v，因此一共有16×16×16＝4096行。

（2）列代表着任务（因为要让每列恰好有一个1）。一共有以下4种需要达成的任务。

❑　Slot（a，b）表示第a和第b列的格子要有字母。

❑　Row（a，b）表示第a行要有字母b。

❑　Col（a，b）表示第a列要有字母b。

❑　Sub（a，b）表示第a个子方阵要有字母b。

因此一共有16×16×4＝1024列。

（3）“1”代表着一个决策达成了一个任务。上面已经将决策写成了三元组（i，j，k）的形式，不难发现它达成了4个任务：Grid（i，j）、Row（i，k）、Col （i，k）和Sub（Pij，k），其中Pij表示第i行第j列所在的子方阵编号。不难算出一共有4096×4＝16384个结点（即矩阵中的“1”）。

这样，本题成功地转化为一个4096行，1024列，1的个数为16384的精确覆盖问题。核心代码如下。

需要特别说明的是，舞蹈链并不是只能用来求解精确覆盖问题。如果每列可以被覆盖多次，或者有些列可以不覆盖，只需对算法稍作修改即可。比如，经典的n皇后问题就可以转化成这样的问题。虽然n皇后问题可以用构造法求解，但一旦稍作变形，构造法就会失效，但舞蹈链仍然适用。

6.4　几何专题

6.4.1　仿射变换与矩阵

仿射变换（Affine Transformation）是一类很常见的变换，平移、缩放、旋转都属于此类。它的精确定义和性质在很多资料中都能找到，这里不再赘述。

在算法竞赛中，选手除了需要熟悉各种仿射变换对应的代码之外，还应懂得它们所对应的矩阵。

还记得矩阵乘法吗？借助于矩阵记号，可以把二维逆时针旋转公式写成矩阵形式。

不难发现，缩放也可以这么写，但平移却不行（想一想，为什么）。怎么办呢？我们需要把二维的点改写成三维：（x，y，1）T。这样一来，平移、旋转和缩放都可以写成矩阵的形式。对点P进行变换，就等价于给P左乘一个3×3的变换矩阵[17]。

这个看似奇怪的（x，y，1）T称为点（x，y）的齐次坐标（Homogeneous Coordinate），它是计算机图形学和射影几何中的重要概念。齐次坐标不仅让“平移”变换拥有了矩阵表示法，而且很好地区分了向量和点——向量的第三维是0而不是1，符合前面说过的“向量＋向量＝向量，向量＋点＝向量，点＋点无定义”的性质。

例题10　仿射变换（Affine Mess, World Finals 2011, LA 5129）

平面上有一幅画，其中有3个特殊点，坐标分别为（x₁，y₁）、（x₂，y₂）和（x₃，y₃）。

首先，我们对画进行旋转变换。旋转时，先在中心为（0，0）、边长为20的正方形边界上指定一个点（x，y），然后把x轴正半轴旋转到穿过（x，y）。旋转后，所有顶点都会被捕捉到最近的整点（如果小数部分恰好为0.5，捕捉到离0较远的坐标点）。

接下来是放大和平移（顺序未知）。放大的中心总是（0，0），放大比例为非0整数（可以是负数，x方向和y方向的放大比例不一定相同）；x和y的平移量均为整数。

现在只知道画上的3个特殊点在变换后的坐标为（x₄，y₄）、（x₅，y₅）和（x₆，y₆），但不知道哪个点对应哪个点。可以确定变换方式么？如果有多种可能的变换，它们是等价的吗？

【输入格式】

输入包含多组数据。每组数据包含6对整数xi和yi（-500≤xi，yi≤500），前3对在第一行，后3对在第二行。前3对表示3个特殊点的初始坐标（保证两两不同），后3对表示这3个特殊点的最终坐标（保证两两不同）。注意，3个特殊点的最终坐标可以按照任意顺序给出。输入结束标志为6对0。

【输出格式】

对于每组数据，如果有一种或多种可能的变换方式，且这些方式全部等价，输出“equivalent solutions”；如果有多解，且不等价，输出“inconsistent solutions”；如果无解，输出“no solution”。

【分析】

本题看上去很麻烦，但其实只需要暴力枚举：先枚举输入和输出的3!＝6种对应关系，然后枚举80种旋转方式（中心在原点，边长为20的正方形的边界上有80个整点），最后计算平移和放大方式即可。注意到先平移再放大可以转化为先放大再平移，所以只需判断先放大再平移。不难发现，x和y方向是独立的。假设旋转后的点的x坐标为x₁，平移和放大之后的点在x₂，放大比例为s，平移位移为d，则可以列出方程：sx₁＋d＝x₂。同理，y方向也可以列出这样一个方程，3个点一共有6个方程。这样，每当对应关系和旋转方式确定下来之后，都有一个方程组，只需求解这些方程组并综合它们的解就能得出最终结论。

例题11　组合变换（Composite Transformations, UVa 12303）

空间中有n个点和m个平面，你的任务是按顺序向它们施加t个变换，输出每个点的最终位置和每个平面的最终方程。一共有3种变换，如表6-1所示。

表　6-1

【输入格式】

输入只有一组数据。第一行是3个整数n，m，t（1≤n，m≤50000，1≤t≤1000）；以下n行每行包含一个点的坐标；用以下m行每行4个整数a，b，c，d，描述一个平面ax＋by＋cz＋d＝0（a，b，c不全为0）；用以下t行描述各个操作。操作中的参数a，b，c，d是绝对值不超过10的实数，最多保留小数点后两位。参数theta是0～359之间的整数。

【输出格式】

对于每个点，输出一行，即变换后的坐标。对于每个平面，输出一行，即变换后平面方程的参数a，b，c和d。输出应保证a²＋b²＋c²＝1，但如果有多种表示方法，任意一种均可。输出应保留两位小数。本题允许一定的浮点误差。

【分析】

最容易想到的方法是逐点变换（对于平面，只需取不共线的3个点分别变换即可），可惜时间复杂度高达O（（n＋m）t），实在是太慢了。

前面提到过，二维仿射变换可以写成矩阵形式。不难想到，三维仿射变换也可以写成矩阵形式，只是齐次坐标将变成四维，因此变换矩阵也将是4×4的。矩阵乘法满足结合律，因此可以把所有变换矩阵事先乘起来，变成所谓的“组合变换矩阵”，然后用它左乘所有点，时间复杂度为O（n＋m＋t）。

需要特别注意的是，有的读者可能会用第4章中介绍的点法式来表示平面，然后用组合变换矩阵分别左乘点和法向量。遗憾的是，这样做是错误的，罪魁祸首在于“缩放”操作。要用点法式也可以，但法向量不应该左乘组合变换矩阵本身，而应该左乘它的逆转置矩阵（inverse transpose）；当然，最简单的做法仍是采用三点式。

关于齐次坐标和变换矩阵的详细内容，以及本题中3个变换所对应的矩阵，请读者自行查阅相关资料。

6.4.2　离散化和扫描法

离散化和扫描法是计算几何中的常用算法设计方法。这里通过几道例题向读者展示两者的基本思路和常用技巧。

离散化。当几何图形复杂起来的时候，即使最常见的面积、周长、体积这些东西也会变得难以计算。一个经典的例子就是求解“圆的并的面积”。平面上有n个大大小小的圆，其中一些圆相互遮挡，求这些圆所覆盖的总面积。其中，被多个圆覆盖到的面积只能算一次（因此答案可能小于所有圆的面积之和）。看上去似乎没什么难的，但如果用现有的几何知识编程求解，会发现这道题目并不简单。

本节将会详细地讨论这个问题。在此之前，让我们先来看一道简单点的题目，这样有助于理解离散化的思想。

例题12　山的轮廓线（The Sky is the Limit, World Finals 2008, LA 4127）

输入n座山的信息，计算它们的轮廓线长度（即从天空往下看，可以看到的线段的总长度）。如图6-18所示，虚线是不可见的，山和山之间的空白地带不算在轮廓线内。每座山都是一个等腰三角形。

图　6-18

【输入格式】

输入包含多组数据。每组数据的第一行为一个整数n（1≤n≤100），即山的个数；以下n行每行3个整数X，H，B（H＞0，B＞0），即山峰的横坐标、高度和山底宽度。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出轮廓线的长度，四舍五入到整数。

【分析】

如果山很多，形成的轮廓线可能会非常复杂。既然从整体上考虑比较麻烦，我们不妨考虑局部，把山分成一个一个的竖直长条，统计每个长条内的轮廓线长度，再累加起来，如图6-19所示。

图　6-19

一共有哪些虚线呢？不难发现，山顶和两个山底各需要一条虚线；两条线段的交点处也需要有一条线段。换句话说，我们可以先把所有端点的x坐标放到一个数组里，再对线段两两求交，把交点的x坐标也放在这个数组里，最后排序、去重，得到所有虚线的x坐标。代码如下。

现在，我们依次考虑每个竖直长条。不难发现，每个竖直长条内部不会有线段相交的情况（否则该交点处还会有虚线），因此我们可以计算出每条线段在竖直长条中点处的y坐标，其中最高的（即y坐标最大的）线段是可见的。把这条线段在竖直长条中的长度累加进答案即可。完整代码留给读者编写。

例题13　核电厂（Nuclear Plants, LA 3532）

某国是一个长n公里，宽m公里的矩形，边平行于坐标轴，左上角的坐标为（0，0），右下角的坐标为（n，m）。该国领土上有一些核电厂。核电厂有大小之分，小核电厂周围0.58公里内都不能种植大麦，大核电厂周围1.31公里内都不能种植大麦。你的任务是计算出可以种植大麦的总面积。

【输入格式】

输入包含多组数据，每组数据的第一行为4个整数n，m，ks和kl（1≤n，m≤10000，ks，kl≤100），其中ks为小核电厂的数目，kl为大核电厂的数目，接下来的ks行每行为一个小核电厂的坐标（x，y）（0≤x≤n，0≤y≤m），再接下来的kl描述大核电厂。输入结束标志为n＝m＝ks＝kl＝0。

【输出格式】

对于每组数据，输出可以种植大麦的国土面积，保留两位小数。输出允许有0.01的绝对误差。

【分析】

以每个小核电厂为圆心各画一个半径为0.58的圆，再以每个大核电厂为圆心各画一个半径为1.31的圆，则答案就是总国土面积减去这些圆的并的面积。因此，我们只需集中精力讨论圆的并的面积这个经典问题。

根据上题的经验，我们可以首先求出两两圆的所有交点，加上每个圆的左右边界，把每个圆离散化成若干个竖直条，如图6-20所示。

图　6-20

为了计算每个竖直条内的面积并，我们需要竖直条内的圆弧按照y坐标从小到大编号为1，2，3，…（见图6-20）。每个圆弧要么是一个圆的下边界（如圆弧1，2），要么是一个圆的上边界（如圆弧3，4），因此我们可以用第1章学过的扫描法，从下到上处理这些圆弧。

❑　首先设“当前圆计数器”cnt为0。

❑　每次遇到下圆弧时cnt加1。如果此时cnt变成了1，则记录该圆弧为“起始圆弧”。

❑　每次遇到上圆弧时cnt减1。如果此时cnt变成了0，说明“起始圆弧”和正在处理的圆弧构成了一个连通区域。只需计算该区域的面积，然后累加到答案中即可。

如何计算这个连通区域的面积呢？不难发现，该区域由上下两个弓形和中间的一个梯形ABCD组成，分别计算面积即可。

还有一种相对容易编写，效率也更快的方法。对于每个圆，求出没有被其他圆覆盖的圆弧区域，按照逆时针顺序连接这些弧对应的弦，这样，整个图形就变成了若干多边形和若干弓形，分别求和后加起来即可，如图6-21所示。

图　6-21

注意，多边形面积应按照有向面积计算。比如，在图6-22中，“洞”上的边呈顺时针排列，它的面积应当从大多边形内减去。

图　6-22

对于每个圆，需要用O（n）时间计算出被其他圆覆盖的角度区间，然后用O（nlogn）时间合并起来，多边形最多一共有O（n²）条边，最多一共有O（n²）个弓形，因此总时间复杂度为O（n²logn）。

例题14　可见的屋顶（Raising the Roof, World Finals 2007, LA 3809）

给定一些三角形的屋顶，你的任务是计算出暴露在阳光下的部分（即从天空向地面俯视时能看到的部分）的总面积。注意，被遮挡的部分不应该被算到总面积中。xy平面是地面，z轴竖直向上。

【输入格式】

输入包含多组数据。输入的第一行为顶点总数V和三角形总数T（1≤V≤300，1≤T≤1000）；以下V行每行3个整数x、y和z，即各个顶点的坐标；再以下T行每行3个整数，即3个顶点的编号（各顶点按照输入顺序编号为1～V）。所有坐标均为不超过100的正整数。输入没有退化的三角形。任意两个输入三角形的内部没有公共点（边界可以接触，但不会部分重合或者相交）。输入结束标志为n＝0。

【输出格式】

对于每组数据，输出可见部分的总面积，保留两位小数。

【分析】

注意，本题要求的是可见部分的原始面积而不是投影面积，所以只能分别求出每个三角形的面积，然后累加起来。可以证明，某个平面图形在另一平面上的投影的面积比上这个图形本身的面积恰好是这两个平面夹角的余弦值，因此只需求出每个三角形从上往下看的投影面积，就能反推出其原始面积。

不难想到这样一种算法，对于每个三角形，删除其被其他三角形挡住的部分，再计算面积，如图6-23所示。

图　6-23

注意，不仅可能会被分成多块，还可能会有“洞”。虽然理论上可以计算出来，但不够简单直观。

另一种算法在概念上比较简单，是解决本题的推荐算法，即把投影图按照x坐标离散化，即把投影分成多个“竖直条”，然后在每个竖直条内单独求解，如图6-24所示。

图　6-24

注意，在投影图中，每个竖直条由多个不重叠的梯形组成（三角形看成退化的梯形），每个梯形的面积取决于对应三角形在左右两条竖直线上的长度。这样，我们的任务就转化为求出每个三角形被每条竖直线截得的可见线段总长度。

对于一条竖直线（实际上是竖直平面，只不过在投影图上看着是竖直线），首先求出每个三角形被该线截得的线段，则问题转化为统计每条线段的可见部分长度。这一步可以用第1章介绍过的扫描法，从上到下处理各个端点事件：当遇到上端点时，新线段进入集合（注意：此线段不一定可见，有可能被其他线段挡住了）；当遇到下端点时，新线段退出集合。注意，当一条可见线段进入集合之前，应先累加“当前可见线段”的长度；当一条可见线段退出集合时，应累加即将退出的线段的可见长度。由于需要插入、删除和取最小值（z坐标最小的才可见），可以用一个优先队列实现。如图6-25所示为这个扫描过程，注意它画在了yz平面，因此“最上方”的线段是可见的。

图　6-25

按照时间顺序，6个事件依次如下。

（1）A进入集合，并且是可见线段。

（2）新的可见线段B进入集合。由于挡住了前可见的线段A，给A累加可见长度。

（3）C进入集合，但并不可见（被B挡住了）。

（4）A离开集合。由于此前A并不是可见线段，此事件没有任何影响。

（5）B离开集合。由于此前B是可见线段，给B累加可见长度。可见线段变为C。

（6）C离开集合。由于此前C是可见线段，给C累加可见长度。

到这里，本题已经基本解决，但有一个小问题需要读者注意，即我们并没有很好地处理平行于y轴的线段和竖直平面。后者可以直接扔掉（因为投影面积为0），但前者可能会导致错误。如图6-26所示，可见长度从中间的L＝1突变到L＝0，等价于一个和左边对称的三角形。解决方法是稍微旋转一下三角形，使得左图中间的竖直线分裂成两条。虽然中间的窄条也是错的，但是因为它实在太窄了，误差可以忽略不计。

图　6-26

求交点、离散化的时间为O（n²logn），每条竖直线的扫描过程需要O（nlogn），因此总时间复杂度为O（n³logn）。

扫描法。扫描法也是一种很常用的几何算法设计策略。事实上，在例题13和例题14中，我们已经用过扫描法了。离散化的主要思想是把复杂的几何图形划分成若干个相对简单的部分，分而治之。由于离散化通常要求事先输入或者计算出一些变量的取值（比如所有交点），我们说它是一个静态的过程；而扫描法则完全不同：不仅需要在处理事件时动态维护信息，还有可能动态生成新事件，而这些动态生成的事件很难或无法事先预知。下面的例题15就是一个绝好的例子。

例题15　画家（Painter, World Finals 2008, LA 4125）

画家Peer在一个矩形的画布上画了一些三角形。画完之后，他开始涂色。最外层的画布涂最浅的颜色，然后次外层的用次浅的颜色，依此类推。如图6-27所示，一共有4个层次的三角形，加上画布本身一共需要5种颜色。你的任务是帮助Peer在涂色之前判断出需要几种颜色。如果三角形出现相交（即其中一个三角形的某条边和另一个三角形的某条边有公共点），则涂色无法进行。

图　6-27

【输入格式】

输入包含多组数据。每组数据的第一行为非负整数n（n≤100000），即三角形的个数；以下n行每行6个整数x₁，y₁，x₂，y₂，x₃，y₃（-100000＜xi，yi＜100000），即三角形3个顶点的坐标。这3个顶点保证不共线。输入结束标志为n＝-1。

【输出格式】

对于每组数据，依次输出需要的颜色数。如果无法涂色，输出ERROR。

【分析】

n的规模太大，即使只是两两判断是否相交也会超时，更别说计算最大深度了。这时，我们再次使用第1章介绍的扫描法，并借助第3章中介绍的排序二叉树维护扫描线，可把时间复杂度降为O（nlogn）。

考虑一条竖直扫描线从左向右移动，每次碰到一个顶点就停下来。如图6-28所示，在任意时刻扫描线上都会有若干条直线。

图　6-28

如果每条线段都记录它上方近邻区域的深度，那么每次插入一个三角形的第一个顶点p₁（最左边顶点）时，p₁p₂和p₁p₃都会插入到扫描线上，计算出它们的深度，并更新最大深度。注意，插入线段p₂p₃的时候无需更新任何深度（想一想，为什么）。

上面的操作需要对一条插入的线段寻找“上一条”和“下一条”线段，还要保存深度。考虑到线段的数目可能很大，用BST最为方便。BST是基于比较的，因此需要定义线段的“小于”运算符，代码如下。

注意，比较运算使用的是扫描线和线段交点的y坐标。为了处理竖直线段，这里让p2.x增加了eps；为了避免不同的线段具有相同的y值，扫描线的x坐标也加了eps（相当于往右移动了一点点）。

有经验的读者可能会说：这个比较函数是动态的，对于不同扫描线，算出的y值不同，这样也可以吗？答案是可以。尽管扫描线的x坐标改变了，但所有线段的相对位置并没有改变（除非线段有相交，这个马上会提到）。那是否可以用线段插入时的y坐标进行比较呢？不可以。如图6-29所示，A在C的下面，但线段AB在CD的上面。

图　6-29

到目前为止，可以涂色的情况已经处理完毕了。线段相交的情况也不难处理：插入线段时判断它和上下相邻线段是否相交（注意，同一个三角形的线段不算相交，这就是Segment结构体中f的作用）；删除线段时判断它的上下相邻线段是否相交。

为了使代码更清晰，我们把事件封装成一个结构体。

然后主算法就不难编写了：先读入三角形，生成事件（每个三角形生成3个事件），然后对事件进行排序（同一个位置先插入后删除），最后按顺序依次处理每个事件。代码如下。

最后就是我们的重头戏——Scanline类。该类保存的数据有扫描线位置、被扫描线穿过的所有线段以及对应的深度（这些线段按照y坐标保存在一棵BST里，以支持前驱后继操作）。与之配合的是每个三角形的信息：3个顶点的坐标，以及3条边在扫描线中的位置（以便删除）。

这里的BST可以用STL中的multimap实现，因为我们只需要插入（insert）、删除（erase），以及求给定结点的前驱（--）、后继（++），并不需要维护其他信息。唯一需要注意的是，++和--会改变迭代器，所以在使用之前应进行备份。当然，也可以用前面介绍过的Treap实现这个BST，代码留给读者编写。

下面重申几个细节。

（1）可以在初始化的时候添加两条长长的线段，即画布的上边界和下边界，避免出现“上一条线段／下一条线段不存在”的情况。

（2）扫描线穿过三角形的最左边顶点p₁时，插入线段p₁₂和p₁₃，并根据二者的位置关系（哪个在上，哪个在下）设置两条线段的深度。

（3）扫描线穿过三角形的中间顶点p₂时，插入线段p₂₃，深度为p₁₂的深度，然后删除线段p₁₂。

（4）扫描线穿过三角形的最右边顶点p₃时，删除p₁₃和p₂₃即可。

（5）在扫描线中插入或删除线段时，判断线段相交。

事件处理的时间为O（nlogn），扫描线上最多有O（n）条线段，因此每个事件排序的时间为O（logn），总时间复杂度为O（nlogn）。实际运算效率也很高。

6.4.3　运动规划

运动规划（motion planning）是来自于机器人领域的一个相当有意思的主题。一般来说，涉及运动规划的题目综合考查了图论和几何的知识，代码复杂，容易出错。下面仅以两个典型例题介绍其中的常见思路。

例题16　拯救公主（Save the Princess, UVa 11921）

平面上有n个圆形障碍物，求从（xp，yp）到（xt，yt）至少要走多长距离。起点和终点保证不同，且不在障碍物的内部或者边界上。障碍物不会相交，但可能会相切，且接触点不能通过。

【输入格式】

输入的第一行为数据组数T（T≤50）。每组数据的第一行为4个整数xp，yp，xt，yt。第二行是整数n（0≤n≤50），即障碍物的个数；以下n行每行包含3个整数x，y，r，即障碍物的圆形坐标和半径。

【输出格式】

对于每组数据，输出最短路径长度。

【分析】

本题可以算作一种“最短路问题”，但和我们在第5章中讨论的内容有一个明显差异，即点和边是什么似乎并不明显。而且从起点到终点的路径有无穷多条。

怎么办？首先，需要将“无穷多”的可能性降为有限多个，方法是排除一些明显不可能最短的路线。

如图6-30所示，路线S→A→B→C→D→E→T有很多可以改进的地方。

图　6-30

❑　A处的拐弯完全没有必要，改成S直接沿直线走到B更好。

❑　S→B→C这条路可以改成先走到S和圆的切点B'，再走到C。

❑　从C到D可以改成走两个圆的公切线C'D'。

❑　从E走到T可以改成从切点E'走到T。

最后，路线改成了S→B'→C'→D'→E'→T。虽然这条路线也不一定是最短路线，但一定比刚才那条路要好。

请读者仔细研究上面的推理。它实际上是一个构图过程：首先求出S和T到所有圆的切线，然后求出每两个圆的公切线，则最短路的“拐点”一定是S、T和这些切点，而连接拐点之间的路径一定是这些切线或者圆弧（沿着圆周走）。这样，我们实际上得到了一张加权图，用Dijkstra算法求最短路即可。

需要注意的是，这些“边”不能穿过障碍物。换句话说，在求出切线之后要先看看它是否和其他圆相交或相切[18]，仅当没有这种情况出现时才把切线和切点加到图中。另外，对于同一个圆上的切点，只有相邻两点需要连边（权值等于劣弧长度），而不需要两两连边。切点和边最多O（n²）个，每次判断切线是否和障碍物相交需要O（n）时间，因此构图总时间为O（n³）。注意到Dijkstra本身只需O（n²）时间，因此可以用动态构图的方法加速。但对于本题的规模来说，事先构图的方法已经足够快，而且更易写易调，是首选方案。

例题17　拿行李（Collecting Luggage, World Finals 2007, LA2397）

飞机到达之后，需要在行李提取处拿行李。行李会在一个多边形传送带上匀速运动，你的初始位置在多边形外部，并且当行李一出现就立刻开始匀速行走（速度严格大于行李运动的速度），直到拿到行李。求出拿到行李的最短时间。

如图6-31所示，行李沿着ABCDEFABCDEF…的顺序沿着多边形的边界匀速移动，你从P点出发沿着图中的方向移动，最后在M点拿到行李。注意，你从P出发的时间正是行李出现在A点的时间。

图　6-31

【输入格式】

输入包含多组数据。每组数据的第一行为多边形的顶点数N（3≤N≤100）；以下N行每行两个整数xi和yi，按照逆时针顺序给出多边形的各顶点坐标；下一行有两个整数px和py，即人的起始点坐标；最后一行有两个正整数VL和VP（0＜VL＜VP＜10000），即行李的速度和人的速度。坐标的绝对值均不超过10000，单位为米；速度的单位为米／分钟。输入保证人的起点在多边形外，行李从输入的第一个顶点出发，按照逆时针顺序移动。输入结束标志为N＝0。

【输出格式】

对于每组数据，输出拿到行李的最短时间，四舍五入到最接近的秒数。

【分析】

和例题16一样，本题也是一个最短路问题，但目标点随时间在变，似乎很难处理。是否可以“化动为静”呢？答案是肯定的。

题目中有一个需要注意的条件：人的速度严格大于行李的速度。这样一来，如果人可以在t时间拿到行李，对于t'＞t时刻，一定也能拿到行李：先在t时刻拿到行李，然后跟着行李慢慢走，等到t'时刻到来。尽管这样的移动方式不是题目规定的“匀速移动”，但不难把它变成匀速的，根据移动总距离和时间即可算出等价的匀速移动速度。

这样，我们只需二分答案，计算行李的位置，然后求起点到行李位置的最短路，判断时间是否来得及。二分的初始上下界不难设定[19]，但最短路部分却需要注意。看上去，我们“只需”以起点、终点（行李的位置）和多边形的各个顶点为顶点，“不被挡住的线段”为边构图后调用Dijkstra算法，但这个“不被挡住的线段”充满了陷阱。

什么样的线段不被挡住呢？如果和多边形的边规范相交，显然会被挡住，因此，关键就在于非规范相交的情形。如图6-32所示，同样是非规范相交，左边的线段AB被挡住了，右边的AB却没有被挡住。一个解决方法是把右图的AB也看成是被挡住了。由于AP和PB都没有被挡住，我们仍然不会遗漏A→P→B这条路[20]。

图　6-32

综上所述，如果一条线段AB除了A点和B点外还有一个点在多边形上，那么AB就算成是被挡住的。反过来呢？如果AB上只有A和B两个点在多边形上，它是不是一定不会被挡住呢？未必。还需要排除“AB完全位于多边形内部”的情况，方法是测试AB的中点是否位于多边形内。

6.5　数学专题

我们在第2章中已经学到了很多和算法密切相关的数学知识和问题，但正所谓“书到用时方恨少”，对于算法来说，数学知识永远不嫌多。本节补充了一些在竞赛中不那么常见，但很有代表性的数学专题。特别是FFT和线性规划，可以说分别是信号处理领域和运筹学领域的核心。

6.5.1　小专题集锦

例题18　绿色的世界（A Greener World , UVa 11017）

有一个网格，每个格点都有一颗梨树。整个网格错切了theta度，于是每个格子都变成了一个菱形。现在在每个菱形的中间种一颗桃树，如图6-33所示。

图　6-33

给定一个格点多边形，求它的面积和内部的桃树棵数（种在多边形边界上的树不统计在内）。

【输入格式】

输入包含最多15组数据。每组数据的第一行为3个整数d、theta和N（1≤d＜10000，44＜theta＜136），其中N是多边形的顶点数；以下N行每行包含两个整数x和y，即多边形的顶点（0≤x，y≤100000）。顶点按照顺时针或逆时针顺序排列。输入结束标志为d＝theta＝N＝0。

【输出格式】

对于每组数据，输出桃树的棵数和多边形面积（四舍五入到最接近的整数）。

【分析】

本题的研究对象是格点。关于格点，最著名的定理莫过于Pick定理：给定一个顶点均为整点（即坐标为整数的点）的简单多边形，其面积A和内部格点数目I与边上格点数目B的关系是：A ＝I＋B/2－1。这个定理有很多精彩的证明，有兴趣的读者可以自行搜索相关资料，这里不再赘述。这里的重点是如何应用定理。

首先不难发现，“错切变换”并不会改变桃树的棵树，而只会影响到多边形的面积（这很容易计算出）。因此，此题等价于：给定一个顶点坐标均为整点的多边形，求它内部的x坐标和y坐标的小数部分均为0.5的点。这个问题留给读者思考[21]。

例题19　有趣的杨辉三角（Interesting Yang Hui Triangle, Shanghai 2006, LA 3700）

给出素数P和正整数N，求杨辉三角形的第N行中能整除P的数有几个。P≤1000，N≤10⁹。

【分析】

根据杨辉三角的定义，第n行第m个数等于C（n，m），因此本题等价于统计C（n，0），C（n，1），…，C（n－1），C（n，n）中有多少个数能被素数P整除。

关于组合数取模，有个很有趣的Lucas定理，公式如下。

其中mi和ni分别是m和n的p进制表示法中的各个数字。根据Lucas定理，不难得到以下推论：C（n，m）是p的倍数当且仅当存在一个ni＞mi。根据乘法原理，只需把n写成p进制形式，则所有ni＋1的乘积就是答案。

例题20　信息解密（Decrypt Messages, Shanghai 2009, LA 4746）

假设从2000年1月1日00:00:00到现在经过了x秒，计算xq mod p，设答案为a。这里p严格大于x。已知p，q，a，求现在时刻的所有可能值。

提示：如果一个年份是4的倍数但不是100的倍数，或者这个年份是400的倍数，这个年份是闰年。闰年的二月有29天，其他年（平年）的二月只有28天。在本题中，如果年份除以10的余数为5或者8，则这一年的最后还会有一个“闰秒”。比如，2005年12月31日23:59:59的下一秒是2005年12月31日23:59:60，再下一秒才是2006年1月1日00:00:00。

【输入格式】

输入的第一行为数据组数T。每组数据包含一行，包含3个整数p，q，a（2＜p≤1000000007，1＜q≤10，0≤a＜p，p保证为素数）。

【输出格式】

对于每组数据，输出所有可能的时间，按照时间顺序排列。如果无解，输出“Transmission error”。

【分析】

除开日历部分，本题其实是要解一个高次模方程xq≡a（mod p）。为了学习它的解法，我们首先介绍原根（为了简单起见，只考虑素数的情况）。

原根。对于素数p，如果存在一个正整数1＜a＜p，使得a¹，a²，a³，…，ap－1模p的值取遍1，2，…，p－1的所有整数，称a是p的一个原根（primitive root）。不难发现，a1，a2，a3，…，ap－1模p的值肯定和1，2，…，p－1一一对应（不重复，也不遗漏）。由欧拉定理可知，ap－1≡1（modp），因此，ap≡a¹（mod p），ap＋1≡a²（mod p），ap＋2≡a³（mod p），……换句话说，如果ai≡aj（mod p），则i≡j（mod p－1）。这里有两个例子：

❑　3是7的原根，因为3→2→6→4→5→1[22]，然后开始循环：1→3→2→6…

❑　2不是7的原根，因为2→4→1→2→4…，过早的循环了。

注意到ap－1≡1（mod p），这个生成序列一定会包含1，且在此之前不会有循环——要是在出现1之前就循环了，就永远不会出现1了，与欧拉定理矛盾。同样的道理，因为ap≡a（modp），前p－1个数实际上形成了一个大循环节。如果不是原根，在此之前一定还会有小循环节（比如，在模7下，2的小循环节是2→4→1），且小循环节的长度一定是大循环节长度p－1的约数。

原根的求法。最后让我们考虑一下如何找到原根。方法很简单：由于原根很多（可以证明有phi（p－1）个），所以可以逐一判断，或者不停地随机猜，然后判断。不管怎样，问题的核心在于判断一个数是不是原根。判断方法实际上已经讲过了：枚举小循环节长度b（它一定是p－1的真因子），判断是否有mb≡1（mod p）（如果是，则表示m不是原根）。虽然这个方法在理论上并不优秀，但在算法竞赛中已经够用。

模方程xq≡a（mod p）。如果找到了p的一个原根m，只需设x＝my，a＝mz，则方程变为mqy≡mz（mod p）。根据上面的结论，qy≡z（mod p－1）。q是已知量，而z可以用前面介绍过的Shank算法得到（看出来了吗？z是以m为底的a的离散对数，且解唯一），因此只需解这个模线性方程就可以得到y（注意，这一步可能多解），最后进行一次模取幂运算，得到x。当然，本题还需要得到当前时间和格式化输出，但这不是本节的重点，故略去。

最后说一句，并不是只有素数才有原根，而且高次方程也不是只有当模有原根的时候才能解。但相关讨论已经超出本书范围，有兴趣的读者可以自行参考相关资料。

6.5.2　快速傅里叶变换（FFT）

快速傅里叶变换是一个很有工程价值的算法，广泛地应用在音频、图像等数字信号处理软件中。傅里叶变换本身的理论很深，这里仅以快速多项式乘法为例介绍它在算法竞赛中的应用。

快速傅里叶变换（FFT）。什么是FFT？FFT（fast Fourier transform）是用来计算离散傅里叶变换（discrete Fourier transform, DFT）及其逆变换（IDFT）的快速算法。那什么是DFT呢？如果没有数学分析的基础，这里很难用严密的语言把它解释清楚；但如果你只需要一个直观感受的话，不妨记住这句话：DFT把时域信号变换为频域信号。

注意，时域和频域只是两种信号分析方法，并不是指两种不同类别的信号。在谈论语言信号的时候，“音量小”指的是时域，而“声音尖”指的是频域。在录音的时候，音频通常按照时域形式保存，即各个时刻采样到的振幅；如果需要各个频率的数据，则需要DFT。

DFT有一个很有意思的性质：时域卷积，频域乘积；频域卷积，时域乘积。如果你不知道什么是卷积（convolution），也没关系，请再记住一句话：多项式乘法实际上是多项式系数向量的卷积。

多项式乘法。给定两个单变量多项式A（x）和B（x），次数均不超过n，如何快速计算二者的乘积呢？最简单的方法就是把系数两两相乘，再相加。这样计算的时间复杂度为O（n²），当n很大时速度将会很慢。注意，高精度整数的乘法是多项式乘法的特殊情况（想一想，为什么），所以多项式乘法的快速乘法也可以用来做高精度乘法。

上述算法之所以慢，是因为表示方法不好。虽然“系数序列”是最自然的表示方法，但却不适合用来计算乘法。在多项式快速乘法中，需要用点值法来表示一个多项式。点值表示是一个“点－值”对的序列｛（x₀，y₀），（x₁，y₁），…，（xn－1，yn－1）｝，它代表一个满足yk＝A（xk）的多项式A。可以证明：恰好有一个次数小于n的多项式满足这个条件。

点值表示法非常适合做乘法：只要两个多项式的点集｛xi｝相同，则只需把对应的值乘起来就可以了，只需O（n）时间。但问题在于输入输出的时候仍需要采用传统的系数表示法，因此需要快速地在系数表示和点值表示之间转换。还记得刚才那句话吗？时域卷积，频域乘积。也就是说，多项式的系数表示法对应于时域，而点值表示法对应于频域，因此只需要用FFT计算出一个DFT，就可以完成转换。

单位根。这样算出来的点值表示法，对应的求值点是哪些呢？答案是2n次单位根。所谓“n次单位根”，是指满足xn＝1的复数。xn＝1的话，x岂不是就等于1？很遗憾，那只是实数域中的情况。在复数域中，1恰好有n个单位根e2kπi/n，其中i是虚数单位（i²＝-1），而eui＝cosu＋isinu。单位根非常特殊，因此FFT才有办法在更短的时间内求出多项式在这些点的取值。

利用FFT进行快速多项式乘法。基础知识已经讲完，下面直接给出具体步骤。

步骤一（补0）：在两个多项式的最前面补0，得到两个2n次多项式，设系数向量分别为v₁和v₂。

步骤二（求值）：用FFT计算f₁＝DFT（v₁）和f₂＝DFT（v₂）。这里得到的f₁和f₂分别是两个输入多项式在2n次单位根处的各个取值（即点值表示）。

步骤三（乘法）：把两个向量f₁和f₂的每一维对应相乘，得到向量f。它对应输入多项式乘积的点值表示。

步骤四（插值）：用FFT计算v＝IDFT（f），其中v就是乘积的系数向量。

FFT算法（包括DFT和IDFT）的详细过程和代码不难在网上找到，这里略去。

例题21　超级扑克II（Super Joker II, UVa 12298）

有一副超级扑克，包含无数张牌。对于每个正合数p，恰好有4张牌：黑桃p，红桃p，梅花p和方块p（分别用pS、pH、pC和pD表示）。没有其他类型的牌。

给定一个整数n，从4种花色中各选一张牌，问有多少种组合可以使得点数之和等于n。例如，n＝24的时候，有一种组合方法是4S＋6H＋4C＋10D，如图6-34所示。

图　6-34

不巧的是，有些牌已经丢失（题目会提供已经丢失的牌的列表）。并且为了让题目更有趣，我们还会提供两个正整数a和b，你的任务是按顺序输出n＝a，n＝a＋1，n＝a＋2，…，n＝b时的答案。

【输入格式】

输入包含不超过25组数据。每组数据的第一行为3个整数a，b，c，其中c是已丢失的牌的张数。第二行包含c个不同的字符串，即已丢失的牌。这些牌形如pS，pH，pC或者pD，其中p是一个正合数。输入结束标志为a＝b＝c＝0。最多有一组数据满足a＝1，b＝50000且c≤10000，其他数据满足1≤a≤b≤100，0≤c≤10。

【输出格式】

对于每组数据，输出p行，每行一个整数。每组数据后输出一个空行。

【分析】

首先考虑这样一个组合问题。有k种元素，均有无穷多个，规定第i种元素选取的个数ci必须属于一个特定的集合Si，当需要选取r个元素时，有多少种选取方法？比如，有苹果、香蕉和桃子3种水果，如果苹果只能选不超过3个，选香蕉的个数必须是5的倍数，而桃子的个数必须是素数，问选r个水果有几种方法。

解决方法是把每个集合写成一个多项式，使得每一项xi的系数取决于i是否在集合中存在：如果存在，则系数等于1；否则，系数等于0。这样，苹果对应的多项式是1＋x＋x²＋x³。同理，香蕉对应的多项式是x⁵＋x¹⁰＋x¹⁵＋…，桃子对应的多项式是x²＋x³＋x⁵＋x⁷＋…。

根据乘法原理和加法原理，不难得出这样的结论：把所有多项式乘起来，则结果的xr项的系数就是选r个元素的方法数[23]。

对应到本题中，每个花色对应一个多项式，乘起来就可以得到所有答案。当然，因为多项式的次数可能很大，需要用FFT加速，时间复杂度为O（nlogn）。

6.5.3　线性规划

线性规划是运筹学中研究较早、发展较快、应用广泛、方法较成熟的一个重要分支。它的重要性和各种应用很容易在各种资料中找到，这里不再赘述。但为了方便读者更快地入门，这里仍会给出一些必要的基础知识介绍。

线性规划问题。给定n个实数变量和一些线性约束[24]，确定这n个变量的值，使得某个线性函数最大化或者最小化。比如下面的式子

同一个问题往往有多种表述方式（比如约束x₁－x₂≥0也可以写成x₂≤x₁）。为了方便，一般在求解前先把线性规划改成标准形式，它满足：目标函数是最大化的；所有线性约束都是“左边小于等于某个常数”的形式；所有变量都是非负的。如上面的式子即为标准形式。

对于不是标准形式的问题，如何化成标准形式呢？如果目标函数是最小化的，目标函数取个相反数即可；约束A≥B写成B≤A；约束A＝B拆成两个约束A≤B和B≤A（如果右边有变量，移项到左边），而对于没有非负约束的变量xi，拆成两个变量xi＝（xi'－xi"）。

图论中很多问题都可以转化为线性规划问题，如最短路问题和最大流、最小费用流问题等。当然，上述问题均已有了自己的成熟解决方案，无须再使用复杂的单纯形法，但对于另外一些问题，如多商品流问题，则直接建立线性规划模型更合适。

线性规划的解法有很多，最经典的算法是单纯形法[25]。该算法思路直观但细节繁琐，这里不再赘述，读者很容易在网上找到较好的讲解和代码实现。

例题22　食物分配（Happiness!, UVa 10498）

有n种食物和m个人，你的任务是买一些食物，使得每个人都不会吃撑，且在此前提下尽量多花钱。对于每个人i来说，每种食物j都有一个系数aij，表示每单位这种食物为这个人带来的愉快值。每个人i还有一个最大愉快值bi，表示当食物为他带来的总愉快值超过bi时，此人将会吃撑。

【输入格式】

输入包含多组数据。每组数据的第一行为两个整数n和m（3≤n，m≤20），即食物的种数和人数；第二行包含n个实数，表示每种食物的单价；以下m行每行包含n＋1个实数，前n个实数分别为系数ai1，ai2，…，ain，最后一个整数为bi。输入结束标志为文件结束符（EOF）。

【输出格式】

对于每组数据，输出花的钱数的最大值，向上取整。

【分析】

本题是非常明显的线性规划，甚至已经写成了标准形式。所有xi＝0就是初始可行解，所以连初始化的过程都不用了。初学的读者可以拿此题作为线性规划的入门题目。

6.6　浅谈代码设计与静态查错

6.6.1　简单的Bash

虽说算法竞赛的重点是算法设计，但编程能力仍然是一项重要的考查方面。因此，参赛者除了要对编程语言非常熟悉（包括语法和库函数）之外，对于复杂程序的设计、编码和调试能力也是有一定要求的。你没有听错，这里使用“设计”一词，为的是表明宏观思维的重要性。强烈建议读者阅读本书附录，以了解更多关于OOP（面向对象编程）、FP（函数式编程）等编程范式（programming paradigm）的内容。学会用设计师而非打字员的思维方式看待程序，是算法走向工程的关键一步。

例题23　简单的Bash（3KP-Bash Project, UVa 10966）

（为了加强严密性，题目文字根据官方数据进行了增删）

你的任务是为一个假想的3KP操作系统编写一个简单的Bash模拟器。由于操作系统本身还没有写完，你暂时需要在模拟器的内存中虚拟出一个文件系统，而不是和真实的文件系统交互。下面介绍3KP操作系统中的文件系统。

文件（file）是数据存储的最小单位。文件名由英文字母（大小写敏感）、数字和点（.）组成，但不能包含两个连续的点。用户创建的文件名不能是单个的点字符（它代表当前目录）。文件名长度不能超过255，文件大小不能超过2⁶³。一个文件可能具有directory属性和hidden属性。

目录（directory）是一个大小为0、具有directory属性的文件，里面可以保存任意数量的目录和文件。空文件系统只有一个文件，叫做“根目录”。在任意时刻，有一个称为“当前目录”的目录。Bash启动时，当前目录就是根目录。

指令中引用文件的时候，可以使用绝对路径或者相对路径。绝对路径以字符“/”开头，如“/home/acm/uva”；相对路径不以字符“/”开头，当前目录和根目录分别用“.”和“..”表示，如当前目录为／home/acm/uva，那么“../../../home/..”就是根目录。

你的Bash模拟器需要支持8个命令，具体如表6-2所示。

表　6-2

续表

命令行包含一条或多条命令，用管道符号“｜”隔开（管道符号的前后不一定有空格）。第一条命令必须是除了grep之外的上述命令之一，而剩下的命令必须是grep。如果违反上述规定，应输出bad usage，并且不执行任何命令。

输入保证满足以下条件：每个命令的必选参数（如果有的话）都是第一个参数，可选参数在必选参数之后，但可能以任意顺序出现。命令和参数、参数和参数之间用一个或多个空格隔开。用户不会忽略必选参数，不会使用非法参数，也不会在除了grep之外的其他命令中使用引号。如果第一条命令是touch或者mkdir，并且命令返回“bad usage”，则不执行后面的命令。

【输入格式】

输入包含多组数据，每组数据对应一次独立的BASH会话。数据的每行为一行命令（长度不超过2048个字符）。命令的参数之间可以有任意数量的空白字符。每次会话的最后一条命令保证为exit。输入结束标志为文件结束符（EOF）。假定每次会话开始时，文件系统为空，当前目录为根目录。

【输出格式】

依次输出每行命令的输出（如果有的话）。

【样例输入】

【样例输出】

【分析】

这是一个典型的模拟题目。题目本身没有太多的算法可言，但由于需求较多，细节复杂，需要合理设计和规划代码，才能在较短的时间内完成一份高质量的代码。

从逻辑上说，整个文件系统是一棵有根树，我们不妨用“儿子列表”的方法来表示它。首先给每个结点分配一个编号，然后编写以下代码。

这叫做核心数据结构设计。其实，我们事先并不知道File结构体应该包含哪些内容，所以上面的代码是逐渐完善的，或者说，是用迭代的方式开发的。

有了核心数据结构之后，通常开始进行主流程设计。不难发现，本题的核心就是“执行命令”，所以可以设计如下的runCommandLine函数作为核心函数。由于管道的存在，该函数的一个重要作用是分离出各个命令，从左到右依次执行，并且负责把前一个命令的输出送入到后一个命令的输入。代码中的VS是一个自定义类型，代表vector＜string＞。

①　trim函数的作用是删除字符串开头和结尾的空白字符，请读者自行实现。

这就是整体的程序逻辑。在编写上述函数时，用到了一个尚未实现的函数，称为runCommand。虽然还没有编写这个函数，但其实我们已经想好了用它干什么——执行单个命令（即不用管道的命令）。这种先使用后定义函数的方法称为“自顶向下，逐步求精”。由于它符合人的思维习惯，因此受到了包括笔者在内的不少人的喜爱。

runCommand函数的实现方法不止一种。由于命令比较少，这里直接把所有命令放到同一个函数中实现，但如果命令过多，可以采用一些工程性更强的方法（如写一个Command的基类，然后派生出很多子类）。

这份代码继续贯彻了“自顶向下，逐步求精”的方针，而且更加彻底：连函数体都没写完，而是使用了一些注释占位置。根据情况，这些注释可能会被几行代码代替，也可能被一个函数调用代替——如果那个指令太复杂，不适合写在runCommand函数内的话。

不管是处理什么命令，首先都得解析命令参数，代码如下。

上面给出了3个相对容易的命令（cd、pwd和exit）的实现。它们用到了两个重要的函数：getDirNode（）和newSession（），其中，启动新的会话是这样的。

获得路径对应的结点也不难，代码如下。

下面是其他命令的实现。touch和mkdir非常接近，下面给出touch的实现，请读者自行实现mkdir命令。

下面的代码涉及3个函数。首先是splitFileName函数（用于分离文件名和路径）和isValidFileName函数（用于检查文件名是否合法）。

下面是创建文件的函数，它也并不复杂，代码如下。

最后是find和ls命令的实现。二者也很接近，这里给出find的实现，请读者自行实现ls。

最重要的就是我们的递归查找函数findFileEx，其代码如下。

comp函数是find命令的输出排序依据，代码如下。

最后是格式化输出函数，代码如下。

6.6.2　《仙剑奇侠传四》之最后的战役

对于查错来说，跟踪调试并不是万能的，输出中间结果也不是总能奏效。静态查错[26]这一常被人忽略的“原始方法”是上述两种工具的有效补充。这里举一个例子，希望读者通过它锻炼自己的“眼力”。

例题24　最后的战役（Final Combat, Wuhan 2009, LA4488）

本题是2007年度最佳RPG游戏之一——《仙剑奇侠传四》中最后一战的简化版。你的任务和原作一样，是用4个主人公打败玄霄和夙瑶，阻止琼华派飞升。

游戏的主角有4个：云天河（YTH）、韩菱纱（HLS）、柳梦璃（LML）和慕容紫英（MRZY）。战斗开始前，你需要恰好选3人上场，并且按照某种顺序排列。令排序后的3个主角分别为H1、H2和H3。和游戏中一样，这一战只需打败夙瑶即可。尽管在游戏中，玄霄也是可以打败的，但在本题中，请假设玄霄拥有不死之身。

如图6-35所示是一张战斗截图，其中H1是YTH，H2是HLS，H3是LML。战斗采用半回合制，即战斗双方的每个人都有一个头像在“进度条”上从左到右移动，只有当头像移到最右端时，对应的角色才能行动。如果有多个头像同时到达最右端，则行动的优先级从高到低依次为YTH、HLS、LML、MRZY、XX和SY。当有角色在行动时，所有头像停止移动。至其行动完毕后，该角色的头像重新开始从进度条左端点向右移动。没有角色行动时，所有头像同时向右移动（但速度不一定相同，见下）。

图　6-35

在本题中，每个角色有4个基本属性：精、气、神、速。

❑　精：生命力。当精的数值小于等于0时，该角色死亡。每个角色都有一个精上限，用maxjing表示。

❑　气：用来进行特殊技攻击。当气不够时，某些特殊技无法使用。每个角色的气上限总是100。

❑　神：用来催动仙术。当神不够时，某些仙术将无法使用。每个角色都有一个神上限，用maxshen表示。

❑　速：决定该角色头像在进度条上的移动速度。速总是1～4的整数。若一个角色的速等于x，则它的头像恰好需要5－x个单位时间从进度条的最左端移到最右端。一个角色的速用su来表示（在本题中，每个角色的速是不变的）。

为简单起见，本题假设玄霄和夙瑶采用如下的简单攻击策略：

❑　在他／她的第4n＋1次（n＝0，1，2，…）行动时，对H1进行武器攻击。

❑　在他／她的第4n＋2次（n＝0，1，2，…）行动时，对H2进行武器攻击。

❑　在他／她的第4n＋3次（n＝0，1，2，…）行动时，对H3进行武器攻击。

❑　在他／她的第4n＋4次（n＝0，1，2，…）行动时，对H1、H2、H3全体进行特殊技攻击。

每个主角都用4个属性来描述玄霄和夙瑶对自己的伤害：d1x和d2x表示玄霄的武器攻击和特殊技攻击对自己的伤害，而d1s和d2s表示夙瑶的武器攻击和特殊技攻击对自己的伤害。

作为一个剧情派玩家，你不想过多的斟酌战斗策略，因此在一名主角需要行动时，你只会考虑以下4种可能。

❑　用武器攻击玄霄或夙瑶（只能选其中一人，不能同时攻击两人）。注意，夙瑶已经使用了九幽猝寒剑护体（见图6-35），因此，如果对她进行武器攻击，攻击者将被反弹致伤，因反弹所受的伤害值等于夙瑶遭受的伤害值，用wad表示。用武器攻击玄霄（如果你乐意的话），则不会被反弹。

❑　用仙术“雨润”回复自己的精（为了避免把事情搞复杂，你决定不使用其他回复类仙术，也不帮其他主角回复精）。使用一次雨润，将消耗yurun_shen个单位的神，增加yurun_jing个单位的精（如果增加后超过了maxjing，精将变回maxjing）。

❑　用道具“鼠儿果”回复自己的神（为了避免把事情搞复杂，你决定不使用其他回复类道具，也不帮其他主角回复神。另外，假设你的鼠儿果有无穷多）。每次只能使用一个鼠儿果，将增加shuerguo_shen个单位的神（如果增加后超过了maxshen，神将变回maxshen）。

❑　进行特殊技攻击。每名主角恰好会一种特殊技，同时攻击夙瑶和玄霄。注意：有些特殊技也是物理攻击（这些特殊技用ssp＝1表示，其他特殊技满足ssp＝0），因此也会被夙瑶反弹致伤，因反弹所受的伤害值等于夙瑶遭受的伤害值，用ssd表示。特殊技需要的气用ssq表示。

你可能已经注意到，仙术和道具都无法回复气。事实上，只有两种方法可以攒气：使用武器攻击或者被武器攻击打中。使用特殊技攻击、被特殊技打中或被反弹致伤时，气不会增加。每次进行武器攻击时，气将增加q1（不管是否被反弹）；每次被武器攻击打中时，气将增加q2。如果增加后气超过100，将变回100。

你是一个完美主义者，因此你不希望战斗中途有任何一位主角死亡，哪怕是暂时的（在原作中，可以使用仙术或道具让死去的主角复活）。特别是在打败夙瑶时，不能让主角与她同归于尽（如主角在武器攻击后被反弹致死）。你的任务是精密计划每个角色的每次行动，使得在上述前提之下，用尽可能短的时间（只计算头像移动时间，不算行动时间）打败夙瑶，赢得这场最后的战役。

【输入格式】

输入包括不超过100组数据。每组数据的第一行为6个正整数，分别是SY_jing（夙瑶的初始精）、XX_su（玄霄的速）、SY_su（夙瑶的速）、yurun_jing、yurun_shen和shuerguo_shen。接下来的4行分别是描述云天河、韩菱纱、柳梦璃和慕容紫英的属性，其中每行包含16个非负整数：maxjing，maxshen，su，d1x，d2x，d2s，wad，ssd，ssq，ssp，q₁，q₂，jing，qi和shen。最后3个整数表示战斗开始时的精、气、神值（1≤jing≤maxjing，0≤qi≤100，0≤shen≤maxshen），其他属性的含义已在题目中描述过。所有待输入参数的取值范围如表6-3所示。

表　6-3

输入结束标志为6个0。

【输出格式】

对于每组数据，输出最短时间和所有可能的角色顺序。每种角色顺序用3个字母来表示，其中每个字母代表一个角色（Y代表云天河，H代表韩菱纱，L代表柳梦璃，M代表慕容紫英）。比如，若H1是韩菱纱，H2是柳梦璃，H3是云天河，则角色顺序用HLY表示。所有可能的角色顺序应按照字典序排列。如果无法在12个回合内取胜，输出-1。一组数据输完之后，打印一个空行。

【样例输入】

【样例输出】

【样例解释】

在第一个样例中，所有人的速都是1，因此，最好情况就是在时刻4，每个己方角色攻击一次后立刻结束战斗。这是可以做到的，让我们慢慢分析。

云天河的特殊技很强（伤害值为900），但气不够（因为20＜100），因此第一回合无法使用。韩菱纱也不能使用特殊技攻击，因为她会立即被反弹致死。柳梦璃的武器攻击很弱，但却可以用特殊技——虽然会被反弹（她的特殊技是物理攻击），但不会致死，因为400＜450。慕容紫英的特殊技也是物理攻击，而且攻击力强，同样会被反弹致死，因此不能使用（因为700＞600）。

总结一下，我们可以用的有云天河的武器攻击（伤害值300），韩菱纱的武器攻击（伤害值300），柳梦璃的武器攻击（伤害值100）和特殊技攻击（伤害值400），以及慕容紫英的武器攻击（伤害值250）。不难发现，只有使用云天河、韩菱纱和柳梦璃，才能在第一轮进攻后立即结束战斗，其排列顺序随意。注意，夙瑶被打败后战斗立即结束，因此不用担心玄霄和夙瑶那些可怕的攻击。

在第二个样例中，云天河特别弱，因此可以直接排除。另外，该样例无法避免玄霄的第一次攻击，而这次强大的攻击能直接打败除柳梦璃之外的所有人，因此似乎柳梦璃是H1的不二人选。但这样的推理忽略了一点，那就是韩菱纱会在玄霄之前行动，因此可以通过用雨润回复精来免于一死。慕容紫英就没有这样幸运了，因为他的速度太慢，需要在玄霄之后才能行动。

所有的武器攻击都很弱，因此最好的策略是先攒气，然后使用攻击力较强的特殊技。柳梦璃的速度特别慢，但是她的气在一开始就是足够的，因此只需要先回复神，再回复精，最后进行特殊技攻击（为什么这样复杂？因为她的特殊技是物理攻击）。注意：战斗结束之前夙瑶只能攻击H1和H2，因此H3少一次攒气的机会。事实上，韩菱纱和慕容紫英都不能是H3，否则将无法攒够气。

总结一下，韩菱纱可以是H1或者H2，柳梦璃没有限制，慕容紫英只能是H1，因此唯一的可能排列是HML。

在第三个样例中，韩菱纱的特殊技（伤害值6861）和柳梦璃的特殊技（伤害值24850）联合起来可以打败夙瑶，柳梦璃需要先回复精，但她的初始神不够。如果我们把她的初始神改成543（足够使用一次雨润），则答案将变成6 LYH LYM。

【分析】

不管主算法是什么，我们总是可以写出如下的算法框架。

问题的关键就在于这个solve函数，即给定最大时间，判断是否能打败夙瑶。由于每个人是独立的，因此，我们可以依次计算所有人在不同位置（H1、H2和H3）时最多可以给夙瑶多大的伤害，然后枚举所有排列方式，判断各个位置上的人对夙瑶的伤害值加起来是否能把她打败。计算单人对夙瑶的伤害时，以当前时间、精气神量作为状态进行记忆化搜索即可。

下面给出solve函数的一种实现。注意，从现在开始，这里给出的代码都是可能有bug的，请读者擦亮眼睛。

dfs过程代码如下。

上述代码看上去很有道理，却隐含着12个错误。这些错误有的比较明显（比如，“＜=”写成了“＜”，q2写成了q1，或者忘记了累加伤害值）；有的虽然难以用肉眼发现，却容易调试出来（比如，记忆化表格没有初始化、位置编号不统一），还有的只在很特殊的情况下才会表现出来，测试样例或简单数据的时候根本无法发现。

强烈建议读者尝试着用肉眼，借助着仔细和严密思考来找到这些错误，这样做对编程能力的提升是巨大的。这并不是一个不可能完成的任务——事实上，笔者在2010年的NOI全国冬令营中曾经用这份代码现场考验了在场的营员[27]，借助于集体的力量，所有错误都被找到了。

6.7　小结与习题

至此，本书的算法部分已经全部讨论完毕。虽然内容看上去很多很杂，但如果你保持浓厚的兴趣，勤加练习，相信你一定能够掌握这些算法的精髓。

1．轮廓线上的动态规划

如表6-4所示的3道例题都十分经典，请读者仔细体会。

表　6-4

下面是习题和翻译（见表6-5）。部分题目相当麻烦，但对思维和编程能力的锻炼和提升作用也是巨大的。

表　6-5

ACM谜题（ACM Puzzles, Dhaka 2007, LA 4058）

输入n（1≤n≤2000），用图6-36中的22种图形铺满一个3行n列的网格有多少种方法？输入答案除以10¹²的余数。

图　6-36

网格覆盖（Ignore the Blocks, UVa 11741）

在一个R ×C的网格中有n个黑格，其余均为白格。要求用1×2的骨牌覆盖所有白格（每个白格恰好被一块骨牌覆盖，且所有黑格均没有被覆盖），计算有多少种方案，并输出方案总数除以10000007后的余数。其中，1≤R≤4，1≤C≤10⁸，0≤N≤100。

网格里的树（Tree in a Grid, UVa 11443）

有一个r行c列的点阵（1≤r≤200，1≤c≤8），行从上到下编号为0～r－1，列从左到右编号为0～c－1。第i行j列的点记为（i，j），它的上、左、下、右相邻点的坐标分别为（i－1，j）、（i，j－1）、（i＋1，j）和（i，j＋1）。（i，j）和这4个点（如果存在的话）之间可以连一条边。

目前，这个点阵中有一些边已经连好，你的任务是继续连边，使得点阵中的所有点连通，但不构成环，然后统计方案总数除以md的余数（1≤md≤1000000）。

神奇的七（Magical Seven, UVa 11276）

给出一个7×n（1≤n＜2⁶⁴）的网格，每个格子看成一个点，有公共边的格子连一条弧，可以得到一个无向图G。设A为G的完美匹配个数，B为G的哈密顿圈的个数，C为满足以下条件的G的生成子图的个数：该生成子图的每个连通分量均为一个圈。你的任务是计算A＋B＋C。

修筑水道（Channel, World Finals 2010, LA 4789）

有一个r（2≤r≤20）行c（2≤c≤9）列的农田，有些格子是土地（用“.”表示），有些格子是石块（用“#”表示）。你的任务是修筑一条从左上角流到右下角的水道。水道应该尽量长，这样才能灌溉到尽量多的土地。

水道不能穿过任何一个石块，而且为了保证水总是沿着水道流，水道不能和自己接触，哪怕只接触一个角也不行（水可能会从这个角流过去）。如图6-37所示是两个接触到自身（因此非法）的水道，你的任务是输出最长的水道。输入保证最长的水道是唯一的。

图　6-37

修建长城（Construct the Great Wall, Chengdu 2011, LA 5762）

给定一个H行W列（1≤H，W≤8）的网格，用一个边沿着网格线的简单多边形围住所有的O，但不能围住任何一个X。要求长度最短。如图6-38（a）所示是合法解，如图6-38（b）所示是非法解（简单多边形的非相邻边不能有公共点）。

图　6-38

2．数据结构综合应用

如表6-6所示的3道例题涉及了两个重要思想：嵌套和分块。

表　6-6

下面是习题和翻译，如表6-7所示。个别题目涉及了例题部分没有涉及到的数据结构，也有的题目虽然可以只用第3章介绍的数据结构解决，却需要更好的综合能力才能解决。

表　6-7

交通堵塞（Traffic Jam, Changchun 2007, LA 4082）

有一个两行C列的城市网格。每个城市用（r，c）表示，即第r行的第c列。一共有3C－2条道路，用两端的城市坐标（r1，c1）－（r2，c2）表示。

要求支持3种操作：打开某条边；关闭某条边；检查某两个点是否连通（即：只通过打开的边相互可达）。初始时所有边关闭。

【输入格式】

输入第一行为数据组数T（1≤T≤11）。每组数据第一行为网格的列数C（1≤C≤100000）。以下是不超过100000条查询，以Exit查询结尾。其中查询格式为：Command r1 c1 r2 c2，其中Command为Open（打开一条边）、Close（关闭一条边）或者Ask（连通性查询）。

【输出格式】

对于每个Ask查询，如果连通，输出Y，否则输出N。

永远的和谐（Harmony Forever, LA 3699）

设计一个数据结构，维护一个集合S（初始为空），支持如下两种指令。

❑　B X：把整数X加入集合S（保证不会重复加入）。

❑　A Y：输出除以Y余数最小的数。如果有多个，输出最后加入的。

【输入格式】

输入包含多组数据。每组数据第一行为整数T（1≤T≤40000）。以下T行每行描述一条指令，格式为“B X”或者“A Y”，其中1≤X，Y≤500000

【输出格式】

对于每条A指令，输出结果。如果集合为空，输出-1。

动态区间不同值（Dyanmic len（set（a[L:R]））, UVa 12345）

在Python语言中，我们可以用len（set（a[L:R]））来统计元素a[L]，a[L＋1]，...，a[R－1]中不同值的个数。下面是交互式命令行下的几条Python语句的执行情况，帮助你理解它的工作原理（注意列表下标从0开始）。

你的任务是高效模拟这个过程。

【输入格式】

输入仅包含一组数据，其中第一行为两个整数n和m（1≤n，m≤50000），第二行为列表a的各个元素值。每个元素都是1～10⁶之间的整数。以下m行的格式有两种情况：M x y（1≤y≤10⁶）表示执行赋值a[x]＝y；Q x y表示输出len（set（a[x:y]））的值。输入保证不会出现访问越界的情况。

【输出格式】

对于每条Q指令，输出结果。

探照灯（Searchlights, Hangzhou 2010, LA 4841）

在一个n行m列（n≤100，m≤10000）的网格中有一些探照灯，每个探照灯有一个最大等级k（代表这个探照灯可以开到等级k，k－1，k－2，…，1）。如果把一个探照灯开到等级L，它可以照亮上下左右各L个格子（L＝1表示它只能照亮它自己所在的格子）。如图6-39所示是一个开到等级为3的探照灯。

图　6-39

你的任务是选择一些探照灯，把它们开到一个相同的等级，使得所有格子被监控。为了节约能量，这个相同的等级应尽量小。一个格子被监控的条件是：要么这个格子本身有一个开着的探照灯，要么这个格子同时被水平方向和垂直方向的探照灯照亮。

魔法师的帽子（Hanging Hats, CERC 2010, LA 4980）

魔法师有n顶帽子需要依次悬挂在墙上。每顶帽子在挂好后都会变成一个底在地面上的等腰三角形。帽子分为窄帽子和宽帽子两种。窄帽子底面宽度等于帽尖（即悬挂点）的高度，而宽帽子的底面宽度等于帽尖高度的两倍。如果一顶帽子的帽尖包含在另一顶帽子里（内部或者边界上），它就是不可见的，反之就是可见的。你的任务是计算出悬挂每顶帽子后，可见帽子的数量。注意，如果悬挂点和已有帽子的帽尖重合，或者位于某个已有帽子的内部或者边界上，那么这顶帽子是挂不上的。

如图6-40所示，帽尖上的数字代表挂的顺序（1是最先挂的，7是最后挂的），帽子2和6是窄帽子，其他是宽帽子。帽子3和4挂不上，而帽子7挂上之后，帽子1、2和7是可见的。

图　6-40

【输入格式】

输入第一行为数据组数T（T≤30）。每组数据第一行为帽子的个数n（1≤n≤100000），以下n行每行包含两个整数xi，yi（xi是悬挂点的横坐标，yi是高度）和一个字母W或者N。

【输出格式】

对于每顶帽子，输出悬挂后可见帽子的个数。如果挂不上这顶帽子，输出FAIL。

切蛋糕（Cutting Cakes, UVa 11607）

平面上有n（n≤1500）个点，其中没有3点共线。另外有m（m≤700000）条直线，你的任务是对于每条直线，输出3个数p，q，r，其中p和q为该直线两侧的点数（p≤q），r是直线穿过的点数。

层次包围盒（Bounding Volume Hierarchy, UVa 12312）

层次包围盒（Bounding Volume Hierarchy, BVH）是由几何体组织成的树状结构。在树结构的最底层，每个几何体自身的包围盒构成了树的叶结点，然后把这些叶结点分组，每组算出一个包围盒，并用一个父结点来表示。重复这个过程，直到构造出根结点。它是所有几何体的包围盒。如图6-41所示是一个二维BVH的例子，其中每个包围盒都是边平行于坐标轴的矩形。

图　6-41

BVH可以用来给光线跟踪算法加速。在本题中，你的任务是实现一个三维BVH，来回答“射线—三角形相交”（ray-triangle intersection）询问，即对于每条给出的射线r，找出和r相交的三角形。如果有多个三角形和r相交，只需找出最靠近起点的交点。整个几何场景不会变化，因此这棵BVH是静态的。

如果你并没有构造BVH的经验，这里有一个简单方法：首先计算每个物体的边平行于坐标轴的包围盒（Axis-Aligned Bounding Box, AABB），然后按照“最宽维度”给所有物体排序（比如，若所有物体的x坐标范围是-10～10，y坐标是-20～5，z坐标是20～50，则应按照z坐标排序，因为z维度的宽度（30）比x（20）和y（25）都大）。排序结束后，把排序后的前一半物体和后一半物体分别递归建树，作为整个BVH的左子树和右子树。注意，两棵子树的AABB可能会有重叠部分，因此在查询时有可能需要访问两棵子树。当物体数目足够小时，我们不再递归建树，而直接把这些物体的AABB作为整个BVH的叶结点。

以上只是构造BVH的一种方法。另一种方法是直接求出坐标范围的中点，得到一个划分平面（比如在上面的例子中，划分平面是z＝35），然后利用常数时间确定每个物体应该在左子树还是右子树。这个方法节省了排序的开销，但同一个物体可能会同时出现在左右两棵子树中（如果划分平面穿过这个物体的话）。

当然，还有其他构造方法，你也可以自创新方法，只要你的程序正确并且足够快，就可以通过本题。

一共有p（3≤p≤40000）个顶点和t（1≤t≤80000）个三角形，询问有q（1≤q≤10000）个。输入保证每条射线的起点严格位于任何三角形的外部。这些三角形总是构成真实世界中的模型，比如著名的Stanford Bunny，因此不用担心退化情况。

修建魔法灯塔（Let the lights guide us, Fuzhou 2010, LA 5106）

你需要在一个n行m列的网格中修建一些魔法灯塔。每行应当恰好选择一个格子来修建灯塔，并且对于任意两行i和i＋1来说，假定这两行的灯塔分别位于第j列和第k列，这两个灯塔的水平距离｜j－k｜不能超过两个灯塔所在格子的魔法值之和。在每个格子修建灯塔的费用可能会不一样，你的任务是让修建所有灯塔的总费用最小。

【输入格式】

输入包含多组数据。每组数据第一行为两个整数n和m（2≤n≤100，1≤m≤5000）。以下n行每行m个不超过10⁵的非负整数Tij，即在每个格子修建灯塔的费用。以下n行每行m个不超过10⁵的非负整数Fij，即每个格子的魔法值。输入保证有解。输入结束标志为n＝m＝0。

【输出格式】

对于每组数据，输出最小总费用。

机械工厂（Machine Works, World Finals 2011, LA 5133）

你有一个空厂房，希望买机器生产产品卖钱。你预测到有一些机器将会在市场上出售，其中每台机器只有一天可以购买（早一天或者晚一天都不行）。其中第i台机器的购买时间为第Di，价格为Pi。由于机器很大，你的厂房最多可以容纳一台机器，因此如果厂房里已经有机器了，必须将它卖掉，价格为Ri。卖机器所得的钱当天就可以拿到，因此可以在同一天用这些钱购置新机器，但新机器第二天（即第Di天）才能使用。第i台机器从购买的第二天开始到被卖出的前一天为止每天都可以获得Gi元的利润。第D天结束时工厂关闭，第D＋1天时卖掉厂房里的机器。

你的任务是使得第D＋1天结束后你所拥有的资金数的最大值。

【输入格式】

输入包含多组数据。每组数据第一行为3个整数N，C，D（N≤10⁵，C≤10⁹，D≤10⁹），即市场上出售的机器个数，初始资金数和工厂营业的天数。以下N行每行4个整数Di，Pi，Ri和Gi（1≤Di≤D，1≤Ri＜Pi≤10⁹，1≤Gi≤10⁹）。输入结束标志为N＝C＝D＝0。

【输出格式】

对于每组数据，输出第D＋1天结束时你所拥有的资金数的最大值。

3．暴力法

在第1章中已经介绍过一些暴力法求解的题目，本章主要补充路径寻找问题和极大极小过程。表6-8中的3道例题都非常经典，请读者仔细体会。

表　6-8

下面是习题和翻译（见表6-9）。这些题目虽然在算法框架上大同小异，但在实现细节上千差万别，因此全部值得一做。

表　6-9

续表

滚球游戏（Marble Game, World Finals 2007, LA 3807）

滚球游戏由一个n×n（2≤n≤4）网格棋盘和m个小球组成。棋盘上恰好有m个单元格里有洞。球和洞均编号为1～m。滚球游戏的目标为把每个球滚到编号相同的洞中。单元格的4条边上可能会有墙，可以阻挡球的滚动（见后）。

每次可以把棋盘朝着上下左右4个方向之一微微抬起，然后所有球同时朝另一个方向滚动，直到碰到墙、洞或者另一个球。球在滚动的过程不能跳起（因而无法越过墙、其他球或者洞），也不能离开棋盘（整个棋盘的四周都有墙）。每个单元格恰好能容纳一个球。当球落入洞之后，洞将被填平，使得今后可以有其他球经过。入洞的球将无法出洞。

如图6-42所示是一个滚球游戏及其解法。你的任务是解决任意一个n×n棋盘上的滚球游戏，找出移动次数最小的解，输出其长度。

图　6-42

旋转游戏（The Rotation Game, Shanghai 2004, LA 3265）

如图6-43所示的形状的棋盘上分别有8个1、2、3，要往A～H方向旋转棋盘，使中间8个方格数字相同。下面左图进行A操作后变为中图，再进行C操作后变为右图，这正是一个目标状态（因为中间8个方格数字相同）。

图　6-43

近乎完美的洗牌（cNteSahruPfefrlefe, World Finals 2005, LA 3272）

你有一副普通的扑克牌，叠成一摞放在桌子上。52张牌从顶部到底部编号为0～51。理想情况下，如果把这些牌均分成上下两部分以后交叉洗牌，结果将是（仍为从顶部到底部排列）：26，0，27，1，28，2，29，3，30，4，31，5，32，6，...，51，25。

你按照这样的方法洗了若干次牌（洗牌次数未知但保证为1～10）。虽然你已经非常小心了，但人无完人，你仍有可能在洗牌后意外的交换两张相邻的牌（假定每次洗牌最多只在一个地方犯错），比如洗成：26，0，27，1，2，28，29，3，30，4，31，5，32，6，...，51，25。

给定洗牌后的结果，问最少犯了多少个错？如果有多组解，输出字典序最小的。

迷你象棋（The Pawn Chess, UVa 10838）

考虑如下的迷你版象棋：4×4的棋盘上有一些黑兵和白兵。黑兵的目标是向下走到第四行，白兵的目标是向上走到第一行。如果一方无棋可走，则立刻输掉。

双方轮流走，白方先走。每次只能移动自己的一个兵，要么直着往前走一格（这个格子必须为空），要么斜着向前走一格，吃掉对方的棋子（不能吃掉己方的棋子，也不能斜着走到空格）。黑兵的“向前”指的是向下；白兵的“向前”指的是向上。

假设双方均采用最优策略，即如果能赢，则应该赢得尽量快；如果必须输，则应该输得尽量慢。

每组输入恰好包含4行4列，其中p为黑兵，P为白兵，'.'为空格。输入保证双方都至少有一种走法。

游戏（The Game, SWERC 2008, LA 4300）

这是一个双人游戏。两个人面对面坐下，每个人正前方有m（1≤m≤4）个杯子，右手边是一只大碗。不难发现，一共有2（m＋1）个杯子和碗，它们摆成一个圈。游戏开始时，这些杯子和碗里有一些石子，加起来不超过15个。每个游戏者碗里的石子数代表他的得分。

游戏规则如下：双方轮流操作，每次游戏者可以选择一个他自己的有石子的杯子，按照逆时针顺序把这些石子放到该杯子的各个相邻杯子／碗中，每次放一个石子。

如图6-44（a）所示的局面中，如果下方的游戏者选择杯子A，则需要把杯子A里的4个石子分到杯子B，C，a和自己的碗里。这样比分从3:4变成了4:4。

图　6-44

有3种特殊情况。

第一，如果最后一个石子放在了自己碗里，可以再操作一次。如果最后一个石子又放在了自己的碗里，还可以再操作一次，次数不限，直到不满足这个条件为止。

第二，如果最后一个石子放到了对方的杯子里，可以把这个杯子和自己对应的杯子交换（A和a对应，B和b对应…）。如果自己对应的杯子为空，则不能交换。双方在整个游戏中各有三次交换机会，而且当一个杯子被交换过以后，接下来的4轮中不能再次被交换（一次连续操作称为一轮）。考虑如下的操作序列：P₀，O₁，O₂，O₃，P₄，O₅，P₆，O₇，其中Pi是己方的操作，Oj是对方的操作。如果P₀中交换了某两个杯子，接下来的四轮（第一轮是O₁，O₂，O₃，第二轮是P₄，第三轮是O₅，第四轮是P₆）都不能再交换这两个杯子，但O₇中可以再次交换。

第三，如果最后一个石子放在己方的杯子里，并且放之前这个杯子是空的，并且这个杯子正对的杯子（上图中A正对着c，B正对b，C正对a）在放石子之后非空，这两个杯子中的所有石头都将移到自己的碗里。注意，这种情况不会引起连锁反应（即接下来轮到对方操作）。

所有杯子都为空时游戏结束。如果轮到某一方操作时他的所有杯子均为空，则下一轮仍该对方操作（但对于交换规则来说，“无法操作”也将算作一轮）；但只要有非空杯子，游戏者必须操作，而不能跳过这一轮。

假设双方都是足够聪明的，我的得分与对手的得分之差最大是多少？

扩展数独（Sudoku Extension, Harin 2009, LA 4763）

你的任务是解决一个扩展的3×3×3数独问题，有些格子只能填奇数（用小写字母o表示），有些格子只能填偶数（用小写字母e表示），还有一些格子用除了o和e之外的小写字母表示。用相同字母表示的格子必须填上相同的数字。

999游戏（Game of 999, UVa 12418）

注意，本题选材自NDS经典悬疑类密室逃脱游戏《9小时9人9扇门》。本题不含任何剧透，只包含一些基本游戏规则的描述。这些规则和原游戏中的有所出入，请玩过游戏的读者仔细阅读题目。

迷宫中有n（n≤10）个房间和m（m≤10）个连接它们的走廊。一些走廊的中部有一扇门，门上有一个1～9的数字。走廊都是单向的，因此这些门也只能从一边打开。编号为1～9的9个人一开始在房间1中，目标是从房间n中的出口逃脱。游戏规则如下。

❑　每扇门只能开一次。一旦有人通过，门将永远锁住。

❑　每扇门只能由3～5个人打开，而且这些人编号之和的数字根必须等于门上的数字。所谓数字根，即反复把各个数字加起来，直至得到一个一位数为止。例如，3，5，6，8可以打开4号门，因为3＋5＋6＋8＝22，它的数字根为2＋2＝4。打开门的所有人都必须通过它，并且其他人都不能通过这扇门。

你的任务是编写一个自动求解器，使得尽量多的人能够逃脱，在此前提下输出所有可能的逃脱组合。注意，每个房间可以多次进入（包括房间n），但一旦逃出迷宫，就不能再回到迷宫了。可以有多条走廊连接同一对房间，但不会有走廊的起点和终点是同一个房间。

数字逻辑（Digital Logic, UVa 11211）

给你一些2输入1输出的逻辑门，你的任务是设计一个4输入4输出的数字电路。

每一个逻辑门由3个0－1整数Y00，Y01和Y11描述。代表输入分别为0，1，2个1时的输出。注意所有的逻辑门都是对称的，所以当有一个输入被设置为1时，不管是哪一个输入门，输出的结果都相同。任何一个逻辑门的输入端都是另外一个逻辑门的输出端或者4个源输入端之一。由于当任何一个输入被悬挂时输出结果将不可预料，所以务必保证一个逻辑门的输出将不会再次变为其的输入，不管是直接的还是间接的（换句话说，逻辑电路将不包含环路），如图6-45所示。

图　6-45

为了让设计更加简单，你需要使用尽可能少的逻辑门。题目保证所有输入数据都可以用最多6个逻辑门实现。

【输入格式】

输入数据最多有30组，每组数据第一行是一个整数n（n＜6）代表逻辑门的种类。接下来n行每行包括4个整数mi，Y00，Y01，Y11，分别表示该类型逻辑门的数目，以及输入端分别有0，1，2个1时的输出。逻辑门的总数量最多只有10（所有种类逻辑门的数量之和最多为10）。接下来一行包括16个整数Y0000，Y0001，Y0010，...，Y1111代表所有可能输入组合的输出情况。Ypqrs'的二进制形式是代表当输入端分别是p，q，r，s时输出端a，b，c，d端的取值。最后一组数据以一行单个0结尾。

【输出格式】

对于每组数据输出数据的组数和一个整数p，代表可以达到要求的最少逻辑门数量。接下来p行s，k，a和b，s是端口的序号（4个输入端口的编号是1～4，逻辑门的编号是5～p＋4），k是该逻辑门的种类（种类的编号按照输入顺序依次编号为1～n），a和b分别是该逻辑门的输入端口编号。注意必须满足a＜s和b＜s。最后一行包含4个整数，为4个输出端口的编号（应该在1～p＋4之间）。每组数据之间输出一个空行。

编辑书稿（Editing a Book, UVa 11212）

你有一篇由n（2≤n≤9）个自然段组成的文章，希望将它们排列成1，2，…，n。你可以用Ctrl＋X（剪切）和Ctrl＋V（粘贴）来完成任务。每次可以剪切一段连续的自然段，粘贴的时候按照顺序粘贴。剪贴版只有一个，所以不能连续剪切两次，只能剪切和粘贴交替。

比如，为了将｛2，4，1，5，3，6｝变为升序，可以剪切1将其放到2前，然后剪切3将其放到4前。再如，对于排列｛3，4，5，1，2｝，只需一次剪切和一次粘贴即可，将｛3，4，5｝放在｛1，2｝后，或者将｛1，2｝放在｛3，4，5｝前。

再如5 4 3 2 1需要3步：5 4 3 2 1→3 2 5 4 1→3 4 1 2 5→1 2 3 4 5。

Flipull游戏（Flipull, UVa 11213）

你是一滴橙色的液体，位于屏幕右方的梯子上。你可以向左方发射方块，消除同色方块，然后将碰到的第一个非同色方块变成发射的方块。你的目标是剩下不超过b个方块（如果最终有多于b个方块而无法继续操作时，你将输掉游戏）。

总共有4种方块，分别为蓝色的三角（T），粉红色的圆圈（O），绿色的方块（#）和十字叉（X）。发射之后，方块将持续往左飞行，直到遇到第一个不同的方块或者撞到墙壁（或者其他障碍物）。在第一种情况下，发射的方块将把原有的方块弹回你手里；在第二种情况下，发射的方块将改变方向，向下飞行。如果向下飞行时落入底部，它将重返回你的手里。注意发射的方块必须至少消除一个同色方块。由于重力作用，当一个方块下方的方块消失时它会自然下落。除了上述4种方块之外，还有一种魔法方块（只可能在游戏开始时位于你的手中），发射后直接变成它所碰到的第一个方块，因此也称为万能方块，如图6-46（a）所示。

图　6-46

初始的游戏局面为4×4、5×5或者6×6大小，从底部到顶部行号依次编为r1～r6，从左到右列号依次被编为c1～c6。梯子上面总共有12格，从底部到顶部依次被编为1～12。在图6-46（a）中，由于墙壁能够反射方块，在12个位置发射方块实际是沿r1，r2，r3，r4，c1，c1，c1，c1，c2，c3，c4，X移动（X代表在12位置发射不会触碰到任何方块，因此是非法步骤）。图6-46（b）有些许不同，管道也能反射方块，因此在12个位置发射事实上是沿r1，r2，X，r4，X，c1，c1，c1，c1，c2，c3，X移动。注意不可能沿着r3和c4移动。

如图6-47所示是图6-46（a）的求解过程（正好剩余3个方块），在8，9，10，11这4个位置各发射一个方块，然后在10号位置发射，然后是2号位置，最后是1号位置。

图　6-47

你的任务是写一个程序，用最少的发射次数完成游戏。

一个烦人的问题（A Vexing Problem, World Finals 2001, LA 2240）

Vexed游戏是James McCombe发明的一种类似俄罗斯方块的游戏。在游戏中，一面木头墙上放置了一些标有字母的白色石块。

如果某个石块的左边（或者右边）是空的，那么该石块可以向左（或者向右）移动一步，木头墙永远不能移动，悬空的石块会自动下掉。每个石块都有一个标记，两个或更多具有相同标记的石块相碰时会形成石块群，石块群会自动消失。如果同时形成了多个石块群，那么它们会一起同时消失。石块群消失后，悬空的石块同样会自动下掉，下掉后石块群同样会消失……如此循环，直到石块不再变化为止。游戏的目标就是让所有的石块消失。

如图6-48（a）到（h）所示就是一个从初始状态到石块全部消失的游戏过程：首先上面的“Y”石块左移，这样两个“Y”石块形成石块群自动消失；然后上面的“X”石块右移，右移后下掉，这样两个“X”石块也形成石块群自动消失。

图　6-48

如图6-49所示是另一个游戏过程：首先最左边的“Z”右移（图a），形成“X”、“Z”石块群（图b）；石块群消失后，又形成“Y”、“Z”、“X”石块群；石块群消失后，最后形成“X”石块群，“X”石块群消失后游戏结束。

图　6-49

写一个程序，任意给出一个游戏，给出步数最少的解。行数NR和列数NC满足4≤NR，NC≤9，第一列、最后一列和最后一行保证为墙（#）输入保证存在一个不超过11步的解。

4．几何专题

第4章中没有介绍的几何内容有很多，本章只选取了其中最经典的几个，如表6-10所示，请读者仔细体会。

表　6-10

下面是习题和翻译（见表6-11）。这些题目在算法上并没有什么特别之处，但程序实现有很多细节需要注意，是不错的编程练习。

表　6-11

给程序员的一封信（A Letter to Programmers, Beijing 2011, LA 5719）

空间里有n（n≤1000）个点，你的任务是执行一段程序，输出程序运行结束后所有点的坐标。一共有5条指令，如表6-12所示。

表　6-12

程序不超过100行，除了n和k之外所有参数的绝对值均不超过1000。

佳佳的机器人（Jiajia's Robot, Wuhan 2009, LA 4492）

佳佳有一个双目机器人。机器人的两只眼睛各能发出一条射线，且两条射线的夹角总是直角。为了帮助机器人进行自定位，佳佳放了两段形如线段的特殊材料，分别为MA和MB。当且仅当两条射线中一条和MA相交，一条和MB相交时，机器人可以自定位。如图6-50所示是一个例子，其中MA和MB为两条黑色线段，灰色区域是可以自定位的区域。给定MA和MB（保证不会退化成点），你的任务是计算可以自定位的区域的面积。

图　6-50

十一月的雨（Roof, CERC 2003 LA 2947）

有n（n≤40000）个屋顶，每个屋顶用一条线段表示。屋顶不会是水平或者竖直的，且屋顶之间没有公共点。

现在天上开始竖直落下雨滴，单位时间单位长度的降雨量是1。屋顶接受到的雨都会从较低的那个端点往下落（或者落到地面，或者落到别的屋顶）。输入屋顶的数据，求出单位时间内从每个屋顶边沿往下落的雨量，如图6-51所示。输入保证任意画一条竖线最多与100个屋顶相交。

图　6-51

过河（River Crossing, UVa 10514）

有一条很宽的河，中间有n（0≤n≤11）个小岛。给出两条河岸线（均为最多有100个顶点的折线）和小岛（均为简单多边形）的信息，求一条过河的路径，使得淌水部分的总长度最短。假定只能从图6-52中看得见的地方过河。

图　6-52

上学（Go to Class, Chengdu 2007, LA 4019）

给出一个校园，校园中有很多纵横道路，横的平行于x轴，竖的平行于y轴。横向道路有n条，纵向道路有m条（1≤n，m≤25）。道路是有宽度的，并且大小不一。道路之间是草坪。现给出所有道路以及宿舍和教室的坐标，求宿舍到教室且不经过草坪的最短距离（见图6-53）。

图　6-53

5．数学专题

本章介绍的Pick定理、Lucas定理、原根、生成函数、FFT、线性规划等在算法竞赛中均不算常见，但都属于很重要的数学工具。表6-13中列出的例题非常经典。

表　6-13

下面是习题和翻译，如表6-14所示。这些题目在程序实现上大都没有太多技巧可言，但在题目分析上有一些技巧。建议读者认真分析所有题目，但只需选择有兴趣的进行程序实现。

表　6-14

海报张贴（Mayor Election, UVa 11640）

有n（n≤50）个人为了参加竞选，在一条长长的走廊上张贴海报。第i个人能张贴的海报总数应当在li和ui之间（包含li和ui，0≤li≤ui≤2000）。第i个人的海报有Pi（1≤Pi≤10）种，其中第j种海报最多只能连续张贴mj（1≤mj≤10）次。

最多询问Q（Q≤100000）次，每次询问已知所有人的海报总数L（1≤L≤100000），求方案总数除以786433的余数。

学习数数的Xavier（Xavier is Learning to Count, Shanghai 2011, LA 5705）

已知正整数p、m和m个不同的正整数Ai，从这些正整数中选出p个，使得它们的和为n，方案总数设为f（n）。求出所有f（n）不等于0的n和对应的f（n）。1≤p≤5，1≤m，Ai≤13000。

提花织物电路板（Jacquard Circuits, World Finals 2007, LA 2395）

古怪的雕刻艺术家Mondrian发明了一种称为“提花织物电路板”的艺术品。该艺术品由一系列形状相同，大小不同的多边形电路板构成，层与层之间用细线连接，如图6-54所示。

图　6-54

方便起见，我们借助于网格来制造这个艺术品。首先需要给定一个称为模板的格点多边形（即各个顶点都是整点的多边形）P，代表电路板的形状。接下来寻找一个形状和P相同的，面积最小的格点多边形作为艺术品的第一层电路板，然后找一个形状相同的，面积第二小的格点多边形作为第二层，依次类推，共M（1≤M≤1000000）层。

每个多边形内部的格点（边界上的不算）都会打一个洞，你的任务是求出所有M个多边形的洞的总数。P是由N（3≤N≤1000）个输入点顺次连接而成。

昂贵的饮品（Expensive Drink, Beijing 2007, LA 4027）

你家那个调皮的小妹妹把水、牛奶、红酒混在一起，还加了点糖，打算给你喝。为了不让自己看上去太不讲理，她说如果你能猜到调制这种“混合饮料”花了多少钱，就可以逃此一劫（别告诉我你想喝它！）。调制饮料的费用等于所有原料的费用。具体来说，如果一种饮料分别用了a₁，a₂，a₃，a₄个单位的水、牛奶、红酒和糖，并且它们的单位价格分别为c₁，c₂，c₃，c₄，则调制饮料的花费为a₁c₁＋a₂c₂＋a₃c₃＋a₄c₄。

你并不清楚这4样东西的市场价格是多少，但是根据常识，0≤c₁≤c₂≤c₃。为了帮助你解决这个难题，小妹妹向你提供了这种饮料中液体的用量（即a₁，a₂，a₃）和另外n（n≤100）种混合饮料的液体用量（即a₁，a₂，a₃）和花费。尽管所有饮料中糖的用量都是未知的，但她向你保证，在上述任何一种混合饮料中，糖的花费a₄c₄一定在区间[L，R]中。

凭借平日的了解，你断定她一定采用最贵的原料，因此你的任务是计算眼下这杯饮料的调制费用的最大值。如果她提供的信息有误，输出“Inconsistent data”；如果费用可以任意大，输出“Too expensive!”。

称糖果问题（Unequalled Consumption, NWERC 2005, 3408）

有n（n≤5）种糖果，每种糖果有无限多个，其中第i种糖果重量为wi（1≤wi≤10）。有q（q≤10）组询问，每次询问一个P（1≤P≤10¹⁵），求最小的w使得至少有P种方案可以用这些糖果称出重量w。输入保证如果有解，解总是不超过100P。

6．代码组织与调试

本章只选了两道题目，如表6-15所示，其中一道代码较长，但是比较好调试，因此重点在于如何写出这个程序；另一个代码不长，却极容易出错，因此重点在于细心查错。这两道题目的通过人数都很少，但对编程、调试能力的锻炼也是很大的。

表　6-15

下面是习题和翻译，如表6-16所示。这些题目都有一定的“实用性”，但要么代码长，要么容易出错。如果想要锻炼自己的编程基本功，这些题目是再合适不过的了。

表　6-16

链接加载器（A Linking Loader, World Finals 2003, LA 2727）

目标模块（object module）是编译器处理源码后的产物。链接加载器（linking loader），也称链接器（linker），用来把分别编译好的目标模块合并到一起。它的主要任务有两个：对代码和数据进行重定位（因为编译器并不知道这些模块将被放在内存中的哪个位置），以及解析模块之间的符号引用。比如，主程序可能会引用一个称为sqrt的平方根函数，而此函数可能定义在其他模块中，因此链接器必须给每个模块的数据和代码分配内存地址，然后把sqrt函数的地址写到主函数代码中的合适位置（可能会有多个）。

一个目标模块按顺序包含0个或多个外部符号定义（external symbol definition），0个或多个外部符号引用（external symbol reference），0个或多个字节的代码和数据（它们可能会包含对外部符号的引用），最后是模块结束标志。在本题中，一个目标模块用一个文本序列来描述，其中每一行的第一个字符为一个大写字母，表示该行的类型。在一行中，相邻数据项之间由一个或多个空白字符隔开，最后一个数据项后可能会有多余的空白字符。一共有4种可能的行。

❑　格式为"D symbol offset"的行是一个外部符号定义，表示symbol的地址比当前模块的首地址大offset。这里的symbol由不超过8个大写字母组成，而offset是一个不超过4位的十六进制整数（只用大小字母表示）。比如，在一个首地址为100₁₆的模块中，"D START 5C"会让START的地址为15C₁₆。此类型的行在单组数据中至多出现100次。

❑　格式为"E symbol"的行是一个外部符号引用，表示本模块可能会引用symbol这个符号（而这个符号很可能在其他模块中定义）。比如，行"E START"表示本模块可以使用START这个符号。在每个模块中，所有的"E"行按照出现顺序从0开始编号。这个编号将被C行使用。

❑　格式为"C n byte₁ byte₂...byten"的行是需要填充到内存中的n个字节（模块代码或数据），其中n是一或两位十六进制整数，且不超过10₁₆。每个byte要么是一个一或两位的十六进制整数，要么是一个美元符号$。美元符号的下一个字节（保证在同一行中）代表一个外部符号引用，该字节的数值等于'E'行的下标。如果这个外部符号有定义，把它的地址拷贝到$和它的下一个字节中，其中地址的高字节拷贝到$所对应的字节中。如果符号无定义，用0000₁₆覆盖那两个字节。比如，若第一个E行中的符号有定义，地址为15C₁₆，则行"C 4 25$0 37"的作用是把这4个地址放到内存中25₁₆，01₁₆，5C₁₆，37₁₆。

❑　单独一行Z表示模块结束。

假定4位十六进制地址总是足够的，各行一定按照D，E，C，Z的顺序出现，并且不会包含格式错误。在本题中，每个模块中的C行应按出现顺序填充到内存中（D和E行只是模块描述，并不对应于实际的内存数据），不同模块按照输入顺序依次填充到内存中。填充时应按照地址从低到高的顺序进行，第一个模块的首地址总是100₁₆。

【输入格式】

输入包含多组数据，每组数据包含一个或多个模块定义，最后一行是$（作为单组数据的结束标志）。输入结束标志也是一行$。

【输出格式】

对于每组数据，参考样例格式进行输出。首先是测试数据编号和模块校验码（计算方法见后），然后是所有定义或引用的外部符号列表，按照符号名排序。对于没有定义的符号，在表里用4个问号表示，但在C行引用的时候作为0处理。如果一个符号定义多次，在地址后打印一个大写字母M，并且以第一次出现的定义为准。

校验码计算方式如下：首先设为0，然后按照地址从低到高顺序依次处理各个字节，每次先把校验码循环左移一位，加上当前内存值，然后忽略进位保留低16位。

相邻两组数据的输出之间应有一个空行。

支杆和弹簧（Struts and Springs, World Finals 2009, LA 4453）

本题的任务是模拟屏幕上窗口的缩放和移动。每个窗口占据屏幕上的一个矩形区域，并且可以包含其他窗口，形成一个层次结构。当顶层窗口缩放时，它们的直接包含窗口可能会改变位置和大小（根据支杆和弹簧的参数，见后），而这些改动可能会进一步影响到这些窗口所直接包含的窗口。

概念上讲，支杆是一根长度固定的小棍，可以放在同一个窗口的两个水平边界之间或者两个垂直边界之间，也可以放在一个窗口的某边界与它父窗口的对应边之间。当一个支杆连接两条边时，这两条边之间的距离将保持不变；但如果把支杆换成弹簧，这两条边之间距离可以改变。

每个非顶层窗口都有6根支杆或者弹簧，分别位于两条水平边界之间、两条垂直边界之间、已经上／下／左／右边界和父窗口的对应边界之间。3条垂直支杆和弹簧的长度之和等于父窗口的高度；3条水平支杆和弹簧的长度之和等于父窗口的宽度。当父窗口的宽度改变时，所有水平弹簧按照相同比例缩放，使得水平方向的支杆与弹簧的长度之和仍然等于父窗口的宽度。父窗口高度改变时类似。如果3根水平／垂直的部件都是支杆，其中最右边／上边的支杆将被替换成弹簧。

给定每个窗口的初始位置和大小（保证恰好有一个顶层窗口包含其他所有窗口），以及所有的支杆和弹簧，你的任务是处理顶层窗口的缩放请求，计算出所有窗口的新大小和位置。

【输入格式】

输入包含多组数据。每组数据第一行为4个整数nwin, nresize, owidth, oheight，其中nwin是指非顶层窗口的个数，nresize是指缩放请求的个数，owidth和oheight是指顶层窗口的宽度和高度。

接下来的nwin行每行包含10个非负整数，描述一个非顶层窗口。其中前两个整数表示初始时刻该窗口左上角相对于到父窗口左上角的偏移量。输入保证处理每个缩放请求之后，所有的支杆和弹簧的长度均为正整数，且不同窗口的边界没有公共点。缩放请求不会让一个窗口跑到一个原本和它相离的窗口内部。输入最多有100个窗口和100个缩放请求，且顶层窗口的宽度和高度不会超过1000000。输入结束标志为4个0。

【输出格式】

对于每组数据中的每个缩放请求，输出nwin行，即每个非顶层窗口的位置（x、y）和大小（宽度、高度）。各个窗口应该按照在输入文件中的顺序排列。

折纸袋（Pockets, World Finals 2006, LA 3568）

有一张正方形的纸，经过反复折叠后变成一个小正方形。你的任务是统计其中的pocket的个数。所谓pocket，指的是从折叠后的小正方形的边界处可以看到的开口（位于两张纸的中间），如图6-55所示。

图　6-55

正式折纸之前，先在纸上折出N条等间距的水平线和N条等间距的垂直线，把纸片分为N×N个等大的小正方形，折痕按照图6-56（a）所示进行编号。每次操作由一条折痕的编号和折叠的方向来确定。如图6-56（b）中的折叠操作用2U表示，图6-56（c）中的折叠操作用1L表示。这里，U和L分别表示向上和向左（折叠方向均是朝向观看者的）。最后纸会被折叠成小正方形的大小。注意，经过折叠后，两条不同的折痕可能会重合（如图6-56（b）中的1，3），此时选择其中的任意一条折痕都将表示相同的折叠操作。

图　6-56

【输入格式】

输入包含若干组数据。对于每一组数据，第一行包含两个数N和K，分别表示水平和垂直方向的折痕个数以及折叠操作的次数。接下来会描述K个折叠的操作，每个操作由一个折痕的编号和折叠的方向来确定（如题目描述中的2U和1L），中间会有至少一个空格的分隔。当N和K均为0时表示输入数据结束。

【输出格式】

对于每组数据，输出数据编号和对应的pocket的总数。具体格式参见样例输出。

解释SQL语句（Interpreting SQL, UVa 10757）

你的任务是实现新一代数据库管理系统——YourSQL的查询处理部分。这是一个关系数据库（relational database），包含多张表，每张表包含一些行和列，其中每个列有一个名字和一个固定类型（数值或字符串），每个单元格的类型等于它所在列的类型。不同列的名字保证不同，不同表的名字也不同。

下面是表6-17（第一行是列名，数值列右对齐，字符串列左对齐）。

表　6-17

SQL查询是一个字符串，用来从服务器读取一张或多张表的一些或全部数据，形成一张临时表，然后把数据传回客户端，最后销毁临时表。SQL查询的格式如下。

相邻两项之间如果有句号（.），表示中间可以没有空白符；反之，则说明必须有空白符。注意，可以用括号来给项（term）分组（比如inner-from）。除了字符串常量之外，大小写不受限制。

查询处理分为以下4个步骤。

第一步：根据FROM子句创建查询表。如果FROM部分只有一个表，那么查询表就是它，否则查询表形如from₁ INNER JOIN from₂ ON name₁＝name₂，此时需要创建临时表，在from₁的各列后面加上from₂的各列（保证两张表没有名字相同的列），求出两张表的笛卡尔积，再选出name₁＝name₂的行。

第二步：根据WHERE子句选择查询表中的行，得到结果表。如果没有WHERE子句，则所有行被选中；否则符合WHERE子句的行被选中。字符串按照字典序比较大小，输入保证被比较的两个列类型相同。所有操作从左到右执行，因此a AND b OR c的含义是（a AND b） OR c。WHERE子句中的名字都代表查询表中的列。

第三步：如果有ORDER BY子句，给结果表排序。如果ORDER BY子句中有多个列名，先按第一列排序，相同时按第二列排序，以此类推。ASCENDING和DESCNDING分别表示升序和降序（默认是升序）。字符串按照字典序比较大小。排序应当是稳定的，即关键字列完全相同的行顺序在排序前后应保持不变。

第四步：在结果表中选择一些列进行输出。如果SELECT后跟着一个星号，选择所有列，否则按照SELECT子句中的顺序输出各个列。注意，同一个列可能会出现多次，应全部输出。

【输入格式】

输入第一行为数据组数。每组数据第一行为整数K（1≤K≤20），即表的个数。接下来是这些表的数据，其中第一行是表名和两个整数M，N（1≤M≤10，0≤N≤100000），即列数和行数。以下M行每行包含一列的名字和类型（S表示字符串，I表示数值）。以下N行每行为表中的一条记录，包含M个字符串或整数（取决于列的类型），用空格隔开。这些字符串是原始形式（不是转义字符串）。接下来是查询，以空行或者文件结束符（EOF）为结束标志。

（输入限制见原题，此处略。）

【输出格式】

对于每条查询，输出结果为表的内容，格式同输入（但不要输出表名和列定义）。

师兄帮帮忙（A Typical Homework （a.k.a Shi Xiong Bang Bang Mang）, UVa 12412）

（题目背景略，有兴趣的读者请自行阅读原题。）

编写一个成绩管理系统（SPMS）。最多有100个学生，每个学生有如下属性。

❑　SID：学生编号，包含10位数字。

❑　CID：班级编号，为不超过20的正整数。

❑　姓名：由不超过10的字母和数字组成，其第一个字符为大写字母。名字中不能有空白字符。

❑　4门课程（语文、数学、英语、编程）成绩，均为不超过100的非负整数。

进入SPMS后，应显示如下主菜单。

添加学生

选择1之后，会出现如下添加学生记录的提示信息。

然后等待输入。本题保证输入总是合法的（即不会有非法的SID、CID，并且恰好有4个分数等），但可能会输入重复SID。在这种情况下，需要输出如下一行提示。

不过名字是可以重复的。你的程序应当不停地打印前述提示信息，直到用户输入0。然后应当再次打印主菜单。

删除学生

选择2之后，会出现如下提示信息。

然后等待输入，在数据库中删除能匹配上述SID或者名字的所有学生，并且打印如下信息（xx可以等于0）。

你的程序应当不停地打印前述提示信息，直到用户输入0。然后应当再次打印主菜单。

查询学生

选择3之后，会出现如下提示信息。

然后等待输入。如果数据库中没有能匹配上述SID或者名字的学生，则什么都不做；否则，输出所有满足条件的学生，按照进入数据库的顺序排列。输出格式和添加的格式相同，但增加了3列：年级排名（第一列）、总分和平均分（最后两列）。所有班级中总分最高的学生获得第1名，如果有两个学生并列第2，则下一个学生的排名为4（而非3）。

你的程序应当不停地打印前述提示信息，直到用户输入单个0。然后应当再次打印主菜单。

显示排名

选择4之后，会出现如下提示信息。

然后自动返回主菜单。

显示统计信息

选择5之后，会出现如下提示信息。

当用户输入班级ID之后（0代表全年级），显示如下信息（注意，“及格”是指分数不小于60分）。

然后自动回到主菜单。

退出系统

选择0之后，程序终止。注意，单科成绩和总分都应格式化为整数，但平均分应恰好保留两位小数。

提示：这个程序适合直接运行，用键盘与之交互，然后从屏幕中看到输出信息。但正因为如此，作为一道算法竞赛的题目，它的输出看上去会比较乱。

公式编辑器（Formula Editor, UVa 12417）

你的任务是编写一个类似于MathType的公式编辑器。从技术上讲，公式就是一个表达式，它是由元素组成的序列。有3种元素：基本元素（算术运算符、括号、数字和字母）、矩阵和分式（见下）。

公式编辑器为每个表达式创建了一个看不见的编辑框。由于矩阵中的每个单元格都是表达式，所以每个单元格也都有一个编辑框。类似的，每个分式的分子和分母分别有一个编辑框。

在如图6-57所示的表达式中，有5个编辑框。F1包围了整个表达式，F2和F3各包围一个矩阵单元格，F4包围了分子，而F5包围了分母。

图　6-57

不难发现，编辑框相互嵌套。如果编辑框A直接包含编辑框B，我们说A是B的父编辑框（比如在图6-57中，F1是F2和F3的父编辑框，F3是F4和F5的父编辑框）。如果A和B拥有相同的父编辑框，我们说A和B是兄弟（比如在图6-57中F4和F5是兄弟，F2和F3也是兄弟）。

光标移动

在任意时刻，光标总是直接包含在某个编辑框中。它可能位于该编辑框中所有元素的左边（即“框首”），也可能位于所有元素的右边（即“框尾”），还可能位于某两个相邻元素之间。如果光标在元素X和元素Y之间，并且X在Y的左边，我们说光标的左相邻元素为X，右相邻元素为Y。

光标支持6种移动方式：上下左右和HOME、END。假定直接包含光标的编辑框为A，则各种移动方式的细节如下。

Home（End）：把光标移到A的框首（框尾）。注意，光标仍然被A直接包含。

Up（Down）：如果A的上（下）方有一个兄弟B，则把光标移动到B的框首，否则检查A的父编辑框（如果A的父编辑框有这样一个兄弟）。如果A的所有祖先编辑框均不含这样的兄弟，则忽略此命令。

Left（Right）：有以下4种情况。

❑　如果光标在A的框首（框尾），则把它放到A的左（右）兄弟B的框尾（框首）。如果没有这样的B，把光标放到A的父编辑框C中（如果有的话），紧挨着A的左边（右边）。

❑　如果光标的左（右）相邻元素是一个分式，把它放到分子的框尾（框首）。

❑　如果光标的左（右）相邻元素是一个n行m列的矩阵，把它放到第[n/2]行第1列（第m列）的编辑框的框尾（框首）。

❑　如果光标的左（右）相邻元素是一个基本元素，把它放到该元素的左（右）相邻位置。

输出格式化

本题的输出为ASCII格式，因此需要把每个编辑框格式化成一个ASCII字符矩形（尽管多数字符都是空格）。表达式的字符矩形由组成它的各个元素的字符矩形（称为内矩形）经过水平拼接而成。各个内矩形根据基线进行对齐（见后），相邻两个矩形之间没有空白，而内矩形和整个矩形的边界之间也没有空白。

每个元素也格式化为一个字符矩形，规则如下。

❑　基本元素恰好占一行，该行也是它的基线。我们用－（注意前后各有一个空格）来表示减号，而其他基本元素都格式化为单个字符。

❑　矩阵元素的格式化步骤为：首先格式化所有单元格，然后排成一个矩阵。同一行的各个ASCII矩形按它们的基线对齐，同一列的ASCII矩形水平对齐（见下）。相邻两行之间有一个空行，而相邻两列之间有一个空列。最后，在每行的前后分别加一个方括号。当行数为奇数时，整个矩阵的基线为中间那一行的基线；当行数为偶数时，整个矩阵的基线为中间那个空行。

❑　分式元素的格式化步骤为：首先格式化分子和分母，然后在中间画一条水平线（由一些连续的“-”字符组成）。这也是整个分式的基线。这一行的宽度等于分子分母的较大宽度加2（即前后各加一个字符）。分子和分母水平对齐。

前面提到的“水平对齐”是首先把水平宽度最大的矩形固定下来，然后水平移动其他矩形，使得它们的水平中心线尽量对齐。如果对不齐，可以往左错开0.5个单位的宽度，如图6-58所示。

图　6-58

注意有一个特例。当整个表达式为空时，ASCII矩形是一个空行——它的宽度为0，但高度为1。这一点在拼接和对齐时尤为重要。

输入处理

输入已转化为了一个命令字符串序列。对于每个字符串，可有以下几种操作。

❑　如果它是单个字符，说明它是一个基本元素。在光标处插入此元素，然后把光标移动到它的右相邻位置。

❑　如果是字符串Matrix（Fraction），在光标处插入一个1行1列矩阵（空分式），然后光标右移一次。注意，光标右移之前，新的矩阵（分式）在光标的右相邻位置。

❑　如果是字符串AddRow（AddCol），首先找到直接包含光标的矩阵，然后在最上方（最左方）添加一行（一列），并把光标移动到此行（列）中，保持列（行）不变。如果直接包含光标的编辑框A并不是矩阵的单元格，需要检查A的父编辑框，直到找到一个矩阵。如果找不到，忽略此命令。

❑　如果是字符串Home, End, Left, Right, Up, Down之一，按前述规则移动光标。

输入包含多组数据，每组数据以命令Done结束。单个数据包含不超过1000条命令，输入总大小不超过200KB。

Mua解释器（I）：词法分析（（Jiandan） Mua （I） - Lexical Analyzer, UVa 12421）

在本系列中，你的任务是实现Lua程序设计语言（5.1版）的一个子集，称为迷你Lua（Mini-Lua，简称Mua）。Mua是刘汝佳设计的一门实验性语言，主要用来实现算法，而不是编写实用程序。

这是本系列的第一题，目的是实现Mua的词法分析模块，即把输入的字节流分解成词法单元（Token）。

一共有6种不同的token。

RESERVED：这是Mua的保留字，列表如下。注意Mua是区分大小写的，因此AND并不是一个保留字。另外，Mua语言并没有用到所有的保留字，我们只是希望任何一个合法的Mua程序都是一个合法的Lua程序。

NUMBER：即数字常量。有如下两种。

❑　整数：十进制整数包含0个或多个数字（0～9），而十六进制整数包含一个前缀（0x或者0X），然后是一个或多个十六进制数字（0～9，a～f，大小写均可）。注意可以有前导0，如0123和123相等。

❑　浮点数总是十进制的，如1.23。可以用科学计数法表示，方法是在后面加上字符e或者E，然后是一个十进制整数（如1.23e2，它等于123.0），小数点和指数不能同时省略，但整数部分可以省略（如.2e3是合法的）。如果省略了整数部分，则必须要有小数点和至少一位小数（因此.e2是非法的）。不支持十六进制浮点小数。注意指数的前面可以有一个前缀＋或者-，如1e＋10和4e－3都是合法的。

注意，“负数”包含两个token，一个“负号”和一个绝对值。比如-34包含两个token，即“-”和“34”。类似的，＋7e8也包含两个token。

STRING：由双引号""或者单引号''包围的字符串常量。只支持4个转义字符：\" \' \\ \n。字符串内部不能有物理换行符。

SYMBOL：有特殊含义的符号，如下所示。

NAME：即标识符。必须以字母开头，后跟若干个字母、数字或者下划线。注意，保留字不能作为标识符。

EOL：物理换行符。

COMMENT：注释。以--开头，换行符结束。注意换行符并不是注释的一部分，而是单独的EOL。

注意，字符串常量外的所有空白字符都应忽略，因此1＋1和1＋1对于词法分析器来说并没有任何区别。另外，词法分析器应当是贪心的，即总是让第一个token尽量长，在此前提下让第二个尽量长，依此类推。比如，abc123≤x应分成abc123，≤和x。

【输入格式】

一个合法的Mua程序。

【输出格式】

对于每个非注释token，按格式[TYPE] token输出，其中TYPE是类型，token是输入中的文本（字符串中的转义字符按照输入原样打印）。对于EOL，token是空白（而不是空行）。

Mua解释器（II）：表达式求值（（Kengdie） Mua （II） - Expression Evaluator, UVa 12422）

在本系列中，你的任务是实现Lua程序设计语言（5.1版）的一个子集，称为迷你Lua（Mini-Lua，简称Mua）。Mua是刘汝佳设计的一门实验性语言，主要用来实现算法，而不是编写实用程序。

这是本系列的第二题，目的是实现Mua的类型系统和表达式求值模块。下文中的文法都用扩展BNF表示，其中｛x｝表示x出现一次或多次，[x]表示x出现0次或1次，而x｜y表示x或者y恰好出现1次（不能同时出现）。

类型系统

Mua是一个动态类型的语言，即变量没有类型，值有类型。换句话说，Mua中不存在“变量声明语句”，变量保存的值自身含有类型信息。

Mua包含6种基本类型：nil, boolean, number, string, function, table。

❑　Nil是值nil的类型，它的主要作用是和其他所有值区分开。一般用来表示“不存在”。

❑　Boolean是false和true的类型。

❑　Number代表实数。在内部，实数用IEEE-754双精度浮点数表示（C/C++或者Java语言中的double类型）。注意整数也用这种方式表示。这不是大问题，因为IEEE-754可以精确的表示整数，只要你不进行不精确的运算（比如除法）。

❑　String（字符串）代表8比特ASCII字符数组。

❑　Function代表函数。函数都是匿名的（尽管存有函数的变量有名字）。

❑　Table实现了关联数组。换句话说，下标不仅可以是number，也可以是其他任意的非nil值。Table中的元素可以有不同的类型（但不能是nil）。

Table和Function都属于对象（object）：变量并不保存它们的值，而只保存这些值的引用（reference）。赋值、参数传递和函数返回值都只操作引用，而不会涉及任何形式的值拷贝。

Lua程序员请注意：Lua还有两种基本类型，即userdata和thread，它们都不被Mua所支持。Mua对FP（函数式编程）的支持很有限。比如，没有lambda表达式，并且函数返回值不能是函数或者闭包。

变量

变量的定义如下。

这里“expr”是指任意合法表达式，因为table的下标可以是任意类型（除了nil）。注意table支持一种看起来像OOP中成员访问的“点语法”，比如a.name等价于a["name"]。

变量在赋值前默认为nil。如果你把nil赋值给一个变量，实际效果是删除了那个变量。类似地，你可以把nil赋值给一个table元素，从而在table中删除该元素。

Lua程序员请注意：Mua不支持Lua的冒号（:）语法。

简单表达式

简单表达式（simpleexp）是表达式的基本组成单元，定义如下。

这里，'｛' '｝'代表空table。

Lua程序员请注意：Mua不支持复杂的table常量，也不支持lambda表达式。

函数调用

刚才出现的functioncall定义如下。

Lua程序员请注意：在Mua中，调用函数的唯一方法是直接指定保存该函数的变量。比如（print）（1）是不合法的，但是a＝（f）合法，因为（f）是一个合法的表达式（见下）。另外，在Lua中，如果参数是字符串常量或者table常量，括号可以省略；Mua不支持这种语法。

表达式

现在我们已经拥有了构造表达式的所有“零部件”了。表达式（expr）定义如下。

注意，上述语法并没有涉及运算符的优先级。Mua中所有的运算符及按照优先级从低到高排列如下。

这些运算符大都可以根据常识判断出含义，下面是一些需要说明的。

❑　不等号不是＜＞，也不是!＝，而是～＝。

❑　％（取余数）定义在整个实数范围内，它等价于a - math.floor（a/b）*b。

❑　^（取幂）是唯一的右结合运算符。

❑　not , and和or总是返回布尔值。只有and和or是短路运算符。在运算之前，需要把操作数转换成布尔型，规则是把nil转化为false，其他非布尔值转化为true

❑　字符串连接运算符是..，而不是＋。

❑　运算符#代表取table或string的长度。假定T的类型是table，则#T是满足T[1]，T[2]，…，T[n]全部存在（即不为nil）的最大非负整数n。这样，每个table都可以看成一个包含#T个元素的数组。后面有些库函数依赖于这一点。

Lua程序员请注意：在Lua中中，字符串连接也是右结合的，不过在本题中，它是否为右结合无关紧要。另外，Lua的and和or运算符不总是返回布尔值，但为了简化语言，Mua中的and和or总是返回布尔值。另一个重要变化是，Mua不支持string和number之间的自动类型转化。

库函数

为了让Mua有一定的使用价值，需要实现一些库函数，包括tonumber, tostring, print，string.rep，string.sub，table.concat，table.sort和一些数学函数，详见原题。

【输入格式】

若干个Mua程序。每个Mua程序中的每行要么为print（expr），要么为var＝expr（即赋值）。所有表达式均符合上述定义。空行标志着一个Mua程序的结束（所有变量都应重置为nil）。所有表达式均可计算出一个合理的值（你不需要考虑NaN、算术运算溢出或者操作数类型不正确等错误的情况）。保证不会修改库函数，但有可能把库函数赋值给其他的变量。程序中不含注释。

【输出格式】

对于每个形如print（expr）的行，输出结果（使用tonumber函数转化非字符串）。

【样例输入】

【样例输出】

Mua解释器（III）：完整解释器（（Last） Mua（III） - Full Interpreter, UVa 12423）

在本系列中，你的任务是实现Lua程序设计语言（5.1版）的一个子集，称为迷你Lua（Mini-Lua，简称Mua）。Mua是刘汝佳设计的一门实验性语言，主要用来实现算法，而不是编写实用程序。

这是本系列的第三题，也是最后一题，目的是实现一个完整的Mua解释器。在尝试本题之前，请确保你已经解决了本系列的前两题，否则的话，很可能会遇到困难。

代码块

一个完整的Mua程序在语法上等价为一个语句块（block）。

注意：你只能在语句块中最后一条语句处进行return或者break（Lua和Mua都不支持continue）。

Lua程序员请注意：在Lua中，你可以在同一行中写两条语句，中间甚至不需要用分号分隔。但在Mua中，一行只能写一条语句，用分号分隔也不行。另外，return只能返回一个值，而不能像Lua那样返回多个值。

简单语句

Mua支持4种简单语句。

❑　空语句：stat -＞

❑　函数调用（但丢弃返回值）： stat -＞ functioncall

❑　赋值： stat -＞ var '=' expr

❑　块语句：do block end

Lua程序员请注意：Mua不支持多重赋值（即a，b＝b，a）。另外，本题不需要尾递归消除。

控制流

While, repeat和if都用到了条件。nil和false代表条件为假，其他值代表条件为真。这3种语句定义如下。

❑　While语句：stat -＞ while expr do block end

❑　Repeat语句：stat -＞ repeat block until expr

❑　If语句：stat -＞ if expr then block｛elseif expr then block｝[else block] end

另外还有两种形式的for循环。第一种形式如下。

这3个expr分别为初值、上／下限（步长为正为上限，否则为下限）和步长（默认值为1）。这3个表达式都只在循环第一次进入之前计算一次，然后用tonumber函数转化为数字。这3个表达式必须都能转化为数字（即tonumber不能返回nil）。注意，NAME是一个局部变量（循环结束后无法再访问）。可以用break语句退出循环，但不能使用continue语句。输入保证在循环内部不会修改循环变量。

第二种形式的for循环更具一般性，它枚举了一个table的所有键值。

这里的iterator，要么是ipairs（table），要么是pairs（table）（这里的table是指一个table类型的表达式，在循环开始前计算出）。二者的区别在于，ipairs从1循环到#table，而pairs枚举所有的键值（顺序任意）。输入保证在循环内部不会修改循环变量。

函数定义

有了上述“零部件”，函数定义是很简单的，定义方法如下。

注意，所有参数都是局部变量。如前所述，你可以用return语句从函数返回（可以有最多一个返回值）。Return语句必须是语句块的最后一条语句，因此如果你想从函数中间返回，需要用前述的“块语句”语法把它包装起来。

作用域和可见性

变量有两种：全局变量和局部变量。可用如下语法定义一个局部变量。

和Lua一样，Mua采用词法作用域（lexically scoped）。局部变量的作用域从声明它的后一条语句开始，到直接包含这个声明的语句块的末尾结束。如果外面的语句块有同名变量，那个变量将不可见（但是仍然存在，只是无法访问到而已）。词法作用域的一大特点是：当一段代码出现一个变量名时，它具体对应于哪个变量（全局变量还是局部变量？哪个作用域的变量）只取决于代码的书写方式，和程序执行方式无关。

因为局部变量只在声明之后才起作用，语句local x＝x是合法的，它的作用是声明一个局部变量x，初始化为在此之前可见的变量x。

repeat语句比较特殊，在内部语句块中声明的变量在until的条件中仍然可以访问。

根据词法作用域规则，局部变量可以被在它作用域内定义的函数访问，但为了简单起见，你可以假定函数都是全局定义的（不会嵌套在其他函数或者语句块中定义），而且整个程序的局部变量（不在任何函数之内，用local声明的变量）都在函数之后声明。

【输入格式】

若干个Mua程序。每个Mua程序的第一行以注释--PROGRAM开头。程序内部不得出现这样的注释。

【输出格式】

对于每个程序，输出程序编号和运行结果。

【样例输入】

【样例输出】

注释

[1]　为了方便和高效，实际上一般采用四进制，即用两个比特表示0、1、2这3种情况。

[2]　即通过上、下、左、右4个方向连通。

[3]　注意待涂色格子左上方的格子并没有标连通分量编号。事实上，我们没有记录它的连通分量编号，而只记了它的颜色。

[4]　当然，在代码上，只是保存着这棵Treap的根结点指针。

[5]　由于在第3章中我们用数组实现了线段树，这里很自然的采用了二维数组实现二维线段树。

[6]　这个先后顺序很重要，因为修改此结点的y树时需要用到“下一层”y树。

[7]　可以用分治法，也可以用Fenwick树。

[8]　如果觉得不够直观，可以令Xi＝larger（i，v），则上式变为X₁＋X₂＋…＋Xk。

[9]　而后面即将介绍的对抗搜索就不属于单智能体搜索，因为有两个游戏者。

[10]　虽然听上去很高级，IDA*其实就是加了h函数的迭代加深搜索（IDDFS）。很多人用过IDA*却并不知道那就是IDA*。

[11]　改编自FC游戏《所罗门之匙II》。这是一个非常优秀的智力游戏，推荐给读者。

[12]　换句话说，h（s）是s到终态距离的乐观估计。这是A*算法和IDA*算法对h函数的要求，称为可接纳性。这里介绍的算法既不是A*，也不是IDA*，而是借用了IDA*的思想，把迭代加深换成了广度优先搜索。即使从来没有接触过IDA*，也不难理解。

[13]　这个顺序很重要。为了进一步加快alpha-beta剪枝的效率，我们还可以采用所谓的“结点排序”（node ordering）策略，优先考虑更利于剪枝的结点。

[14]　这只是为了文字叙述方便。整个舞蹈链的头结点在代码中并没有保存在特别的变量中，因为它总是结点0。

[15]　这只是为了文字叙述方便，在代码中并不需要体现。第i列的虚拟结点就是结点i。

[16]　所幸网上已有很多相关资料，有兴趣的读者可以自行查阅。

[17]　注意，P是列向量（即3×1矩阵），这样的矩阵乘法是合法的。

[18]　为什么相切也不行？因为如果相切的话，路径可以拆分成两段。

[19]　比如，下界为0，上界为走到最近顶点后再走半个周长所需的总时间。

[20]　这和例题16中“禁止切线和其他圆相切”的道理是一样的。

[21]　没有思路？仔细再看看这张图吧。如果还是无法参透其中的奥秘，把头稍稍转一下试试。

[22]　这里的箭头是指后一个数可以看成是前一个数生成的（它等于前一个数乘以3再模7）。

[23]　如果说得更学术一点，设ai为选取i个元素的方法数，则序列a₀，a₁，a₂，…的生成函数就是上述多项式的乘积。生成函数（generating function，也称母函数）这一重要数学工具极为重要，有兴趣的读者可以自行查找更多资料。

[24]　所谓线性约束，就是这些变量的线性组合大于等于、小于等于或者等于某个常数。注意约束左右两边总是可以取等号。

[25]　具体来说，算法竞赛中一般采用两阶段法的单纯形法。

[26]　即用肉眼观察代码，找到bug。

[27]　在投影仪中显示出带语法高亮的代码，只能在编辑器中修改代码和翻页，不能编译运行。

附录A　训练指南：使用UVa/LA题库

在书的正文部分，我们已经学到了很多经典算法和数据结构，也完成了相当数量的题目（包括例题和习题），但这样还不够，因为那些例题和习题大都针对某个经典的知识点[1]，但真实的比赛中有很多“杂题”和“综合题”，无法归到单一的知识点范围内，而且时不时还会出现一些“非典型”知识点。本附录旨在帮助读者更加全面的选择训练题目，加强正文部分没有涉及的方面。

读者应该已经注意到了，包括本附录在内，本书的所有题目均选自UVa[2]和LA[3]题库，例题／习题中的题号和统计数据均来自于这两个题库。两个题库加起来有7000多道题目（不过有些重复），完全没必要（也不大可能）全部都做，而是应当根据自己的情况选择最合适的题目进行练习。本附录中的题目都是笔者比较熟悉的，质量相对比较高，值得一做。

一般来说，初学者的主要任务是熟练掌握经典算法和加强编码能力，并不介意被“剧透”题目的解法，因此很多简单题目在表里直接给出了算法的类型；而那些偏难的题目由于会吸引水平较高的读者来做，多数时候都只给出了题目类型和大意，以免影响读者进行独立思考。

A.1　UVa在线比赛推荐

UVa上经常不定期的举办比赛，其中比较经典的有：

❑　ACM/ICPC区域赛／总决赛热身赛

❑　Waterloo校内比赛的同步赛

❑　各种系列比赛，比如NCPC、Huge Easy Contest、Next Generation Contest等。

下面是其中笔者较为推荐的比赛，如附表A-1所示，其中一些虽然较为早期，但综合性强，且能锻炼基础；另一些比赛属于专题比赛（如数学比赛、图论比赛、搜索比赛等），很适合专项训练。

附表　A-1

续表

A.2　LA套题（ACM/ICPC真题）推荐

LA上的题目全部选自历届ACM/ICPC真题，包括总决赛和分区赛，但不包括分区赛的网上预赛[4]。

相比之下，最为推荐的是欧洲赛区的比赛题目，如附表A-2所示。

附表　A-2

亚洲赛区的题目并不逊色于欧洲，但由于赛区太多，很难以表格方式整体呈现。总的来说，每年的中国大陆赛区、日本赛区和韩国赛区的风格偏向欧洲（对算法基础的要求较高），孟加拉（通常在达卡）、印度和马来西亚的算法性次之[5]。中国台湾、越南和伊朗的题目比较中规中矩，但不时也会出现很不错的题目。

考虑到读者会比较关注中国大陆赛区，现在把2001-2007年中国大陆大多数赛区的题目情况进行总结，如附表A-3所示。

附表　A-3

考虑到2008年及以后赛区的比赛会有不少队伍拿来当做实战练习，这里就不对它们进行描述了。

北美赛区很多，题目最出色的当属ECNA、Greater New York、Mid Atlantic、Mid Central和Rocky Montain。这些赛区的算法性并不如欧洲和亚洲赛区强，但几乎从不会有偏难怪的题目，适合练习基础。

A.3　UVa在线比赛单题推荐

下面按照题号顺序分卷给出笔者推荐的题目（见附表A-4～附表A-24）。这些题目难度不一，请读者参考统计数据，x/y％表示有x个用户提交过，其中y％的用户通过了此题。请注意，题目越大，说明加入题库的时间越晚。新加入题库的题目，其提交量通常不会很大，因此统计数据并不一定能客观反映其真实难度。

附表　A-4

续表

附表　A-5

续表

附表　A-6

续表

附表　A-7

附表　A-8

附表　A-9

续表

附表　A -10

续表

附表　A-11

续表

附表　A-12

续表

附表　A-13

续表

附表　A-14

附表　A-15

续表

附表　A-16

续表

附表　A-17

续表

附表　A-18

续表

附表　A-19

续表

附表　A-20

续表

附表　A-21

续表

附表　A-22

续表

附表　A-23

续表

附表　A-24

续表

注：以前的regional比赛题中不乏好题（自第12050题开始），但并未收录于此表中。有兴趣的读者可自行查阅相关资料。

注释

[1]　即所谓的“专题练习”。

[2]　参考http://uva.onlinejudge.org。

[3]　参考http://livearchive.onlinejudge.org。

[4]　这些比赛的题目可以在国内的几大OJ，如POJ、ZOJ、HDU上提交。

[5]　也有例外。比如2008年的马来西亚赛区有几道算法性很强的题目。

附录B　Java、C#和Python语言简介

很多人思想中存在一个误区，认为编程语言之间大同小异，熟悉之后用哪个都一样。诚然，很多语言的代码确实“长得像”（比如C++和Java有很多相似语法），但它们之间也存在很多本质上的差异，直接影响到程序编写方式和调试、测试等流程，甚至还决定了项目和团队的管理方式。

本附录重点通过Python、Java和C#向读者展示C++中不存在或者不常用（往往是因为不好用）的元素。

B.1　Java

Java绝非一门语言那么简单——请把它理解成一个强大的平台[1]。下面将要介绍的东西，有些是Java这门语言的特性，而有些是这个平台的特性。换句话说，如果你使用其他JVM语言[2]，同样可以享受到这些特性。

多数算法竞赛都支持Java语言。对于选手来说，在竞赛中使用Java而非C++语言的主要动力有以下几点。

出错时会抛出异常，对调试大有帮助。相比之下，调试C++程序要麻烦得多。如果写入了非法内存（比如数组下标越界、使用无效指针），很多时候程序并不会立即崩溃，而是覆盖了某些“无辜”的变量，从而造成各种莫名其妙的问题。

更加强大的库。在C++标准库中[3]，字符串处理函数相当有限，连最常见的split、trim等功能都没有，更谈不上正规表达式[4]和大整数。事实上，很多平时采用C++参赛的选手学习Java的主要目的是使用大整数（BigInteger）。

面向对象编程（OOP）。如果你是通过算法竞赛接触的C++，很可能你并不熟悉OOP，因为C++程序可以是纯过程式的。Java强制使用OOP：即使只写一个主函数main，也必须写在一个公共类（public class）中。对OOP的深入讨论已经超出了本书范围，即使在这个附录中笔者也不打算讨论。但有一点希望读者注意：不要为了OOP而OOP，也不要为了用设计模式（design patterns）而用设计模式[5]。

引用数据类型。对于熟悉C++的选手来说，学习Java最需要注意的也许是数据类型。Java的数据类型分两种：基本数据类型（primitive type）和引用数据类型（reference type），前者是指那些大小固定的类型，比如int, long, double, char, boolean等[6]，后者是指大小不定长的类型，具体来说就是类、接口和数组。如果变量a是一个对象或者数组，那么a里实际保存的只是一个引用[7]。Java的这个特性可以减少栈溢出的可能。如果在C++中声明一个int a[1000000]的局部变量，需要1000000*sizeof（int）个字节的栈空间（很可能会引起栈溢出），但在Java中声明一个同样大小的int数组，只在栈中存放了一个引用，而数组中的那些元素占用的是堆内存。

为了加深理解，考虑下面的代码[8]。

由于cuss＝greet仅仅复制了数组的引用，当修改cuss[4]的时候，greet[4]也会改变，因为二者实际上引用了相同的字符。

另一个例子是函数调用。在C++中，函数的参数可以是值（默认情况），也可以是引用（用&符号修饰），但在Java里，参数只能是值。注意，引用类型的“值”并不是对象或者数组的内容，而是它们的“地址”。换句话说，如果参数是引用类型的，那么函数体可以拿到该变量的“地址”，从而修改这个变量的内容。

垃圾回收。手工创建一个对象需要使用new运算符，但和C++不一样的是，Java并没有delete运算符，你无法手工删除一个对象。这样一来不用的内存岂不是浪费了？并非如此。Java有一套垃圾回收（gabage collection ,gc）机制，自动判断哪些内存不再被使用，然后释放这些内存，以便今后使用。垃圾回收的原理已经超过了本附录的范围，有兴趣的读者可以自行阅读相关资料。

字符串。在字符串上栽跟头的新手并不在少数。在Java中，String只是一个普通的类，属于引用类型，因此判断两个字符串a和b是否相等不能用a=＝b，而应该是a.equals（b），因为a=＝b判断的是二者是否引用的同一个“地址”。类似地，a＝b只是把b这个字符串的引用赋值给了a。如果要做一份真正的新拷贝，应该用a＝b.clone（）。类似地，创建数组的全新拷贝也可以用clone（）函数[9]。

Java的字符串是不可变（immutable）的，有时候会给字符串处理带来诸多不便。常用的处理方法是用toCharArray（）把它转化为字符数组后处理，最后转回字符串（String类有一个构造函数的参数就是字符数组）。如果是要不断地给一个字符串后面添加内容，则应该用StringBuffer类。

输入输出。最后需要注意的是输入输出效率问题。最常用的Scanner类通常比scanf系列慢5～10倍，应尽量避免；下面的代码[10]包装了速度较快的BufferedReader和StringTokenizer（比split函数略快），推荐选手在比赛中使用。

这样就可以用下面的代码读取数据了。

数据结构。最后，请认真学习Java集合框架（Java Collection Framework，JCF）。这是一些类似于STL容器的东西，其中ArrayList相当于vector，TreeSet相当于set等。需要注意的是Java泛型和C++模板是两个完全不同的东西，建议读者弄清楚他们的区别，这里不再赘述。

B.2　C#

C#是微软.NET框架中的主力语言[11]，从3.0开始加入了很多有用的特性，在语言方面已经相当成熟；更棒的是，随着开源框架Mono的逐渐成熟和稳定，C#已经成为一门跨平台的语言，具备了的更大的灵活性。C#的程序看上去和Java有些相似，这里为什么还要单独讲述呢？除了方便那些对Windows程序开发有着浓厚兴趣的选手向工程师过度之外，最主要的原因是C#有着良好的FP（Functional Programming，函数式编程）支持。那到底什么是FP呢？为什么它这么重要呢？

什么是FP？冯·诺依曼机器的基本计算模型是通过一条条的机器指令逐步把输入数据加工成最终的计算结果，在计算过程中有大量内存单元被反复修改；而FP的基本思想是把所有的状态看成一个全集，程序的基本组成部分就是这些状态之间的映射，也就是函数，通过函数之间的复合运算把输入数据映射到最终的结果。顺便说一句，世界上最早的两种编程语言是Fortran和LISP，其中LISP就属于FP类型的语言。

为什么FP如此重要？在现实的工程中，数据的输入／输出需要从标准IO层次进行操作的情况比较少，大部分的算法是以类接口的形式对外提供。工程代码相对比赛代码对质量的要求更高。最重要的一点是，在一般比赛算法程序中大量使用的全局变量、全局数组等，在现实的工程中一般是不出现的，一方面是因为代码的可维护性的要求，另一方面则是现实中普遍存在的高并发环境。现在许多服务型程序的运行环境一般都有少则2路，多则24路的CPU，所以程序设计必须能够考虑到充分利用硬件的优势，在这种场合下，大量使用全局数据是非常不明智的选择。

使用FP的语言。Haskell等纯粹的FP语言因为对于大多数的程序员来说太过“惊世骇俗”而一直应用在比较小众的领域[12]，在主流静态语言中最早引入FP的良好支持并且得到广泛应用的当属C# 3.0，C#在2.0以及之前的版本中仍然是以传统的面向对象语言的面目出现，跟Java大同小异，在3.0版本中则加入了对FP的基本支持，主要是加入了对λ（lambda['læmdə]）表达式的支持，而λ表达式则是FP语言的基石。

举个例子，如果有两个长度为n的整型数组x和y，要计算二者的内积，不难写出下面的C++代码。

我们来详细分析这段代码的特征。

❑　它的所有语句通过某种规则来操作一个不明显的变量c。

❑　这段代码基本上没有体现出一种从简单元素构造复杂元素的思想。

❑　这段代码的阅读者必须在脑中执行一遍才能充分理解。

❑　算法是通过重复每次一个字的赋值或者就地修改来实现。

❑　它通过a，b两个命名来指定参数，如果需要复用，需封装在一个更复杂的过程定义中。而这样做会带来更复杂的问题（比如函数参数按值传递还是按引用传递）。

下面我们给出一个使用FP的思维方式写出的版本[13]。

我们来详细解释一下这段代码。

❑　Zip是一个函数，输入参数是两个序列和一个函数（元素的合并规则），返回按照规则产生的新序列。Zip函数在FP中属于高阶函数，即把函数作为输入参数或者把函数作为结果返回的函数。

❑　（x，y）＝＞ x*y是一个匿名函数（也就是λ算子），返回两个输入参数的积。这个匿名函数传递给Zip，作为两个序列的合并规则。

❑　Sum把序列映射到该序列中所有元素之和。

这段代码有如下的特点。

❑　它仅仅需要输入参数作为变量，没有用到任何需要修改的状态作为中间变量。

❑　它是从更小的元素（基础类库提供的函数）复合组装起来的。

❑　它的含义更为清晰，在已经了解C#中的基础构建元素（基础类库提供的函数）的前提下，与前面的版本相比更加接近思维模式的直观表达。事实上，在其他更为纯粹的FP语言，上面的代码会更加简洁优美[14]。

❑　它的基本概念是函数，以及函数的组合，而非变量、状态修改以及循环等更容易导致代码潜在问题的概念。

是不是迫不及待地想试试了？没问题，只要安装了.NET Framework 4.0（注意：不需要安装任何版本的Visual Studio，包括Express版），就已经拥有了C#编译器，位于％SystemRoot％\ Microsoft.NET\Framework\＜framework-version＞[15]。如果你并没有安装它，或者你使用的是非Windows系统，请安装跨平台Mono[16]。

除了FP之外，C#的不少特性和Java语言很类似，比如OOP、引用数据类型、异常处理、垃圾回收等，但也有一些显著差别（比如，int，double等Java中的基本数据类型在C#中也拥有“方法”——1.2.ToString（）＋3.ToString（）返回字符串“1.23”[17]。另外，还可以用ref修饰符让函数采用引用传递而非值传递。C# 4.0甚至还支持命名参数）。

值得一提的是，从C#3.0开始，C#就支持类型推断了。比如，你可以写var s＝new System.Text.StringBuilder（），而不用明确的指明s的类型。编译器可以通过赋值语句的右边推断出s的类型。

注意，千万不要把类型推断和动态类型搞混了。在采用动态类型的语言（如B.3节中介绍的Python）时，变量没有类型，只有值才有类型。换句话说，同一个变量可以先保存int，再换成double，接下来保存一个对象等。但类型推断不同；虽然上面的s在声明时没有指定类型，但编译器已经为它推断出了类型，并且这个类型是不可以更改的（不信可以接下来写一个s＝1试试，肯定会报错）。

最后，建议大家熟悉集合框架（System.Collections），理由不用多说了吧。

B.3　Python

作为算法竞赛的选手来说，如果不会Python就太可惜了。即使不打算用它编写任何大规模的程序，快速开发实用小程序的能力往往也能让你做事效率大幅度提高。

多数算法竞赛和OJ（包括UVa和LA）都不支持Python，但主流的Linux操作系统都预装了Python。考虑到多数竞赛的比赛环境都是Linux，选手完全可以用Python编写各种小巧的实用工具来辅助自己。

如何学习Python？很简单，直接写程序。只要写出的程序符合自己的需要，就不用深究其中的道理——除非你很好奇。这是一门实践性很强的语言，很多时候都可以“望文生义”。更棒的是，有一个称为REPL（交互命令行）东西可以帮助你即学即用——每敲一行代码就能马上看到执行结果，无须编译[18]。

在算法竞赛中，用Python编写数据生成器是再合适不过的了。下面的代码生成一个长度在5～10之间，恰好包含一个大写字母，其他字符为小写字母的串。

原理是这样的：首先随机选择一个4～9之间（包括4和9）的整数L作为大写字母的个数，然后随机生成一个长度为L的大写字符串。这是如何做到的？首先，我们随机生成一个长度为L的列表[19]（list），其中每个元素都是一个大写字母，如下所示。

这个语法叫做列表解析（list comprehension），是一种构造列表的简单方法。range（5）生成列表[0，1，2，3，4]。这条语句用大白话说就是“对于列表[0，1，2，3，4]中的每个数i，调用一次choice（lowercase），把结果拼成一个列表”。接下来用join函数把列表里的字符串连接起来。S.join（L）的作用是把字符串列表L中的各个字符串拼接起来，用字符串S分隔。[20]

最后随机出小写字符出现的位置p，然后插入到大写字母串中。s[a:b]代表列表或者字符串的第a个元素到第b－1个元素。a和b都可以省略，a默认为0，b默认为列表长度。换句话说，L2＝L[:]的作用是创建一个和L一样的数组。由于Python的所有值都是引用类型的，因此L2＝L只是把L中保存的引用拷贝到了L2。

其中id（a）返回的是变量a保存的引用。建议读者不要在代码中使用这个函数，仅用它来更好的理解python。刚才的实验还证实了多重赋值的合法性，因此可以简单地用a，b＝b，a来交换两个变量a和b。

Python也支持FP（虽说和C#比较起来，Python的FP实在有点逊色）。比如，计算30!可以采用如下写法[21]：

类似的东西还有很多。总之，当你熟练掌握Python之后，能大大缩短编写数据生成器、对拍器、“猜想验证器”等小程序的时间，平时还能用它编写各种实用程序呢。事实上，Python是笔者最常用的编程语言之一，在工作中曾用Python做过桌面GUI程序、网站、数据处理程序、分布式任务调度器以及各类程序的插件（如Blender）。

Python也有很多不足，但随着pyinstaller、SWIG、pypy和execnet等优秀项目的出现和发展，很多问题都得到了解决或者弱化。尽管还有一些不如人意的地方[22]，但它的简单实用是毋庸置疑的。

注释

[1]　事实上，作为一门语言，很多人已经认为Java有些跟不上时代了，但很少有人会否认Java这个平台的成功。

[2]　即可以运行在JVM（Java虚拟机）上的语言，比如clojure和Scala。

[3]　特指C标准库和STL，不包括boost等第三方库，即使它的应用已经相当广泛。

[4]　也译作“正则表达式”，英文是regular expression。

[5]　很多人认为学习OOP的必要环节之一是学习设计模式。

[6]　注意Java的long类型是64位整数。另外，Java没有C/C++中的无符号整数。

[7]　这是一个类似于“地址”的东西，但因为Java程序运行在虚拟机中，这个“地址”并不直接对应于机器的物理内存。

[8]　选自《Java in a Nutshell》第2.10小节：http://docstore.mik.ua/orelly/java-ent/jnut/ch02_10.htm。

[9]　注意clone（）是浅拷贝，因此对多维数组的拷贝也许会出乎你的意料。强烈建议读者学习浅拷贝的相关内容。

[10]　摘自http://www.cpe.ku.ac.th/～jim/java-io.html。

[11]　如果你的Windows安装了.NET框架，实际上你已经有了C#编译器，在Windows安装目录下的Microsoft.NET\Framework子目录，程序名叫csc.exe。

[12]　Haskell是学术界的产物，虽然有很多“大牛”活跃在Haskell社区，但对于普通程序员来说仍然有些晦涩难懂。尽管如此，很多“流行编程语言”都或多或少地借鉴了Haskell中的元素，如果读者想要入门的话，推荐《Real World Haskell》一书。

[13]　需用C# 4.0编译，因为Zip是C# 4.0的新特性。

[14]　比如Haskell可以写成sum （zipWith （*） a b）。

[15]　％SystemRoot％是指你的Windows安装目录。

[16]　如果还想要一个轻量级的IDE，MonoDevelop是个不错的选择。

[17]　但它们仍然是值类型，而不是引用类型。

[18]　专业人士一般用ipython代替这个自带的命令行，但初学阶段不必如此。

[19]　类似于C++的数组，但长度可变，而且每个元素的类型不必相同。

[20]　在上面的代码中，S是由两个单引号组成的空串，而不是单个双引号。

[21]　这是Python 2.x的写法。Python 3.x需要先from functools import *。

[22]　最突出的问题是Python 3.x和2.x并不兼容。

主要参考书目

［1］Thomas H.Cormen, Charles E.Leiserson, Ronald L.Rivest, Clifford Stein. Introduction to Algorithms, Second Edition, The MIT Press, 2001

［2］Jon Kleinberg, éva Tardos. Algorithm Design. Addison Wesley, 2005

［3］Sanjoy Dasgupta.Christos Papadimitriou, Umesh Vazirani. Algorithms. McGraw Hill Higher Education, 2006

［4］Ronald L.Graham, Donald E.Knuth, Oren Patashnik. Concrete Mathematics. Addison-Wesley Professional, 1994

［5］Joseph O'Rourke. Computational Geometry in C, second edition, Cambridge University Press, 1998

［6］Mark de Berg, Otfried Cheong, Marc van Kreveld, Mark Overmars, Computational Geometry: Algorithms and Applications, 3rd Edition, Springer-Verlag Berlin and Heidelberg GmbH & Co. K, 2008

［7］G. Polya. How to Solve It: A New Aspect of Mathematical Method. Princeton University Press2nd Edition, 1971

［8］Philip Schneider, David H. Eberly. Geometric Tools for Computer Graphics. Morgan Kaufmann, 2002

［9］周培德．计算几何——算法分析与设计．北京：清华大学出版社，2000

［10］中国计算机学会（执行主编王宏）．全国信息学奥林匹克年鉴．河南：中原出版传媒集团，2006—2010

［11］中国计算机学会．IOI国家集训队资料，2002—2010

算法艺术与信息学竞赛

《算法竞赛入门经典》

本书分为三部分：语言篇、算法篇和竞赛篇。首先以实践导向的方式讲解了C/C++的基本语法，然后介绍了算法和数据结构的基础知识，最后是动态规划、数学和图论三大专题。全书短小精悍，但内容全面，既可作为教材，又方便自学。

《算法竞赛入门经典——训练指南》

本书是《算法竞赛入门经典》的重要补充，旨在补充该书中没有涉及或者讲解不够详细的内容，以构建一个较完整的知识体系，同时通过大量有针对性的题目让抽象复杂的算法和数学具体化、实用化。全书分为算法设计基础、数学基础、实用数据结构、几何问题、图论算法与模型、更多算法专题等，通过150多道例题深入浅出地介绍了上述领域的各个知识点、经典思维方式以及程序实现的常见方法和技巧，并在每章末尾和附录中给出丰富的分类习题，供读者查漏补缺、强化学习效果。

《算法艺术与问题求解》

本书的最终目标是让读者感受到算法艺术之美，并灵活掌握更多问题求解的技巧。第一部分是算法竞赛中的高级话题，包括难题、巧题赏析、命题技巧、各种算法竞赛的纵览与分析等，而第二部分则是一些应用空间更加广阔的内容，包括多语言编程、数学软件和算法包，以及若干工程、研究领域的简单介绍，拉近了竞赛和业界的距离。

丛书介绍

算法在计算机科学乃至于整个科学界的作用日益明显。它们不仅具有重要的理论意义，而且解决了生产生活中的很多实际问题。程序设计竞赛就是这样一类以算法为核心但是偏重实用性的比赛。随着各类比赛规模的逐渐扩大，程序设计竞赛在各高校、IT公司和其他社会各界中越来越受到认可和重视。很多研究工作者和从事IT行业的人尽管不参加这类竞赛，但也希望具有这方面的能力，受到这方面的专业训练。

本丛书的前身是5年前的同名图书《算法艺术与信息学竞赛》。5年来，更多的人加入到参赛、命题和组织的队伍中来，各类竞赛的参赛和命题水平也有了长足的进步。作者深知当年的经典之作开始显得题目陈旧，知识的广度和深度也无法达到当今高水平比赛的要求了。因此，将原书的内容扩充、完善后分成三本，以丛书的形式依次展现给读者。这三本书循序渐进，从零语言基础开始讲起，直到超越竞赛本身，真正把算法当成“艺术”。

●《算法竞赛入门经典》

●《算法竞赛入门经典——训练指南》

●《算法艺术与问题求解》