今さら森博嗣「笑わない数学者」のビリヤードの問題を解く(3)～計算量オーダーを考える

# 0. はじめに

前回の記事で森博嗣の「笑わない数学者」における通称「ビリヤードの問題」を一般化した問題について $n \le 17$ まで計算した結果を示した。不思議なことにボールが一個増えるごとに計算時間が綺麗に十倍になり，計算量のオーダーは $O(10^n)$ に見えたが，計算できた範囲が極めて狭かったので $n$ が大きくなると巡回セールスマン問題のように $O(n!)$ のオーダーになる可能性も否定できない。

ということで本問題の計算量オーダーについて考えてみる。

https://qiita.com/TETSURO1999/items/be4a8b460a690a48236b?1

# 1. 計算量オーダーを考える

とはいったが闇雲に考えても手が出ないので，とりあえず探索空間の広さについて考えてみる。一般化したビリヤードの問題を解くプログラムでは，可能な限りボールの数値の取りうる範囲を狭めることによって探索範囲を限定し，コストの高い重複チェックの回数を減らしている。

それでは，この探索空間の広さはどうなっているのだろうか？探索空間とは，ボールの個数 $n$ としたとき，全てのボールの数値の総和 $\sigma = n(n - 1) + 1$ となり，かつ１と２の玉を必ず含んでいるボールの組み合わせのことだと考える。※ここではボールの配置（順番）は問わない。

手計算で組み合わせ（数）を調べた結果を以下に示す。調べ方はそんなに難しくはない。例えばボールの個数が五個のとき，まず最初の $b_1$ ～ $b_4$ は最小値である１，２，３，４を並べ，残りの $b_5$ は総和が２１になるように１１とする。次に $b_4 < b_5$ が成立する範囲で $b_4$ を一つずつ増やし，$b_5$ は一つずつ減らしていく。これ以上 $b_4$ が増やせなくなると，$b_3$ を一つ増やし，$b_4 = b_3 + 1$ とする。$b_5$ は総和が２１になるように調整し，再度 $b_4 < b_5$ が成立する範囲で $b_4$ を一つずつ増やし，$b_5$ は一つずつ減らしていく。

<table>
<caption>表１　ボールの個数と組み合わせ</caption>
<thead>
<tr><th style="text-align:center">ボールの個数</th>
    <th style="text-align:center">総和</th>
    <th style="text-align:center">組み合わせ数</th>
    <th style="text-align:center">組み合わせ</th></tr>
</thead>
<tbody>
<tr><td style="text-align:right">1</td>
    <td style="text-align:right">1</td>
    <td style="text-align:right">1</td>
    <td>1</td></tr>
<tr><td style="text-align:right">2</td>
    <td style="text-align:right">3</td>
    <td style="text-align:right">1</td>
    <td>1 2</td></tr>
<tr><td style="text-align:right">3</td>
    <td style="text-align:right">7</td>
    <td style="text-align:right">1</td>
    <td>1 2 4</td></tr>
<tr><td rowspan=3 style="text-align:right">4</td>
    <td rowspan=3 style="text-align:right">13</td>
    <td rowspan=3 style="text-align:right">2</td>
    <td>1 2 3 7</td></tr>
<tr></tr><tr><td>1 2 4 6</td></tr>
<tr><td rowspan=13 style="text-align:right">5</td>
    <td rowspan=13 style="text-align:right">21</td>
    <td rowspan=13 style="text-align:right">7</td>
    <td>1 2 3 4 11</td></tr>
<tr></tr><tr><td>1 2 3 5 10</td></tr>
<tr></tr><tr><td>1 2 3 6 9</td></tr>
<tr></tr><tr><td>1 2 3 7 8</td></tr>
<tr></tr><tr><td>1 2 4 5 9</td></tr>
<tr></tr><tr><td>1 2 4 6 8</td></tr>
<tr></tr><tr><td>1 2 5 6 7</td></tr>
</tbody>
</table>

# 2. 計算方法

上記の手計算の手続きをプログラムを作成して機械的に求める。

$n$ 個のボールがビリヤードの問題の条件の一部を満たす，すなわち全ボールの数値の合計が $\sigma = n(n -1) + 1$ になる組み合わせの数を考える。組み合わせの数をカウントするだけなので $b_i < b_{i + 1}$ のように整列しているものとする。$b_1 = 1$ かつ $b_2$ として，３番目以降のボールの数値の範囲について考える。

$k$ 番目のボールの数値 $b_k$（$3 \le k < n$）の最小値を $l_k$，最大値を $h_k$ とする。このうち最小値 $l_k$ は前のボールの値 $b_{k - 1}$ を決めた時点で決まっており，$l_k = b_{k - 1} + 1$ となる。

次に $b_k$ の最大値 $h_k$ について考える。$b_k$ より前のボールの数値の総和を $p$ とし，$b_k$ より後のボールの数値の総和を $q$ とする。$p$ は既知であるから，$q$ が最小値を取るときに $b_k$ は最大値 $h_k$ を取る。

```math
\sigma = \underbrace{b_1 + b_2 + \cdots b_{k - 1}}_{p} + b_k + \underbrace{b_{k + 1} + \cdots + b_{n - 1} + b_n}_{q}
```

$q$ の最小値を考える。各項は前項の値よりも必ず大きいという条件より，全ての項が前項より１だけ大きいときに最小値となる。

```math
\begin{aligned}
q &\ge (h_k + 1) + \cdots + (h_k + n - k) \\
&= (n - k) h_k + \frac{1}{2}(n - k)(n - k + 1)
\end{aligned}
```

以上より

```math
\begin{aligned}
\sigma &= p + b_k + q \\
&\ge p + (n - k + 1) h_k + \frac{1}{2}(n - k)(n - k + 1)
\end{aligned}
```

```math
(n - k + 1)h_k \le n(n - 1) + 1 - p - \frac{(n - k)(n - k + 1)}{2}
```

```math
h_k = \left\lfloor \frac{n(n - 1) + 1 - p}{n - k + 1} - \frac{n - k}{2} \right\rfloor
```

となる。

こうして $b_k$ の下限値 $l_k$ と上限値 $h_k$ を得られたので，この範囲内で $b_k$ の値を選び，次のボール $b_{k + 1}$ の範囲を順次求めていく。$n$ 個のボールの数値の総和が決まっていることから $b_{n - 1}$ の値を決めると $b_n$ の値も一意に定まる。すなわち $(b_{n - 1}, b_n)$ のペアの取り得る組み合わせは $h_{n - 1} - l_{n - 1} + 1$ 個となる。とくに $k = n - 1$ のとき

```math
h_{n - 1} = \left\lfloor \frac{n(n - 1) - p}{2} \right\rfloor
```

となり，ペアの組み合わせ数は $h_{n - 1} - l_{n - 1} + 1$ となる。

このようにボールを一個ずつ配置しながら取りうる範囲を限定しつつも最終的な組み合わせ数は最後の２個のときに決まるので，プログラムは再帰的なコードになる。

# 3. 実装コード

実装コードを以下に示す。今回は C# を用いた。驚いたことに C ネイティブよりも C# のほうが１割ほど速かったのだ。C# すげえな。

```c#:count.cs
using System;
using System.IO;
class COUNT {
	static	int		ball_num;	// ボールの個数
	static	int		ball_sum;	// ボールの総和
	static	long	count_case( int pos, int sum, int low ) {
		int		a = ball_sum - sum;
		int		b = ball_num - pos;
		int		c = ball_num - pos + 1;
		int		high = ( 2 * a - b * c ) / ( 2 * c );
		long	count = 0;
		if( pos < ball_num - 2 ) {
			for( int i = low; i <= high; i++ )
				count += count_case( pos + 1, sum + i, i + 1 );
		} else {
			for( int i = low; i <= high; i++ )
				count += ( ball_sum - sum - i - 1 ) / 2 - i;
		}
		return( count );
	}
	static	int		Main( string[] args ) {
		if( args.Length < 1 ) {
			Console.Error.WriteLine( "Usage: count(.exe) [ball-num]" );
			return( -1 );
		}
		if( ( ball_num = int.Parse( args[0] ) ) < 5 ) {
			Console.Error.WriteLine( "ボールの個数 {0} が不正です!!\n", ball_num );
			return( -1 );
		}
		ball_sum = ball_num * ( ball_num - 1 ) + 1;
		long	count = count_case( 3, 3, 3 );
		Console.Out.WriteLine( "{0}\t{1}", ball_num, count );
		return( 0 );
	}
}
```

# 4. 計算結果

計算結果を以下に示す。なお $n = 25$ のときの計算時間は半日ほどかかった。

<table>
<caption>表２　ボールの個数と組み合わせ数</caption>
<thead>
<tr><th style="text-align:center">ボールの個数</th>
    <th style="text-align:center">組み合わせ数</th>
    <th>増加率</th></tr>
</thead>
<tbody>
<tr><td style="text-align:right">1</td>
    <td style="text-align:right">1</td>
    <td style="text-align:center">-</td></tr>
<tr><td style="text-align:right">2</td>
    <td style="text-align:right">1</td>
    <td style="text-align:center">1.000</td></tr>
<tr><td style="text-align:right">3</td>
    <td style="text-align:right">1</td>
    <td style="text-align:center">1.000</td></tr>
<tr><td style="text-align:right">4</td>
    <td style="text-align:right">2</td>
    <td style="text-align:center">2.000</td></tr>
<tr><td style="text-align:right">5</td>
    <td style="text-align:right">7</td>
    <td style="text-align:center">3.500</td></tr>
<tr><td style="text-align:right">6</td>
    <td style="text-align:right">23</td>
    <td style="text-align:center">3.286</td></tr>
<tr><td style="text-align:right">7</td>
    <td style="text-align:right">84</td>
    <td style="text-align:center">3.652</td></tr>
<tr><td style="text-align:right">8</td>
    <td style="text-align:right">331</td>
    <td style="text-align:center">3.940</td></tr>
<tr><td style="text-align:right">9</td>
    <td style="text-align:right">1,367</td>
    <td style="text-align:center">4.130</td></tr>
<tr><td style="text-align:right">10</td>
    <td style="text-align:right">5,812</td>
    <td style="text-align:center">4.252</td></tr>
<tr><td style="text-align:right">11</td>
    <td style="text-align:right">25,331</td>
    <td style="text-align:center">4.358</td></tr>
<tr><td style="text-align:right">12</td>
    <td style="text-align:right">112,804</td>
    <td style="text-align:center">4.453</td></tr>
<tr><td style="text-align:right">13</td>
    <td style="text-align:right">511,045</td>
    <td style="text-align:center">4.530</td></tr>
<tr><td style="text-align:right">14</td>
    <td style="text-align:right">2,348,042</td>
    <td style="text-align:center">4.595</td></tr>
<tr><td style="text-align:right">15</td>
    <td style="text-align:right">10,919,414</td>
    <td style="text-align:center">4.650</td></tr>
<tr><td style="text-align:right">16</td>
    <td style="text-align:right">51,313,463</td>
    <td style="text-align:center">4.699</td></tr>
<tr><td style="text-align:right">17</td>
    <td style="text-align:right">243,332,340</td>
    <td style="text-align:center">4.742</td></tr>
<tr><td style="text-align:right">18</td>
    <td style="text-align:right">1,163,105,227</td>
    <td style="text-align:center">4.780</td></tr>
<tr><td style="text-align:right">19</td>
    <td style="text-align:right">5,598,774,334</td>
    <td style="text-align:center">4.814</td></tr>
<tr><td style="text-align:right">20</td>
    <td style="text-align:right">27,119,990,519</td>
    <td style="text-align:center">4.844</td></tr>
<tr><td style="text-align:right">21</td>
    <td style="text-align:right">132,107,355,553</td>
    <td style="text-align:center">4.871</td></tr>
<tr><td style="text-align:right">22</td>
    <td style="text-align:right">646,793,104,859</td>
    <td style="text-align:center">4.896</td></tr>
<tr><td style="text-align:right">23</td>
    <td style="text-align:right">3,181,256,110,699</td>
    <td style="text-align:center">4.919</td></tr>
<tr><td style="text-align:right">24</td>
    <td style="text-align:right">15,712,610,146,876</td>
    <td style="text-align:center">4.939</td></tr>
<tr><td style="text-align:right">25</td>
    <td style="text-align:right">77,903,855,239,751</td>
    <td style="text-align:center">4.958</td></tr>
</tbody>
</table>

計算可能な範囲において，綺麗に $O(5^n)$ の関係を描いているように見える。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
footer {
  fontColor black
  fontSize 16
}
</style>
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490655/0cbadde3-903a-11cd-9ce0-3299791d2432.png>
footer 図１　ボールの個数と組み合わせ数
@enduml
```

ボール数が１個増えるたびに組み合わせ数は増加するが，この増加率が５に漸近しているように見える。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
footer {
  fontColor black
  fontSize 16
}
</style>
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490655/3728044d-4a6f-af2b-b52b-d72a44fbdaf4.png>
footer 図２　ボールの個数と組み合わせ数の増加率
@enduml
```

ということで探索空間の広さが $O(5^n)$ であれば，計算量 $O(10^n)$ のアルゴリズムって，まあまあ良く出来ているんじゃないかと思う。

# 5. 次回

次回に続きます。

https://qiita.com/TETSURO1999/items/eefbec683a26cc221a57?1