[NOIP模拟]Factorial Surplus Tail

来源:互联网 发布:剑三秀姐捏脸数据 编辑:程序博客网 时间:2024/05/16 17:51

题目描述:
FST 作为 OIer ,经常会遇到和阶乘有关的问题,但是一个数的阶乘末尾总是会有很多 0 ,FST 认为这很不美观,但是 FST 觉得如果 0 的个数是偶数的话,还是可以接受的。
所以就有这样一个问题,FST 想知道 012......(n1)!n!中有多少数的末尾 0 个数是偶数。(注意0!是1,0算偶数)
输入格式:
读入有若干行,每行一个正整数 n ,最后一行是一个 -1 。
输出格式:
对于每个 n 输出一行,为 012......(n1)!n!中末尾 0 个数是偶数的个数。
样例输入:
2
3
10
-1
样例输出:
3
4
6
数据范围:
测试点 1、2:n≤10;数据组数=1;
测试点 3、4:n≤10000;数据组数=10;
测试点 5、6、7、8:n≤109;数据组数=105
测试点 9、10:n≤1018;数据组数=105
题目分析:
找规律+dp。表示不看题解,很难找到这个规律。
第一次 DP,预处理计算[0,5k)的答案和 0 个数的奇偶变化,可以从 5k1转移。可以观察下规律: (0 表示偶数,1 表示奇数)
k=0: 0
k=1: 00000
k=2: 0000011111000001111100000
k=3: 0000011111000001111100000(重复五遍···)
有 2 种可能:
k是奇数,那么 k-1是偶数,0 个数的奇偶不会变,ansk=ansk15(相当于直接把上一部分重复五遍,0、1的个数分别是上一段的5倍)
k是偶数, 那么 k-1是奇数, 0 个数的奇偶会变, ansk=3ansk1+2(5k1ansk1)(长度是上一个的5倍,但是内容上是,三段相同,两端相反,并且交替出现,即正反正反正,参考k=2时,表达式的意思是0的个数等于上一段0的个数的3倍加上上一段1的个数的2倍,1的个数就反过来算)。k为偶数会难想一点,再如k=4,那么就是第一段与k=3相同,第二段将k=3的0、1分别取反,然后第三段又相同······一共五段。
值得注意的是,我们表示的是从0开始,比如k=2,表示的是从0~24,k=3是0~124,这在后面会决定处理的方法。
如果你要问我这规律是怎么发现的,那我只能说是题解告诉我的(不,其实是无可奉告>_<)。
部分代码:

void prework()//一切皆如上述解释{    mi[0]=1;    for(int i=1;i<=25;i++) mi[i]=mi[i-1]*5;    num[0][0]=1; //k=0for(int i=1;i<=25;i++)    {        if(i%2==1)        {            num[i][0]=num[i-1][0]*5;//分别记录了01的个数            num[i][1]=num[i-1][1]*5;        }        else        {            num[i][0]=3*num[i-1][0]+2*num[i-1][1];            num[i][1]=3*num[i-1][1]+2*num[i-1][0];        }    }}

第二次 DP,把 n+1 转化为 5 进制,从高位开始做。
然后我们可以分类讨论,例如当前位的数是 3,当前位是奇数位,现在的奇偶性为偶,那么转移答案为 ansk2+(5kansk)1,因为我们预处理的答案相当于从偶数开始,然后奇数位会改奇偶性,分配给 3 段就是(ansk) (ansk取反) (ansk)。
如果你没看懂,这很正常。如果你一眼就看懂了,orz大佬。
先放代码:

ans=0;tmp=0;for(int i=25;i>=0;i--)//相当于在拆成五进制数,从高位做起{    for(int j=1;j<=(n/mi[i]);j++)    {        ans+=num[i][tmp];        if(i%2==1) tmp^=1;//根据五的几次方,奇次方,要正反正的反转    }    n%=mi[i];}ans+=num[0][tmp];//因为我们预处理的是[0~5^k),假如我们询问125,上面代码虽然处理了125个数,但是算的是0~124,125还要单独算一下printf("%I64d\n",ans);

首先解释为什么这里变成了奇次方会出现反转(上面我复制的那段题解中说的是奇数位反转,根据题解代码来讲,我觉得应该题解说错了,因为奇数位对应的是偶数次方),而第一次dp是偶数k才反转。考虑一个数329,它化成五进制为2301,对于最高位2,它虽然是329除以125得到的。但是范围是在k=4里,所以会反转。
最后再模拟一下,还是329:首先除以125的得到2(余79),然后因为i%2==1(此时i=3),所以会要反转的情况,即第一段是k=3(答案加k=3的0的个数),第二段是k=3取反(答案加k=3的1的个数)。剩余79除以25得3(余4),i%2==0······直到最后。
注意tmp是接着用的,因为上一段的tmp会影响这里。比如我们第一次处理奇次方(假设此时是大于k=3,小于k=4),那么我们询问的就是k=4的一部分,假设处理了三段(利用k=3),正反正,然后还剩余一截,其实是剩在了第4段里(这一段是要反转的),虽然我们剩余部分的处理要利用k=2,但这里要反转,所以tmp要接着用才能让它是反转的,否则你tmp重置为0,就不会把其反转。
附代码:

#include<iostream>#include<cstring>#include<string>#include<cstdlib>#include<cstdio>#include<ctime>#include<queue>#include<set>#include<map>#include<cctype>#include<iomanip>#include<cmath>#include<algorithm>using namespace std;long long n,mi[100],ans,num[100][2],tmp;long long readlong(){    char ch;long long i=0,f=1;    for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());    if(ch=='-') {ch=getchar();f=-1;}    for(;ch>='0'&&ch<='9';ch=getchar()) i=(i<<3)+(i<<1)+ch-'0';    return i*f;}void prework(){    mi[0]=1;    for(int i=1;i<=25;i++) mi[i]=mi[i-1]*5;    num[0][0]=1;    for(int i=1;i<=25;i++)    {        if(i%2==1)        {            num[i][0]=num[i-1][0]*5;            num[i][1]=num[i-1][1]*5;        }        else        {            num[i][0]=3*num[i-1][0]+2*num[i-1][1];            num[i][1]=3*num[i-1][1]+2*num[i-1][0];        }    }}int main(){    //freopen("fstagain.in","r",stdin);    //freopen("fstagain.out","w",stdout);    prework();    while(n=readlong(),n!=-1)    {        ans=0;tmp=0;        for(int i=25;i>=0;i--)        {            for(int j=1;j<=(n/mi[i]);j++)            {                ans+=num[i][tmp];                if(i%2==1) tmp^=1;            }            n%=mi[i];        }        ans+=num[0][tmp];        printf("%I64d\n",ans);    }    return 0;}
原创粉丝点击