在 C语言初学者代码中的常见错误与瑕疵(7) 中,我给出的重构代码中存在BUG。这个BUG是在飞鸟_Asuka网友指出“是不是时间复杂度比较大”,并说他“第一眼看到我就想把它当成一个数学问题来做”之后,我又重新对问题进行了数学式的思考后发现的。
  这个BUG源于在(1<=A,B<=1000)条件下对矩形个数的数量级心里没数。当时觉得这个题目的目的是考察穷举,由于题目限定了A、B的范围,所以结果应该不是很大。事实证明这种想法是一厢情愿。
  通常情况下,我不喜欢用数学方法解决C语言编程问题。因为很多问题,一旦在数学上能够轻易解决,编写代码往往是索然无趣的,对学习和练习C语言几乎没有什么益处。譬如,求1+2+3+……+100,如果不知道或假装不知道数学解法,代码可能是这样的

#include <stdio.h>

int main( void )
{
int i , sum = ; for ( i = ; i <= ; i ++ )
{
sum += i ;
}
printf("%d\n",sum); return ;
}

  而若使用数学方法,代码则是

#include <stdio.h>

int main( void )
{
printf("%d\n", ( + ) * / ); return ;
}

  前者,C语言和编程成分的含量很高,数学含量却很低;而后者数学含量很高,可C语言和编程成分的含量几乎为0。
  从解决实际问题的角度来说,显然应该用后一种方法;而从学习C语言和练习编程的角度来说,则应该使用前一种方法。所以一个好的编程问题,应该是没有数学解的,至少应该没有显而易见、容易得到的数学解。否则,只是用C语言对公式做一个简单的翻译(这大概是FORTRAN语言的哲学),编程的味道就全没有了,谭浩强的很多题目就是如此(参见:滥用变量综合症)。“矩形的个数”问题应该说还不是那么容易得到数学解的。
  所以,在飞鸟_Asuka 网友说他“第一眼看到我就想把它当成一个数学问题来做”之后,我也尝试着用数学的方式思考了一下。我发现,在同一条直线上的n个不同的点一共可以构成(n-1)n/2条不同的线段,A*B的矩形相邻两边各有A+1和B+1个不同的点,因而可以分别构成(A+1)A/2和(B+1)B/2条不同的线段,这样构成的矩形的个数一共就是(A+1)A/2×(B+1)B/2个。当A和B取最大值1000时,结果显然不小于25×1010,而这个值显然大于231-1,甚至也大于232-1(多数编译器中 unsigned 类型所能表示的最大整数)。这样,原来的重构代码中用int类型作为结果的类型,显然错了。
  我一向认为C语言程序员应该心中有“数”,即对表达式中的数据和结果有最起码的范围估计。没想到,这次由于刻意回避数学解法,却立刻遭到了违背信条的报应。正应验了Muphry's law所言:Anything that can go wrong, will go wrong。
  冯诺依曼也认为程序员至少应该清楚计算过程中数据的数量级,为此他反对浮点数。与之相反,约翰.巴科斯则盲目乐观地发明了浮点数,很多程序员尽情地享受浮点数的方便,却由于盲目乐观屡屡被浮点数这种“有缺陷的抽象”所伤。更有甚至,很多程序员连浮点数最基本的原理都不懂,竟然能写出k=sqrt(n) 这样狗屁不通的句子。(参见:似是而非的k=sqrt(n)
  回过头来再谈我代码的BUG。这个BUG的另一个教训是没有进行比较充分的测试,如果测试一下边界情况可能不难发现这个BUG。后来我又重新测试了一下,发现程序运行时间比较长,这说明飞鸟_Asuka 网友指出“是不是时间复杂度比较大”的问题也是存在的。但当时为了算法叙述的方便,就没有按照下面的方法写count()函数:

int count( int A , int B )
{
int x1 , y1 ;//第一个点的坐标
int x2 , y2 ;//第二个点的坐标
int num = ; for ( x1 = ; x1 < B ; x1 ++ )// for ( x1 = 0 ; x1 <= B ; x1 ++ )
for ( y1 = ; y1 < A ; y1 ++ )// for ( y1 = 0 ; y1 <= A ; y1 ++ )//穷举第一个点的各种可能
for ( x2 = x1 + ; x2 <= B ; x2 ++ )// for ( x2 = 0 ; x2 <= B ; x2 ++ )
for ( y2 = y1 + ; y2 <= A ; y2 ++ )// for ( y2 = 0 ; y2 <= A ; y2 ++ )//穷举第二个点的各种可能
num ++ ; // {
// if ( x1 < x2 && y1 < y2 )
// num ++ ;
// }
return num ;
}

  如果写成这种形式,不难发现第二层循环与第三层是可以对调的,对调后为:

   for ( x1 =  ; x1 < B ; x1 ++ )
for ( x2 = x1 + ; x2 <= B ; x2 ++ )
for ( y1 = ; y1 < A ; y1 ++ )
for ( y2 = y1 + ; y2 <= A ; y2 ++ )
num ++ ;

  这时应该能够看出,最内两层循环次数为 A + A-1 + A-2 +……+1,最外两层循环的循环次数为B + B-1 + B-2 +……+1,因而结果可以直接得到,即(A+1)A/2*(B+1)B/2。

  算法问题解决了,又产生了新的问题,那就是如何表示这么大的整数,这是一个数据结构的问题。(或许,这才是题目的本意?)
  办法之一就是使用表示范围更大的整数类型,例如C99中的long long int类型。
  如果编译器不支持C99也没有表示范围更大的整数类型,那就只有自己着手构造新的数据结构了。
  矩形个数的最大值大约为25×1010,这并不是一个很大的数,一个int不够,那就用两个好了。这里把存储大数的数据结构设计为一数组,

typedef int BIG_NUM[] ;

  数组的第0个元素存储低6位,第1个元素存储高位。存储低6位的原因是避免乘法运算时溢出(乘数不超过1001,与一个6十进制整数相乘不超过109,在int类型的表示范围之内)。

  通过调用

BIG_NUM num ;
count( num , A , B );

将结果写到num中。

count()函数的实现:

void count( BIG_NUM m , int A , int B )
{
m[] = ;
m[] = ;
mul_sum( m , A );//将1+2+……+A的结果乘入m
mul_sum( m , B );//将1+2+……+B的结果乘入m
}

  由于结果是累乘得到的,所以初始化为1。为防止溢出,只能

void mul_sum( BIG_NUM m , int n )
{
int t1 = n % == ? n / : n ,
t2 = n % != ? (n + ) / : n + ;
mul( m , t1 );
mul( m , t2 );
}

  小心翼翼地一个个地乘(每个乘数都不得超过1001)。t1,t2这两个变量是为了回避BIG_NUM类型的除法运算。

void mul( BIG_NUM m , int n )
{
m[] *= n ;
m[] *= n ;
m[] += m[]/;//进位
m[] %= ;
}

  每乘以一个数,立刻就处理进位问题。
  最后还要考虑如何输出:

void output( BIG_NUM m )
{
if ( m[] == )
{
printf( "%d\n" , m[] );
return ;
} printf( "%d" , m[] );
printf( "%06d\n" , m[] ); }

  这里的"%06"是为了保证低位不够6位时仍能正确输出。

  完整的代码如下:

/*
矩形的个数
在一个3*2的矩形中,可以找到6个1*1的矩形,4个2*1的矩形3个1*2的矩形,
2个2*2的矩形,2个3*1的矩形和1个3*2的矩形,总共18个矩形。
给出A,B,计算可以从中找到多少个矩形。 输入:
本题有多组输入数据(<10000),你必须处理到EOF为止
输入2个整数A,B(1<=A,B<=1000) 输出:
输出找到的矩形数。 样例: 输入:
1 2
3 2 输出:
3
18 作者:薛非
出处:http://www.cnblogs.com/pmer/ “C语言初学者代码中的常见错误与瑕疵”系列博文 */ #include <stdio.h>
typedef int BIG_NUM[] ; void count( BIG_NUM , int , int );
void mul_sum( BIG_NUM , int );
void mul( BIG_NUM , int );
void output( BIG_NUM ); #define POW 1000000
#define WID 6 int main( void )
{
int A , B ; while ( printf( "输入2个整数A,B(1<=A,B<=1000)\n" ),
scanf( "%d%d" , &A , &B )!= EOF
)
{
BIG_NUM num ;
count( num , A , B );
output( num );
}   return ;
} void output( BIG_NUM m )
{
if ( m[] == )
{
printf( "%d\n" , m[] );
return ;
} printf( "%d" , m[] );
printf( "%0*d\n" , WID , m[] ); } void mul( BIG_NUM m , int n )
{
m[] *= n ;
m[] *= n ;
m[] += m[]/POW;//进位
m[] %= POW;
} void count( BIG_NUM m , int A , int B )
{
m[] = ;
m[] = ;
mul_sum( m , A );//将1+2+……+A的结果乘入m
mul_sum( m , B );//将1+2+……+B的结果乘入m
} void mul_sum( BIG_NUM m , int n )
{
int t1 = n % == ? n / : n ,
t2 = n % != ? (n + ) / : n + ;
mul( m , t1 );
mul( m , t2 );
}

  写的有点丑。

要心中有“数”——C语言初学者代码中的常见错误与瑕疵(8)的更多相关文章

  1. 一个超复杂的间接递归——C语言初学者代码中的常见错误与瑕疵(6)

    问题: 问题出处见 C语言初学者代码中的常见错误与瑕疵(5) . 在该文的最后,曾提到完成的代码还有进一步改进的余地.本文完成了这个改进.所以本文讨论的并不是初学者代码中的常见错误与瑕疵,而是对我自己 ...

  2. C语言初学者代码中的常见错误与瑕疵(5)

    问题: 素数 在世博园某信息通信馆中,游客可利用手机等终端参与互动小游戏,与虚拟人物Kr. Kong 进行猜数比赛. 当屏幕出现一个整数X时,若你能比Kr. Kong更快的发出最接近它的素数答案,你将 ...

  3. C语言初学者代码中的常见错误与瑕疵(9)

    题目 字母的个数 现在给你一个由小写字母组成字符串,要你找出字符串中出现次数最多的字母,如果出现次数最多字母有多个那么输出最小的那个. 输入:第一行输入一个正整数T(0<T<25) 随后T ...

  4. C语言初学者代码中的常见错误与瑕疵(7)

    问题: 矩形的个数 在一个3*2的矩形中,可以找到6个1*1的矩形,4个2*1的矩形3个1*2的矩形,2个2*2的矩形,2个3*1的矩形和1个3*2的矩形,总共18个矩形.给出A,B,计算可以从中找到 ...

  5. C语言初学者代码中的常见错误与瑕疵(23)

    见:C语言初学者代码中的常见错误与瑕疵(23)

  6. C语言初学者代码中的常见错误与瑕疵(19)

    见:C语言初学者代码中的常见错误与瑕疵(19)

  7. C语言初学者代码中的常见错误与瑕疵(14)

    见:C语言初学者代码中的常见错误与瑕疵(14) 相关链接:http://www.anycodex.com/blog/?p=87

  8. 分数的加减法——C语言初学者代码中的常见错误与瑕疵(12)

    前文链接:分数的加减法——C语言初学者代码中的常见错误与瑕疵(11) 重构 题目的修正 我抛弃了原题中“其中a, b, c, d是一个0-9的整数”这样的前提条件,因为这种限制毫无必要.只假设a, b ...

  9. C语言初学者代码中的常见错误与瑕疵(1)

    曾在豆瓣上看到过一个小朋友贴出他自己的代码(http://www.douban.com/group/topic/40293109/),当时随口指点了几句.难得这位小朋友虚心修正.从善如流,不断地改,又 ...

随机推荐

  1. 解决redhat的未注册问题

    昨天安装第五步的时候:开始是没有网,,,居然ping不通  网  ,服务器也ping不通,,,,,可能和我前几天删除了网络适配器有关,,把linux桥接对应的适配器给删了,,, 解决办法是打开虚拟网络 ...

  2. Spring AOP基于配置文件的面向方法的切面

    Spring AOP基于配置文件的面向方法的切面 Spring AOP根据执行的时间点可以分为around.before和after几种方式. around为方法前后均执行 before为方法前执行 ...

  3. 一个 Android 任务队列的实现

    最近在做Android项目时遇到这样一个问题:客户端向服务器请求数据,而在网络信号太差的情况下,数据迟迟不到,甚至丢失.服务器为了解决这个问题做了频繁的重发,android 客户端就会收到很多不想要的 ...

  4. Android笔记(一):从this关键字发散

    this指的是直接包含它的类的实例. 例如: public class MyClass{ int num; public MyClass(int num){ this.num = num; } } 这 ...

  5. RIAidea – Focus on Flash/Flex/AIR » About Me

    RIAidea – Focus on Flash/Flex/AIR » About Me Browse > Home > About Me Tuesday, May 20, 2014 | ...

  6. js中slice(),splice(),split(),substring(),substr()的使用方法和区别

    1.slice(): Array和String对象都有 在Array中  slice(i,[j]) i为开始截取的索引值,负数代表从末尾算起的索引值,-1为倒数第一个元素j为结束的索引值,缺省时则获取 ...

  7. android 屏幕尺寸的理解

    对android设备屏幕尺寸单位的理解 一.android移动设备(手机和平板)常用的关于屏幕的一些单位: 1.px:像素点,应该是一个统一的单位,与我们国际单位米(M)应该是一回事,它应该是屏幕尺寸 ...

  8. CSS学习笔记:Media Queries

    CSS3提供了Media Queries(媒体查询)的概念,可以利用它查询以下数据: 1.浏览器窗口的宽和高: 2.设备的宽和高: 3.设备的手持方向,横向/竖向: 4.分辨率. @media规则的语 ...

  9. 【linux】线上服务器要关注哪些参数

    服务器(nginx/apache): 1.吞吐率. 2.并发连接数. 3.qps. 4.并发连接数详细数据统计,包括读取请求.持久连接.发送响应内容.关闭连接.等待连接. 5.连接线程池利用率. 关系 ...

  10. 第十一周(11.24-12.01)----WBS功能分解

    功能 子功能 二级子功能 预计花费时间(小时) 游戏基础功能 显示首界面 绘制产产品主logo及不同难度下的布局 4   游戏 难度选择(初级.中级.高级) 4     退出整个程序 1     放弃 ...