题目描述:
FST 作为 OIer ,经常会遇到和阶乘有关的问题,但是一个数的阶乘末尾总是会有很多 0 ,FST 认为这很不美观,但是 FST 觉得如果 0 的个数是偶数的话,还是可以接受的。
所以就有这样一个问题,FST 想知道 0!,1!,2!......(n−1)!,n!中有多少数的末尾 0 个数是偶数。(注意0!是1,0算偶数)
输入格式:
读入有若干行,每行一个正整数 n ,最后一行是一个 -1 。
输出格式:
对于每个 n 输出一行,为 0!,1!,2!......(n−1)!,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 个数的奇偶变化,可以从 5k−1转移。可以观察下规律: (0 表示偶数,1 表示奇数)
k=0: 0
k=1: 00000
k=2: 0000011111000001111100000
k=3: 0000011111000001111100000(重复五遍···)
有 2 种可能:
k是奇数,那么 k-1是偶数,0 个数的奇偶不会变,ansk=ansk−1∗5(相当于直接把上一部分重复五遍,0、1的个数分别是上一段的5倍)
k是偶数, 那么 k-1是奇数, 0 个数的奇偶会变, ansk=3∗ansk−1+2∗(5k−1−ansk−1)(长度是上一个的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=0时
for(int i=1;i<=25;i++)
{
if(i%2==1)
{
num[i][0]=num[i-1][0]*5;//分别记录了0、1的个数
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,当前位是奇数位,现在的奇偶性为偶,那么转移答案为 ansk∗2+(5k−ansk)∗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;
}
该博客介绍了如何解决NOIP比赛中关于阶乘尾部0个数是偶数的问题。通过找规律和动态规划的方法,解析了预处理计算和转化为5进制的思路,以及在不同情况下的状态转移方程。博主分享了部分代码并解释了代码逻辑,特别是奇数次方出现反转的现象。

7136

被折叠的 条评论
为什么被折叠?



