0. はじめに
将棋の竜王戦の決勝トーナメントを以下に示す。一見とても不公平に見えるが,竜王戦の参加者は実力順に一組から六組までにカテゴライズされており,それらの組予選を勝ち抜いた者同士のトーナメントのため,実力上位の一組出場者から順に優遇されるという仕組みである。プロ野球のクライマックスシリーズやサッカーの天皇杯なども同様の仕組みであり,決して珍しくはないだろう。他に将棋にはパラマス方式を採用した銀河戦予選もある。
このようにトーナメント表というのは,さまざまなバリエーションが存在し得る。$n$ 人が参加するトーナメントの場合,試合数は $n - 1$ になるが,いったいどのくらいのバリエーションが考えられるのだろうか・・・と考えを巡らせていると,昔解けなかった問題を解決するヒントを思いついた。
1. 問題
昔解けなかった問題とは森博嗣氏の小説「笑わない数学者」に登場する 24 点ゲームである。最初の二つの問題の出題者は劇中の登場人物である数学者・天王寺翔蔵博士,最後の問題は西之園萌絵嬢である。
- レベルⅠ
- まず,ちょっとした算数の問題を出そう。考えたまえ。10 が二つ,4 が二つある。どんな順番でも良いから,これらを全部使って,足したり,引いたり,掛けたり,割ったりして,答を 24 にしたまえ。
- レベルⅡ
- では,今度は 7 が二つ, 3 が二つだ。この四つの数で同じように 24 を作りたまえ。
- レベルⅢ
- 私ね,問題考えたの。難しいわよ。8,8,3,3 で,24 を作れます?
中国で 24 点ゲームというトランプを用いたゲームが元ネタのようだ。1~10 までのカードから四枚抽出し,四則演算を駆使して 24 を作ることを競い合う。数字が 1~10 に限られているのは,中国も日本と同じく九九を学ぶので一桁の掛け算は容易にできるからだろう。10 の掛け算も容易なのは言うまでもない。
2. まずは手計算で解く
十年ぶりの再挑戦である。ちなみに十年前は一問も解けなかった。
2.1 レベルⅠ
10, 10, 4, 4 で 24 を作る。試しに全部足してみると
10 + 10 + 4 + 4 = 28 \tag{1-1}
となり,24 より大きな数字になるため引き算か割り算が必要となる。とはいえ単純な引き算では上手く行かない。
\begin{aligned}
10 - 10 + 4 + 4 &= 8 \\
10 + 10 + 4 - 4 &= 20 \\
\end{aligned} \tag{1-2}
次に割り算を使ってみよう。$10/4$ も $4/10$ も整数にならないが
\frac{10 \times 10}{4} = 25 \tag{1-3}
であればいい感じの(24 に近い)整数になる。ここから 1 を引けば 24 になるので
\frac{10 \times 10}{4} - 1 = \bbox[4pt, border:2px dotted blue]{\frac{10 \times 10 - 4}{4} = 24} \tag{1-4}
となる。
2.2 レベルⅡ
7, 7, 3, 3 で 24 を作る。試しに全部足してみると
7 + 7 + 3 + 3 = 20 \tag{2-1}
となり,24 より小さな数字になるため「掛け算が必要」と考えた。試しに掛け算を使ってみると
7 \times 3 = 21 \tag{2-2}
となる。これに 3 を加えれば 24 が得られるので
7 \times 3 + 3 = \bbox[4pt, border:2px dotted blue]{7 \times \left( 3 + \frac{3}{7} \right) = 24} \tag{2-3}
となる。
2.3 レベルⅢ
8, 8, 3, 3 で 24 を作る。試しに全部足してみると
8 + 8 + 3 + 3 = 22 \tag{3-1}
となり,24 より小さな数字になるため「掛け算が必要」と考えた。試しに掛け算を使ってみると
8 \times 3 = 24 \tag{3-2}
となるが,これでちょうど良いのが困る。0 を足す,または 1 を掛けるにしても残る 8 と 3 では 0 も 1 も作れないからだ。この後 $8 \times 8$ や $3 \times 3$ をベースにして考えてみたがダメだった。実は「掛け算が必要」という前提が間違っているため,このまま考えても正解に辿り着くことは出来なかったのだ。
3. 計算機による力業で解く
ということで計算機による力業で解いてみよう。
まず組み合わせのパターンを求める。加減乗除の四則演算は全て二項演算,すなわち入力が二つ,出力が一つの関数であると考えると,二人のうち一人が敗退して一人が勝ち抜くトーナメントと同じように考えられる。トーナメントといってもフラットなものからシード付きのもの,将棋の竜王戦の決勝トーナメントのような変則トーナメントまで様々あるが,エントリーが四人だと以下の五種類しかないことが分かる。
演算子ⓧⓨⓩは加減乗除のいずれかであり,ABCD に当てはめる数字も後で決めるので,ここではトーナメントの種類(形)だけ考えれば良い。
これを数式で表すと以下のようになる。またプログラムへ実装し易いよう逆ポーランド記法で表すと以下のようになる。
No | 数式 | 逆ポーランド記法 |
---|---|---|
1 | ${\bf A}\ ⓧ\ ({\bf B}\ ⓨ\ ({\bf C}\ ⓩ\ {\bf D}))$ | ${\bf A}\ {\bf B}\ {\bf C}\ {\bf D}\ ⓩ\ ⓨ\ ⓧ$ |
2 | ${\bf A}\ ⓧ\ (({\bf B}\ ⓨ\ {\bf C})\ ⓩ\ {\bf D})$ | ${\bf A}\ {\bf B}\ {\bf C}\ ⓨ\ {\bf D}\ ⓩ\ ⓧ$ |
3 | $({\bf A}\ ⓧ\ {\bf B})\ ⓨ\ ({\bf C}\ ⓩ\ {\bf D})$ | ${\bf A}\ {\bf B}\ ⓧ\ {\bf C}\ {\bf D}\ ⓩ\ ⓨ$ |
4 | $(({\bf A}\ ⓧ\ {\bf B})\ ⓨ\ {\bf C})\ ⓩ\ {\bf D}$ | ${\bf A}\ {\bf B}\ ⓧ\ {\bf C}\ ⓨ\ {\bf D}\ ⓩ$ |
5 | $({\bf A}\ ⓧ\ ({\bf B}\ ⓨ\ {\bf C}))\ ⓩ\ {\bf D}$ | ${\bf A}\ {\bf B}\ {\bf C}\ ⓨ\ ⓧ\ {\bf D}\ ⓩ$ |
四つの数字のうち同じ数字を二つずつ用いる場合の組み合わせは六種類しかない。
No | ${\bf A}$ | ${\bf B}$ | ${\bf C}$ | ${\bf D}$ |
---|---|---|---|---|
1 | $\color{red}{p}$ | $\color{red}{p}$ | $q$ | $q$ |
2 | $\color{red}{p}$ | $q$ | $\color{red}{p}$ | $q$ |
3 | $\color{red}{p}$ | $q$ | $q$ | $\color{red}{p}$ |
4 | $q$ | $\color{red}{p}$ | $\color{red}{p}$ | $q$ |
5 | $q$ | $\color{red}{p}$ | $q$ | $\color{red}{p}$ |
6 | $q$ | $q$ | $\color{red}{p}$ | $\color{red}{p}$ |
トーナメント(演算の組み合わせ)が五種類,数値の組み合わせが六種類,演算子ⓧⓨⓩは加減乗除のいずれかであるから $4^3 = 64$ 通り,以上より $5 \times 6 \times 64 = 1920$ 通りのパターンを網羅すれば良いことが分かる。
加算と乗算については交換法則が成り立つので,うまくやればパターン数をもっと絞れると思うが,今回の記事ではそこまで踏み込まないことにする。
4. プログラム実装
4.1 数式の網羅
まず数式の組み合わせ(トーナメント)は五種類しかない。数値 A,B,C,D
および演算子 x,y,z
は後ほど文字列置換を行う。
var eq = [
"A B C D z y x",
"A B C y D z x",
"A B x C D z y",
"A B x C y D z",
"A B C y x D z"
];
演算子は四則演算の四種類である。
var op = ["+", "-", "*", "/"];
二つの整数 p, q
より数式の組み合わせパターンを網羅すると以下のようになる。
var list = [];
for(var h = 0; h < eq.length; h++) {
for(var i = 0; i < op.length; i++) {
for(var j = 0; j < op.length; j++) {
for(var k = 0; k < op.length; k++) {
var s = eq[h];
s = s.replace("x", op[i]);
s = s.replace("y", op[j]);
s = s.replace("z", op[k]);
if(p == q) {
list.push(calc(s, p, p, p, p));
} else {
list.push(calc(s, p, p, q, q));
list.push(calc(s, p, q, p, q));
list.push(calc(s, p, q, q, p));
list.push(calc(s, q, p, p, q));
list.push(calc(s, q, p, q, p));
list.push(calc(s, q, q, p, p));
}
}
}
}
}
実際に数値 A,B,C,D
を置換して計算する関数 calc()
を示す。関数 eval_rpn()
は逆ポーランド記法の計算を行う。※RPN: Reverse Polish Notation
var calc = function(s, a, b, c, d) {
s = s.replace("A", a);
s = s.replace("B", b);
s = s.replace("C", c);
s = s.replace("D", d);
var v = eval_rpn(s);
return {val:v, eq:s};
};
4.2 逆ポーランド記法の演算
逆ポーランド記法の計算はとても簡単に実装できる。入力文字列のパースを行って数値が来るとスタックに積み,演算子が来るとスタックから二つの数値を取り出して演算し,結果を再びスタックに積めば良いだけだ。※単項演算子のマイナス -
は考えないものとする。
function eval_rpn(s) {
var a = s.split(" ");
var n, x, y, z;
var ret = [];
for(var i = 0; i < a.length; i++) {
var s = a[i];
/*--*/ if(s == "+") {
y = ret.pop(); x = ret.pop(); z = x.add(y); ret.push(z);
} else if(s == "-") {
y = ret.pop(); x = ret.pop(); z = x.sub(y); ret.push(z);
} else if(s == "*") {
y = ret.pop(); x = ret.pop(); z = x.mul(y); ret.push(z);
} else if(s == "/") {
y = ret.pop(); x = ret.pop(); z = x.div(y); ret.push(z);
} else {
n = parseInt(s); z = new Fraction(n, 1); ret.push(z);
}
}
return ret.pop().toString();
}
4.3 分数計算
浮動小数点演算で実装すると,結果として整数を得られるはずでも丸め誤差によって整数を得られない場合がある。このため分数計算を行うクラスを実装した。とはいっても四則演算のルールとはとてもシンプルである。
\left\{ \begin{aligned}
\frac{a}{b} + \frac{c}{d} &= \frac{ad + bc}{bd} \\
\frac{a}{b} - \frac{c}{d} &= \frac{ad - bc}{bd} \\
\frac{a}{b} \times \frac{c}{d} &= \frac{ac}{bd} \\
\frac{a}{b} \div \frac{c}{d} &= \frac{ad}{bc} \\
\end{aligned} \right. \tag{4-1}
有理数を分子 num
と分母 den
の二つの整数で保持し,コンストラクタで約分を行うようにした。なお分母 den
は基本的に正の値となるように扱うが,計算過程でゼロになってしまう場合もある。その場合も例外を発生させずに計算を継続すること自体は可能である。
分母 $\text{den}$ | ||||
---|---|---|---|---|
$\text{den} > 0$ | $\text{den} = 0$ | |||
分子 $\text{num}$ | $\text{num} > 0$ | 約分する (正の有理数) |
$\text{num} = 1,\quad\text{den} = 0$ (正の無限大) |
|
$\text{num} = 0$ | $\text{num} = 0,\ \text{den} = 1$ (ゼロ) |
$\text{num} = 0,\quad\text{den} = 0$ (不能) |
||
$\text{num} < 0$ | 約分する (負の有理数) |
$\text{num} = -1,\ \text{den} = 0$ (負の無限大) |
||
正の∞は $1/0$,負の∞は $-1/0$,ゼロは $0/1$ として内部データを保持するという意味である。
function Fraction(num, den) {
//----------------------------------------------------------
// コンストラクタ ※同時に約分を行う
//----------------------------------------------------------
if(den == 0) {
/**/ if(num > 0) num = 1;
else if(num < 0) num = -1;
} else if(num == 0) {
den = 1;
} else {
var neg = false;
if(num < 0) {
num = -num;
neg = true;
}
for(var q = 2; q <= den; q++) {
while(num % q == 0 && den % q == 0) {
num /= q;
den /= q;
}
}
if(neg) num = -num;
}
this.num = num;
this.den = den;
//----------------------------------------------------------
// 加算
//----------------------------------------------------------
this.add = function(x) {
var n = this.num * x.den + x.num * this.den;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 減算
//----------------------------------------------------------
this.sub = function(x) {
var n = this.num * x.den - x.num * this.den;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 乗算
//----------------------------------------------------------
this.mul = function(x) {
var n = this.num * x.num;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 除算
//----------------------------------------------------------
this.div = function(x) {
var n = this.num * x.den;
var d = this.den * x.num;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 文字列変換
//----------------------------------------------------------
this.toString = function() {
if(this.den == 1)
return this.num.toString();
else
return this.num + "/" + this.den;
};
}
無限大∞の計算について,現在の実装では無限大同士の加算を行うと $0/0$ になってしまう。
\frac{1}{0} + \frac{1}{0} = \frac{1 \cdot 0 + 0 \cdot 1}{0 \cdot 0} = \frac{0}{0} \tag{4-2}
本来,分母が共通であるから
\frac{1}{0} + \frac{1}{0} = \frac{1 + 1}{0} = \frac{2}{0} = \frac{1}{0} \tag{4-3}
としたいところだが,今回の問題(四つの自然数による演算)では無限大同士の演算は生じないため手を抜いている。
4.4 実装コード
今回,ささっと手軽に作れる JavaScript で組んだ。
JavaScript プログラム game24.js の実装コードはコチラ
var args = WScript.Arguments.Unnamed;
var ret = main(args);
try {
WScript.Quit(ret);
} catch(e) {
/* 何もしない */
}
//--------------------------------------------------------------
// メイン関数
//--------------------------------------------------------------
function main(args) {
//----------------------------------------------------------
// ヘルプメッセージ
//----------------------------------------------------------
if(args.Count < 2) {
WScript.Echo("二つの数値を二回ずつ用い,四則演算によって得られる数値を求めます。");
WScript.Echo("");
WScript.Echo("GAME24(.JS) [数値(1)] [数値(2)]");
return false;
}
//----------------------------------------------------------
// 数値の読み込み
//----------------------------------------------------------
var p = parseInt(args(0));
var q = parseInt(args(1));
//----------------------------------------------------------
// 数式の組み合わせ(逆ポーランド記法)
//----------------------------------------------------------
var eq = [
"A B C D z y x",
"A B C y D z x",
"A B x C D z y",
"A B x C y D z",
"A B C y x D z"
];
//----------------------------------------------------------
// 数式を計算する
//----------------------------------------------------------
var calc = function(s, a, b, c, d) {
s = s.replace("A", a);
s = s.replace("B", b);
s = s.replace("C", c);
s = s.replace("D", d);
var v = eval_rpn(s);
return { val:v, eq:s };
};
//----------------------------------------------------------
// 数式の組み合わせパターンを網羅する
//----------------------------------------------------------
var op = ["+", "-", "*", "/"];
var list = [];
for(var h = 0; h < eq.length; h++) {
for(var i = 0; i < op.length; i++) {
for(var j = 0; j < op.length; j++) {
for(var k = 0; k < op.length; k++) {
var s = eq[h];
s = s.replace("x", op[i]);
s = s.replace("y", op[j]);
s = s.replace("z", op[k]);
if(p == q) {
list.push(calc(s, p, p, p, p));
} else {
list.push(calc(s, p, p, q, q));
list.push(calc(s, p, q, p, q));
list.push(calc(s, p, q, q, p));
list.push(calc(s, q, p, p, q));
list.push(calc(s, q, p, q, p));
list.push(calc(s, q, q, p, p));
}
}
}
}
}
//----------------------------------------------------------
// 計算結果を出力する
//----------------------------------------------------------
for(var i = 0; i < list.length; i++)
WScript.Echo([list[i].val, list[i].eq].join("\t"));
return true;
}
//--------------------------------------------------------------
// 逆ポーランド記法の計算 ※返値は文字列である
//--------------------------------------------------------------
function eval_rpn(s) {
var a = s.split(" ");
var n, x, y, z;
var ret = [];
for(var i = 0; i < a.length; i++) {
var s = a[i];
/*--*/ if(s == "+") {
y = ret.pop(); x = ret.pop(); z = x.add(y); ret.push(z);
} else if(s == "-") {
y = ret.pop(); x = ret.pop(); z = x.sub(y); ret.push(z);
} else if(s == "*") {
y = ret.pop(); x = ret.pop(); z = x.mul(y); ret.push(z);
} else if(s == "/") {
y = ret.pop(); x = ret.pop(); z = x.div(y); ret.push(z);
} else {
n = parseInt(s); z = new Fraction(n, 1); ret.push(z);
}
}
return ret.pop().toString();
}
//--------------------------------------------------------------
// 分数クラス
//--------------------------------------------------------------
function Fraction(num, den) {
//----------------------------------------------------------
// コンストラクタ(約分)
//----------------------------------------------------------
if(den == 0) {
/**/ if(num > 0) num = 1;
else if(num < 0) num = -1;
} else if(num == 0) {
den = 1;
} else {
var neg = false;
if(num < 0) {
num = -num;
neg = true;
}
for(var q = 2; q <= den; q++) {
while(num % q == 0 && den % q == 0) {
num /= q;
den /= q;
}
}
if(neg) num = -num;
}
this.num = num;
this.den = den;
//----------------------------------------------------------
// 加算
//----------------------------------------------------------
this.add = function(x) {
var n = this.num * x.den + x.num * this.den;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 減算
//----------------------------------------------------------
this.sub = function(x) {
var n = this.num * x.den - x.num * this.den;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 乗算
//----------------------------------------------------------
this.mul = function(x) {
var n = this.num * x.num;
var d = this.den * x.den;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 除算
//----------------------------------------------------------
this.div = function(x) {
var n = this.num * x.den;
var d = this.den * x.num;
return new Fraction(n, d);
};
//----------------------------------------------------------
// 文字列変換
//----------------------------------------------------------
this.toString = function() {
if(this.den == 1)
return this.num.toString();
else
return this.num + "/" + this.den;
};
}
chakra.cmd は Edge の JavaScript エンジンを用いてを高速実行するバッチファイルである。もちろん使わなくても動くが,使ったほうが精神衛生上良いだろう。
chakra.cmd の実装コードはコチラ
@echo off
setlocal
if "%1"=="" goto USAGE
rem ----------------------------------------------------------------------------
rem スクリプトファイルの拡張子が省略された場合 .JS とする
rem ----------------------------------------------------------------------------
if "%~x1"=="" (
set SCRIPTFILE=%~1.JS
) else (
set SCRIPTFILE=%~1
)
rem ----------------------------------------------------------------------------
rem スクリプトファイルが存在する場合
rem ----------------------------------------------------------------------------
if exist %SCRIPTFILE% (
set SCRIPTPATH=%SCRIPTFILE%
goto SKIP
)
rem ----------------------------------------------------------------------------
rem スクリプトファイルをパスから検索する
rem ----------------------------------------------------------------------------
for %%I in ( %SCRIPTFILE% ) do set SCRIPTPATH=%%~$PATH:I
if "%SCRIPTPATH%"=="" (
echo %SCRIPTFILE% が見つかりません!!
exit /b
)
:SKIP
rem ----------------------------------------------------------------------------
rem スクリプトファイルのオプション解析
rem ----------------------------------------------------------------------------
set SCRIPTARGS=%SCRIPTPATH%
:LOOP
if "%~2"=="" goto BREAK
set SCRIPTARGS=%SCRIPTARGS% %2
shift /2
goto LOOP
:BREAK
rem ----------------------------------------------------------------------------
rem スクリプトの実行
rem ----------------------------------------------------------------------------
cscript //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} %SCRIPTARGS%
exit /b
:USAGE
rem ----------------------------------------------------------------------------
rem ヘルプメッセージ
rem ----------------------------------------------------------------------------
echo Microsoft Edge の chakra エンジンを用いて Javascript を高速実行します。
echo.
echo CHAKRA(.CMD) [スクリプトファイル名(.JS)] [スクリプトオプション] ...
echo.
echo スクリプトファイルの拡張子を省略すると .JS とします。
exit /b
5. 実行例
引数を何も指定しないで実行するとヘルプメッセージが表示される。
chakra game24.js
二つの数値を二回ずつ用い,四則演算によって得られる数値を求めます。
GAME24(.JS) [数値(1)] [数値(2)]
5.1 レベルⅠの答え
レベルⅠの問題を解かせると下記のようになる。なお findstr
コマンドの引数の文字列の最後の文字はスペースではなくタブ文字である。コマンドプロンプトからタブ文字をうまく入力できない場合はバッチファイルを作成するなどして対応して頂きたい。
chakra game24.js 10 4 | findstr /R /C:"^24 "
24 10 10 * 4 - 4 /
で,実行すると答えが 24 になる数式が得られる。逆ポーランド記法を解釈すると以下のような数式になる。イヤコレ,1920 通りもあるのに答えは一種類しかないのか・・・
24 = \frac{10 \times 10 - 4 }{4} \tag{5-1}
5.2 レベルⅡの答え
答えは四種類出てくるが,実質同じである。
chakra game24.js 7 3 | findstr /R /C:"^24 "
24 7 3 3 7 / + *
24 7 3 7 / 3 + *
24 3 7 / 3 + 7 *
24 3 3 7 / + 7 *
逆ポーランド記法を通常の数式に変換すると分かり易い。
\left\{ \begin{aligned}
24 &= 7 \times \left( 3 + \frac{3}{7} \right) \\
24 &= 7 \times \left( \frac{3}{7} + 3 \right) \\
24 &= \left( \frac{3}{7} + 3 \right) \times 7 \\
24 &= \left( 3 + \frac{3}{7} \right) \times 7
\end{aligned} \right. \tag{5-2}
5.3 レベルⅢの答え
イヤコレ,暗算で解ける人は天才だろうな。
chakra game24.js 8 3 | findstr /R /C:"^24 "
24 8 3 8 3 / - /
答えを見ると啞然とする。掛け算を使わないで,二回の割り算と引き算だけで四個の数字の総和 22 よりも大きな数を作ることが出来てるのだ。この問題のトリックは鮮やかで感動してしまう。
24 =\frac{8}{3 - \displaystyle \frac{8}{3}} \tag{5-3}
6. まとめ
10 以下の自然数 $p,q$ を二つずつ用いて 24 を作ることの出来るパターン数 $N_{24}(p,q)$ を求めた。ちなみにパターン数をカウントするときは以下のように find
コマンドを使うとよい。
chakra game24 8 4 | findstr /R /C:"^24 " | find /C /V ""
54
パターン数 $N_{24}(p,q) = N_{24}(q,p)$ であるから $p \le q$ となる範囲のみ示す。なお,小説内で出題された個所を赤色 #FF0000
で示す。これ以外に 24 を得られるパターンが一つしかない組み合わせを青色 #0000FF
で示す。
$p$ | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ||
$q$ | 1 | 0 | |||||||||
2 | 0 | 0 | |||||||||
3 | 0 | 16 | 2 | ||||||||
4 | 12 | 12 | 8 | 6 | |||||||
5 | 19 | 13 | 1 | 13 | 1 | ||||||
6 | 6 | 20 | 4 | 0 | 11 | 7 | |||||
7 | 0 | 8 | 4 | 2 | 36 | 0 | 0 | ||||
8 | 4 | 20 | 1 | 54 | 1 | 8 | 0 | 0 | |||
9 | 0 | 0 | 38 | 0 | 1 | 0 | 0 | 0 | 0 | ||
10 | 0 | 36 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
このうち 24 を作ることの出来るパターンの一例を示す。青色 #0000FF
は組み合わせが一つしかないが,比較的単純なパターンである。当然のことながら著者の森博嗣氏は敢えて難しいところを選んでいることが分かる。
$\!q$ | $\!p$ | |||||
---|---|---|---|---|---|---|
$\!1$ | $\!2$ | $\!3$ | $\!4$ | $\!5$ | $\!6$ | |
$\!1$ | $\!-$ | |||||
$\!2$ | $\!-$ | $\!-$ | ||||
$\!3$ | $\!-$ | $\!2\!\cdot\!2(3\!+\!3)$ | $\!3\!\cdot\!3\!\cdot\!3\!-\!3$ | |||
$\!4$ | $\!4(4\!+\!1\!+\!1)$ | $\!2(4\!+\!2\!\cdot\!4)$ | $\!3\!\cdot\!4\!+\!3\!\cdot\!4$ | $\!4\!+\!4\!+\!4\!\cdot\!4$ | ||
$\!5$ | $\!(5\!-\!1)(5\!+\!1)$ | $\!2(2\!+\!5\!+\!5)$ | $\!\color{blue}{5\!\cdot\!5\!-\!\displaystyle\frac{3}{3}}$ | $\!4(5\!+\!5\!-\!4)$ | $\!\color{blue}{5\!\cdot\!5\!-\!\displaystyle \frac{5}{5}}$ | |
$\!6$ | $\!6(6\!-\!1\!-\!1)$ | $\!2\!\cdot\!6\!+\!2\!\cdot\!6$ | $\!3\!\left(\!6\!+\!\displaystyle\frac{6}{3}\!\right)$ | $\!-$ | $\!6(5\!+\!5\!-\!6)$ | $\!\,\,\,6\!+\!6$ $\!+\!6\!+\!6$ |
$\!7$ | $\!-$ | $\!2(7\!+\!7\!-\!2)$ | $\!\color{red}{7\!\left(\!3+\!\displaystyle\frac{3}{7}\!\right)}$ | $\!7\!\left(\!4\!-\!\displaystyle\frac{4}{7}\!\right)$ | $\!\,\,\,5\!+\!5$ $\!+\!7\!+\!7$ |
$\!-$ |
$\!8$ | $\!8\!+\!8(1\!+\!1)$ | $\!2\!\cdot\!2\!\cdot\!8\!-\!8$ | $\!\color{red}{\displaystyle\frac{8}{3\!-\!\displaystyle\frac{8}{3}}}$ | $\!\,\,\,4\!+\!4$ $\!+\!8\!+\!8$ |
$\!\color{blue}{5\!\cdot\!5\!-\!\displaystyle\frac{8}{8}}$ | $\!\displaystyle\frac{8\!\cdot\!6}{8\!-\!6}$ |
$\!9$ | $\!-$ | $\!-$ | $\!\,\,\,3\!+\!3$ $\!+\!9\!+\!9$ |
$\!-$ | $\!\color{blue}{5\!\cdot\!5\!-\!\displaystyle\frac{9}{9}}$ | $\!-$ |
$\!10$ | $\!-$ | $\!\,\,\,2\!+\!2$ $\!+\!10\!+\!10$ |
$\!-$ | $\!\color{red}{\displaystyle\frac{10\!\cdot\!10\!-\!4}{4}}$ | $\!\color{blue}{5\!\cdot\!5\!-\!\displaystyle\frac{10}{10}}$ | $\!-$ |
さて,なぜ「24点」ゲームなのかを考えてみる。10 以下の自然数 $p,q$ を二つずつ用いて作ることが出来る整数と,それらを実現できる $(p,q)$ の組み合わせ数を以下に示す。
ある程度組み合わせ数が多くないとゲームとしては成立しないであろうから「24点」はいい感じの設定である。表4を見て分かるように 24 を作ることのできる $(p,q)$ の組み合わせは 31 通りあり,図2を見ると 24 の前後では極大値になっている。だが,他にも 20 や 30 の組み合わせも多いことに気づく。※約数が多いと組み合わせ数も多くなるようだ。
しかし 20 を作ることのできる組み合わせ 35 のうち,パターン数が一つしかないものは存在しない。また,パターン数が二つしかないもの $(p,q)=\lbrace(5,6),\ (5,7),\ (5,8),\ (5,9)\rbrace$ も決して難しくはないだろう。
\left\{ \begin{aligned}
5 \times \left(5 - \frac{6}{6}\right) &= 20 \\
5 \times \left(5 - \frac{7}{7}\right) &= 20 \\
5 \times \left(5 - \frac{8}{8}\right) &= 20 \\
5 \times \left(5 - \frac{9}{9}\right) &= 20
\end{aligned} \right. \tag{6-1}
$p$ | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ||
$q$ | 1 | 0 | |||||||||
2 | 0 | 0 | |||||||||
3 | 0 | 12 | 0 | ||||||||
4 | 60 | 40 | 8 | 4 | |||||||
5 | 32 | 6 | 8 | 26 | 7 | ||||||
6 | 0 | 40 | 4 | 41 | 2 | 0 | |||||
7 | 0 | 0 | 30 | 4 | 2 | 0 | 0 | ||||
8 | 0 | 46 | 0 | 20 | 2 | 8 | 0 | 0 | |||
9 | 30 | 4 | 0 | 4 | 2 | 0 | 0 | 0 | 0 | ||
10 | 120 | 73 | 71 | 77 | 71 | 77 | 69 | 69 | 69 | 31 |
同様に 30 を作ることのできる組み合わせ 32 のうち,パターン数が一つしかないものは僅か一つのみ $(p,q) = (3,10)$ である。10, 10, 3, 3 で 30 を作るのは難しいが,本記事を読破した方なら分かるであろう。
\frac{10}{\displaystyle \frac{10}{3} - 3} = 30 \tag{6-2}
ちなみに足し算と掛け算は交換法則が成り立つので順番を入れ替えただけのパターンが存在するため,足し算と掛け算のみから成るものはパターン数が多くなる。逆に引き算と割り算は交換法則が成り立たないのでパターン数が少ないものは引き算と割り算を使っている。
$p$ | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ||
$q$ | 1 | 0 | |||||||||
2 | 0 | 0 | |||||||||
3 | 0 | 24 | 4 | ||||||||
4 | 0 | 6 | 0 | 0 | |||||||
5 | 60 | 24 | 24 | 4 | 4 | ||||||
6 | 30 | 22 | 10 | 6 | 26 | 2 | |||||
7 | 0 | 4 | 0 | 6 | 6 | 2 | 0 | ||||
8 | 0 | 6 | 0 | 4 | 4 | 2 | 30 | 0 | |||
9 | 0 | 0 | 16 | 0 | 4 | 32 | 0 | 0 | 0 | ||
10 | 4 | 14 | 1 | 2 | 52 | 2 | 0 | 0 | 0 | 0 |
ということで 24 点ゲームは 24 を作ることができる組み合わせ自体は多いが,その中に高難度のものが含まれるという点で絶妙な設定だと思われる。「笑わない数学者」でのルールは,二つの数字を二回ずつ用いるという亜流バージョンではあるが,おそらくオリジナルの 24 点ゲームも同様の傾向ではないかと思われる。
7. 参考文献
-
森博嗣,笑わない数学者 MATHEMATICAL GOODBYE,1996年
※自分が持っているのは2011年発行の第36刷(文庫版)である - 中国で人気のある算数ゲーム「24点」の紹介,神戸大学経済研究所,2021年
- javascriptで分数クラス - Qiita
- 【C++】分数クラスのすすめ+無限大への拡張【競プロ】- Qiita
- C++で分数class - Qiita
- 逆ポーランド記法を利用した数式の計算(1) 逆ポーランド記法の計算 - Qiita
- 逆ポーランド記法とスタック - Qiita
- スタック&逆ポーランド記法 - Qiita
- Javascriptで逆ポーランド記法の式生成と計算 - Qiita
- JavaScriptでなんちゃって演算子オーバーロード---四則演算子 - Qiita