题解 P1134 【阶乘问题】
最下面,将对karma的题解进行一些解释。
本题要求我们“计算N!的最右边的非零位的值。”
首先,如果不知道什么是阶乘的话,先去百度。
1. 初次审题
N!就是N的阶乘,你已经知道它什么意思了。
最右边非零位的值?应该没有人会不懂这是什么意思。
如17005722最右边非零位的值是2,479001600最右边非零位的值是6。
怎么求呢?
最简单的是暴力算。
int不够开longlong,longlong不够开高精。
运行太慢开O2,O2不行开臭氧。
空间(压位高精)可以炸,时间(O4,O5,O1134)一定炸。
我们可以发现,两个数乘积的最右边的值,只与这两个数最右边的值有关。如5*7=35,665*137=91105。最右边的值都是5。
那么我们可以乘的时候只记最后一位,有零就全去掉。就像这样:
{
int ans=1;//初值为1
for (int i=1;i<=n;++i)//遍历
{
ans*=i;
while (ans%10==0)
ans/=10;//去零
ans%=10;//只保留最后一位
}
}
当然过不了。
如n=15时:14!=87178291200,我们只保留了末尾数字2,乘15会得到30,即答案为3。但是,15!=130767438000,正确答案为8。12*15=180正确,2*15=3由于保留位数太少而错。
那么,我们多保留几位就可以了。
{
int ans=1;//初值为1
for (int i=1;i<=n;++i)//遍历
{
ans*=i;
while (ans%10==0)
ans/=10;//去零
ans%=100000000;//保留最后好多位
}
ans%=10;//保留最后一位
}
可以过,但不够好。
2.这可是数论题
让我们有对待数论题该有的亚子。
本题的罪恶之点就是‘0’,那么是谁引起的呢?
2*5=10,是2和5团伙作案。
我们知道,能被2整除的数和能被5整除的数相乘会产生末尾的0。
而在1到10中,有5个数能被2整除(2,4,6,8,10),只有2个能被5整除(5,10)。因此在2和5的缠斗中,2总是能活下来
2比5多,我们可以在处理的时候将所有要乘的2和所有要乘的5拿出来。用2的个数减去5的个数(减去的数相乘得到了末尾的0,然后被忽略),把剩下的2再乘回去。
{
int ans=1;
int num_2=0,num_5=0;
for (int i=1;i<=n;++i)
{
int ii=i;//建立一个备份,因为一会儿会改变它的值
while (ii%2==0)
{
num_2++;
ii/=2;
} //去所有的2,像4这样的数因子中有不止一个2(4=2*2)
while (ii%5==0)
{
num_5++;
ii/=5;
} //去5,同上
ans=ans*i%10;
}
num_2=num_2-num_5;//剩下多少2
for (int i=1;i<=num_2;++i)
ans=ans*2%10;//乘回去
}
在最后,我们让ans连续乘很多2,由于连续乘很多2的末尾数是有规律的,可把最后的循环改一下。
int c[4]={6,2,4,8};
ans=ans*c[num_2%4]%10;
规律:乘1个2为2,2个为4,3个为8,4个为6,5个为2······四个一循环。
这题本应该在这里结束的。
3.神奇之处
上一个解法还不错,但时间复杂度没降下来。
我们说末尾的0是2和5造成的,但主谋还是5。谁少谁是主谋
那么我们处理时把所有可以被5整除的数先跳过不乘。
利用下面的程序我们可以看到一些有意思的东西。
#include<cstdio>
#define xman 30
using namespace std;
int main()
{
for (int n=1;n<=xman;++n)
{
int ans=1;
for (int i=1;i<=n;++i)
if (i%5!=0)
ans=ans*i%10;
printf("%d\n",ans);
}
}
结果:
除1以外,我们发现结果是有规律的。记下来:
int a[10]={6,6,2,6,4,4,4,8,4,6}
- 由此,我们可以直接得到任意数这么处理后的末尾数。那么可以将此数和 被跳过数的乘积的末尾数 相乘得到答案。(至于0,我们有方法处理)
再看被跳过的那些数。
是一个5的幂和一个阶乘的乘积。
- 阶乘的末尾数可以递归的处理。
5的幂?有点难处理。因为乘5会产生0,这个过程如果我们只保留末尾数,答案会出错。(见上方15!)
这时,神奇的8,出来了。
除1以外,我们的最终答案一定是2,4,6,8中的一个。
因为与5相乘剩余的2,会让去0后的数成为偶数。
又有2*6=12,4*6=24,6*6=36,8*6=48。
所以答案乘6得到的数末尾数不变。
末尾数乘积只与末尾数有关,有2*16=32,······
同时,乘10末尾数也不变。2*10=20,······
那么,2*2*5==2*2*8······
- 所以乘5时我们可以换成乘8!
同时乘8的末尾数也有规律:
b[4]={6,8,4,2}
那么,它来了:
#include<cstdio>
using namespace std;
int a[10]={6,6,2,6,4,4,4,8,4,6},b[4]={6,8,4,2};
int toans(int n)
{
if (n==0)
return 1;//边界
int ans=1;
ans=ans*a[n%10];//不乘5倍数的阶乘
int times_5=n/5;//没乘5的次数
ans=ans*b[times_5%4]%10;//以8代5,处理幂
ans=ans*toans(n/5)%10;//递归处理阶乘
return ans;
}
int main()
{
int n;
scanf("%d",&n);
if (n==1)//特判
{
printf("1");
return 0;
}
printf("%d",toans(n));
return 0;
}
用while循环代替递归,则是:
#include<cstdio>
using namespace std;
int a[10]={6,6,2,6,4,4,4,8,4,6},b[4]={6,8,4,2};
int main()
{
int ans=1,n;
scanf("%d",&n);
if (n==1)
{
printf("1");
return 0;
}
while(n)
{
ans*=a[n%10];
n=n/5;
ans=ans*b[n%4]%10;
}
printf("%d",ans);
return 0;
}
这种神奇的做法,log(n)的复杂度,速度达到了一流。
有多快?
我想本题的数据完全可以大个几万倍
- 特判
为什么我们会需要特判?
我们用“2*2*5==2*2*8······”来证明可以用乘5代替乘8,但是你会发现式子中两边都有一个特别的‘2’。
这要求我们的的答案不仅仅只是偶数,还要能支付一个‘2’来订购‘以8换5’业务。
对于,1!=1,2!=2,3!=6。它们都无法支付‘2’(即在除2后不是偶数),因此都不能由8替5。
但是,2!,3!的末尾数都是偶数,又正好要乘6,结果因此不变,运气好过了
1就不行了,但测试数据没有1!
最后,本题的AC代码可以很短:
#include<cstdio>
int main()
{
int a[]{6,6,2,6,4,4,4,8,4,6},b[]{6,8,4,2},r=1,n;
scanf("%d",&n);
while(n)r=r*a[n%10]*b[(n=n/5)%4]%10;
printf("%d",r);
return 0;
}