The Last Non-zero Digit
Time Limit: 1000MS Memory Limit: 65536K
Total Submissions: 5783 Accepted: 1810
Description
In this problem you will be given two decimal integer number N, M. You will have to find the last non-zero digit of the 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 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
题目大概意思:
求组合数 在十进制下的末尾非零位的值。其中 .
分析:
首先容易想到时间复杂度为 的算法:
先计算出 的质因子 与 的指数,分别记为 ,则 的因子 的指数为 ,记为 ,那么 即为所求,再线性地计算 其中在逐个相乘的过程中,用每一次尽可能令当前的 除以 与 ,直到 与 都除够了 次。
然而
的取值范围较大,且题目为多组案例,这样的解法无法达到时间要求。
接下来我们思考,当一个数 乘以另外一个数 后, 的末尾非零位会如何变化:
- 如果 的末尾是 或 则乘以这个数无论如何都不会对末尾非零位造成影响
- 如果 的末尾是 中的一个,则 ,即 的末尾非零位就等于 的末尾非零位与 的末尾的乘积的末尾
- 如果 的末尾是 中的一个,若 不是 的倍数,则与第 种情况相同
- 同理,如果 的末尾是 ,若 不是 的倍数,则与第 种情况相同
因此如果我们能得到区间 中的每一个整数在除去因子 与因子 后的末尾位(这些末尾位只可能是 中的一个),那么让这些末尾位在 的状态下相乘得到 ,在让 乘以 个 , 个 得到 (实际上只乘了 或 中的一个若干次,因为 ),则 的末尾就是所求了。在计算 的过程中,始终满足前两种情况,即只乘 ;在由 计算 的过程中,始终满足后两种情况,因为 不会同时是 与 的倍数。因此, 的末尾非零位的值就等于以上这些数的末尾在 的状态下相乘。
其中 与 可以通过计算 的质因子指数与 的质因子指数相减得到,而 的质因子 的指数可以以如下方式计算出来:(证明略)
// 计算 x! 的质因子 f 的指数
int cnt_fact(int x, const int f)
{
int res = 0;
while (x)
{
res += x / f;
x /= f;
}
return res;
}
接下来考虑除去因子
后末尾为
的数的个数的计算,不妨分别记为
. 由于个数的可加性,计算区间
中的
同样可以通过区间
的个数减去
的个数得到。
由于我们只需要计算区间 中每一个整数在除去因子 与因子 后的末尾位的乘积,因此无需对区间中的每一个数进行计算,只需计算区间中的数在除去因子 后以 为末尾的个数即可。
那么如何计算区间 中的数除去因子 后以 为末尾的个数呢?
首先考虑区间中奇数的计算,对于那些末尾为 的数,可以直接计入统计,而对于末尾为 的,则需要除以 后再进行判断,如果这些末尾为 的数的集合中的每个数除以 后,仍存在末尾为 的,则需要再除以 ,因此可以递归地进行判断,如下为代码:
// 计算所有≤x的奇数在除去因子5后, 以 end(end∈{1,3,7,9}) 结尾的数的个数
int cnt_odd_end(int x, int end)
{
if (x < end) return 0;
return x / 10 + (x % 10 >= end) + cnt_odd_end(x / 5, end);
}
而对于区间中的偶数,由于我们统计的是除去因子
与
后的末尾位,因此只需将这些数全部除以
后将偶序列转化为奇偶交替的序列,并把奇序列用
函数统计,偶序列递归处理即可,如下为代码:(这里省去了对偶序列的单独处理,而是直接写为了对整个序列的处理)
// 计算所有≤x的数在除去因子2和5后, 以 end(end∈{1,3,7,9}) 结尾的数个数
int cnt_end(int x, const int end)
{
if (x < end) return 0;
return cnt_odd_end(x, end) + cnt_end(x / 2, end);
}
在最后将所有数乘在一起的时候,可以采用快速幂的方法降低时间复杂度。在整个算法中,统计
与
的时间复杂度是
;计算以
为末尾的个数时,
函数递归调用了
次,每一次会调用一次
,而
函数的时间复杂度是
. 因此算法的总时间复杂度是
.
下面贴出完整代码:
#include <cstdio>
#include <algorithm>
using namespace std;
int cnt_fact(int x, const int f);
int cnt_end(int x, const int end);
int cnt_odd_end(int x, int end);
int mod_pow(int x, int p, const int m);
int main()
{
int N, M;
while (~scanf("%d%d", &N, &M))
{
int cnt2 = cnt_fact(N, 2) - cnt_fact(N - M, 2);
int cnt5 = cnt_fact(N, 5) - cnt_fact(N - M, 5);
int end3 = cnt_end(N, 3) - cnt_end(N - M, 3);
int end7 = cnt_end(N, 7) - cnt_end(N - M, 7);
int end9 = cnt_end(N, 9) - cnt_end(N - M, 9);
int cnt10 = min(cnt2, cnt5);
cnt2 -= cnt10;
cnt5 -= cnt10;
int res = mod_pow(3, end3, 10) * mod_pow(7, end7, 10) * mod_pow(9, end9, 10) * mod_pow(2, cnt2, 10) * mod_pow(5, cnt5, 10) % 10;
printf("%d\n", res);
}
return 0;
}
// 计算 x! 的质因子 f 的指数
int cnt_fact(int x, const int f)
{
int res = 0;
while (x)
{
res += x / f;
x /= f;
}
return res;
}
// 计算所有≤x的数在除去因子2和5后, 以 end(end∈{1,3,7,9}) 结尾的数个数
int cnt_end(int x, const int end)
{
if (x < end) return 0;
return cnt_odd_end(x, end) + cnt_end(x / 2, end);
}
// 计算所有≤x的奇数在除去因子5后, 以 end(end∈{1,3,7,9}) 结尾的数的个数
int cnt_odd_end(int x, int end)
{
if (x < end) return 0;
return x / 10 + (x % 10 >= end) + cnt_odd_end(x / 5, end);
}
// 计算 x^p % m
int mod_pow(int x, int p, const int m)
{
int res = 1;
while (p)
{
if (p & 1)
{
res = res * x % m;
}
x = x * x % m;
p >>= 1;
}
return res;
}