午後のひとときに、プログラミングの問題を解いてみます。
問題
Image may be NSFW.
Clik here to view.
図のように、大円を半径1とし、大円を除くn個の円が数珠つなぎになっており、
3時の方角に半径1/2の円を大円に内接するよう描き、
半径をp倍(p≦1)した円を反時計回りに大円と前後の円の3つと接するよう描く。
これを踏まえて、
問題1
nを与えると、上図のような図を描くプログラムないし、それに準ずるフローチャートを作れ。
問題2
大円に内接するn個の円を大きいほうから順に1からnまで番号を振ったとして、
n番目の円を描く以前に、2<m<nとなるm番目の円が、
1番目の円と接するまたは交わってしまう最小のnを求めよ、
問題3
問題2で求めたn未満において、内接するn個の円の面積の合計が最大となるnを求めよ。
なお、手計算でどうにか出来る問題ではないので、電卓、エクセル、プログラミング、なんでもありとします。
シンキングタ~イム
まずは、数学的考証からはじめます。
n個の円を内接させるとして、
pを求めたいが、どのように求めるのが得策なのだろうか。
数学には、幾何学、代数学、解析学という3つの柱がありますが、今回の問題を解く方法は、解析学となります。
とはいっても、図形を描くわけですから幾何学も関わるし、数値計算をするわけですから代数学も関わります。
では、解析学のどんなものを使うのでしょうか。
解析学というと、極限とか微分とか積分とかを習ってきたかとは思います。
今回使うのは二分法です。
pの取りうる範囲は、
題意より、
0<p≦1
となりますので、
pの取りうる最大値として、p_max=1
pの取りうる最小値として、p_min=0
p=(p_max+p_min)/2
とし、近似的にpを求めていきます。
Image may be NSFW.
Clik here to view.
大円、1番目の円、2番目の円、3円の中心を結んで三角形を作ります。
赤線は、(大円の半径)-(1番目の円の半径)
緑線は、(大円の半径)-(2番目の円の半径)
青線は、(1番目の円の半径)+(2番目の半径)
という関係にあります。
赤線と緑線に挟まれる角をθとして、
余弦定理から、
青2=赤2+緑2-2・赤・緑cos(θ)
2・赤・緑cos(θ)=赤2+緑2-青2
cos(θ)=(赤2+緑2-青2)/(2・赤・緑)
θ=arccos((赤2+緑2-青2)/(2・赤・緑)
のように式変形をして、赤、緑、青の線分の長さから、θを求める式を作ることが出来ます。
この式を順々にやっていき、
三角形の辺が
(大円の半径)-(n番目の円の半径)
(大円の半径)-(1番目の円の半径)
(n番目の半径)+(1番目の半径)
までの、θの総和、∑θを求める。
∑θが2πより大きいならば、pは大きすぎるので、現在のpをp_maxとします。
∑θが2πより小さいならば、pは小さすぎるので、現在のpをm_minとします。
新しい範囲の中間値としてpを求めて、これを繰り返します。
数学的には無限に繰り返すことになりますが、プログラミングにおいては変数の桁数は有限なので、繰り返しは有限回でよく、変数pのビット数を繰り返せば十分である。
問題1の解をJavascriptとHTML5で書いてみると、
var n = 7;
var p, p_max, p_min;
var R, r = new Array(n);
var a = new Array(n+1);
var i, j;
R = 350;
r[0] = R/2;
a[0] = 0;
p_max = 1.0;
p_min = 0.0;
for (j=0; j<64; j++) {
p = (p_max+p_min)/2;
for (i=1; i<n; i++) {
r[i] = p*r[i-1];
a[i] = a[i-1]+Math.acos((Math.pow(R-r[i-1],2)+Math.pow(R-r[i],2)-Math.pow(r[i-1]+r[i],2))/(2*(R-r[i-1])*(R-r[i])));
}
r[i] = p*r[i-1];
a[i] = a[i-1]+Math.acos((Math.pow(R-r[i-1],2)+Math.pow(R-r[0],2)-Math.pow(r[i-1]+r[0],2))/(2*(R-r[i-1])*(R-r[0])));
if ( a[i] > 2*Math.PI ) {
p_max = p;
} else if ( a[i] < 2*Math.PI ) {
p_min = p;
}
}
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(400,750-R,R,0,2*Math.PI,true);
ctx.stroke();
for (i=0; i<n; i++) {
ctx.beginPath();
ctx.arc(400+(R-r[i])*Math.cos(a[i]),750-R-(R-r[i])*Math.sin(a[i]),r[i],0,2*Math.PI,true);
ctx.stroke();
}
核心部分だけですが、こんな感じです。
このプログラムで、n≧2におけるpを求めて、エクセルで表にまとめてみる。
Image may be NSFW.
Clik here to view.
問題2は、pの値が、前のpよりも大きくなっているかを調べればよく、真偽の列で、FALSEを示したら、途中の円が1番目の円と接するか交わっていることとなる。
つまり、最小のnは17ということです。
答え
n=17
問題3は、∑sの列で、内接するn個の円の面積の総和を、pから求めています。
nが5のとき、∑sの値が最大となっています。
答え
n=5
いかがでしたでしょうか。
今回の問題を作る上でと言うか、数学の図形問題を出題すると、ある程度正確な図形を描く必要に駆られます。
今回の問題は円しか使わず、ましてや全てがぴったりと接していなければならないということで、半径を適当に取って描くというわけにはいきません。
例えば、合同な円が内接していたり、すでに比が与えられていたり、nが極々小さい値であるならば、机上の手計算でも出来なくはないですが、nが大きくなればやりたくはないですよね。
nを2から10までの、pと∑sを出力した画像を貼っておきます。
Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.Image may be NSFW.
Clik here to view.
ではでは