回《笔试常见的“阶乘”编程题,你写对了么?》

原帖链接:http://www.cnblogs.com/kym/archive/2009/10/05/1578224.html    
    我机器上没有C#的开发环境,所以没法测试作者这个代码的耗时,不过10000的阶乘在5秒内完成,不知道作者的代码是否能达到?我想起前段时间在HDU做的一道ACM题,题目的时限要求是1秒内能计算10000的阶乘(当然这是代码跑在它的服务器的时间)。

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1042

      如果按照飞林沙文章中那种常规的思路,逐位相乘,再对齐相加,缺点很明显,如果n值比较大,运算次数将非常多,必定会超时,1万的阶乘想在1秒内完成肯定无法达成。
   
我的思路是把数据分组,每组上限为9999,最多可容纳2万组,每组4位整数,则可以容纳8万位整数(当然,组数可以随你要计算的n的大小进行调整),利用组与组的错位相乘再相加,可以避免楼主这样逐位进行运算。

代码如下:

#include<iostream>
#include
<stdio.h>
#include
<string>
#include
<iomanip>
#include
<algorithm>
using namespace std;
#include 
"time.h"

const int MAX_GROUPS = 20000;//最多2万组,每组最多4位整数,即最多可容纳8万位整数
const int MAXN = 9999;//每组的上限值
const int GROUP_LEN = 4;//每组的最大长度

class BigInteger
{
private:
    
int data[MAX_GROUPS];
    
int len;
    
void init()
    {
        memset(data,
0,sizeof(data));
    }
public:
    BigInteger()
    {
        init();
        len 
= 0;
    }
    BigInteger(
const int b);
    BigInteger(
const BigInteger &);

    
bool operator > (const BigInteger&)const;
    BigInteger 
& operator=(const BigInteger &);
    BigInteger 
& add(const BigInteger &);
    BigInteger 
& sub(const BigInteger &);
    BigInteger 
operator+(const BigInteger &const;
    BigInteger 
operator-(const BigInteger &const;
    BigInteger 
operator*(const BigInteger &const;
    BigInteger 
operator/(const int &const;
    
void print();
};
BigInteger::BigInteger(
const int num)
{
    
int res,tmp = num;
    len 
= 0;
    init();
    
while(tmp > MAXN)
    {
        res 
= tmp - tmp / (MAXN + 1* (MAXN + 1);
        tmp 
= tmp / (MAXN + 1);
        data[len
++= res;
    }
    data[len
++= tmp;
}
BigInteger::BigInteger(
const BigInteger & rhs) : len(rhs.len)
{
    
int i;
    init();
    
for(i = 0 ; i < len ; i++)
    {
        data[i] 
= rhs.data[i];
    }
}
bool BigInteger::operator > (const BigInteger &rhs)const
{
    
int ln;
    
if(len > rhs.len)
    {
        
return true;
    }
    
else if(len < rhs.len)
    {
        
return false;
    }
    
else if(len == rhs.len)
    {
        ln 
= len - 1;
        
while(data[ln] == rhs.data[ln] && ln >= 0
        {
            ln
--;
        }
        
if(ln >= 0 && data[ln] > rhs.data[ln]) 
        {
            
return true;
        }
        
else 
        {
            
return false;
        }
    }

}

BigInteger 
& BigInteger::operator = (const BigInteger &rhs)
{
    init();
    len 
= rhs.len;
    
for(int i = 0 ; i < len ; i++)
    {
        data[i] 
= rhs.data[i];
    }
    
return *this;
}
BigInteger
& BigInteger::add(const BigInteger &rhs)
{
    
int i,nLen;

    nLen 
= rhs.len > len ? rhs.len : len;
    
for(i = 0 ; i < nLen ; i++)
    {
        data[i] 
= data[i] + rhs.data[i];
        
if(data[i] > MAXN)
        {
            data[i 
+ 1]++;
            data[i] 
= data[i] - MAXN - 1;
        }
    }
    
if(data[nLen] != 0
    {
        len 
= nLen + 1;
    }
    
else 
    {
        len 
= nLen;
    }

    
return *this;
}
BigInteger 
& BigInteger::sub(const BigInteger &rhs)
{
    
int i,j,nLen;
    
if (len > rhs.len)
    {
        
for(i = 0 ; i < nLen ; i++)
        {
            
if(data[i] < rhs.data[i])
            {
                j 
= i + 1;
                
while(data[j] == 0) j++;
                data[j]
--;
                
--j;
                
while(j > i)
                {
                    data[j] 
+= MAXN;
                    
--j;
                }
                data[i] 
= data[i] + MAXN + 1 - rhs.data[i];
            }
            
else 
            {
                data[i] 
-= rhs.data[i];
            }
        }
        len 
= nLen;
        
while(data[len - 1== 0 && len > 1
        {
            
--len;    
        }
    }
    
else if (len == rhs.len)
    {
        
for(i = 0 ; i < len ; i++)
        {
            data[i] 
-= rhs.data[i];
        }
        
while(data[len - 1== 0 && len > 1
        {
            
--len;    
        }
    }
    
return *this;
}
BigInteger BigInteger::
operator+(const BigInteger & n) const 
{
    BigInteger a 
= *this;
    a.add(n);
    
return a;
}
BigInteger BigInteger::
operator-(const BigInteger & T) const
{
    BigInteger b 
= *this;
    b.sub(T);
    
return b;
}
BigInteger BigInteger::
operator * (const BigInteger &rhs) const
{
    BigInteger result;
    
int i,j,up;
    
int temp,temp1;

    
for(i = 0; i < len; i++)
    {
        up 
= 0;
        
for(j = 0; j < rhs.len; j++)
        {
            temp 
= data[i] * rhs.data[j] + result.data[i + j] + up;
            
if(temp > MAXN)
            {
                temp1 
= temp - temp / (MAXN + 1* (MAXN + 1);
                up 
= temp / (MAXN + 1);
                result.data[i 
+ j] = temp1;
            }
            
else 
            {
                up 
= 0;
                result.data[i 
+ j] = temp;
            }
        }
        
if(up != 0)
        {
            result.data[i 
+ j] = up;
        }
    }
    result.len 
= i + j;
    
while(result.data[result.len - 1== 0 && result.len > 1) result.len--;
    
return result;
}
BigInteger BigInteger::
operator/(const int & b) const
{
    BigInteger ret;
    
int i,down = 0;

    
for(i = len - 1 ; i >= 0 ; i--)
    {
        ret.data[i] 
= (data[i] + down * (MAXN + 1)) / b;
        down 
= data[i] + down * (MAXN + 1- ret.data[i] * b;
    }
    ret.len 
= len;
    
while(ret.data[ret.len - 1== 0) ret.len--;
    
return ret;
}
void BigInteger::print()
{
    
int i;

    cout 
<< data[len - 1];
    
for(i = len - 2 ; i >= 0 ; i--)
    {
        cout.width(GROUP_LEN);
        cout.fill(
'0');
        cout 
<< data[i];
    }
    cout 
<< endl;
}
int main()
{  
    clock_t   start,   finish;   
    
double     duration;   
    
int i,n;
    BigInteger result,num;
    scanf(
"%d",&n);
    start   
=   clock();    
    result 
= BigInteger(1);
    
for(i = 2;i <= n; ++i)
    {
        num 
= BigInteger(i);
        result 
= result * num;
    }
    result.print();
    finish   
=   clock();   
    duration   
=   (double)(finish   -   start)   /   CLOCKS_PER_SEC;  
    printf(   
"%f   seconds\n",   duration   );   
    
return 0;
}

下面给出测试结果,

我的机器配置:酷睿双核2.0G,内存3G,
计算10000的阶乘,我的机器用时3
.312秒,在HDU的服务器上,10000的阶乘用时不到900毫秒。
计算12345的阶乘,我的机器用时5.078秒。
计算20000的阶乘,我的机器用时13.656秒。

C/C++确实是会快些的,HDU的这道题目对Java程序的时限是5秒内完成1万的阶乘,但c/c++的时限是1秒,由此可以看出。

大家也可以试试在HDU的服务器上提交下你的代码,看看能否在1秒内通过。

此外,还有2个常见的N!题目,就是N!的尾数0的个数和N!的非零末尾数,有兴趣的同学可以自己看看。

作者:洞庭散人

出处:http://phinecos.cnblogs.com/    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
2
0
(请您对文章做出评价)
« 上一篇:《Core Java 2》读书笔记(二)
» 下一篇:ExtJS中TreePanel的使用

posted on 2009-10-06 11:51 Phinecos(洞庭散人) 阅读(2310) 评论(12)  编辑 收藏 所属分类: ACM

评论

#1楼 2009-10-06 13:25 OwnWaterloo      

终于看到个像样点的了。

说白了就是一个多精度与单精度的乘法嘛……

C/C++语言中, unsigned xxx int 是按2进制存储。
设hp内部表示中,每个单元的类型是uintN_t, 那么它能表示的范围是:
[0,2^n-1](10进制)
若只作加法,只要保证每个单元的范围在[0,2^(n-1)-1], 就不会溢出。
若只作乘法,只要保证每个单元的返回在[0,2^[n/2]-1], 就不会溢出。

举例:
uint8_t作为内部的单元类型 —— 即每个位的类型。
若只需要作加法, 则每个位可以保存的数的范围是[0,127]。
若需要作乘法,则每个位可保存的数的范围是[0,15]。
对uint32_t来说, 这2个范围分别是:
[0,2147483647] 和 [0, 65535]
(都是10进制)

这是内部表示中, 位的类型与位的理论上限的关系。
说它是理论上限,因为它并不总是要取到这个上限。

-------- -------- -------- --------

而实际上,只有high precision的内部表示是不够的,high precision必须能够输入和输出,才是有意义的。
这就涉及到内部表示的进制的问题。

内部表示的进制: 在内部表示中, 当选取一个在理论范围之内的上限N之后,就可以认为内部表示的是一个N+1进制数

如果内部表示的每一个位, 不能之间转换输入输出进制的若干个位
或者内部表示的若干个位, 不能转换为输入输出进制的一个位
输入输出就会会很麻烦。
想想10进制与2、8、16进制的转换, 再对比2、8、16进制之间的转换。

所以, 内部的进制通常会取一个满足上面关系之一的最大值。


比如, uint8_t 用于10进制时,加法取[0,99] —— 内部是100进制。
100进制的每1位,恰好是10进制的2位。
而uint32_t 用于10进制乘法时,取[0,9999] —— 内部是10000进制。
10000进制的每1位, 恰好是10进制的4位。

这是9999与4的由来

-------- -------- -------- --------

原文中用long(C#中是确定的64位??)来表示十进制的一个digit,简直…… 内存真够不值钱的……

并且,在32位机中, 32位运算肯定比64位(由2个32bit拼装)与8位(需要填充)要快。
原文不慢才怪……
  回复  引用  查看    

#2楼[楼主] 2009-10-06 13:30 Phinecos(洞庭散人)      

@OwnWaterloo
我认为原文中使用long并不是耗时的主要原因,当然long的运算比之int也许会耗时一些,但也算不上主要因素吧,而是由于采取逐位相乘,再对齐相加这个思路,当n很大的情况下,运算次数将非常多,所以这个做法过于费时,而我这里的做法是把逐位运算变成逐组运算,计算次数就减少了许多。
  回复  引用  查看    

#3楼 2009-10-06 13:40 OwnWaterloo      

原文我就不去回复了, 免得陷入语言争论 ……

说真的, 如果在C#中选取正确的表示方式,我也相信两者不会有数量级的差异。
1. 比如将long改掉(又慢又费内存)

2. 比如输出时不用整体用StringBuilder转为字符串
而是直接1个10000进制位转换为4个10进制位, 立即输出。
可以避免第2次无端的内存浪费。

将一个32bit当作10000进制的一个位,而不是65535进制的一个位,就是为了能够使用O(1)的buffer完成输入输出工作。
不使用这个优势太可惜了。

但C#的使用者, 有多少能想到这些层面?
动不动就一个List<long>, StringBuilder, 然后一切交给clr打理。


这些改掉之后, 依然会慢:
C/C++可不会对uint32_t* 作任何越界检查, 也不会作溢出检查, 也……

所以嘛, C#语言的保姆功能太多, 怎么可能快得过不需要这些功能的C/C++?
  回复  引用  查看    

#4楼 2009-10-06 13:50 OwnWaterloo      

@Phinecos(洞庭散人)
嗯, 因为他的表示法中, long只表达一个10进制位。
而实际上,(如果long是64位), 完全可以表达一个1000000000进制的位。
内存占用, 运算次数,都是8倍。

而且32位上作32位乘法,只用1个指令, 作64位乘法,就是3个。
  回复  引用  查看    

#5楼 2009-10-06 13:53 OwnWaterloo      

btw: 楼主为什么不采用 *= 而是 * ?

result = result * num;

和下面相比

result *= num;

明显下面更容易被优化, 减少内存复制工作。
  回复  引用  查看    

#6楼[楼主] 2009-10-06 13:56 Phinecos(洞庭散人)      

@OwnWaterloo
确实,原文中使用long是说不过去,一般大家都用的char型数组来表示大数
  回复  引用  查看    

#7楼 2009-10-06 14:01 OwnWaterloo      

如果还要继续优化, 就只能用快速傅里叶变换了。
浮点精度我不怎么懂, 不知道对这个问题浮点的精度是否足够-_-
  回复  引用  查看    

#8楼 2009-10-06 14:48 my d[未注册用户]

@OwnWaterloo
比如输出时不用整体用StringBuilder转为字符串

操作IO的成本不低啊大哥,一次操作IO会慢N多,不到万不得已,还是放在内存里比较划算,buffer就是一种用来解决这种问题的机制……
  回复  引用    

#9楼 2009-10-06 15:01 OwnWaterloo      

@my d
大哥,想什么呢。
fprint就一定会调用底层IO操作?
fprint就没有缓冲?
setbuf是干什么的?

Console.Write有没有缓存我不了解。
但C#中也有带缓冲的Stream, 只是我不知道语法是怎样的而已。

为什么要一层又一层的, 自以为是的去缓冲一下?

这是其一。



其二:
让内部表示的进制与输出的进制满足一个关系,就不用整体缓冲。
并不代表就一定不采用整体缓冲的方式
只是说这种表示法可以使用O(1)的空间将内部表示转换成目的进制。

至于输出的destination:
如果是ostream_iterator, fprintf, 那就是直接输出到stream中。
如果是back_inserter(s), 那就是整体缓存到string中。至于要不要再输出到stream,还是显示在什么地方,再说。

这是一种灵活性
1. 你可以整体缓存。
2. 也可以使用O(1)的缓存空间, 输出到别的地方。
而原文中的方式, 就没有提供这种灵活性, 而是绑定到策略1上

那个GetResult函数, 做了2件而非1件事: 计算内部表示,整体转换到String。
这就违反了SRP : 一旦不需要整体缓存, GetResult函数就不能被复用了。
  回复  引用  查看    

#10楼 2009-10-06 15:14 OwnWaterloo      

@my d
园子里的兄弟动辄喜欢谈"架构"。
却在这么小的一个地方上都没有"整体考量"的"洞察力"……
少笑死人了!

  回复  引用  查看    

#11楼 2009-10-06 17:45 JimLiu      

高精也就除法麻烦点……
不过乘法和除法要做快了不容易,方法很多
  回复  引用  查看    

#12楼 2009-10-08 19:39 silverPerson      

弱弱的问一句什么是HDU的服务器?我怎样向它提交我的代码?难道是传说中的云计算?   回复  引用  查看    

导航

统计

公告

Add to Google
订阅到抓虾
鲜果阅读器订阅图标
订阅到有道阅读
用QQ邮箱阅读空间订阅我的博客。
姓名:邓以克
网名:phinecos(洞庭散人)
位置:杭州乐港科技
MSN: phinecos@msn.com

搜索

 

随笔分类(722)

随笔档案(588)

相册

常去的站点

我的好友

我的站点

积分与排名

最新评论

阅读排行榜

评论排行榜

60天内阅读排行