3n+1問題(問題100)
http://acm.uva.es/p/v1/100.htmlを昨夜はずっと解いてました。問題を簡単に書くと、次のようになります。
整数nが与えられます。次に、計算ルールとして、nが偶数のときはnを2で割り、奇数のときはnを3倍して1を足します。これを、nが1になるまで続けます。そして、nが1になるのに必要なこの計算の合計回数をnの寿命とします。で、問題は、整数iとjが与えられたとき、iとjの間の数の中で最大の寿命を求めよというものです。
プログラムとしては、非常にシンプルに書けます。寿命を求める関数をcycleとすると、問題は次のようなプログラムになります。(とりあえず、C言語で書いてみる)
int solve( int i, int j ) { int max = 0; for( int n=i; n<=j; n++ ) if( cycle(n) > max ) max = cycle(n); return max; }
また、寿命を求める関数cycleは次にようになります。
int cycle( int n ) { int c=1; while( n !- 1 ){ if( n%2 -- 1 ) n = 3*n+1; else n = n / 2; c = c + 1; } return c; }
しかし、それでは面白くないし、いちいち再計算をしなければならないため、実行時間が大きくなってしまう。実行結果をテーブルにキャッシングしておくという手があるけど、それでは小手先の改良に過ぎない。
もっと根本的な改良はないかと言うことで、ダイナミックプログラミングの要素を取り入れることにした。すなわち、n=1から始める幅優先探査であらかじめテーブルを作成してしまおうということです。
このプロセスは次のようになります。
count = 1; An = {1}; Live[n] : the table of cycle(n); while( size of An > 0 ){ forall n ∈ An Live[n] = count; if( n*2 <= MAX ) add n*2 to An'; if( (n-1)%3==0 && (n-1)/3%2!=0 && (n-1)/3>1 ) add (n-1)/3 to An'; An = An'; count = count + 1; }
かなり適当な言語になってしまいましたけど、こんな感じです。これは、計算ルールの適用で、nになる数字は、n*2と、(n-1)/3になります。ですから、A1={1}からはじめて、A2={2}, A3={4}, A4={8}, A5={16}, A6={32,5}, A7={64,10}, A8={128,21,3}...と続けていくことで、テーブルを作ってしまおうというやつです。
もしここで、求めなければならない最大の整数をn、可能性のある最大寿命がkであったとします。このとき、最初の解き方で必要な計算オーダーはknとなります(たぶん)。次に、改良した解き方の計算オーダーは、nになります(かなり自信がない)。ですから、改良した方が圧倒的に速くなるはずです。
さて、これでサンプルとして与えられている入力に対して、出力は正しい結果を返しました。そこで、オンラインジャッジ!!をしてもらいました。
結果は、不合格。
何がいけないんだろうと、いろいろテストをしてみました。そしたら、恐ろしいバグ!?を発見してしまいました。そのバグとは、nの最大値の見積もりでした。
私は、nの最大値は、調べたい数字の3倍ぐらい取っておけば良いと考えていました。すなわち、iとjの間の全ての整数の寿命を調べるためには、nの最大値はmax(i,j)*3ぐらいと想定してプログラムを書いていました。なぜこのような最大値の想定が必要かというと、今回考えた幅優先探査によるアルゴリズムは、最大値の制限を設けないと、永遠にAnの探索を行ってしまいます。ですから、最大値MAXを与えることで、新たにAn'を作るときに最大値MAXを超える数値は登録しないようにすることで、収束させることをもくろみました。また、このアルゴリズムを利用すると、最大値MAXまでのすべての表を生成することはできません。特にMAXに近づくにつれて、寿命が求められていない数字が多くなります。ですから、ある程度の数までぎっしり詰まったテーブルを生成するには、MAXを大きく取る必要があります。
で、問題文によると、iとjの最大値は1,000,000です。これは、20ビットほどで表現できる数です。さて、ではこの最大値までぎっしり詰まったテーブルを求めるために、MAXはいくつにすればよいでしょうか。
それで、MAXに対する、テーブルの隙間ができる数を、実験して求めました。
10 4 100 15 1000 27 10000 255 100000 703 1000000 1819 10000000 9663
で、ぜんぜんたらねえ!!これ以上MAXを大きくすると、メモリが足らなくて終了してしまう!!
ということで、ちとこれより対策を考える。とりあえず、2乗までは必要なさそうなので、MAXが32ビットに収まってくれると良いんだけど。。。