2014寒假专题训练题解
分治
分治法是自己一直以来不太熟悉也不重视的算法,所以这次的题目做得也不好。
第一题kth,求一个无序序列从小到大第k个元素。马上想到的思路就是先排序然后直接输出,因为C++STL里的sort的速度很快,所以没有担心太多就实现了这个方法。很简单粗暴但是弊端也很明显:有大量无用功。然后经同学提醒想到了第二种算法:在快排的过程中直接找到第k大。因为快排有一个特性就是一趟排序后作为关键词的元素一定处在它排序后也就是正确的位置上。所以如果关键词元素就是要找的第k个元素,剩下的递归过程就不用继续了。而且就算当前没有找到,也可以根据k和当前位置的关系判断有哪一半是需要继续排序的,哪一半是可以忽略的。提交后的成绩好像是90,有一个点没过,算法是正确的,据说是测评机问题……但是如果直接用排序+定位输出也可以过90分,如果这是在NOIP赛场上,选择后一种是不是更吃亏呢?毕竟实现第二种方法所用时间是实现前一种方法(直接调用STL的函数)的好几倍,而实际效益却相差不是很大。不过NOIP应该不会出这么弱又这么坑的题吧……
第二题roots,求一元三次方程的根(保留4位小数)。因为“二分法求方程的根”已经由数学老师在数学课上比较详细地讲解过,所以第一个想到的就是这个方法。(后来同学说如果结果保留的小数位少一些比如只保留两位小数时可以乘以100之后枚举,不过在这一题当然是行不通了。)但是实现起来却是困难重重,最后只是看样例过了就直接做下一题,明知有问题也不愿多试几组数据,所以测评的时候只拿到了极低的分数。后来改的时候也遇到了很多麻烦,主要问题是答案输出(fixed和setprecision)、四舍五入(要先把得到的小数乘以10000,加上0.5后取整再除以10000)、判断一个double型数据是否等于0(印象中double型数据似乎不能直接和0比,但是实际上可以,应该是记错了或者自己顾虑太多)。最后确定下来的方法大概就是在主函数里从-100枚举到100,以相邻两个数为区间端点进行二分求根,如果左端点的值与右端点的值异号则说明有一个根在该区间内,取区间中点计算函数值。若中点函数值与左端点函数值异号则在根左半区间,否则在右半区间,然后继续递归,直到区间的端点已经足够精确就可以输出答案(至少应5位,为了保险起见我递归了19次即保留了19位小数,但是后来发现其实 double 的精度都不足 19 位)。还有对于特殊情况的处理,比如端点的函数值恰好为0,因为我是-100到100都调用求根函数所以有可能对于一个恰好为方程解的整数端点输出两次,为此我设置了标记数组以判断一个整数解是否已经输出过(似乎有更好的方法就是在求根函数中对于区间的右端点函数值是0的情况不进行判断,等到主函数中下一次循环时候的左端点自然就是目前的右端点,这样的话还要考虑对100这个值再手动判断一次)。最后花了不少时间勉强算是过了,自己感觉写得很不干脆,处理方法都有点拖泥带水,不太满意。
第三题reverse,求一个无序序列中逆序对(序号严格递增数值却严格递减的一对数)的个数。没有思路,直接搜索,即对于每一个序列中的元素扫描它后面的元素,如果有比它小的元素则结果+1。结果是40分,应该说搜索能骗到这么多分也不错了(而且这个搜索似乎没有优化的余地)。因为题目是在分治法的专题里,所以有往分治这方面想,但是想到的方法基本上复杂度和搜索差不多,所以也就放弃了。标程用的方法是与归并排序相结合的方法(果然是分治专题,分治的两大经典例子归并排序和快速排序都考到了……)。归并排序的特点是合并两个子序列时两个子序列已经是有序的,而且左子序列的元素序号都小于右子序列,那么如果左子序列中有一个元素A大于右子序列的一个元素B,则左子序列中A后面的元素必然都大于右子序列的B元素,则可以计算得出关于元素B的部分逆序对个数(即B前面的元素与B构成的逆序对,还有一部分是B与其后的元素构成的逆序对)。写起来并不复杂但是因为从来没有写过归并排序所以也就带着抗拒情绪拖了几天,终于在训练的最后一天写完了。写完自己编了几组数据,但是不知为何还是错了,后来发现自己忘了把改过的代码放到评测机收取代码的文件夹里了……血淋淋的教训。
第四题mason,麦森数,题目描述没什么用,求2^p-1的位数与后500位的值。赤裸裸的高精度嘛。一开始想的确实是只要保存后500位的值就够了,但是不知道怎么求2^p-1的位数,于是最后还是决定把2^p-1全部算出来。时间复杂度可想而知。而且由于对高精度乘法的不熟悉,写高精度乘法用了不少时间。再经过同学提醒后解决了两个问题:快速平方(自己好像还是第一次听说这个算法,即2p = (2p/2)2)和最后答案-1的问题(还以为要处理借位的问题,其实2^p的末位根本不可能是0)。60分。讲解之后明白了不用求出完整的2^p,只要保存最后500位,至于位数,其实可以直接根据p算出来的(w=p*lg2),做题时同学这样告诉我,我还不太相信……看来以后要多注意运用数学知识对算法进行优化。
做完这些题发现自己确实在分治法方面还太欠缺了。
动规 day1
动态规划,我一直以来对其抱着敬畏之心的一种算法(策略),终于在毫无思想准备的情况下与我碰撞了……其实早在做NOIP2002过河卒的时候就接触到动规了(那时还是学长教我的记忆化搜索),再到后来看书做掉数字三角形,再到暑假里做LIS、01背包问题,其实自己不知不觉已经做了一些动规的题了,但是毕竟没有正规地学习过,做到这些题目时自然就暴露了自己的真实水平了。
第一题pie,给定一些馅饼从天上掉落的时间与位置,人1秒内可以移动1个单位距离,求能接到的馅饼的最大数目。通过这道题我充分明白了自己的弱点:只会写递归,不会写递推。递归的缺点很明显,如果有记忆化搜索的话(做的时候没有意识到要用记忆化)时间倒是没问题,但是当数据大的时候可能会导致系统栈溢出,而且如果状态定义得不够好,就会导致记忆化搜索需要的内存空间太大(似乎递推也需要注意这个问题)。做这题的时候没有清晰的做动态规划题的顺序,所以也就没怎么注重状态转移方程的书写,完全是凭借感觉写下了递归求解的程序。因为人在某个位置时面临3种选择:在原地待到下一秒、移动到左边的位置与移动到右边的位置。所以很快就可以写出递归的函数,加上边界的处理就可以了。写完之后信心满满地开始做下一题了(因为这种方法看上去确实很正确)。测评结果出来当然很让人失望:10分。其他点几乎都超时。讲解的时候发现标程的解法很巧妙:每个馅饼都映射到平面直角坐标系的一个点,掉落的时间为纵坐标,位置为横坐标,这样就转化成了与基本的数字三角形问题非常相近的问题(只不过原来只能走左下或右下,现在还可以向下走)。这样,每走一步,就移动到了下一层,即时间变化1。妙!
状态转移方程: s[i,j] = v[i,j]+max{s[i-1,j-1], s[i,j-1], s[i+1,j-1]} (s[i,j]表示在时间为j时站在i位置能得到的最大馅饼数 v[i,j]表示j时间在i位置掉下的馅饼数)
第二题charm,01背包问题。这个不用多讲,自己在暑假里也已经做过了不少类似的简单题,不过……自己还是忘了怎么用递推写,于是继续递归写,而且又没有用记忆化搜索,结果当然是又大部分超时。用递推写再加上用滚动数组后AC。万恶的递归……
经典的01背包状态转移方程: s[i,w] = max{s[i-1,w], s[i-1,w-c[i]]+v[i]} (s[i,w]表示把i件物品放入容量为w的背包里能得到的最大价值 c[i]表示第i件物品的费用 v[i]表示第i件物品的价值)
第三题separate,把一个大数字n分成x部分,使得这x部分乘积最大。想了很久,我自己的想法是可以当做01背包问题来做:设这个数字的长度为l,则一共有l-1个位置可以插入分隔线来分割这个数字,所以问题转化为在这l-1条分隔线中取x-1条使得各部分乘积最大。赤裸裸的背包问题啊。但是实现起来又比较麻烦。思路也许正确但是似乎效果不佳,初次评测0分,第二次也是0分……一直拖了两天才AC。标程的方法不太一样,状态转移方程见下。有个效果不错的优化就是预先把数字n从第a位到第b位组成的数字算出来存到数组里,但问题是实现这个优化的方法……出于习惯我用stringstream,把数字字符串的子串截取出来输出到stringstream里然后再读出到一个long long的整数里。但是我太高估了字符串流的效率……后来之所以一直有4个点超时也是因为字符串流效率低下。改成“乘十相加”之后快了不少。
状态转移方程: s[i,j] = max{s[k,j-1]*a[k+1,i] | 1<k<i} (s[i,j]表示将前i位分为j个部分能得到的最大乘积,a[i,j]表示n从第i位到第j位组成的数字)
第四题lcs,最长公共子序列。这道题刚在MIT的公开课上看过,印象很深,所以很快就写出来了,但是……只过了4个点。其余点全部栈溢出。百思不得其解。一般遇到栈溢出的情况都是因为递归次数过多,但是这题的代码里我是用递推的,完全没有写任何函数,不知道哪来的栈溢出一说?后来明白了,原来我把数组定义在了main函数里,当数据很大时数组也很大,自然就内存溢出了。吃一堑长一智。
状态转移方程: c[i,j] = c[i-1,j-1]+1 (a[i]=b[j])
c[i,j] = max{c[i-1,j], c[i,j-1]} (a[i]!=b[j])
(a,b表示两个序列 c[i,j]表示a[1..i]与b[1..j]的LCS长度)
动态规划果然是神奇的算法。
动规 day2
第一题buses,求行驶一定里程所需最小的车费,可以任意换车。
状态转移方程: m[i] = min{m[j]+m[i-j], f[i] | j<i} (m[i]表示行驶i公里所需最小费用 f[i]表示乘车i公里的车费)
第二题dividing,把一堆价值不同的硬币分给两个人使得他们得到的总钱数之差最小。自己的思路,对于每一个硬币都有两种选择:把它分给A,或者分给B。(A、B代指两人)递归很好写,也很容易超时,记忆化搜索又开不起那么大的数组(这种递归方法相当于状态有三个参数:第i个硬币,A分到的钱数a和B分到的钱数b)。递推一时想不到。结果当然又是超时,而且只拿到10分。标程的方法很巧妙:把硬币总数除以2作为背包容量,把硬币的价值作为花费,使得价值尽量最大(同时背包也是尽量装满的)。强大。
状态转移方程不再赘述。
第三题elephant,求重量严格递增,智商严格递减的大象序列。思路很明确,先排序,然后求最长下降子序列。
状态转移方程: f[i] = max{f[j]+1 | j<i且a[j]>f[i]} (f[i]表示前i个元素的最长下降子序列长度,a即序列)
第四题run,求在n个城市之间来回飞k趟所需最小花费。题目很复杂,很容易滋生抗拒心理。经老师提醒想到最短路模型……但是复杂得多。最后自己用动规写,发现其实和图论算法差不多。但是因为数据结构没有设计好(用了一个包含一维数组与二维数组的结构体数组),导致后面查错很麻烦,纠结了很久改了很多细节的地方才AC。
状态转移方程: f[i,j] = min{f[i-1,k] + m[k, j, i-1] | k<j} (f[i,j]表示已经飞了第i趟航班,目前在j城市已花的最小费用,m[i,j,t]表示从城市i到城市j的第t趟航班的机票价钱)
注意套用熟悉的模型。