【编程之美】2.20程序理解和时间分析
转自:http://blog.csdn.net/zhuhuiby/article/details/6742980
题目如下:
阅读以下C#代码,回答问题:
using System; using System.Collections.Generic; using System.Text; namespace FindTheNumber { class Program { static void Main(string[] args) { int [] rg = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17, 18,19,20,21,22,23,24,25,26,27,28,29,30,31}; for(Int64 i = 1; i < Int64.MaxValue; i++) { int hit = 0; int hit1 = -1; int hit2 = -1; for (int j = 0; (j < rg.Length) && (hit <= 2); j++) { if((i % rg[j]) != 0) { hit++; if(hit == 1) { hit1 = j; } else if (hit == 2) { hit2 = j; } else break; } } if((hit == 2) && (hit1 + 1 == hit2)) { Console.WriteLine("found {0}", i); } } } } }
1> 这个程序要找的是符合什么条件的数?
2> 这样的数存在么?符合这一条件的最小的数是什么?
3>
在电脑上运行这一程序,你估计要多长时间才能输出第一个结果?时间精确到分钟(电脑配置:单核CPU2.0GHz,内存和硬盘资源充足)
----------------------------------------------------
我自己做的时候太想当然了,觉得是求 2-29的最小公倍数 没有考虑到 30可以分解为2*3*5
---------------------------------------------------------------------------
我的解答:
- 1> 第一个问题不难,只要认真分析程序,就能看出来程序求的是这样的数,这个数不能被 rg[k]和rg[k+1]整除(0 <= k < n-1),同时能被其余所有数(即rg[0],…,rg[k-1]和r[k+2],…,r[n-1])整除。
- 2> 该问的入手点是寻找rg[k]和rg[k+1]。
首先,rg[k]肯定大于15。若rg[k]<=15的话,那么rg[k]*2也在rg数组中,并且不能被 i 整除,所以这样的 i 肯定找不到。
其次,rg[k]和rg[k+1]不能由其余的rg数组中的数组合相乘而得,比如18,可以由2乘上9得到,所以若 i 能整除 2 和 9, 则必能整除18. 由此,我们可以得到:
16=2*8, 18 = 2*9, 20 = 4*5, 22 = 2*11, 24 = 3*8, 26 = 2*13, 28 = 4*7, 30 = 2*15。
这样乍一看,似乎没有满足条件的rg[k]和rg[k+1],但是我们注意在上述一串等式中,16=2*8,其中的2是8的因子,所以只要 i 能整除8,就必能整除2,因此没有必要要求 i 能整除 2*8。 而其余的等式中,两个乘数没有因子关系,所以i 若能整除两个乘数,则肯定能整除其乘积。
由此,我们得到了唯一满足条件的rg[k]和rg[k+1],即16,17。
这样,剩下的问题就是求不能整除16,17,却能整除其余所有数的整数中最小的那一个。我们先把2到31中的素数都列出来(17除外):{2,3,5,7,11,13,19,23,29,31}。而2到31中(16,17除外)的数都是由这些素数作为因子组合相乘得到的,其中,要得到8,至少要3个2,要得到27至少要3个3,要得到25,至少要2个5,其余的素因子都只需一个就够了。
因此,这个最小的数就是 2^3, 3^3, 5^2, 7, 11, 13, 19, 23, 29, 31的乘积,答案为:2123581660200。(因为题目要求不借助电脑,所以我手算了两次,竟然都算错了,最后只好拿计算器算) 。
- 3> 要估算时间,我们先确定一个原子操作(或者说原子过程更合适),这里我们取内层for循环里的整个if语句块,该段程序主要包括一个取模操作和一个判断,如果进入if语句的话,还包括1次加法操作,1~2次判断和一次赋值操作。
我们知道加法、判断等操作基本都在几个时钟周期内就可以完成,而除法操作却需要数十个时钟周期,而取模操作也是通过除法操作得到的(还记得汇编语言里,执行除法操作之后,一个寄存器里存结果,另一个寄存器里存余数),另外,对64位整数的除法明显要慢于32位整数,综合这些因素,我们可以假设该原子操作需要100个时钟周期。因此2GHz的CPU在1秒内能跑2*10^9 / 100 = 2*10^7 即2000万次原子操作,做过ACM的同学就会有一个直观概念,这和我们通常做时限为1S的题时估算的计算次数差不多。
接下来估算原子操作执行的次数:外层循环跑了2123581660200次,内层循环取决于 i 的情况,当i为奇数的时候,内层最多跑5次即可结束,因为2,4,6都不能整除奇数;当i为偶数的时候,情况要复杂一些,但是也可以一个一个的详细分析。这里我们粗略估计,就算内层循环平均可以跑10次,外层循环少跑一些,去掉零头,总的原子操作执行了2*10^13次。
所以需要 2*10^13 / (2*10^7) = 10^6秒约为277个小时。
JOJ的2042题目也是一个程序理解题目,这个题目非常有意思,给出了下面一段C++源代码,要求计算出最后的输出结果,源代码如下:
#include<cstdio> int main(void) { int x = 987654321, c = 0, d = 1, e = 6; while(x--){ c += d, d += e, e += 6; } printf("%d/n", c); return 0; }
原题目如下:
We can use axioms to calculate programs just like what we do in algebras. Dijkstra is the one who advocates such constructive approach whereby a program is designed together with its correctness proof. In short, one has to start from a given postcondition Q and then look for a program that establishes Q from the precondition. Often, analyzing Q provides interesting hints to finding the program. This approach is quite different from the well known Hoare Logic.
For example, the following program is calculated by Dijkstra's approach. Unfortunately, its annotation is lost so that its function is hard to grasp. You are to help with finding the final value of the variable c. Note that the program is designed under 128-bit architecture.
代码就是上面那一段。
这个题目通过小数据计算可以看出规律:x=1, c = 1; x=2, c=8; x=3, c=27; x=4, c=64,于是可以猜测这段程序是用来计算x^3的。用计算器计算出987654321^3,提交上去就AC了。
这个题目是超级大牛SIYEE出的。从题目本身的叙述中就学到了很多东西。又知道了一个数的立方还可以这样计算。可惜数学功底差,不知道在数学上是如何推导出来的。
-----------------------------------------------------------------------------------------------
我自己把公式给推出来了:
初始条件:
容易得出:
进而得出:
最后得出: