PAT-Basic Level Practice 小结

最近在做PAT (Basic Level) Practise。我是在《构建之法》上了解到PAT(Programming Ability Test)的。花了好长时间才做完Basic Level的练习。Basic Level是什么水平呢?看一下官网的考试大纲:

乙级(Basic Level)

考生应具备以下基本能力:
1.基本的C/C++的代码设计能力,以及相关开发环境的基本调试技巧;
2.理解并掌握最基本的数据存储结构,即:数组、链表;
3.理解并熟练编程实现与基本数据结构相关的基础算法,包括递归、排序、查找等;
4.能够分析算法的时间复杂度、空间复杂度和算法稳定性;
5.具备问题抽象和建模的初步能力,并能够用所学方法解决实际问题。

 


虽然和Basic Level还有一段距离,有几道练习题我觉得挺有意思的,下面做一下小结。

 

https://www.patest.cn/contests/pat-b-practise/1008

1008. 数组元素循环右移问题 (20) 。参见《编程珠玑》2.1节问题B,2.3节介绍了三种算法:「杂技」、「块交换」、「求逆」,习题2.6.3 、2.6.4。(块交换算法自己没看)

求逆算法可以用下图来记忆,我觉得比书上翻转手掌的例子更好。

 

 

https://www.patest.cn/contests/pat-b-practise/1015

1015. 德才论 (25)

7月9日第一次做这道题的时候,我还没使用过堆排序。我采用的是基于双向链表的插入排序,双向链表表头挂在「类-总分-德分」三维数组,以缩短链表长度,二维和三维数组的长度由输入的L和H确定,每一维都设有一个IsEmpty标志。

运行结果如下:

我本来以为这是不错的解法了,但是我看到其他人使用了不到80ms的时间。于是,我继续琢磨是否可以通过增加维度(偶数分数/奇数分数插入不同链表,输出时再合并)、缩小第三维数组的长度(根据实际输入的最小最大分数而不是可能的最小最大分数来确定左边界和右边界)。测试结果显示没用。

接下来的一周,我开始用上堆排序,见识了它的高效。7月15日,我想用堆排序再试一下这道题。测试结果如下图最后两行所示,两次相同的提交的结果都没有预期的块。第二行是我最初提交的代码。

随后,我想起7月11日做的28题https://www.patest.cn/contests/pat-b-practise/1028 1028. 人口普查(20),这道题刚好可以将年月日YYYY/MM/DD按每位数值占用半个字节的方式保存在一个整型变量中,用整型比较来代替依次比较年、月、日。于是我想试着类别、总分、德分、才分(不参与比较)、准考证号(取反)保存到一个long long类型变量,如下图。测试结果不错。

对最初解法念念不忘的我,又试了一下将堆排序和最初的解法结合起来:使用4个数组保存4个类别的考生得分。我不想分配4倍于输入的内存空间,又不想调用realloc()(一是麻烦;二是我猜可能有性能损失~FIXME)。最后我分配了2倍的空间,如下,class1和class3 item升址填入,class2和class4 item降址填入。

然而,测试结果不如我所愿。

关于堆排序和zero-based/one-based数组。

我用的堆排序代码来自严蔚敏《数据结构》。书上的代码是基于one-based数组的,下标从1开始。我在改为zero-based数组时,把建堆的下标值初始化为(n-1)/2,直到7月18日才发现应该是n/2-1,奇怪的是居然通过了测试。可能check point的 n都是偶数:)

对于自己动态分配的连续空间,可以多分配一个单元的空间,如下,这样就可以将&a[-1]传给使用one-based数组的函数,而其他使用zero-based数组的函数仍然认为a[0]才是第一个元素。a[-1]能派上用场的另一个例子是作为插入排序的sentinel/哨兵,减少for循环的条件判断次数。

 

我没有使用C库的qsort()函数,因为感觉调用函数进行比较的方式开销会比较大。FIXME

 
 

https://www.patest.cn/contests/pat-b-practise/1020

1020. 月饼 (25)。浮点数和0比较。

 

 

https://www.patest.cn/contests/pat-b-practise/1028

1028. 人口普查(20)。对生日的比较,上面1015题已经提到过。姓名长度不超过5个字符,可以用强制类型转换转为整型进行赋值,替代拷贝函数,可减少函数调用的开销。

7月19日我在https://github.com/OliverLew/PAT/blob/master/PATBasic上看到另一种解法是直接进行strcmp()。

 

 

https://www.patest.cn/contests/pat-b-practise/1030

1030. 完美数列(25)。使用二分查找确定一个完美子数列的右边界。麻烦的一点是,这里要查找的不是目标值的位置,而是最后一个不大于目标值的位置。比如在数列{3,3,3,5}中查找<=3或<=4的最右元素的位置。

我本来想优化的一点是尽快退出循环:如果相邻的值相同,则继续往右移动,不重复计算完美数列。我好像做过测试,没用,可能check point没有包含这种情况。

调试时我有一个错误的想法:如果n - maxLen < maxLen或者n -1 - rightMostIndex < maxLen,就可以退出循环了。错误的原因是下一个元素向右覆盖的完美数列的开始部分和当前元素向右覆盖的完美数列重叠,同时前者还可能向右继续覆盖,这样就有可能大于maxLen。可以提前退出循环的情况应该是:可能覆盖的最大长度(n-i)小于已有的maxLen。未做测试。

我还试了一下调用sort程序来排序,如下。然而测试发现子进程读不到第二行的第一个数字。可能和内核缓冲有关?FIXME

 

 

https://www.patest.cn/contests/pat-b-practise/1034

1034. 有理数四则运算(20)

第一次了解到求最大公约数(Greatest Common Divisor)的Binary GCD算法。

https://en.wikipedia.org/wiki/Binary_GCD_algorithm

 

 

https://www.patest.cn/contests/pat-b-practise/1035

1035. 插入与归并(25)

可以使用memcmp来比较中间序列和输入序列。

 

 

https://www.patest.cn/contests/pat-b-practise/1036

1036. 跟奥巴马一起编程(15)

可使用printf()的format参数的filed width来打印多个空格和多个0。

  

 

https://www.patest.cn/contests/pat-b-practise/1041

1041. 考试座位号(15)

这道题目的输入格式是:“准考证号 试机座位号 考试座位号”,要求以「试机座位号」为索引输出其余两项。「准考证号」是14位数字,可以保存为long long型变量。

假设「准考证号」不是纯数字,则应该保存为字符串。如果「试机座位号」是每行的第一项,那么可以先读「试机座位号」,再将其余两项读到「试机座位号」索引的项。遗憾的是它是第二项,这意味着需要在读入后将「准考证号」和「考试座位号」复制到「试机座位号」索引的项。如果将「准考证号」保存在单独的数组中,并在「试机座位号」索引项中填入对应的索引,则可以避免「准考证号」的复制操作。

 

 

https://www.patest.cn/contests/pat-b-practise/1045

1045. 快速排序(25)

注意这道题有一个可以简化代码的条件:输入的数值互不相同。

我试了几种解法,先放结果。

基本思路是从左到右遍历序列,判断当前元素是否大于左边最大元素lMax,是否小于右边最小元素rMin。关键是如何判断是否小于rMin。我的解法中都使用了一个数组A来保存原始数组a各元素的索引,然后按a[A[i]]升序对A[i]排序,如下:

我想到的第一种解法(最后一行):

1.使用「双向链表」L来保存各元素的A中的相对先后顺序

2.对于每个大于lMax的元素a[i]:将a[i]在L中对应的元素L[j]移出,如果L[j]前驱为空,说明它是剩余元素中的最小值,即它是主元,否则不是;若a[i]>lMax,则将lMax更新为a[i]。

3.为了快速找到a[i]对应的L[j],我又使用了一个反向索引数组r,L[j]=L [r[i]],如上图所示。

第二种解法(倒数第二行)从左到右同时遍历一次a[]和A[]。首先a[i]是主元须有a[i] >lMax且i==A[i](每次partition迭代后主元就位)。然后我们希望a[i]<rMin,我们不需要知道rMin,而是将这个条件转为:小于a[i]的元素都落在a[i]左边。这可以通过遍历A[]时,跟踪已遍历A[i]中的最大值lessValMaxIdx来得知:如果lessValMaxIdx<i,即说明小于a[A[i]]=a[i]的元素都在a[i]的左边。因此,确定主元的条件是:a[i] >lMax且i==A[i]且lessValMaxIdx<i。和上一种解法相比,这种解法代码少,内存开销少。

7月23日更新begin

看了其他人的解法,我发现上面所说的三个条件存在冗余。我们只需要判断两个条件:a[i]>lMax且i=A[i]。不需要i>lessValMaxIdx这个条件,是因为如果满足前面两个条件,则一定有i>lessValMaxIdx,否则就意味着小于a[i]的元素个数大于i(zero-based),也就意味着与i=A[i],即有i个元素小于a[i]矛盾。

反过来,我们也可以用i<lessValMaxIdx和i=A[i]这两个条件找出主元。

7月23日更新end

倒数第三行是在第二种解法的基础上,将输入序列复制到A[]上,看看减少解索引操作、增加元素交换开销能否优化表现。结果显示不能。

第一行使用快速排序,加入a[l]、a[(l+h)/2]、a[h]三者取中的处理,运行超时。

可以复用已被遍历的a[]空间来保存将要打印的主元。

 

https://www.patest.cn/contests/pat-b-practise/1049

1049. 数列的片段和(20)

推导每个元素被覆盖的「片段」数;利用左右对称性。

 

https://www.patest.cn/contests/pat-b-practise/1050

1050. 螺旋矩阵(25)

我的解法需要特殊处理最里层只有一个元素的情况。FIXME

 

https://www.patest.cn/contests/pat-b-practise/1051

1051. 复数乘法 (15)

这道题的计算结果可能打印出来是-0.00,我的处理是先sprintf()到缓冲区,如出现“-0.0.0”,则改为”0.0.0”(实部)或”+0.00”(虚部)。FIXME

 

https://www.patest.cn/contests/pat-b-practise/1054

1054. 求平均值 (20)

这道题没有说明「1.」、「.1」这种格式是否合法。我使用strtod()函数进行转换。开始我的判断条件中含有isnormal(),结果一直出错。仔细看手册才知道isnormal(0.0)=FALSE。

 

https://www.patest.cn/contests/pat-b-practise/1055

1055. 集体照 (25)

使用堆排序,避免字符串拷贝。每一排先生成打印顺序索引数组。

 

https://www.patest.cn/contests/pat-b-practise/1058

1058. 选择题(20)

可以对每道题的答案进行字符串比较。

 

https://www.patest.cn/contests/pat-b-practise/1060

1060. 爱丁顿数(25)

这道题像是在根据两个单调函数的图像求交叉点坐标。

 

https://www.patest.cn/contests/pat-b-practise/1062

1062. 最简分数(20)

这道题有一个check point是N1/M1 > N2/M2。我是通过在我认为不可能进入的case添加*(int *)0 = 0;得到段错误知道的……

 

https://www.patest.cn/contests/pat-b-practise/1063

1063. 计算谱半径(20)

不必在循环中求谱半径……

 

https://www.patest.cn/contests/pat-b-practise/1065

1065. 单身狗(25)

关键是判断出席的非单身狗中是否携伴侣一同出席。

对于couple ID名单,我使用以双方ID为索引的数组a[]来保存对方的ID+1,则对于单身狗,对应索引项的值为0。

遍历出席ID名单时,将每个ID存入q[],如果a[id]!=0(非单身狗):如果a[a[id] - 1] > 0(尚未遍历到TA的另一半),则将a[id]取负,作为可能单人出席的标志;否则,表明另一半已经出现在名单里,将a[a[id] - 1]取负。

ID名单遍历结束后,遍历q[]:如果a[q[i]]>0,说明couple双双出席,不输出,并执行q[i] = q[--qlen],重复检查q[i]。

最后对q[]排序输出。

 

https://www.patest.cn/contests/pat-b-practise/1066

1066. 图像过滤(15)

一边scanf(),一边printf(),不用缓存整个输入。

 

https://www.patest.cn/contests/pat-b-practise/1067

1067. 试密码(20)

需要处理带有空格的行输入。scanf(“%[^ ]s”)好像不好处理行结尾的换行符,FIXME。

使用fgets(),将结尾的换行符strip掉。

 

https://www.patest.cn/contests/pat-b-practise/1068

1068. 万绿丛中一点红(20)

矩阵之外的像素点的颜色值相当于0,为了避免对位于边缘的像素点进行特殊的处理,一般在矩阵缓冲区中多分配空白的margin区域。

开始我分配一个数组M[]存入整个像素矩阵,分配一个数组a[]保存备选的像素点坐标。这样在对a[i]排序时,需要重复计算坐标在M[]中对应的偏移量。应该直接在a[]存入偏移量。

另一种方案是将颜色值保存到a[]。那M[]是否还有必要分配?因为题目只要求与相邻8个点比较,所以实际上只需要分配3行带margin的循环缓冲区。每次读入row[i]后,从上一行r[(i-1+3)%3]中选择满足条件的像素点。读入最后一行后,将下一行row[(i+1)%3]填0,再从最后一行选择像素点。

 

开始我被这个条件误导了:「色差超过TOL的点才被考虑」。错了N遍之后我才注意到另一个条件:「有独一无二颜色的那个像素点」。也就是说,如果忽略了第二个条件,那么可能将并非颜色独一无二的像素点选入备选的目标像素点。

因为像素的颜色值范围比较大(2^24),使用两个位图来标记颜色是否独一无二。

 

https://www.patest.cn/contests/pat-b-practise/1069

1069. 微博转发抽奖(20)

关键是有效地判断是否重复转发。最大转发人数是1000,我的处理是使用1024bit/256byte的位图来标记是否重复转发,位图索引为转发ID(不超过20字符)的哈希值:将转发ID强制转换为3个long long型变量,每个long long型变量经过移位和异或操作取低10位,再异或这三个新的值作为哈希值。如果未置位,即抽中;否则需要遍历已中奖名单,同上面1028题类似,可以用强制转换为long long型变量进行比较,来避免字符串比较。

 

https://www.patest.cn/contests/pat-b-practise/1070

1070. 结绳(25)

我的理解是可以串成的最长绳子显然是用最长的两段绳子串起来的。可是给的样例中输入有2个15,输出的最大长度却是14,是因为「每段绳子每次串连后,原来两段绳子的长度就会减半」,「长度结果向下取整」?诡异的是,直接取最长的两段绳子除2,能通过几个check point。

总之,我自己最后没有做出来,但是通过50多次故意加入各种错误的提交,我硬是根据最大两个长度的奇偶性加上判断某位是否为1通过了check point 1~4,试出check point 0输入的最大三个长度是115、115、13……

我在排行榜上别人签名放的github看到了正确解法后才明白这道题的意思:

https://github.com/OliverLew/PAT/blob/master/PATBasic

他的解法使用double型变量,按照升序遍历执行result=(result + len[i])/2。

7月27日更新 begin

其实不需要使用double型,使用int型就可以。

7月27日更新 end

FIXME考虑输入「所有整数都不超过104」,输出「取为不超过最大长度的最近整数」。可以将double型替换为整型,但是将每个数左移x位,这x位用于保留原来浮点运算的小数位。并将升序遍历改为降序遍历,最终结果为∑c[i],0<=i<=n-1

c[i] = b[i]/2^(i+1)=b[i]>>(i+1) for 0<=i<=n-2

c[n-1] = b[n-1]/2^(n-1)=b[n-1]>>(n-1)

b[i] = a[i]<<x。

当c[i]==0时,就可以退出循环,忽略后面的元素。那么x最小可以取多少呢?

假设被舍弃的位均为1,这些数的和为S=1/2^L + 2/2^(L-1) + … + L/2 = ∑d[i],d[i]=i/2^(L+1-i),1<=i<=L,L = CeilRound (lg(max+1)/lg2),S = (L-1)+1/2^L,则S能直接影响的高位位数为HL = CeilRound(lg((L-1)+1)/lg2) = CeilRound(lg(L)/lg2) = CeilRound(lg(CeilRound (lg(max+1)/lg2))/lg2) =

= CeilRound(lg(CeilRound (lg(10001)/lg2))/lg2) = CeilRound(lg14/lg2)= 4。

x的取值需要保证舍弃这HL位数对最终结果没有影响,则x应该大于HL。测试发现x取5会有误差,暂时取x=8。FIXME。

既然我们可以中途退出循环,那么我们不需要对整个序列排序,因此我们可以将累加c[i]嵌入升序堆排序过程,当c[i]==0时,就退出排序过程。测试结果不错(提交了两次相同代码):



 

 

格式控制

一行输出多个元素,以空格分隔,行首行尾不出现空格。

1.以整型为例,先按照格式“%d”打印第一个元素,再在1~n-1循环中打印” %d”。

2.或者在0~n-1循环中判断是否为行首元素。

3.使用一个char *变量指向格式控制字符串,传给printf(),初始值指向“%d”,在循环中,每次调用printf()之后,都指向” %d”。

按照M行N列的格式输出。(https://www.patest.cn/contests/pat-b-practise/1013 1013. 数素数 (20))

打印每个元素时:前导字符=行首?(首行?无:回车):空格。

 

最后纪念一下排名

posted @ 2017-07-20 16:22  AlbumCover  阅读(380)  评论(0编辑  收藏  举报