POJ1150 The Last Non-zero Digit - 数论 - 模运算

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 0MN2×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=NM+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}}} 10cnt10i=NM+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 的末尾非零位会如何变化:

  1. 如果 y y y 的末尾是 0 0 0 1 1 1 则乘以这个数无论如何都不会对末尾非零位造成影响
  2. 如果 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 的末尾的乘积的末尾
  3. 如果 y y y 的末尾是 { 2 , 4 , 6 , 8 } \{2,4,6,8\} {2,4,6,8} 中的一个,若 x x x 不是 5 5 5 的倍数,则与第 2 2 2 种情况相同
  4. 同理,如果 y y y 的末尾是 5 5 5 ,若 x x x 不是 2 2 2 的倍数,则与第 2 2 2 种情况相同

因此如果我们能得到区间 [ N − M , M ] [N-M,M] [NM,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} cnt2cnt10 2 2 2 c n t 5 − c n t 10 cnt_5-cnt_{10} cnt5cnt10 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)! (NM)! 的质因子指数相减得到,而 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] (NM,N] 中的 c n t k cnt_k cntk 同样可以通过区间 [ 1 , N ] [1,N] [1,N] 的个数减去 [ 1 , N − M ] [1,N-M] [1,NM] 的个数得到。

由于我们只需要计算区间 ( N − M , N ] (N-M,N] (NM,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 
#include 
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;
}

你可能感兴趣的:(模运算,数论)