15年以上前、Androidアプリで「数当てマジック」を体験できるアプリを作ったことがあります。
……が、評判があまりに悪く、そっと葬りました。
今回はそのアプリを弔う意味も込めて、数当てマジックのタネ明かしをしてみます。
想定シナリオ
マジシャン(以下、ま):「好きな2桁の数字を頭の中でイメージしてください。」
客:「はい」
ま:「その数字を3で割ったときの余りはいくつですか?」
客:「2です。」
ま:「その数字を5で割ったときの余りはいくつですか?」
客:「3です。」
ま:「その数字を7で割ったときの余りはいくつですか?」
客:「4です。」
ま:「……その数字は53ですね。」
客:「おぉ、すご~い!」
という感じですが、実際には7で割ったときの余りを即答できる人なんてほぼいないので、お客さんは(実はマジシャンも)電卓必須のマジックです。
タネ:計算する式
お客さんが考えた2桁の数字を$x$とします。
$x$を3で割った余り、5で割った余り、7で割った余りをそれぞれ$\alpha$、$\beta$、$\gamma$とおきます。
このとき、次の式を計算します。
$$70\times \alpha+21\times \beta+15\times\gamma \tag{1} $$
この式から得られた値を105未満になるまで105を引き続ける(つまり105で割った余りを取る)と、その余りが$x$になります。
例えば上の例だと
$70\times2+21\times3+15\times4 = 263$
105未満になるまで、105で繰り返し引きます。
$263 - 105 = 158$
$158 - 105 = 53$
$53$ が出てきましたね。
なぜ当たるのか?
まずポイントは
$3\times5\times7=105$
という積です。
次は式(1)の係数の作り方に着目すると
3の余りである$\alpha$の係数70は$7\times5\times2$
5の余りである$\beta$の係数21は$3\times7$
7の余りである$\gamma$の係数15は$3\times5$
そして係数をすべて足すと106になります。
まだ見えにくいと思うので、全部式で書き下します。
愚直に証明してみる
ある数$x$を3で割ったときの商、5で割ったときの商、7で割ったときの商をそれぞれ$a, b, c$とします。
式(1)の結果から105を引いた回数を$d$とします。
すると次の4つの式ができます。
$$
\begin{equation}
3a+\alpha=x \tag{2}
\end{equation}
$$$$\begin{equation}
5b+\beta=x \tag{3}
\end{equation}
$$$$\begin{equation}
7c+\gamma=x \tag{4}
\end{equation}
$$$$\begin{equation}
70\alpha+21\beta+15\gamma-105d=x \tag{5}
\end{equation}
$$
(2)?(4)の式を(5)に代入します。
\displaylines{
70x-210a+21x-105b+15x-105c-105d=x \\
\Leftrightarrow106x-210a-105b-105c-105d=x \\
\Leftrightarrow105x=210a-105b-105c-105d
}
お、なんか全体が105で割り切れる!美しい!
と思ったのは私だけでしょうか。
右辺が105で割り切れるので、左辺の$105??$も当然105で割り切れます。
つまり式(5)は、105で割った余りが必ず$??$になるように設計されているわけです。
応用:3桁にも拡張できる
このアイデアは、お客さんが考える数字を2桁に制限する必要はありません。
たとえば 3, 5, 7, 11で割った余りから数を当てる場合、扱える範囲は 1155未満(=3×5×7×11) の数字まで広がります。
ちなみに式は3, 5, 7, 11で割った余りをそれぞれ$\alpha,\beta,\gamma,\delta$とすると、
$385\alpha+231\beta+330\gamma+210\delta$で得られた答えを1155で繰り返し引けば導けます。
検証スクリプト
Pythonで検証してみました。
検証結果
? uv run main.py
No error found
検証スクリプト
def main():
"""数あてゲームの核となる式が正しいか全範囲で検証する"""
for i in range(1, 1154):
if estimate_number(i) != i:
print(f"Error: {i} -> {estimate_number(i)}")
break
print("No error found")
def estimate_number(number: int) -> int:
"""
中国剰余定理を使って、各法での剰余から元の数を復元する
Args:
number: 復元したい数(1155未満である必要がある)
Returns:
復元された数
"""
if number >= 3 * 5 * 7 * 11:
raise ValueError("Number is too large")
ans = (
385 * (number % 3)
+ 231 * (number % 5)
+ 330 * (number % 7)
+ 210 * (number % 11)
) % 1155
return ans
if __name__ == "__main__":
main()
……いや、誰がこんなの暗算したいの?という話で。
だからアプリにしたんですが、
「そもそも11で割った余りなんて求めたくない」
と言われてしまいました。悲しい。
今回は素数の組み合わせでしたが本当は互いに素な数の組み合わせであれば、同じようなマジックが可能です。
(誰も計算したくないと思いますけど)
ちなみにこのトピックが面白いと思った方は「中国の剰余定理(Chinese Remainder Theorem)」で検索してみてください。
たぶんもっと楽しいです。