题意:
要求你求出n!(n−m)!)\frac{n!}{(n-m)!)}(n−m)!)n!中最后一个非0的数字.
题目:
In this problem you will be given two decimal integer numberN,M. You will have to find the last non-zero digit of the NPM^{N}P_{M}NPM.This means no of permutations of N things taking M at a time.
Input
The input contains several lines of input. Each line of the input file contains two integers N (0 <= N<= 20000000), M (0 <= M <= N).
Output
For each line of the input you should output a single digit, which is the last non-zero digit of NPM. For example, if NPM^{N}P_{M}NPM is 720 then the last non-zero digit is 2. So in this case your output should be 2.
Sample Input
10 10
10 5
25 6
Sample Output
8
4
2
分析:
说实话这道题上来我就没看懂题意,这怎么就NPM^{N}P_{M}NPM==n!(n−m)!)\frac{n!}{(n-m)!)}(n−m)!)n!了?In a word ,我感觉到了不友好。
然后我就开始了啃书环节,具体在《挑战程序设计》P293,之后恶意铺面而来,花费我一晚上,终于摸透了这个题,必须滴好好说道说道。
(1)首先是若求n!的最后一位,我们可以将所有的因数10去掉,问题就转换为了求这个数的最后一位。根据以往的做题经验,这时候只要找到所有的因子2和5,放着对最后特判对于最后一位的影响就好了,所以正常while循环暴力找因子数,然后超时了,代码如下:
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;const int N=2e7+10;
int a[5][N];
int b[5][4]={{6,2,4,8},{5,5,5,5},{1,3,9,7},{1,7,9,3},{1,9,1,9}};
int get(int x){if(x==2) return 0;if(x==5) return 1;if(x==3) return 2;if(x==7) return 3;if(x==9) return 4;
}
void init(){for(int i=2; i<N; ++i){int t=i;int su=0,sm=0;while(t%2==0) t/=2,++su;while(t%5==0) t/=5,++sm;a[get(2)][i]=a[get(2)][i-1]+su;a[get(5)][i]=a[get(5)][i-1]+sm;a[get(3)][i]=a[get(3)][i-1];a[get(7)][i]=a[get(7)][i-1];a[get(9)][i]=a[get(9)][i-1];if(t%10 && t%10!=1) ++a[get(t%10)][i];}
}int main()
{init();int n,m;while(~scanf("%d%d",&n,&m)){int su=a[get(2)][n]-a[get(2)][n-m];int sm=a[get(5)][n]-a[get(5)][n-m];if(su<sm) printf("5\n");else {//printf("+++++++ %d %d\n",su,sm);su-=sm;int d3=a[get(3)][n]-a[get(3)][n-m];int d7=a[get(7)][n]-a[get(7)][n-m];int d9=a[get(9)][n]-a[get(9)][n-m];int tmp=b[get(3)][d3%4]*b[get(7)][d7%4]*b[get(9)][d9%4];//printf("+++++++++ %d %d\n",su,tmp);tmp*=su?b[get(2)][su%4]:1;int ans=tmp%10;printf("%d\n",ans);}}return 0;
}
这并不冤枉,其实开数组2e7就该知道有问题,试了编译器,可以运行,就硬着头皮写下来了,不出意料,果真超时了。那这里就用到了“白书”的定理,我懒得敲了。
具体代码如下:
int sum(int n,int p){//计算n!中质因子m的出现次数return n==0?0:n/p+sum(n/p,p);
}
(2)当我们对(1~n)去除因子2,5后发现最后一位的值,只可能是 1,3, 7,9这四个数,因为最后一位若为1,n!相乘对最后一位值的变化没有影响,所以可以不用考虑,只考虑 3,7 ,9,即可。这时发现了规律,例如,即当存在多个3时,只考虑个位值,出现了循环节{1,3,9,7},注意第一位为整除时,所以为值为1。你以为到这就算完了,还不够!
(3)如上超时代码,不能打表开数组存,所以每次输入就直接调用函数,直接对n!进行讨论,将其分为奇偶两个部分:
【1】对于偶数序列,我们只需将它除 2 即可递归转化为奇数序列(其实就是消去因子2)。
【2】对于奇数序列,我们可以发现,每 10个数字中就有 3 , 7 , 9 各一个,但又因为(1~n)中有 5的倍数,所以继续除 5,递归消去因子。
(4)这里就差不多了,但还是要注意:
一,当因子2的个数小于5的个数时,由于此时末尾必为奇数(1,3,7,9中一个),所以相乘最后一位必为5,直接输出。
二,当因子2的个数大于5的个数时,需要考虑因子2对结果的一个影响,这时也有与前面一样的规律{6,2,4,8},理解了之后让我们欢乐敲代码吧;
AC模板:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,m;
int e[4][4]={{6,2,4,8},{1,3,9,7},{1,7,9,3},{1,9,1,9}};
int sum(int n,int p){//计算n!中质因子m的出现次数,用到“挑战程序设计”P293推论return n==0?0:n/p+sum(n/p,p);
}
int odd(int n,int p){//奇数数列中末尾为x的数出现的次数,消去1~n数中存在的因子5return n==0?0:n/10+(n%10>=p)+odd(n/5,p);
}
int even(int n,int p){//末尾为x的数的出现次数,消去1~n数中存在的因子2;return n==0?0:even(n/2,p)+odd(n,p);
}
int main()
{while(~scanf("%d%d",&n,&m)){int su=sum(n,2)-sum(n-m,2);int sm=sum(n,5)-sum(n-m,5);int a=even(n,3)-even(n-m,3);int b=even(n,7)-even(n-m,7);int c=even(n,9)-even(n-m,9);if(su<sm){printf("5\n");continue;}int ans=e[1][a%4]*e[2][b%4]*e[3][c%4]%10;if(su!=sm)ans*=e[0][(su-sm)%4];printf("%d\n",ans%10);}return 0;
}