资源描述
王道论坛www.cskaoyan.com王道论坛计算机考研机试指南王道论坛2013.01.06写在前面的话各位王道的小崽子们,今天你们考完初试了,感觉解放了吧?轻松了吧?无论结果如何,总算坚持到了最后。但是,其实你的考研生活只刚刚走出了第一步,接下来会有初试成绩出来前的煎熬、分数线出来的煎熬、准备复试以及复试的煎熬以及录取结果出来前的煎熬,这些都远远比初试更折磨人,未来的两个月你会感觉到王道没有吓唬你们。王道是个好姑娘,四年多的时光里陪伴了接近二十万计算机考研人,不离不弃。今年不小心又压中一道算法题,说实话,王道的书里有那么多的题,知识点又只有那么多,总能瞎猫碰见死耗子吧?王道尊重的不是考研这个行业,而是你们这群执着的小崽子们的梦想!看着你们圆梦,我们内心充满了成就感。初试考完了,是不是应该好好放松放松?是不是初试考得好,录取就肯定没有问题了?对不起,这个不是计算机专业研究生考试的规则。目前已经有越来越多的高校采用上机考试的形式来考察考生的实际动手编程能力,并且机试在复试中所占的比例非常高,并且很多高校规定复试成绩不及格者,一律不得录取。目前国内高校开展 ACM 教学的高校非常少,而 ACM 是目前所有高校机试所采取的唯一形式,因此提早开始准备和练习,对于一个完全没有接触过 ACM 的计算机考研人来说,是必须的!为了方便各位道友练习机试,我们编写了本书,搭建了九度Online Judge(http:/ac.jobdu.com),并收集了全国各大高校的复试上机真题,希望能给大家复试上机考试提供强有力的支持。你可以直接使用王道论坛的帐号进行登录。如果您在使用过程中遇到问题,欢迎你到复试机试讨论专区发贴提出。目前已经收录了我们能够收集到的各高校上机复试真题,欢迎大家继续向我们提供各高校上机真题,具体请站内信或者电子邮件联系浩帆(Email:qihu#zju.edu.cn)。此外,华科的上机题我们经过了变型,将其中一些便于修改成OJ判题的题目收录进了我们的OJ。考研其实没有什么诀窍,就是每天比别人早起一点,晚睡一点,比别人早准备一点,勤奋一点。考研离我已经很远了,同时我也坚信一个写不出合格代码的计算机专业的学生,即使考上了研究生,无非也只是给未来失业判个缓期执行而已。小崽子们,要忠实于自己心底的梦想,勇敢地坚持下去,而当下,请开始准备复试吧,熬过这两个月,一切就都好了。第1章 从零开始一 机试的意义众所周知,机试是计算机考研当中非常重要的一个环节。在越来越注重实践动手能力的今天,越来越多的知名高校在计算机研究生招生考试当中采用了机试的形式,通过这种考试手段来考察考生分析问题并利用计算机程序解决问题的能力。通过机试,可以考察一个考生从实际问题当中抽象得出数学模型的能力,利用所学的计算机专业知识对该模型进行分析求解的能力,以及利用计算机编程语言,结合数据结构和算法真正解决该实际问题的能力。所以,我们在准备机试的过程中要特别注意以下几个方面:1、如何将一个实际问题抽象成数学问题。例如将高速公路网抽象成带权图,这就是一种简单的、直接的抽象。2、如何将我们所学的计算机专业知识运用到解决抽象出来的数学模型上去。这就要求我们在脑子里事先熟知一些常用的数据结构和算法,再结合模型求解的要求,很快地选择合适的编程思想来完成算法的设计。甚至可以利用一些经典算法特征,加入一些自己的优化,使得编写的程序更优雅、更高效(当然这是建立在充分理解经典算法的基础上)。3、如何将我们为解决该数学模型所设计的算法编写成一个能被计算机真正执行的计算机程序。我们认为,关于这个能力的定义有三个层次:1)会编写(默写)一些经典算法的程序代码。2)能够将自己的想法或设计的算法转换为程序代码。3)能够使得自己编写的程序在大量的、多种多样的、极限的测试数据面前依旧正常完成功能(程序的健壮性)。我们在准备机试的训练过程中,就要依次经历这三个层次,从而最后能够在实际考试当中取得理想的成绩。本教程从分析经典机试真题出发,引入近几年频繁被考察的数据结构和算法,利用 C/C+语言讲解例题,并加以一些相关知识的扩展,希望在读者准备计算机考研机试的过程中充当指引者的角色。同时,由于笔者自身实力的限制以及编写时间的不足,教程中难免存在一些疏漏和错误,也欢迎读者提出、指正。二 机试的形式绝大部分机试所采用的形式,归结起来可以概括为:得到题目后,在计算机上完成作答,由计算机评判并实时告知结果的考试过程。机试考试中的问题往往有五部分组成。首先是问题描述,问题描述描述该问题的题面,题面或直接告知考生所要解决的数学问题或给出一个生活中的实际案例,以待考生自己从中抽象出所要解决的数学模型。第二是输入格式,约定计算机将要给出的输入数据是以怎样的顺序和格式向程序输入的,更重要的是它将给出输入数据中各个数据的数据范围,我们通过这些给出的数据范围确定数据的规模,为我们设计算法提供重要依据。第三是输出格式,明确考生将要编写的程序将以怎样的顺序和格式向输出输出题面所要求的答案。第四第五部分即输入、输出数据举例(Sample)。好的 Sample 不仅能为考生提供一组简单的测试用例,同时也能明确题意,为题面描述不清或有歧义的地方做适当的补充。另外我们也要特别注意,题目中给定的两个重要参数:1、时间限制。2、空间限制。这两个重要的参数限定了考生提交的程序在输出答案之前所能耗费的时间和空间。我们来看一个典型的题目描述,从而了解机试题的问题形式。例 1.1 计算 A+B (九度 OJ 题号:1000)时间限制:1 秒 内存限制:32 兆 特殊判题:否题目描述:求整数 a,b 的和。输入:测试案例有多行,每行为 a,b 的值,a,b 为 int 范围。输出:输出多行,对应 a+b 的结果。样例输入:124569样例输出:3915通过该例,我们基本明确了机试试题的问题形式,以及问题各部分所起到的作用。这里补充解释一下所谓特殊判题(Special Judge)的含义,特殊判题常被应用在可能存在多个符合条件的答案的情况下,若评判系统采用了特殊判题,那么系统只要求输出任何一组解即可;若系统对该题并没有采用特殊判题,那么你必须严格按照题目中对输出的限定输出对应的答案(如输出字典序最小的解)。得到题目后,考生在计算机上立即编写程序,确认无误后,将该程序源代码提交给评判系统。评判系统将考生提交的源代码编译后,将后台预先存储的输入测试数据输入考生程序,并将该程序输出的数据与预先存储在评判系统上的“答案”进行比对得出结果。评判系统评判考生程序后,实时地将评判结果返回考生界面,考生可以根据该结果了解自己的程序是否被评判系统判为正确,从而根据不同的结果继续完成考试。三 评判结果本节将对评判系统评判考生提交程序后返回的结果做详细的说明,并且针对不同的返回结果,对可能出现错误的地方作出初步的界定。Accepted(答案正确):你的程序对所有的测试数据都输出了正确的答案,你已经得到了该题的所有分数,恭喜。Wrong Answer(答案错误):评判系统测试到你的程序对若干组(或者全部)测试数据没有输出正确的结果。出现该种错误后,一般有两种解决方向:如果对设计的算法正确性有较大的把握,那么你可以重点考虑代码健壮性,即是否存在某些特殊数据使程序出现错误,比如边界数据,比如程序中变量出现溢出。另一种方向,即怀疑算法本身的正确性,那么你就需要重新考虑你的算法设计了。Presentation Error (格式错误):评判系统认为你的程序输出“好像”是正确的,只是没有严格按照题目当中输出所要求的输出格式来输出你的答案,例如你忽略了题目要求在每组输出后再输出一个空行。出现这种错误,往往预示着你离完全正确已经不远了,出现错误似乎只是因为多输出了一些空格、换行之类的多余字符而已。但这不是绝对的,假如在排版题(后文会有介绍)中出现格式错误,那么有可能你离正确的答案仍然有一定的距离。Time Limit Exceeded (超出时间限制):你的程序在输出所有需要输出的答案之前已经超过了题目中所规定的时间。若这种结果出现在你的评判结果里,依然有两种方向可供参考:1、假如你确定算法时间复杂度能够符合题目的要求,那么依旧可以检查是否程序可能在某种情况下出现死循环,是否有边界数据可能会让你的代码不按照预想的工作,从而使程序不能正常的结束。2、你设计的算法时间复杂度是否已经高于题目对复杂度的要求,如果是这样,那么你需要重新设计更加高效的算法或者对你现行的算法进行一定的优化。Runtime Error (运行时错误):你的程序在计算答案的过程中由于出现了某种致命的原因异常终止。你可以考虑以下几个要点来排除该错误:1、程序是否访问了不该访问的内存地址,比如访问数组下标越界。2、程序是否出现了除以整数 0,从而使程序异常。3、程序是否调用了评判系统禁止调用的函数。4、程序是否会出现因为递归过深或其他原因造成的栈溢出。Compile Error (编译错误):你提交的程序并没有通过评判系统的编译,可根据更详细的编译信息修改你的程序。Memory Limit Exceeded (使用内存超出限制):你提交的程序在运行输出所有的答案之前所调用的内存已经超过了题目中所限定的内存限制。造成这种错误的原因主要有两个方面:1、你的程序申请过多的内存来完成所要求的工作,即算法空间复杂度过高。2、因为程序本身的某种错误使得程序不断的申请内存,例如因为某种原因出现了死循环,使得队列中不断的被放入元素。当然也千万别忽略自己的低级错误,比如在声明数组大小时多打了一个 0。Output Limit Exceeded (输出超出限制):你的程序输出了过多的东西,甚至超出了评判系统为了自我保护而设定的被评判程序输出大小的最高上限。一般来说该种错误并不常见,一旦出现了也很好找原因。要么就是你在提交时忘记关闭你在调试时输出的调试信息(我经常输出 DP 时的数组来动态的观察状态的转移);要么就是程序的输出部分出现了死循环,使得程序不断地输出而超出系统的限制。以上几种结果就是评判系统可能会返回的几个最基本的结果。若返回Accepted,则你可以获得该题的所有分数。若返回其它错误,则根据不同的考试规则,你的得分将会有一定的差异。若你参加的考试采用按测试点给分规则,你依然能够获得你通过的测试点(即该程序返回正确结果的那部分测试数据)所对应的分数;但是,若你参加考试采用所有数据通过才能得分的评分规则,那么很可惜,到目前为止你在这道题上的得分依旧是 0 分。假如评判结果显示你提交的程序错误的,你可以在修改程序后再次提交该题,直到获得满意的分数或者放弃作答该题。四 复杂度的估计本节将详细讨论题目中所给定的时间限定和空间限定对我们程序设计的指导作用。如例 1.1 所示,该题给予我们的程序 1 秒的运行时限,这也是最常见的时间限制(或最常见的时间限制数量级)。对于该时限,通常,我们所设计的算法复杂度不能超过百万级别,即不能超过一千万。即若算法的时间复杂度是 O(n2),则该 n(往往在题目中会给出数据范围)不应大于 3000,否则将会达到我们所说的千万数量级复杂度,从而程序运行时间超出题目中给出的用时限定。举例来说,我们不能在 1 秒时限的题目当中对 10000 个整数进行冒泡排序,而必须使用快速排序等时间复杂度为 O(nlogn)的排序算法,否则程序很可能将会得到运行时间超出限制的评判结果。因此你可以对你的程序在最坏情况下的复杂度进行一个估算,假如确定其在百万数量级之内,那么你的程序一般是不会超出时间限制的。对于其它时间限制的情况,可以参考 1 秒时限对时间复杂度的要求,做出一定的估计,从而保证自己的程序运行所需的时间不会超过题目中对运行时间的限制。我们同样可以知道,例 1.1 中限定的内存空间为 32 兆,即你的程序在评测系统中运行时,不得使用超过 32 兆大小的内存。空间限定则比较好处理,你可以 简 单 的 计 算 你 所 申 请 的 内 存 空 间 的 大 小 ( 例 如 我 们 可 以 轻 易 的 计 算 intmat300300所占用的内存大小)。只要该大小没有超过或过分接近空间限定(运行时需要一些额外的空间消耗),那么你的程序应当是符合空间限制条件的。现如今的机试题,一般不会对空间做过多的限制,大多数情况只对时间做出明确的要求,所以考题在空间上将会尽量满足考生程序的需要。正因为如此,我们在很多情况下都应该有“空间换时间”的思想。五 OJ的使用我们该去何处开始我们的机试训练和模拟考试呢?可能大部分人已经听说过在线评判系统(OnlineJudge) ,例如HDOJ(http:/acm.hdu.edu.cn/ ),POJ(http:/poj.org/ )。但是这些OJ都是为ACM/ICPC训练而设计的,虽然形式与机试大同小异但是难度却有较大差别,普通的考生在其中训练不仅打击了信心,也在一定程度上浪费了时间。因此,推荐专门为我们计算机考研机试所设计的九度OJ(http:/ac.jobdu.com/ ),它收录了近三百道各大高校近几年来的机试真题,真正为我们准备机试提供了极大的便利。我们在它的首页上可以看到九度 OJ 所包含的各种功能。首先,我们需要点击“注册”按钮,为自己注册一个账号,有了该账号你就能在九度 OJ 上进行日常练习,同样也有资格参加九度 OJ 举行的各类比赛和模拟考试。现假设我们已经有了一个账号,并且想要开始在线练习。那么,可以在页面左边的导航栏找到在线练习,我们可以选择题目汇总来查看九度 OJ 收录的所有在线练习题,或者点击考研机试题集来查看九度为各位考生收录的近几年各大高校机试真题。例如我们选择题目汇总中的题号为 1000 的问题,例 1.1 中的例子便出现在页面当中。当用户完成作答以后,可以在页面左侧点击“提交答案”按钮,将源代码提交给九度在线评测系统,提交完成后,系统会自动跳转到评测状态页面,通过查看你刚才提交的源代码的评测状态,你就可以知道你是否通过了该题,从而达到锻炼自我的目的。在本文中,凡是涉及九度 OJ 上收录的例题,本书会明确该题的题号。同样的,九度 OJ 也经常为广大考生提供在线模拟考试训练。我们可以点击“比赛及考试”按钮,来查看过往的比赛以及近期的比赛安排,从而有选择的参加在线比赛,考察自己的训练质量,为日后进一步训练做出导向总结本节主要介绍机试的意义、形式,以及机试题的形式和考察的方法,并在最后给出了我们可以完成机试训练的网站九度 Online Judge。第2章 经典入门本章通过对频繁出现在考研机试真题中的一些经典问题进行讲解,并对解题方法做详细的说明,旨在使读者快速的了解机试的考察形式和答题方法,以便进一步的学习。一 排序排序考点在历年机试考点中分布广泛。排序既是我们必须要掌握的基本算法,同时也是学习其他大部分算法的前提和保证。首先我们来学习对基本类型的排序。所谓对基本类型排序,即我们对诸如整数、浮点数等计算机编程语言内置的基本类型进行排序的过程。例 2.1 排序 (九度教程第 1 题)时间限制:1 秒内存限制:32 兆特殊判题:否题目描述:对输入的 n 个数进行排序并输出。输入:输入的第一行包括一个整数 n(1=n=100)。接下来的一行包括 n 个整数。输出:可能有多组测试数据,对于每组数据,将排序后的 n 个整数输出,每个数后面都有一个空格。每组测试数据的结果占一行。样例输入:41432样例输出:1234来源:2006 年华中科技大学计算机保研机试真题这便是一例曾经出现在机试中关于排序考点的真题。面对这样的考题,读者可能很快就会联想到数据结构教科书上各种形形色色、特点不同的排序算法。例如,我们可以自然的想到选择冒泡排序来完成此题。但是在确定使用冒泡排序之前,我们不应忽略对其复杂度的考量,我们应特别注意在选择将要采用的算法时估计其复杂度。以此处为例,假如我们采用冒泡排序来完成此题,我们应该注意到冒泡排序的时间复杂度为 O(待排序个数的平方),在此例中即 O(n2)。而 n的取值范围也在题面中明确地给出(1=n=100),这样我们可以估算出 n2 的数量级仅在万级别,其时间复杂度并没有超过我们在前一章中所讲的百万数量级复杂度,所以使用冒泡排序在该例限定的一秒运行时间里是完全可以接受的;同时冒泡排序的空间复杂度为 O(n),即大致需要 100 * 32bit(数组长度 * int 所占内存)的内存,这样,所需的内存也不会超过该例限定的内存大小(32 兆)。只有经过这样的复杂度分析,我们才能真正确定冒泡排序符合我们的要求。于是,我们可以开始着手编写该例的解题代码。代码 2.1#include int main () int n;int buf100; /定义我们将要使用的变量n,并用buf100来保存将要排序的数字while (scanf (%d,&n) != EOF) /输入n,并实现多组数据的输入for (int i = 0;i n;i +) scanf (%d,&bufi); /输入待排序数字for (int i = 0;i n;i +) for (int j = 0;j bufj + 1) int tmp = bufj;bufj = bufj + 1;bufj + 1 = tmp; /冒泡排序主体for (int i = 0;i n;i +) printf(%d ,bufi); /输出完成排序后的数字,注意,题面输出要求在每个数字后都添加一个空格printf(n); /输出换行return 0;初见这段代码,读者可能对主循环体的循环条件大为不解。while(scanf(%d,&n) != EOF),它在完成输入n的同时又完成了什么看起来令人迷惑的条件判断?要回答这个问题,我们首先要明确两点:第一,scanf函数是有返回值的(尽管大多时候都会被人忽略),它将返回被输入函数成功赋值的变量个数。在此例中,若成功完成输入并对n赋值,scanf函数的返回值即为1。我们即是通过该返回值来完成循环条件的判断的。第二,我们也要注意到,该例题面中明确了输入数据会有多组,我们将要对每组输入都输出相应答案。并且,事先我们并不知道将会有多少组数据被输入到程序中。于是,我们可以使用该循环测试条件来完成对输入是否结束的判断。过程如下:如仍有测试数据未被测试完,那么该组测试的开头一行将如题面所说,为一个整数(n),其将会被scanf语句赋值给变量n,那么scanf返回值为1,不等于EOF(-1),循环条件成立,程序进入循环体,执行程序;如果输入已经到达结尾(输入文件到达末尾或在命令台输入中输入Ctrl+z),scanf函数无法再为变量n赋值,于是scanf函数返回EOF(end of file)。至此,循环条件不成立,程序跳过循环体,执行return 0,使程序正常的结束。该循环判断条件既保证了可以对多组测试数据进行计算,同时又使程序在输入结束后能够正常的退出。反之,如果我们不使用循环,程序在处理完一组数据后就会退出,后续测试数据无法被正常的处理。但若我们使用死循环而不加以任何退出条件,那么程序在处理完所有的测试数据后仍就不能正常的退出,虽然程序已经输出了所有测试数据的答案,但评判系统还是会认为程序在限制时间内无法运行完毕,于是返回超时的评判结果。正因为如此,初学者很容易因为这个原因,出现莫名其妙的程序超时。所以,我们必须使用该循环判断条件来完成以上两个功能。值得一提的是,若输入为字符串而程序采用gets()的方法读入,则相同功能的循环判断语句为while(gets(字符串变量)。解决了关于循环测试条件的疑惑,对于代码其它部分,相关注释已在代码中给出,读者应该非常容易理解它的工作流程。另外需要指出的是,假如你使用vc6.0编译这段程序,很可能会出现编译错误。这是因为vc6.0对for循环测试条件中定义的指示变量i作用域的不同规定造成的。C+标准指出,在for循环循环条件中定义的变量,其作用域仅限于for循环循环体内部(就像在for循环循环体内定义的局部变量)。于是我们在多个for循环中都重新定义指示变量i来完成相应的功能。而vc6.0中则不同,在for循环退出以后,指示变量i依然可见(就像在for循环循环体外定义的局部变量)。所以我们在后续for循环循环条件中定义的新指示变量i会与该旧的指示变量i产生冲突,而无法被成功的定义,于是编译器于此给出了编译错误。因此,我个人并不推荐使用vc6.0编译器。假如你在练习和考试中,不得不使用该编译器,那么请在for循环循环条件第一次定义指示变量i后,不必重新定义,而是直接使用这个我们已经定义的并且依旧可见的变量来完成新的for循环功能(莫忘初始化)。在明确以上注意点后,我们重新回到排序问题本身上来。该例程使用冒泡排序来完成题目要求。但假如我们修改题目中n的取值范围,使其最大能达到10000,我们又应该如何解决该问题呢。读者可能很快就能反应过来,冒泡排序因其较高的时间复杂度(O(10000 * 10000) 已经超过了百万数量级)而不能再被我们采用,于是我们不得不使用诸如快速排序、归并排序等具有更优复杂度的排序算法。那么,新问题出现了,恐怕大部分读者都对此感到恐惧:是否能正确的写出快速排序、归并排序等的程序代码?其实,我们并不用为此感到担心,C+已经为我们编写了快速排序库函数,我们只需调用该函数,便能轻易的完成快速排序。代码 2.2#include #include using namespace std;int main () int n;int buf10000;while (scanf (%d,&n) != EOF) for (int i = 0;i n;i +) scanf (%d,&bufi);sort (buf,buf + n); /使用该重载形式,表明将要使用自己定义的排列规则for (int i = 0;i n;i +) printf(%d ,bufi);printf(n);return 0;因 为 我 们 将 要 在 程 序 中 使 用 了 sort 库 函 数 , 我 们 在 头 文 件 方 面 包 含 了algorithm 头文件,并使用 using namespace std 语句声明了我们将会使用标准命名空间(sort 被定义在其中)。此例中,sort 函数的两个参数代表待排序内存的起始地址和结束地址(sort 函数还有另外的重载形式,在后文中会有所提及),在此例中起始地址为 buf,结束地址为 buf + n。该函数调用完成后,buf 数组中的数字就已经通过快速排序升序排列。我们只需对其输出即可。可能习惯于用 C 来编写程序或对 C+不很熟悉的读者会对某些 C+特性感到有所困难。那么,关于 sort,我给出两种建议:要么记住 sort 并不复杂的基本用法;要么使用 C 中快速排序的函数 qsort(可自行学习)。那么假如我们将要对给定的数组进行降序排列该如何操作呢?有一个简单的方法,我们只需简单的将升序排列后的数组倒序,便能得到降序排列的数组。但是这里,我们还要介绍另一种使用 sort 函数的方法来完成降序排列。代码 2.3#include #include using namespace std;bool cmp (int x,int y) /定义排序规则return x y;int main () int n;int buf100;while (scanf (%d,&n) != EOF) for (int i = 0;i n;i +) scanf (%d,&bufi);sort (buf,buf + n,cmp); /使用该重载形式,我们表明将要使用自己定义的排列规则for (int i = 0;i n;i +) printf(%d ,bufi);printf(n);return 0;如该代码所示,我们新定义了一个 cmp 函数,来实现对于新的排序规则的定义。关于 cmp 函数的定义规则我们只需简单的记得,当 cmp 的返回值为 true 时,即表示 cmp 函数的第一个参数将会排在第二个参数之前(即在我们定义的规则中,cmp 表示第一个参数是否比第二个参数大,若是,则排在前面)。为了实现降序排列,我们只要判断两个参数的大小,当第一个参数比第二个参数大时返回true。然后,只需要调用 sort 函数的另一种重载方式:sort(排序起始地址,排序结束地址,比较函数),如代码 2.3 所示,我们便完成了对我们给定的内存区间的降序排列。对于利用 sort 函数为其它基本类型(如 double,char 等)排序的方法也大同小异,读者可以自己动手进行尝试。使用内置函数 sort 的方法,简单易懂,且不易出错,对各种类型的排序写法也大同小异。所以,强烈推荐大家使用 sort 来完成排序。例 2.2 成绩排序 (九度教程第 2 题)时间限制:1 秒内存限制:32 兆 特殊判题:否题目描述:有 N 个学生的数据,将学生数据按成绩高低排序,如果成绩相同则按姓名字符的字母序排序,如果姓名的字母序也相同则按照学生的年龄排序,并输出 N个学生排序后的信息。输入:测试数据有多组,每组输入第一行有一个整数 N(N=1000),接下来的 N行包括 N 个学生的数据。每个学生的数据包括姓名(长度不超过 100 的字符串) 、年龄(整形数)、成绩(小于等于 100 的正数)。输出:将学生信息按成绩进行排序,成绩相同的则按姓名的字母序进行排序。然后输出学生信息,按照如下格式:姓名 年龄 成绩样例输入:3abc 20 99bcd 19 97bed 20 97样例输出:bcd 19 97bed 20 97abc 20 99提示:学生姓名的字母序区分字母的大小写,如 A 要比 a 的字母序靠前(因为 A 的ASC 码比 a 的 ASC 码要小)。来源:2000 年清华大学计算机研究生机试真题如读者所见,在该例中,我们不再对基本类型进行排序,而是对一些由基本类型组成的结构体进行排序。排序的规则在题面中已经明确给出。虽然该例中待排序的个体的规模(N)表明我们可以使用冒泡排序,但这里我们依旧采用内置的 sort 函数来完成解题,从而体会其方便快捷的特点。代码 2.4#include #include #include using namespace std;struct E char name101;int age;int score;buf1000;bool cmp(E a,E b) /实现比较规则if (a.score != b.score) return a.score b.score;/若分数不相同则分数低者在前int tmp = strcmp(a.name,b.name);if (tmp != 0) return tmp 0; /若分数相同则名字字典序小者在前else return a.age b.age; /若名字也相同则年龄小者在前int main () int n;while (scanf (%d,&n) != EOF) for (int i = 0;i n;i +) scanf (%s%d%d,bufi.name,&bufi.age,&bufi.score); / 输入sort(buf,buf + n,cmp); /利用自己定义的规则对数组进行排序for (int i = 0;i n;i +) printf (%s %d %dn,bufi.name,bufi.age,bufi.score); /输出排序后结果return 0;首先,我们定义了用来表示学生个体的结构体,其包含三个数据域:姓名、年龄、成绩,并由输入部分对其分别进行赋值。由于这是我们自己定义的结构体,不属于 C+的内置基本类型,计算机并不知道两个结构体之间的如何比较大小,更不可能自动对其进行排序。于是,我们要做的即是向计算机说明该排序依据。与上一例相同,我们采用定义一个比较函数 cmp 的方法来实现,并在其中对规则的各要素进行详细的说明(如代码 2.4 所示)。一旦建立了 cmp 函数,该函数便可作为计算机对其进行排序的依据,所以我们依然可以使用 sort 函数对其进行快速排序,但是不要忘记,将 sort 的第三个参数设定为 cmp 函数,使 sort 函数知道该应用什么规则对其进行定序。假如,你忽略了第三个参数,那么系统会以不知道排序规则为由(找不到合适的“小于”运算符)而给出编译错误。同样的,与编写 cmp 类似,我们也可以直接定义该结构体的小于运算符来说明排序规则。代码 2.5#include #include #include using namespace std;struct E char name101;int age;int score;bool operator (const E &b) const /利用C+算符重载直接定义小于运算符if (score != b.score) return score b.score;int tmp = strcmp(name,b.name);if (tmp != 0) return tmp 0;else return age b.age;buf1000;int main () int n;while (scanf (%d,&n) != EOF) for (int i = 0;i n;i +) scanf (%s%d%d,bufi.name,&bufi.age,&bufi.score);sort(buf,buf + n);for (int i = 0;i n;i +) printf (%s %d %dn,bufi.name,bufi.age,bufi.score);return 0;由于已经指明了结构体的小于运算符,计算机便知道了该结构体的定序规则(sort 函数只利用小于运算符来定序,小者在前)。于是,我们在调用 sort 时,便不必特别指明排序规则(即不使用第三个参数),只需说明排序起始位置和结束位置即可。虽然,该方法与定义 cmp 函数的方法类似,我个人还是建议使用第二种方法。这样,首先可以对重载算符的写法有一定的了解;其次,在今后使用标准模板库时,该写法也有一定的用处。后文中,将会广泛使用该方法编写例程。若读者坚持使用定义 cmp 函数的写法,只需自行将重载小于运算符中的语句转化为定义 cmp 函数函数体的相关语句即可,并在调用 sort 函数时加上第三个参数 cmp。相信通过本节的学习与练习,读者不会再对排序问题感到恐惧。有了 sort函数,你所需要做的只是按照题面要求为其定义不同的排序规则即可。练习题:特殊排序(九度教程第 3 题);EXCEL 排序(九度教程第 4 题);字符串内排序(九度教程第 5 题);二 日期类问题关于日期运算的各种问题,同样被频繁选入机试考题当中。但是这类问题都有规律可循。只要把握住这类问题的核心,解决这类问题就不会再有太大的难度。例 2.3 日期差值 (九度教程第 6 题)时间限制:1 秒 内存限制:32 兆 特殊判题:否题目描述:有两个日期,求两个日期之间的天数,如果两个日期是连续的我们规定他们之间的天数为两天输入:有多组数据,每组数据有两行,分别表示两个日期,形式为 YYYYMMDD输出:每组数据输出一行,即日期差值样例输入:2011041220110422样例输出:11来源:2009 年上海交通大学计算机研究生机试真题该例题考察了日期类问题中最基本的问题求两个日期间的天数差,即求分别以两个特定日期为界的日期区间的长度。这里值得一提的是,解决这类区间问题有一个统一的思想把原区间问题统一到起点确定的区间问题上去。在该例中,我们不妨把问题统一到特定日期与一个原点时间(如 0000 年 1 月 1 日)的天数差,当要求两个特定的日期之间的天数差时,我们只要将它们与原点日期的天数差相减,便能得到这两个特定日期之间的天数差(必要时加绝对值)。这样做有一个巨大的好处预处理。我们可以在程序真正开始处理输入数据之前,预处理出所有日期与原点日期之间的天数差并保存起来。当数据真正开始输入时,我们只需要用 O(1)的时间复杂度将保存的数据读出,稍加处理便能得到答案。值得一提的是,预处理也是空间换时间的重要手段(保存预处理所得数据所需的内存来换取实时处理所需要的时间消耗)。另外,日期类问题有一个特别需要注意的要点闰年,每逢闰年 2 月将会有 29 天,这对我们计算天数势必会产生重大的影响。这里,我们必须明确闰年的判断规则当年数不能被 100 整除时若其能被 4 整除则为闰年,或者其能被400 整除时也是闰年。用逻辑语言表达出来即为 Year % 100 != 0 & Year %4 = 0 | Year % 400 = 0,当逻辑表达式为 true 时,其为闰年;反之则不是闰年。从中我们也可以看出,闰年并不严格的按照四年一次的规律出现,在某种情况下也可能出现两个相邻闰年相隔八年的情况(如 1896 年与 1904 年)。所以,这里我们推荐严格按照上述表达式来判断某一年是否是闰年,而不采用某一个闰年后第四年又是闰年的规则。代码 2.6#include #define ISYEAP(x) x % 100 != 0 & x % 4 = 0 | x % 400 = 0 ? 1 : 0/ 定义宏判断是否是闰年,方便计算每月天数int dayOfMonth132 = 0,0,31,31,28,29,31,31,30,30,31,31,30,30,31,31,31,31,30,30,31,31,30,30,31,31; /预存每月的天数,注意二月配合宏定义作特殊处理struct Date /日期类,方便日期的推移int Day;int Month;int Year;void nextDay() /计算下一天的日期Day +;if (Day dayOfMonthMonth ISYEAP(Year) ) /若日数超过了当月最大日数Day = 1;Month +; /进入下一月if (Month 12) /月数超过12Month = 1;Year +;/进入下一年;int buf50011332; /保存预处理的天数int Abs(int x) /求绝对值return x 0 ? -x : x;int main () Date tmp;int cnt = 0; /天数计数tmp.Day = 1;tmp.Month = 1;tmp.Year = 0; /初始化日期类对象为0年1月1日while(tmp.Year != 5001) /日期不超过5000年buftmp.Yeartmp.Monthtmp.Day = cnt; /将该日与0年1月1日的天数差保存起来tmp.nextDay(); /计算下一天日期cnt +; /计数器累加,每经过一天计数器即+1,代表与原点日期的间隔又增加一天int d1 , m1 , y1;int d2 , m2 , y2;while (scanf (%4d%2d%2d,&y1,&m1,&d1) != EOF) scanf (%4d%2d%2d,&y2,&m2,&d2); /读入要计算的两个日期printf(%dn,Abs(bufy2m2d2 - bufy1m1d1) + 1); /用预处理的数据计算两日期差值,注意需对其求绝对值return 0;该代码展示了我们处理这类问题的基本方法。首先定义了一个类,不仅可以用来表示日期,还能够自动的计算出下一个日期。我们利用该类,一方面不断计算出当前日期的下一个日期,另一方面累加计数器,计算当前日期与原点日期的天数差,并将其保存在内存中,有了这些预处理出的数据,我们在计算两个特定日期差时,只要将两个日期与原点时间日期差相减,取绝对值,并根据题目需要加上1(“两个日期是连续的我们规定他们之间的天数为两天”),就能得出答案 。我们也可以考虑一下,假如问题需要我们输出某个特定的日期,是那年的第几天,我们该怎样利用我们已经得到的数据来计算呢?我们只需要用该日期与原点日期的天数减去那年元旦与原点日期便可得知该日期是当年的第几天。在日期类中,为了判断该年是否是闰年,我们定义了一个宏。利用上文中提到的逻辑表达式来判断该年是否是闰年,并且根据该表达式的逻辑值使宏表达式为1或0,通过该宏表达式选择保存每月天数的数组的不同列来取得该月应该具有的天数,从而保证了当闰年时,2月29日存在。另外,这段代码还有三个值得我们注意的地方。其一,在保存某个特定日期与原点日期的天数差时,我们使用了三维数组,用年、月、日分别表示该数组下标,这便将日期本身与其存储地址联系了起来 ,这样我们在存取时不必再为查找其所在的存储地址而大费周章,而只需要直接利用它的年月日数字即可找到我们保存的值。将数据本身与数据存储地址联系起来,这是 Hash 的基本思想,也是 Hash 的一种基本方式。只不过这里的 Hash 不会产生冲突,我们并不需要为解决其冲突而另外下功夫。这种思想会在后文再着重讲解。其二,该例程的输入采用了某种技巧。因为题面规定用一个连续的八位数来代替日期,我们使用%4d 来读取该八位数的前四位并赋值给代表年的变量,同理使用%2d%2d 来读取其它后四位并两两赋值给月日。这种利用在%d 之间插入数字来读取特定位数的数字的技巧,也值得大家去利用,在本例中,它为处理输入带来了极大的便利。其三,我们将 buf50011332这个相对比较耗费内存的数组定义成全局变量,这不是偶然的。由于需要耗费大量的内存,若在 main 函数(其它函数也一样)之中定义该数组,其函数所可以使用的栈空间将不足以提供如此庞大的内存 ,出现栈溢出,导致程序异常终止。所以,今后凡是涉及此类需要开辟大量内存空间的情况,我们都必须在函数体外定义,即定义为全局变量。或者在函数中使用malloc 等函数动态申请变量空间。读者必须牢记这一点。可能部分读者已经发现,此例本身也带有一些缺陷,它并没有明确的告知我们输入数据的数据范围。我们可以大致认为其年是从0年到5000年(事实上也在这个范围内),那么我们才可以估算该题的复杂度。关于这题的复杂度计算,我就不在赘述了,读者可以自己动手,计算其是否符合要求。在接触了日期类最基本问题以后,其他问题也可以在其基础上得到答案。例 2.4 Day of week(九度教程第 7 题)时间限制:1 秒 内存限制:32 兆 特殊判题:否题目描述:We now use the Gregorian style of dating in Russia. The leap years are yearswith number divisible by 4 but not divisible by 100, or divisible by 400.For example, years 2004, 2180 and 2400 are leap. Years 2004, 2181 and 2300 are notleap.Your task is to write a program which will compute the day of weekcorresponding to a given date in the nearest past or in the future using todaysagreement about dating.输入:There is one single line contains the day number d, month name M and yearnumber y(1000y3000). The month name is the corresponding English namestarting from the capital letter.输出:Output a single line with the English name of the day of week corresponding tothe date, starting from the capital letter. All other letters must be in lower case.样例输入:9 October 200114 October 2001样例输出:TuesdaySunday来源:2008 年上海交通大学计算机研究生机试真题该题是一例以英文命题的机试真题。其大意为,输入一个日期,要求输出该日期为星期几。我们照样可以利用上例的思路来解答该题。星期几是以七为周期循环的,那么我们只需要知道:1.今天是星期几;2.今天和所给定的那天相隔几天。利用其对7求余数,我们便可以轻易的知道所给定的那天是星期几了。代码 2.7#include #include #define ISYEAP(x) x % 100 != 0 & x % 4 = 0 | x % 400 = 0 ? 1 : 0int dayOfMonth132 = 0,0,31,31,28,29,31,31,30,30,31,31,30
展开阅读全文