阶乘的强悍溢出技能

【题目描述】

输入n,计算S=1!+2!+3!+…+n!的末6位(不含前导0)。,n!表示

前n个正整数之积。

【样例输入】

10

【样例输出】

37913

一、阶乘溢出性能体验

按正常逻辑,本题可以分三步:

①求1-n的阶乘。

②把每项阶乘加起来。像这样一项一项地把各个式子加到一起,就是累加器。

③取和的末6位。

但是别忘了还有一个条件:

一看到这么大的数,又是阶乘,免不了心里一沉,直觉冥冥中指引恐怕long long型也罩不住。

先拿求n的阶乘探探底。

#include<stdio.h>
int main(){
    for(int n=1; n<=1e6; n++){
        long long factorial = 1;
        for(int i=1; i<=n; i++){
            factorial *= i;
        }
        printf("%d %lld\n", n, factorial);
    }
    return 0;
}

结果输出一堆0,肯定是鸟鸣山一样,完鸟。

把1e6改成100,仍然是一堆0,看来早就超限了。

没错,21!的已经输出负数,阶乘的溢出能力实在是太强悍了。

也就是说,用long long型只能计算到20!。如果用int型呢,只能计算到12!。

这可咋办?

二、大数取余神药

据说有一味神药:要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在每步计算之后对n取余,结果不变。

也就是说:

t=(a*b+c-d)%n中t的值可以分步取余求出:

t1=a*b/%n;
t2=(t1+c)%n;
t=(t2-c)%n;

是何道理呢?其实很简单,因为任何整数除以n的余数都是这个数减去n的倍数后余下的数。

如果x%n=y,它的意思是x=kn+y(x、y、k为整数,y<x)。

显然,所有含n的项都能被n整除,不会产生余数,所以可将其去除,变为:

它们分别是a、b、c、d除以n的余数,所以有:

三、代码实现

在神药的帮助下,本题可改为如下两步:

①求1-n的阶乘。用for循环实现累乘,每执行一次乘法都对1e6取余。

②把每项阶乘加起来。用for循环实现累加,每加一次都对1e6取余。

代码如下:

#include<stdio.h>
int main(){
    const int MOD = 1e6;
    int n, S = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        int factorial = 1;
        for(int j = 1; j <= i; j++)
            factorial = factorial * j % MOD;
        S = (S + factorial) % MOD;
    }
    printf("%d\n", S);
    return 0;
}

将1e6定义为MOD常量可以改善程序的可读性,也便于修改(代码中有两次用到这个数,使用MOD表示的情况下,如需修改只需改一次MOD的值)。

另一点需要注意的是,两行取余代码千万不要写成下面这种牛皮plus形式:

factorial *= j % MOD;
S += factorial % MOD;

因为*=、+=这种赋值运算符和=一样,优先级是非常低的,而%的优先极和*、/是一样的,所以会先计算取余,再进行乘赋值、加赋值。

比如下面的代码:

#include<stdio.h>
int main(){
    int i=10;
    i *=50+2;
    printf("%d\n", i);
    return 0;
}

它的结果是520,而不502。

三、代码优化

前面给出的求阶乘之和的代码可以进一步优化,将两层循环减少为一层循环。

如果读到这里,请别急着往下读,先自己思考一下方法。

原理很简单:n!可表示成前n个正整数之积,也可以表示成(n-1)!*n,所以只要保留上一次循环求得的阶乘,就可以通过累乘的方式求得n的阶乘。这时候就要把factorial这个变量放到循环体之外定义。

代码如下:

#include<stdio.h>
int main(){
    const int MOD = 1e6;
    int n, factorial=1, S = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        factorial = factorial * i % MOD;
        S = (S + factorial) % MOD;
    }
    printf("%d\n", S);
    return 0;
}

四、耗时测试

阶乘,最大的阶乘,阶乘之和。这样的豪华的阵容吞噬时间的能力必然很是强悍吧!

如果想了解代码的运行时间,可以用计时函数clock()。它定义在< time.h>头文件中,该函数返回从程序启动到调用 clock() 时的 CPU 时间。从这个描述咱们要明白:

①统计时间从启动程序开始。这意味着,输入数据的时间也是计算在内的。

②返回时间与clock()代码放置位置有关。

③返回的不是秒数。

针对上述特点,如果咱们想获取程序的纯运行时间(不计用户输入时间),可以采用如下对策:

(1) 借助命令行工具刨除数据输入时间。

①同时按下Win+R键,快速打开运行窗口。

②在运行窗口中输入“cmd”并按回车,打开命令行窗口。

③直接在光标处输入编译后的可执行文件所在盘符,如D:。

④输入cd folder1\folder2(表示D:\folder1\folder2,指程序执行文件所在路径,可以通过复制粘贴的方式输入)

⑤确认进入程序所在路径,然后输入:echo 52|u。操作系统会自动把52输入,其中u是程序名。如果要输入多条数据,可以写成echo 52 92|u。

(2) 计时函数clock( )放在程序结束之前。这样就能返回整个程序的执行时间。

(3) 返回秒数:用clock( )/CLOCKS_PER_SEC可以得到秒数。可以想象有个法号叫CPU的和尚在敲钟,clock( )能告诉你它敲了多少下,CLOCKS_PER_SEC指的是敲钟的速度(每秒敲多少下),二者相除就得到敲了多少秒。

代码如下:

#include<stdio.h>
#include<time.h>
int main(){
    const int MOD = 1e6;
    int n, S = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        int factorial = 1;
        for(int j = 1; j <= i; j++)
        factorial = (factorial * j % MOD);
        S = (S + factorial) % MOD;
    }
    printf("%d\n", S);
    printf("Time used = %.2f\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

将优化前后的代码分别测试结果如下:

n

18

19

20

21

22

23

24

25

26

答案

348313

180313

820313

260313

940313

580313

940313

940313

940313

n

1600

3200

6400

12800

25600

51200

102400

204800

409600

答案

940313

940313

940313

940313

940313

940313

940313

940313

940313

优化前

时间

0.03

0.05

0.12

0.38

1.44

5.74

23.28

93.43

381.62

优化后

时间

0.03

0.03

0.03

0.03

0.03

0.03

0.03

0.03

0.03

从表中数据可得出:

(1) 优化后的一层循环都是瞬间运行完毕,而优化前的两层循环随着n的变大,运行时间越来越长,呈指数上升。

(2) 程序的运行时间大致和n的平方成正比(因为n每翻一番,运行时间近似翻两番)。据此可估计n=1e6时,程序大致需要37分钟才能执行完。

这涉及到一个估算时间复杂度的技巧:很多程序的运行时间与规模n存在着近似的简单关系,这种关系可以通过计时函数来估测。比如上表通过对比每次将n翻倍的运行时间来验证这一关系。

(3) 从24开始,答案始终不变。这是因为25!末尾有6个0,所以从第5项开始,后面的所有项都不会影响和的末6位数字。所以,对于优化前的代码,只需要在程序的最前面加一条语句“if(n>25)n=25;”,你就不用等几十分钟才能看到n=1e6时的结果了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金创想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值