『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

末尾に並ぶ0の個数をカウントします

【Python演習】n!の末尾に並ぶゼロの個数

(1) 任意の整数 $n$ を受け取って、$n$ の階乗 $n!$ を計算し、末尾に並ぶ $0$ の個数を返す関数を実装してください。
(2) $100!$ は一の位から連続して $0$ がいくつ並びますか。
 
(解答条件:外部ライブラリは使用しないものとします)

【解答A】階乗計算についてはこちらの記事を参照してください。
 
(1) 処理速度のことを考えず、シンプルに実装するなら、次のようなコードになるでしょう。

# count trailing_zeros

# In[1]

import math

# 数字を受け取って末尾に並ぶ0の個数をカウントする関数
def trailing_zeros(n):
    count = 0
    for i in reversed(str(n)):
        if i == '0':
            count += 1
        else:
            break
    return count

def fact_trailing_zeros(n):
    val = math.factorial(n)
    return trailing_zeros(val)

コードが見やすいように、任意の数値の末尾のゼロを数える関数 trailing_zeros() を別個に作って、階乗の末尾のゼロをカウントする関数 fact_trailing_zeros() で呼び出すようにしています。trailing_zeros() では、reversed() で文字列の逆側からループさせて 0 を見つけるごとにカウンター変数に 1 を加え、0 以外の文字列を見つけたら break でループから抜けるようにしています。

(2) (1) で実装した fact_trailing_zeros() に 100 を渡して、$100!$ の末尾に並ぶゼロを求めてみましょう。

# In[2]

# 100!の末尾に並ぶゼロをカウント
val = fact_trailing_zeros(100)

print(val)  # 24

$100!$ の末尾には $24$ 個の $0$ が並ぶことがわかりました。

【解答B】しかし、階乗の計算は $n$ が大きくなると、膨大な計算量となってしまいます。実はこの問題では必ずしも階乗を直接計算する必要はありません。もっと効率の良いアルゴリズムを考えてみます。

(1) $100!$ の末尾に並ぶゼロの個数を手計算で求めてみましょう。末尾の $0$ は $10$ を掛けた時に出現します。$10$ を素因数分解すると、
\[10=5\times 2\]となります。つまり、$100!$ に含まれる $5$ と $2$ のペア一組が一つの $0$ を作っていることになります。そこでまず、$100!$ に $5$ が何個含まれているか考えてみます。$100!$ を書き下すと、
\[100!=100\cdot 99\cdot 98\cdots 3\cdot 2\cdot 1\]のようになりますが、$5$ の倍数である
\[100,\:95,\:90,\:85,\:80,\:\cdots 25,\:20,\:15,\:10,\:5\]には、それぞれ少なくとも一個ずつの $5$ が含まれていることになります。この一個ずつをカウントすると、$100//5=20$ 個となります(「$//$」は切り捨て除算です)。さらに、上に並んだ数字を $5$ で除してみると(今カウントした $5$ を除くと)、
\[20,\:19,\:18,\:17,\:16,\:15,\:14,\:13,\:12,\:11,\:10,\:9,\:8,\:7,\:6,\:5,\:4,\:3,\:2,\:1\]となります。このうち
\[20,\:15,\:10,\:5\]はさらに一個ずつの $5$ が含まれていることがわかります。これをカウントすると、$20//5=4$ となります。さらに、それぞれの数字を $5$ で割ると
\[4,\:3,\:2,\:1\]となって、もう $5$ を含む数字はありません。よって、$100!$ には $20+4=24$ 個の $5$ が含まれていることがわかりました。ここまでの手続きを Python で実装すると、以下のようなコードになります。

# In[3]

n = 100
s = 0

# nが5未満になるまで切り捨て除算を繰り返しつつ
# カウンター変数に値を加えていく
while n > 5:
    n //= 5
    s += n

print(s)  # 24

同様に、$100$ を順次 $2$ で切り捨て除算していくと、
\[\begin{align*}100//2&=50\\[6pt]50//2&=25\\[6pt]25//2&=12\\[6pt]12//2&=6\\[6pt]6//2&=3\\[6pt]3//2&=1\\[6pt]\end{align*}\]となるので、合計で $50+25+12+6+3+1=97$ 個の $2$ が含まれていることがわかります。$0$ を生成するのは $5$ と $2$ のペアなので、少ない方に合わせて、$24$ 個の $0$ が末尾に並ぶことがわかります(【解答A】のコードの実行結果と一致しています)。
 
計算間違いがあるといけないので、念のために Python でも確かめておきましょう。

# In[4]

n = 100
s = 0

# nが2未満になるまで切り捨て除算を繰り返しつつ
# カウンター変数に値を加えていく
while n > 2:
    n //= 2
    s += n

print(s)  # 97

コード In[3] や In[4] を一般化して、整数 $n!$ に含まれる整数 $k$ の個数を返す関数を実装してみます。

# In[5]

# n!に含まれるkの個数を返す関数
def fact_factor(n, k):
    s = 0
    while n > k:
        n //= k
        s += n
    return s

val = fact_factor(100, 2)

print(val)  # 97

fact_factor(n, 2) と fact_factor(n, 5) のうち、小さい方が $n!$ の末尾に並ぶゼロの個数となります。

# In[6]

# n!の末尾に並ぶゼロの個数をカウントする関数
def fact_trailing_zeros_2(n):
    val_1 = fact_factor(n, 2)
    val_2 = fact_factor(n, 5)
    return min(val_1, val_2)

# 100!の末尾に並ぶゼロの個数
val = fact_trailing_zeros_2(100)

print(val)  #24

コメント