[NOIP模拟]Factorial Surplus Tail

该博客介绍了如何解决NOIP比赛中关于阶乘尾部0个数是偶数的问题。通过找规律和动态规划的方法,解析了预处理计算和转化为5进制的思路,以及在不同情况下的状态转移方程。博主分享了部分代码并解释了代码逻辑,特别是奇数次方出现反转的现象。

题目描述:
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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值