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
题目大概意思:
求组合数 C N M C_N^M CNM 在十进制下的末尾非零位的值。其中 0 ≤ M ≤ N ≤ 2 × 1 0 7 0≤M≤N≤2×10^7 0≤M≤N≤2×107 .
分析:
首先容易想到时间复杂度为 O ( n ) O(n) O(n) 的算法:
先计算出 C N M C_N^M CNM 的质因子 2 2 2 与 5 5 5 的指数,分别记为 c n t 2 , c n t 5 cnt_2,cnt_5 cnt2,cnt5 ,则 C N M C_N^M CNM 的因子 10 10 10 的指数为 min ( c n t 2 , c n t 5 ) \min(cnt_2,cnt_5) min(cnt2,cnt5) ,记为 c n t 10 cnt_{10} cnt10 ,那么 ( ∏ i = N − M + 1 N i ) / 1 0 c n t 10 m o d    10 {(\prod_{i=N-M+1}^N{i})}/{10^{cnt_{10}}}\mod10 (∏i=N−M+1Ni)/10cnt10mod10 即为所求,再线性地计算 ∏ i = N − M + 1 N i 1 0 c n t 10 \frac{\prod_{i=N-M+1}^N{i}}{10^{cnt_{10}}} 10cnt10∏i=N−M+1Ni 其中在逐个相乘的过程中,用每一次尽可能令当前的 i i i 除以 2 2 2 与 5 5 5 ,直到 2 2 2 与 5 5 5 都除够了 c n t 10 cnt_{10} cnt10 次。
然而
N
N
N 的取值范围较大,且题目为多组案例,这样的解法无法达到时间要求。
接下来我们思考,当一个数 x x x 乘以另外一个数 y y y 后, x x x 的末尾非零位会如何变化:
- 如果 y y y 的末尾是 0 0 0 或 1 1 1 则乘以这个数无论如何都不会对末尾非零位造成影响
- 如果 y y y 的末尾是 { 3 , 7 , 9 } \{3,7,9\} {3,7,9} 中的一个,则 ( x y ) m o d    10 = ( x m o d    10 ) ⋅ ( y m o d    10 ) m o d    10 (xy)\mod{10}=(x\mod10)·(y\mod10)\mod10 (xy)mod10=(xmod10)⋅(ymod10)mod10 ,即 x y xy xy 的末尾非零位就等于 x x x 的末尾非零位与 y y y 的末尾的乘积的末尾
- 如果 y y y 的末尾是 { 2 , 4 , 6 , 8 } \{2,4,6,8\} {2,4,6,8} 中的一个,若 x x x 不是 5 5 5 的倍数,则与第 2 2 2 种情况相同
- 同理,如果 y y y 的末尾是 5 5 5 ,若 x x x 不是 2 2 2 的倍数,则与第 2 2 2 种情况相同
因此如果我们能得到区间 [ N − M , M ] [N-M,M] [N−M,M] 中的每一个整数在除去因子 2 2 2 与因子 5 5 5 后的末尾位(这些末尾位只可能是 { 1 , 3 , 7 , 9 } \{1,3,7,9\} {1,3,7,9} 中的一个),那么让这些末尾位在 m o d    10 \mod10 mod10 的状态下相乘得到 x ′ x' x′ ,在让 x ′ x' x′ 乘以 c n t 2 − c n t 10 cnt_2-cnt_{10} cnt2−cnt10 个 2 2 2 , c n t 5 − c n t 10 cnt_5-cnt_{10} cnt5−cnt10 个 5 5 5 得到 x x x (实际上只乘了 2 2 2 或 5 5 5 中的一个若干次,因为 c n t 10 = min ( c n t 2 , c n t 5 ) cnt_{10}=\min(cnt_2,cnt_5) cnt10=min(cnt2,cnt5)),则 x x x 的末尾就是所求了。在计算 x ′ x' x′ 的过程中,始终满足前两种情况,即只乘 { 1 , 3 , 7 , 9 } \{1,3,7,9\} {1,3,7,9} ;在由 x ′ x' x′ 计算 x x x 的过程中,始终满足后两种情况,因为 x x x 不会同时是 2 2 2 与 5 5 5 的倍数。因此, C N M C_N^M CNM 的末尾非零位的值就等于以上这些数的末尾在 m o d    10 \mod10 mod10 的状态下相乘。
其中 c n t 2 cnt_2 cnt2 与 c n t 5 cnt_5 cnt5 可以通过计算 N ! N! N! 的质因子指数与 ( N − M ) ! (N-M)! (N−M)! 的质因子指数相减得到,而 N ! N! N! 的质因子 f f f 的指数可以以如下方式计算出来:(证明略)
// 计算 x! 的质因子 f 的指数
int cnt_fact(int x, const int f)
{
int res = 0;
while (x)
{
res += x / f;
x /= f;
}
return res;
}
接下来考虑除去因子
2
,
5
2,5
2,5 后末尾为
{
1
,
3
,
7
,
9
}
\{1,3,7,9\}
{1,3,7,9} 的数的个数的计算,不妨分别记为
c
n
t
1
,
c
n
t
3
,
c
n
t
7
,
c
n
t
9
cnt_1,cnt_3,cnt_7,cnt_9
cnt1,cnt3,cnt7,cnt9 . 由于个数的可加性,计算区间
(
N
−
M
,
N
]
(N-M,N]
(N−M,N] 中的
c
n
t
k
cnt_k
cntk 同样可以通过区间
[
1
,
N
]
[1,N]
[1,N] 的个数减去
[
1
,
N
−
M
]
[1,N-M]
[1,N−M] 的个数得到。
由于我们只需要计算区间 ( N − M , N ] (N-M,N] (N−M,N] 中每一个整数在除去因子 2 2 2 与因子 5 5 5 后的末尾位的乘积,因此无需对区间中的每一个数进行计算,只需计算区间中的数在除去因子 2 , 5 2,5 2,5 后以 1 , 3 , 7 , 9 1,3,7,9 1,3,7,9 为末尾的个数即可。
那么如何计算区间 [ 1 , N ] [1,N] [1,N] 中的数除去因子 2 , 5 2,5 2,5 后以 1 , 3 , 7 , 9 1,3,7,9 1,3,7,9 为末尾的个数呢?
首先考虑区间中奇数的计算,对于那些末尾为 k k k 的数,可以直接计入统计,而对于末尾为 5 5 5 的,则需要除以 5 5 5 后再进行判断,如果这些末尾为 5 5 5 的数的集合中的每个数除以 5 5 5 后,仍存在末尾为 5 5 5 的,则需要再除以 5 5 5 ,因此可以递归地进行判断,如下为代码:
// 计算所有≤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);
}
而对于区间中的偶数,由于我们统计的是除去因子
2
2
2 与
5
5
5 后的末尾位,因此只需将这些数全部除以
2
2
2 后将偶序列转化为奇偶交替的序列,并把奇序列用
c
n
t
_
o
d
d
_
e
n
d
cnt\_odd\_end
cnt_odd_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);
}
在最后将所有数乘在一起的时候,可以采用快速幂的方法降低时间复杂度。在整个算法中,统计
c
n
t
2
cnt_2
cnt2 与
c
n
t
5
cnt_5
cnt5 的时间复杂度是
O
(
log
2
N
)
O(\log_2{N})
O(log2N) ;计算以
{
1
,
3
,
7
,
9
}
\{1,3,7,9\}
{1,3,7,9} 为末尾的个数时,
c
n
t
_
e
n
d
cnt\_end
cnt_end 函数递归调用了
log
2
N
\log_2N
log2N 次,每一次会调用一次
c
n
t
_
o
d
d
_
e
n
d
cnt\_odd\_end
cnt_odd_end ,而
c
n
t
_
o
d
d
_
e
n
d
cnt\_odd\_end
cnt_odd_end 函数的时间复杂度是
O
(
log
2
N
)
O(\log_2N)
O(log2N) . 因此算法的总时间复杂度是
O
(
log
2
2
N
)
O(\log_2^2{N})
O(log22N) .
下面贴出完整代码:
#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;
}