4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonでFizzBuzz問題をいろいろな書き方で表現してみる

Last updated at Posted at 2023-01-17

はじめに

プログラミングは専門ではなかったとはいえ、FizzBuzz問題というのがあるのを今更知って、面白そうなのでいろいろ考えてみたメモです。

FizzBuzz問題とは

プログラミング言語で初歩的なプログラムを作成する能力があるかを見分ける簡易な試験としてよく知られているそうです(私は知りませんでしたが…)
ルール
「1から100までの数字を画面に表示する」
「3の倍数のときは数字の代わりにFizzと表示する」
「5の倍数のときは数字の代わりにBuzzと表示する」
「3かつ5の倍数のときは数字の代わりにFizzBuzzと表示する」

一般解

というわけで、最初にもっとも一般的な回答な回答を紹介。
定義どおりに、判定・分岐、繰り返しをする形式です。

for i in range(1, 101):
    if i % 3 == 0 and i % 5 == 0:
        print('FizzBuzz')
    elif i % 3 == 0:
        print('Fizz')
    elif i % 5 == 0:
        print('Buzz')
    else:
        print(i)

最小文字数解

次にテクニカルな感じですが、Pythonで最も短く書く方法として知られる解を紹介。
これは思いついたものではなく拾い物です。
繰り返しが0からで3は余りが2のときという発想がすごいです。

for i in range(100):print(i%3//2*"Fizz"+i%5//4*"Buzz"or-~i)

FizzBuzz問題の構造

2つの解を紹介しましたが、これらは(1)判定方法(2)繰り返し方法(3)文字列への変換方法の3つをどう組み合わせるかで考えられるように思います。

以下ではそれぞれどのようなバリエーションがあるのかを現思いついた範囲でまとめてみます。
(自分で書いてみた範囲でのバリエーションを列挙しているので、他にも書き方は有ると思います。特に判定方法は数学的な話なのでもっとあると思います)

(1) 判定方法のバリエーション

最小文字数解の判定があまりにクールなので、思考停止に陥りそうになりましたが、その後に思いついた判定のバリエーションはおそらく以下の6つです(気づいていないだけで他にもあるとは思いますが)。

1.i%3 i%5

割った余りを計算する方法。
1始まりで1, 2, 3, 4, 5, 6...と回す場合、それぞれ3, 5の倍数のときに0になります。

2.i**2%3 i**4%5

フェルマーの少定理「p を素数、a を p の倍数でない任意の整数としたとき、a**(p-1)%p=1が成立する」を使った方法
1始まりで1, 2, 3, 4, 5, 6...と回す場合、それぞれ3, 5の倍数のときに0になります。

3.str(i/3).split('.')[1] str(i/5).split('.')[1]

数値を割ってから文字列にして、小数点以下の数値に分割して判定に使います。
結局は1の余りで判定しているものと同じですが、縛りとして剰余演算子を使わないというときには、こうした手法もありえます。
1始まりで1, 2, 3, 4, 5, 6...と回す場合、それぞれ3, 5の倍数のときに0になります。

4.i in list(range(3, 101, 3)) i in list(range(5, 101, 5))

range関数で3飛ばしの配列を用意して、その配列中に数値が存在するかを比較演算子で判定する方法。
1始まりで1, 2, 3, 4, 5, 6...と回す場合、それぞれ3, 5の倍数のときにTrueになります。

5.i%3//2 i%5//4

最小文字数解で使われている計算方法で、0始まりで0, 1, 2, 3, 4, 5...と回す場合に、i%3は表示順3番目の数値は余りが2になるので、2で切り捨て除算すると3の倍数だけ1、i%5は表示順5番目の数値は余りが4になるので、4で切り捨て除算すると5の倍数だけ1となります。

6.事前に判定結果を用意してしまう

range関数と同じ考え方ですが、i_3 = [0, 0, 1] * 34 i_5 = [0, 0, 0, 0, 1] * 20 または配列インデックスであれば i_15 = [0, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 0, 0, 3] * 7とすれば正解が用意できます。

参考出力例(3の倍数)

表示順 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
i 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
i%3 1 2 0 1 2 0 1 2 0 1 2 0 1 2 0
i**2%3 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0
str(i/3).split('.')[1] 333.. 666.. 0 333.. 666.. 0 333.. 666.. 0 333.. 666.. 0 333.. 666.. 0
i in list(range(3, 101, 3)) False False True False False True False False True False False True False False True
表示順 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
i%3 0 1 2 0 1 2 0 1 2 0 1 2 0 1 2
i%3//2 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1

(2) 繰り返し方法のバリエーション

1. for(while)文

繰り返しなので、真っ先に出てくるのがこれですね。

for i in range(1, 101):が標準的かと思います。

2. map関数

挙動的には第二引数の配列数だけ関数を呼び出すようなものですが、正確にはもう少し奥深い関数らしいので、別途調べてみてもらったほうが良いと思います。

FizzBuzz問題で使うには list(map(function, list(range(1, 101)))) とすることで、1~100までの数を function に投げてそこでFizzBuzz判定をする方法が考えられます。

3. 再帰関数

再帰関数自体を説明しだすと長くなりますが、FizzBuzz問題で使う場合は単に引数を1ずつ変化させていくことで繰り返しを実現できることになります。

(3) 文字列への変換方法のバリエーション

FizzBuzz問題は基本的には数列を文字列へ変換するような処理になるため、その変換手法のバリエーションを以下に示します。

1. if文での条件分岐

一般解の手法です。判定式に合う場合分けをそのまま表現可能です。

2. boolでFizzとBuzzを結合

Pythonは文字列*True, 文字列*Falseという判定が可能で、Trueの場合は文字列を返して、Falseの場合はFalseが返ります。また0はFalse、0以外はTrueと判定される仕様なので、文字列に計算結果の数値をかける表記が可能です。
そのため以下のような表記でFizzにもBuzzにもならなかった場合に数値を返すことができます。

for i in range(1, 101):
    print('Fizz'*(not i%3) + 'Buzz'*(not i%5) or i)

3. 配列

配列で[数値, 'Fizz', 'Buzz', 'FizzBuzz']を用意しておき、インデックス番号で選択する方法があります。

4. 文字列スライス

Pythonは文字列のスライスに'FizzBuzz'[a:b]という表記(a, bは数値)が可能で、'FizzBuzz'[0:4] → Fizz'FizzBuzz'[4:8] → Buzz'FizzBuzz'[0:8] → FizzBuzz'FizzBuzz'[4:4] → 空文字=0=Falseとすることができるので、配列のインデックス番号のように、添字を用意すれば変換可能となります。

実際のコードいろいろ

上記のバリエーションを使って書いたコードをいろいろと列挙してみます。

boolでFizzとBuzzを結合する形式

for i in range(1, 101):
    print(('' if i % 3 else 'Fizz') + ('' if i % 5 else 'Buzz') or i)

下のコードはやってることは上と一緒ですが、notを使うことで個人的に可読性が高いので好みです

for i in range(1, 101):
    print('Fizz'*(not i%3) + 'Buzz'*(not i%5) or i)

listを使うとprintの中に強引に入れ込めます(可読性悪いのでお遊び)

print('\n'.join(['Fizz'*(not i%3) + 'Buzz'*(not i%5) or str(i) for i in range(1, 101)]))

配列のインデックスを計算して参照する形式

[i+1, 'Fizz', 'Buzz', 'FizzBuzz']というlistを用意して、インデックスとなる0, 1, 2, 3を数値として計算して与えています。

for i in range(100):
    print([i+1, 'Fizz', 'Buzz', 'FizzBuzz'][i%3//2+i%5//4*2])
for i in range(1, 101):
    print([i, 'Fizz', 'Buzz', 'FizzBuzz'][3-i**2%3-i**4%5*2])

配列を入れ替えて論理演算子で書くこともできます。

for i in range(1, 101):
    print(['FizzBuzz', 'Buzz', 'Fizz', i][(i%3 and 1) + (i%5 and 2)])

配列を入れ替えずに剰余演算部にnotを付けても同じ挙動になります。

for i in range(1, 101):
    print([i, 'Fizz', 'Buzz', 'FizzBuzz'][(not i%3 and 1) + (not i%5 and 2)])

文字列をスライスする形式

やってることはシンプルです。スライス用のインデックスをどう用意するかだと思います。

for i in range(1, 101):
    print('FizzBuzz'[(4 if i % 3 else 0):(4 if i % 5 else 8)] or i)

条件分岐ではなく演算でも書けます。

for i in range(100):
    print('FizzBuzz'[4*(1-i%3//2):4*(1+i%5//4)] or i+1)

Pythonの1以上はTrue、0はFalseや下表にしめす論理演算子の仕様を使うと次のようにも書けます。

for i in range(1, 101):
    print('FizzBuzz'[i%3 and 4:i%5 and 4 or 8] or i)

abを論理演算子で判定させた結果は以下の表のようになります。
この結果からPythonでは、andは左側がTrueのときは右側の値、左側がFalseのときは左側の値を返し、orは左側がTrueのとき左側の値、左側がFalseのときは右側の値を返すことがわかります。
そのため左側に判定式、右側に数値を配置しておけば狙いの配列が得られます。
参考:https://ja.wikipedia.org/wiki/%E7%9F%AD%E7%B5%A1%E8%A9%95%E4%BE%A1

a 論理演算子 b 出力結果
True and 0 0
True and 1 1
False and 0 False
False and 1 False
True or 0 True
True or 1 True
False or 0 0
False or 1 1

for(while)文を使わない形式(map関数)

map関数は基本的にリスト内包表記で置き換えられるので、可読性的にもこういうのもあるよという位置づけな気がします。

def FizzBuzz(i):
    return 'Fizz'*(not i%3) + 'Buzz'*(not i%5) or str(i)

print('\n'.join(list(map(FizzBuzz, list(range(1, 101))))))

上のコードの関数をlambda式で書くと1行でも書けます。

print('\n'.join(list(map(lambda i:'Fizz'*(not i%3) + 'Buzz'*(not i%5) or str(i), list(range(1, 101))))))

for(while)文を使わない形式(再帰関数)

for(while)文を使わない方法としては一番すっきり書ける気がします。
100から引いていくより、判定がわかりやすいので、カウンタとして i を入れています

def loop(i, j):
    print('Fizz'*(not i%3) + 'Buzz'*(not i%5) or i)
    if i < j:
        loop(i + 1, j)

loop(1, 100)

剰余演算子%を使わない形式

除算演算子/を使っていいなら割って小数点以下が0かどうかの判定などで対応できます。

def divide(i, j):
    return not int(str(i/j).split('.')[1])

for i in range(1, 101):
    print('Fizz'*divide(i, 3) + 'Buzz'*divide(i, 5) or i)

剰余演算子%, 除算演算子/を使わない形式(配列)

Pythonはlistを掛け算すると繰り返せるので、[0, 0, 1] * 34, [0, 0, 0, 0, 1] * 20として判定用の配列を用意する方法
判定用の配列をどう使うかはすでに記載の方法でいろいろありえると思います。

i_3 = [0, 0, 1] * 34
i_5 = [0, 0, 0, 0, 1] * 20
for i in range(100):
    print('Fizz'*i_3[i] + 'Buzz'*i_5[i] or i+1)

判定用配列の数値を配列のインデックスにした場合
1から15までの配列を用意しておきます。

i_15 = [0, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 0, 0, 3] * 7
for i, j in enumerate(i_15[0:100]):
    print([i+1, 'Fizz', 'Buzz', 'FizzBuzz'][j])

ほぼ同じことを文字列でもできます。

i_15 = '001021001201003' * 7
for i, j in enumerate(i_15[0:100]):
    print([i+1, 'Fizz', 'Buzz', 'FizzBuzz'][int(j)])

判定用配列の数値を文字列スライスのインデックスにした場合

i_3 = [4, 4, 0] * 34
i_5 = [4, 4, 4, 4, 8] * 20
for i in range(100):
    print('FizzBuzz'[i_3[i]:i_5[i]] or i+1)

算術演算子を使わない形式(配列)

list(range(0, 101, 15))の形式で判定用の配列を用意する方法
各数字の倍数のリストをin演算子で比較判定しています。判定用の配列は0開始でなく、各数字でもいいですが、見やすさ優先で0開始にしています。
以下のコードだと剰余演算子%, 除算演算子/だけでなく算術演算子を1つも使用せずに書けます。

for i in range(1, 101):
    if i in list(range(0, 101, 15)):
        print('FizzBuzz')
    elif i in list(range(0, 101, 3)):
        print('Fizz')
    elif i in list(range(0, 101, 5)):
        print('Buzz')
    else:
        print(i)

for(while)文, if文, 算術演算子を使わない形式

map関数を使って、in演算子で判定、論理演算子の短絡評価を使えば、for(while)文、if文、算術演算子も使わず書けます。
15の倍数判定がある分、配列のインデックスを得るには+が使えないと少し冗長になります。

m_3 = list(range(3, 101, 3))
m_5 = list(range(5, 101, 5))

def fizzbuzz(i):
    return [str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i in m_3 and i in m_5 and 3) or (i in m_3 and 1) or (i in m_5 and 2)]

print('\n'.join(list(map(fizzbuzz, list(range(1, 101))))))

15の倍数判定がないので、文字列スライスの方がすっきり書けます。

m_3 = list(range(3, 101, 3))
m_5 = list(range(5, 101, 5))

def fizzbuzz(i):
    return 'FizzBuzz'[i not in m_3 and 4:i not in m_5 and 4 or 8] or str(i)

print('\n'.join(list(map(fizzbuzz, list(range(1, 101))))))

事前に用意した配列をmap関数で強引に配列を足し合わせて配列で照合するのもできます。

i_15 = [0, 0, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 0, 0, 3]
i_15 = sum(list(map(lambda x: i_15, list(range(7)))), [])
def FizzBuzz(i, j):
    return [str(i), 'Fizz', 'Buzz', 'FizzBuzz'][j]

print('\n'.join(list(map(FizzBuzz, list(range(1, 101)), i_15[0:100]))))

一応上記の3つの式はすべてワンライナーにすれば=もなくなります。

print('\n'.join(list(map(lambda i: 'FizzBuzz'[i not in list(range(3, 101, 3)) and 4:i not in list(range(5, 101, 5)) and 4 or 8] or str(i), list(range(1, 101))))))

変な縛り(コード中に数字を使わない形式)

コード中に数字0, 1, 2, 3, 4, 5, 6, 7, 8, 9を使わないで書く必要があるが、数字の代替となる変数を生成できれば、通常と同じになります。
縛りとして面白かったです。
https://hackmd.io/@ibarakicisUnofficial/BJY5DywnD

zero = int(not True)
one = int(True)
three = one + one + one
five = three + one + one
hundred_one = int(str(one) + str(zero) + str(one))

for i in range(one, hundred_one):
    print('Fizz'*(not i % three) + 'Buzz'*(not i % five) or i)

さいごに

いろんな縛りを設けて解く方法があるので、プログラミングしていくうえでずいぶん勉強になったなーと思います。
他にもいろんな手法があるようなので、調べてみたいです。

4
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?