午後のひとときに、数学やプログラミングの問題を考えてみる。
箱には、1cmからNcmまで1cm刻みの棒が1本ずつ計N本入っており、
この箱から無作為に3本を取り出したとき、
その3本で三角形が作れる確率を求めるものとする。
問題1
N=10のときの確率を求めよ。
問題2
N=100のときの確率を求めよ
問題3
N=1000のときの確率を求めよ
問題4
N=10000のときの確率を求めよ
問題n
N=10nのときの確率を求めよ
問題∞
N→∞のときの確率を求めよ。
なお、どの問題も、机上だけではなく、電卓、エクセル、プログラミング、なんでもありとします。
シンキングタ~イム
まずは数学で、どこまでやれるのか。
プログラミングをするにしても数学的な考察は必要ですね。
こういった類の問題は、小さい方から試してみて、何らかの法則を見出すのが良いでしょう。
N=1のとき、三角形は作れません。
N=2のとき、三角形は作れません。
N=3のとき、
1cm、2cm、3cmでは三角形になりません。
三角形になるための条件はなんだろうか。
三角形の辺の長さをa、b、cとすると、
a, b, c > 0
a+b > c ∧ b+c > a ∧ c+a > b
です。
ここで、a, b, cに大小関係を設定します。
題意より、同じ長さがないので等号はなくて、
0 < a < b < c
としましょうか。
すると、
a+b > c
だけに限定されましたね。
N=4のとき、
(a,b,c)=(2,3,4)
が見つかり、分子は1です。
では、分母は何でしょうか?
N本の棒から無作為に3本取るので、
NC3=N(N-1)(N-2)/(3*2*1)=N(N-1)(N-2)/6
で、N=4なので、
4*3*2/6=4
よって、N=4のとき、1/4となります。
N=5のとき、(a,b,c)は、
(2,3,4),(2,4,5)
(3,4,5)
で分子は3
分母は5*4*3/6=10
より、3/10
N=6のとき、(a,b,c)は、
(2,3,4),(2,4,5),(2,5,6)
(3,4,5),(3,5,6)
(3,4,6)
(4,5,6)
で分子は7
分母は6*5*4/6=20
要領がつかめてきました。
問題1を、同様に数え上げで解いてみましょう。
N=10のとき、(a,b,c)は、
(2,3,4),(2,4,5),(2,5,6),(2,6,7),(2,7,8),(2,8,9),(2,9,10)
で7個
(3,4,5),(3,5,6),(3,6,7),(3,7,8),(3,8,9),(3,9,10)
(3,4,6),(3,5,7),(3,6,8),(3,7,9),(3,8,10)
で11個
(4,5,6),(4,6,7),(4,7,8),(4,8,9),(4,9,10)
(4,5,7),(4,6,8),(4,7,9),(4,8,10)
(4,5,8),(4,6,9),(4,7,10)
で12個
(5,6,7),(5,7,8),(5,8,9),(5,9,10)
(5,6,8),(5,7,9),(5,8,10)
(5,6,9),(5,7,10)
(5,6,10)
で10個
(6,7,8),(6,8,9),(6,9,10)
(6,7,9),(6,8,10)
(6,7,10)
で6個
(7,8,9),(7,9,10)
(7,8,10)
で3個
(8,9,10)
で1個
合計50個あり、
分母は
10*9*8/6=120
50/120=41|6}%
※{}は連文節
手計算でもどうにか出来ましたね。
今回のアルゴリズムを使って、プログラミングしてみましょうか。
triangle_prob1.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
unsigned long long i, j, k, n, u, l;
if ( argc == 2 ) {
n = atoll(argv[1]);
if ( n < 3 ) n = 10;
} else {
n = 10;
}
u = 0;
for (i=n-3, j=1; i>0; i--,j++) {
for (k=0; k<j && i>=k; k++) {
u += i-k;
}
}
l = n*(n-1)/2*(n-2)/3;
printf("%llu/%llu=%lg\n",u,l,(double)u/l);
return EXIT_SUCCESS;
}
uが分子、lが分母です。
l = n*(n-1)*(n-2)/6;
とせず、
l = n*(n-1)/2*(n-2)/3;
とするのに疑問を持つかもしれませんが、
数学では気にしなくて良いことが、プログラミングでは気にしなければならないことがあります。
その一つにオーバーフローです。
数学であれば、変数はいくらでも大きい値を設定できますが、プログラミングではそうはいかず、何らかの上限や下限があります。
nが大きくなると、n自身はオーバーフローしていなくても、n*(n-1)*(n-2)でオーバーフローしまうということはあります。
数学的に求めたn*(n-1)*(n-2)/6の値がプログラム的な上限に達していなくても、n*(n-1)*(n-2)の計算段階でオーバーフローしてしまうと、/6をしても正しい値にはなりません。
n*(n-1)の段階でオーバーフローしていれば論外ですが、この段階でオーバーフローしていなければ、n*(n-1)/2で余裕が出来、n*(n-1)/2*(n-3)でオーバーフローしないかもしれません。
数学的な考察として、n*(n-1)は必ず2で割り切れますし、n*(n-1)*(n-2)は必ず6で割り切れます。
まぁ、こういう考えが出来るかどうかで、nの有効範囲が多少違ってくることもあるでしょう。
実は、今回の問題ではまったく関係ないので、余計なことをしましたw。
さて、プログラムが出来ましたので、パラメータを与えてみましょう。
> triangle_prob1 10
50/120=0.416667
> triangle_prob1 100
79625/161700=0.492424
> triangle_prob1 1000
82958750/166167000=0.499249
> triangle_prob1 10000
83295837500/166616670000=0.499925
問題1の解
50/120=0.416667
問題2の解
79625/161700=0.492424
問題3の解
82958750/166167000=0.499249
問題4の解
83295837500/166616670000=0.499925
これくらいは今どきのパソコンなら瞬殺するが、少しずつ時間がかかり始めているので、
数え上げの効率を上げてみよう。
N=10のときの個数に着目すると、
7
6+5
5+4+3
4+3+2+1
3+2+1
2+1
1
と、横に倒したピラミッド状になっており、
どのみち足し合わせるので、
縦読みすると、
7+6+5+4+3+2+1
5+4+3+2+1
3+2+1
1
となって、明らかに計算しやすくなった。
こっちのアルゴリズムのほうが簡単ですので、プログラミングしてみます。
triangle_prob2.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
unsigned long long i, n, u, l;
if ( argc == 2 ) {
n = atoll(argv[1]);
if ( n < 3 ) n = 10;
} else {
n = 10;
}
u = 0;
for (i=n-2; i>1; i-=2) {
u += i*(i-1)/2;
}
l = n*(n-1)/2*(n-2)/3;
printf("%llu/%llu=%lg\n",u,l,(double)u/l);
return EXIT_SUCCESS;
}
unsignedを使っているので、iのfor文を1増やして、計算は(i+1)*i/2ではなく、i*(i-1)/2にしてます。
そうしないと、パラメータに奇数を与えると、地獄みますよw
さてさて、プログラムは格段に速くなりました。
大きいNに対して手計算は面倒です。
問題nは、N=10^nです。
これは何らかの一般式を考える必要が出てきました。
n=1のとき、50/120
n=2のとき、79625/161700
n=3のとき、82958750/166167000
n=4のとき、83295837500/166616670000
n=5のとき、83329583375000/166661666700000
n=6のとき、83332958333750000/166666166667000000
何か規則性が見えてきました。
問題1、2では規則性は解りにくかったですが、問題3、4をプログラムで出力してみると、分母と分子に規則性が見えてきます。
n=3以上の分子に着目すると、
8295750
83295837500
83329583375000
83332958333750000
…
83…329583…37500…0
これを分解して、
80…000000…00000…0
+ 3…300000…00000…0
+ 29580…00000…0
+ 3…30000…0
+ 7500…0
と考えると、
レピュニット数をうまく使います。
R[n]=(10n-1)/9
これは1がn桁続く数の式ですので、3倍すれば、
3*R[n]=(10n-1)/3
は、3がn桁続く数の式となります。
分子は、
8*103n-2+((10n-3-1)/3)*102n+1+2958*102n-3+((10n-3-1)/3)*10n+(3/4)*10n
と立式することが出来た。
10nの項をまとめたり、簡素化出来るかとは思いますが、これくらいで留めておきます。
さて、この式、n≧3では合っているのだが、n=1や2はどうなのだろうか?
n=1のとき、
8*103n-2=80
((10n-3-1)/3)*102n+1=-330
2958*102n-3=295.8
((10n-3-1)/3)*10n=-3.3
(3/4)*10n=7.5
80-330+295.8-3.3+7.5=50
n=2のとき、
8*103n-2=80000
((10n-3-1)/3)*102n+1=-30000
2958*102n-3=29580
((10n-3-1)/3)*10n=-30
(3/4)*10n=75
80000-30000+29580-30+75=79625
合ってますね。
不思議ですよね。
分母も同じように分解してnの式に出来ますね。
各自立式してみてください。
さて、N=3とかはどうやって求めるの?となるかと思われた方もいるかと思います。
N=10n
としたので、両辺のlog10を取る。
log10(N)=n*log10(10)=n
n=log10(N)
のようにnを与えれば良い。
n | 8*103n-2 | ((10n-3-1)/3)*102n+1 | 2958*102n-3 | ((10n-3-1)/3)*10n | (3/4)*10n | Total |
---|---|---|---|---|---|---|
log10(1) | 0.08 | -3.33{0} | 2.958 | -0.333{0} | 0.750 | 0.125 |
log10(2) | 0.64 | -13.30{6} | 11.832 | -0.665{3} | 1.500 | 0.000 |
log10(3) | 2.16 | -29.91{0} | 26.622 | -0.997{0} | 2.250 | 0.125 |
log10(4) | 5.12 | -53.12{0} | 47.328 | -1.328{0} | 3.000 | 1.000 |
log10(5) | 10.00 | -82.91{6} | 73.950 | -1.658{3} | 3.750 | 3.125 |
log10(6) | 17.28 | -119.28{0} | 106.488 | -1.988{0} | 4.500 | 7.000 |
log10(7) | 27.44 | -162.19{0} | 144.942 | -2.317{0} | 5.250 | 13.125 |
log10(8) | 40.96 | -211.62{6} | 189.312 | -2.645{3} | 6.000 | 22.000 |
log10(9) | 58.32 | -267.57{0} | 239.598 | -2.973{0} | 6.750 | 34.125 |
log10(奇数)の場合、+0.125の誤差は出ているが、気になれば床関数なりガウス記号や奇数のときだけ別の処理で取り除くことも容易に出来る。
プログラミングの方は、高速化が済んでいるが、一般式で作るのもありだろう。
また、文字列として考えて構築するのもありだろう。
問題∞は、厳密lim計算しなくても、
分子の先頭が8、分母の先頭が桁あわせすると16となり、
8/16=0.5
確率は50%となることは容易に想像できますね。
今回の数学、プログラミングを交えた問題はいかがだったでしょうか。
数学的に考察して、
プログラムを組んで大きな値を出力してみると、
出力結果から規則性が見えてきて、
その規則性から数学的に再考察する。
プログラミングは、その問題に即した計算をすることを目的に作れるので、私のような数学屋にとっては、とてもありがたいツールです。
ではでは
knifeのmy Pick