「ループ・再帰・gotoを使わず1から100までの数値を印字する」Conner Davis 氏の回答の考察

2019年6月に以下の記事が投稿されました。

英語版の記事「How to print 1 to 100 in C++ without a loop, goto or recursion - Quora」から興味深い回答を抜き出して、それにランク付けをしながら和訳してくださっている記事です。

初級や中級は「まぁあるよね(C++知らないけれど……)」という感じですが、 上級とされた「マイクロソフト社のデータサイエンティスト Conner Davis 氏」の回答が面白かった ので、ご紹介を兼ねてその発想の源泉を推測してみることにしました。

Conner Davis 氏の回答

以下に Conner Davis 氏の回答の和訳を引用します。

マイクロソフト社のデータサイエンティスト Conner Davis 氏は 「C++のことは全くわからないのだが」 と断りつつ想像の遥か斜め上を行く回答を書かれています.[9]

氏いわく
『まず 11x=n=0xn の両辺を微分して (1x)2=n=1nxn1 を得ます.
次に x=10001 とし, 両辺を 1000 で割ります. そうすると... 10009992=0.001002003004098099100 最初の300桁はこのように続きます.
つまりC++で任意精度演算を使って 9992 を300桁まで計算し印字すれば1から100まで印字したことになります.』

とのこと。

確かに 0. があって 001 の次に 002 が来て…ってなっていますね。

実際にプログラムを書いてみる

私もC++は全くわからないけれど、各言語にある任意精度演算ライブラリを使えば行けるかなと、使い慣れている Perl で書いてみました。久々にコアモジュールの Math::BigFloat のお世話になります。

eccentric-sequence-davis.pl
#!/usr/bin/env perl

use strict;
use warnings;
use feature qw(say);
use Math::BigFloat;

Math::BigFloat->accuracy(300-2);
my $x = 1000 / Math::BigFloat->new(999**2);
my $str = $x->bstr();

say "--- $str";

$str =~ s/^0\.//;
say join " ", map { $_+0 } $str =~ /(\d\d\d)/g;
output
$ perl eccentric-sequence-davis.pl
--- 0.001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

Ruby にも標準添付ライブラリ bigdecimal があるのでやってみます。

eccentric-sequence-davis.rb
#!/usr/bin/env ruby

require 'bigdecimal'

x = BigDecimal("1000").div(BigDecimal("999") ** 2, 300-1)
str = x.to_s("F")

puts "--- #{str}"

str.sub!(/^0\./, "")
puts str.scan(/\d\d\d/).map{ |s| s.to_i(10) }.join(" ")
output
$ ruby eccentric-sequence-davis.rb
--- 0.0010020030040050060070080090100110120130140150160170180190200210220230240250260270280290300310320330340350360370380390400410420430440450460470480490500510520530540550560570580590600610620630640650660670680690700710720730740750760770780790800810820830840850860870880890900910920930940950960970980991001
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

着想の背景を想像する

和訳記事では 「想像の遥か斜め上」 と書かれていた Conner Davis 氏の回答ですが、その着想にどのような背景があるか、数学的視点計算機科学的視点から想像してみます。

数学的な視点は母関数

数学には 母関数 という考え方があります。ざっくり言うと、 数列 anxn の係数として持たせて総和を取ることで、数列の情報をその総和である関数に持たせること です。

n 番目の数列の値を an と書いて、数列全体を {an} などと書きますが、例えば今回のような数列 an=n

f(x)=n=0nxn

または Conner Davis 氏が書いたように n1 から開始した

f(x)=n=1nxn1

という形式的冪級数x の多項式で特にべき乗の上限がないもの)になります。 n01 どちらから始めるかは必要に応じて調整することにしましょう。

この形式的冪級数が表す f(x) は何かというと、Conner Davis 氏の言う通りの回答となります。

数学や理工学の専門課程で冪級数を扱っていれば、この形式的冪級数が表す関数は馴染みのあるものですし、たとえそうでなくても(氏の項別微分とは逆に)項別積分をして

n=0xn

の形に持ち込めば、高校生3年生が習う等比級数の和の公式

n=0rn=11r(|r|<1)

が適用できることがわかるでしょう。

形式的冪級数は「形式的」という通り、その収束性をあまり気にせず牧歌的に冪級数同士を項別計算して、形式的冪級数が表す母関数を求めることもしばしば行われるようです。収束性の議論を大切にする初等解析学の立場から見ると少々強引かつ不安にも思えますが、そういうものだと思っておくとよさそうです。

実際に an=n の形式的冪級数の関数表示を、項別計算などを使って求めてみましょう。

f(x)=n=1nxn1

より

f(x)xf(x)=n=1nxn1n=1nxn=1+n=1(n+1)xnn=1nxn=1+n=1xn=1+x1x=1x+x1x=11x

なので

(1x)f(x)=11x

つまり

f(x)=1(1x)2

となります。

計算機科学的な視点は任意進数表現

先程の数学的視点で、今回表示したい数列 an=n の母関数がわかりました。ただこの母関数から数列を表示しようと思った場合、そのままでは使いづらいです。

10進数ではない N 進数の話

計算機科学、IT技術領域は10進数以外の様々な進数が使われます

  • コンピュータ内部は 0 か 1 だけのビットの世界:2進数
  • 2進数は扱いづらいので、4桁ずつまとめて表示:16進数
  • 各モードで8つの選択肢があるので、各モードを桁として各桁列挙:8進数(ファイルパーミッション表示など)

2進数が基本でありつつ、特に2進数だと冗長表現になるところで 2の冪乗進数、特に16進数が使われることが多いようです。

同じく、10進数が基本でありつつ、様々な理由で 10n (10の冪乗)進数も使い所があります。

特に今回使用しようとしている任意精度数値ライブラリのようなものを自作しようとした場合も10の冪乗進数が考えやすいでしょう。32ビットの int であれば符号なしで最大40億少々しか格納できませんが、自分で配列を定義して、その各配列に格納された0〜9の数値が各桁だとして配列自体が整数だとすれば任意精度の数値を作ることができます。ただ、もともとの int が21億ほど格納できるのに、各配列の int に格納する数値が 0〜9 なのは効率的に問題があるので、各配列の数を 0 から m-1 まで広げて m 進数表現とするのは良さそうです。10進数に馴染みのある我々が直感的に分かりやすい実装は m を 10 の冪乗にすることです(私が学生時代に習ったのは10000進数を自作して円周率計算するというものでした)。

つまり、10の冪乗進数は我々が馴染みのある10進数に埋め込みやすい という意味でアドバンテージがあります。

100進数を10進数に埋め込む例

100進数があった場合、0〜99が一桁となります。別途90個の文字を用意するのも手間なので、数字2文字で1桁という約束として、位取りを _ 記号で行うことにすれば100進数を書くことができます。

この表記では、99までの数字は10進数と見分けが付きません(桁の約束が違いますが)。10進数で 100 は、この100進数では 1_00 です。10進数で 2525 は、この100進数では 25_25 です。たとえ10進数の25に対して100進数で別の文字が割り当てられていたとしても、位取りとして10進数との変換は些細なことです。これは2進数と16進数(16=24)でも同じことです。

N 進数の小数点表記は後ほど扱いますが、上記の整数部分での「埋め込み」と同様のことが言えます。10進数の 25/990.25252525... という循環小数ですが、上記の100進数では 25/99 = 0.25_25_25_25_25... といった表記になります。

任意進数の数を10進数に変換する

任意進数を求める時は以下のようにします。

N進数の数の n 桁目を dn(ただし 0dn<N) と定義します。なお、最初の桁の添字は 0、つまり最初の桁は d0 とします。その N 進数での数 d=d2d1d0 (隣接する各 di は積ではなく位取りを表しています)を10進数に変換するには以下のようにします。

n=0dnNn(0dn<N)

2進数で 1110 は 14 ですが、この式に当てはめると N=2(d3,d2,d1,d0)=(1,1,1,0) なので

n=042ndn=231+221+211+200=8+4+2=14

となります。

埋め込みやすいという意味で10の冪乗進数を採用すればいい、そして今回は「1から100」までと10進数3桁までの表示を求められているということであれば 1000進数を使うと良さそうだ という発想になるでしょう。

今までは整数の話でしたが、整数ではなく小数点がある場合に N 進数を表す際、上記での総和の N の冪乗部分が負になっていきます。

N進数の小数点表記

N進数の数 d が小数点表記を持つ場合を考えます。整数部の桁を小さい方から d0,d1,d2, とします。小数点第 n 位を負の添字 n を使って dn とすると

d=d2d1d0.d1d2d3

と表され、10進数での値は

d=ndnNn

と表されます。添字 n は、存在する桁の正負の添字全てに渡るものとします。

例えば 10進数で 0.5 は 2進数では 0.1 です。0.5=21×1 より。

なお、10進数では有限小数展開の値も、別のN進数で表すと無限小数展開になる場合があります。

10進数の 0.1 が 2進数では無限小数展開 0.0001˙100˙(頭ドットは循環小数の表記法)になることは

0.1=24+25+28+29+

という無限和で表せることから導くことができます。

実際、上記右辺の和 S を計算すると

S=24+25+28+29+=n=1(24n+24n1)=n=1324n+1=32n=1(116)n=321161116=321161615=110=0.1

となります。ここでも等比級数の無限和の公式

k=1rk=r1r(|r|<1)

を使用しました。

母関数と任意進数を組み合わせる

数学的発想の母関数は、数列 {an} に対し

n=0anxn

でした。

計算機科学的発想のN進数の10進数への計算は、N進数 d=d2d1d0 に対し

ndnNn

でした。

記号や添字の違いはありますが、 見かけはだいぶ似ています

1000進数だということで N=1000 としてしまうと、dnNn

1+21000+310002+410003+

となってしまい素直に発散してしまいますが、N=10001 とすると1未満の数となり、小数点以下に上述のような桁の約束が生まれることになります。これは「11000 進数」ということではなく、1000 進数の1未満の正の数の無限小数展開となっています。

前出の通り、N進数の小数点表示については整数部分よりも若干の考察が必要ですが、10進数の小数点表示についての基本的な知識はそのまま10の冪乗進数に適用することができます。

この辺りの前提知識が、Conner Davis 氏の発想の源泉なのかなと想像しています。

1000進数の小数点展開で1から100を出す

では、改めて Conner Davis 氏の答えをなぞるように、母関数と1000進数の小数点展開を使った回答を作ってみることにしましょう。

数列 {an} の一般項が an=n の時、母関数

f(x)=n=0anxn=n=0nxn

は、 |x|<1 の範囲で等比級数の公式

n=0xn=11x

が成り立つので、項別微分して

n=1nxn1=1(1x)2

が言える(項別微分の妥当性は省略。また収束半径は上記同様 |x|<1 )。

両辺に x をかけると、

n=0nxn=x(1x)2

となる。ここで、 n0 での nxnx の値に関係なく 0 になるので、見やすさのため総和記号の添字を 0 から開始するよう変更している。

この表示は節冒頭の母関数 f(x) そのものであり、

f(x)=n=0nxn=x(1x)2

が言える。

ここで x=11000 を代入すると、

f(11000)=n=0n1000n=10009992

となり、級数の総和を

n=1(n)1000n

と見れば、 dn

dn={0(n0)n(n<0)

と定義し、かつ N=1000 とすると、少なくとも n100 では |dn|<N であり、1未満の小数点展開

n=1dnNn

となっている。

この1000進数の小数展開は、少なくとも n100 の範囲においては10進数のそれへの自然な対応となっており、

f(11000)=10009992=11000+210002+310003+=0.001002003004097098099100

という10進数の小数点展開を得る。

議論の細部に対する疑問

今まで考察した母関数や1000進数の議論の中で、いくつか省かれた細部に対する疑問があると思います。

こちらについては、疑問があれば見出しを追加、解説を追加した場合はストックした方へ通知を送ります(いいねと合わせてストックして頂けると嬉しいです)。

収束性や項別演算の妥当性について

(未執筆)

1000以上の項が1000進数を乱さないか

(未執筆)

一般化の考察

(未執筆)

当初の問題に他にも回答があるか

C++ は置いといて、多くのプログラミング言語にはループを内包している表現がいくつもあります。

  • 範囲データ(Perl や Ruby の .. など)
  • 正規表現のグローバルマッチ
1.upto(100) { |i| puts i }
print join " ", 1..100;

もちろん任意精度の数値ライブラリや、それこそプログラミング言語の根底の実装がループを内包しているので、 今回の問題はいかにループという構文に直接触れず、間接的にそれを使うかという頭の体操 のような印象を持ちました。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account