ZHUOWARE BACKYARD - NOTE INDEX|ZHUOWARE BACKYARD TOP|ZHUOWARE blog

Math Olympic Quiz

少し前に友人からきいた,かつての数学オリンピックの問題です.
	
1から90の整数から5つの整数を引いて小さい順に発表するとき、
連続した整数が現れない取り方は何通りある?

詳細条件
1、3、5、7、9 はOK
1、4、5、7、9 は、4と5が連続しているため条件に該当しません。
当時,コレを解くコードをいくつかの言語で書いてみました. せっかくですので,ここに掲載してみたいと思います.

考え方

答えを返すような関数T を定義してみよう.
6個から2個を選ぶ場合の数を知りたければ T(6,2) とすれば答が得られるようなTである.
一般解はすぐにはわからないので,実際に 6個から2個を選ぶ過程を考えてみる.

<----numSet=6--->
#1 #2 #3 #4 #5 #6
X 
^
|
a)まず, #1 を選ぶとする.その場合の数は,
次の問題に帰着する:
<---4----> 4つから1つを選ぶ,つまり T(4,1)
#3 #4 #5 #6 .... それは4とおり.

以下,最初の1個を左から何番目にとるか,
すべて列挙してみよう.
b)2個めのときは,次は
<--3--> 3つから1つを選ぶ,つまりT(3,1)
#4 #5 #6 .... それは3とおり.

c) 3個めのときは.
<-2> 2つから1つを選ぶ,つまりT(2,1)
#5 #6 .... それは2とおり.

d) 4個めのときは
<1> 1つから1つを選ぶ,つまりT(1,1)
#6 .... それは1とおり.

つまり
T(6,2) = T(4,1) + T(3,1) + T(2,1) + T(1,1).
という展開が成り立つ.

どんなT(s,c)も,この展開を繰り返せば,自明なT(n,1)=n の和に帰着する.
ところで,「はて?」と思われるかたも多いと思います.T()の,よく知られている再帰的定義は,下記のほうだと思います.
T(a,n) = T(a-1, n-2) + T(a, n-1)
こちらを利用するのがスマート,ではありますよね...まあ,気にしないことにします!w (こちらは,再帰の深さが瞬間的に見通せないですし...私のほうは,一回再帰するたびに,選ぶ個数の残りが減っていくので,再帰の深さが自明という利点があります.(屁理屈))

これで,一応実装はできます. ご存知のように,この手の再帰的定義では,求まった計算結果を保存しておいて,より大きな数に対する計算の際に参照してやると,無駄な再計算を省くことが出来て,大変効率的です. ですが,ここでは,単純化のため,基本的に実装しないことにします(たまに,実装したものもありますがw).

以上の方針で,いくつかの言語でコードを書いてみました.


Perl,キャッシュ実装版

遊び方: Perl処理系を用意しておいて、下記をsolve.pl にsaveして、
% perl solve.pl
とします.
--------
# Sep. 11, 2008 zhuo
sub CalcCombInternal {
my ( $nSet, $nChoice ) = @_;
if ( $nChoice == 1 ) { return $nSet; }
my $total = 0;
for ( my $nSubSet = $nSet - 2; $nSubSet >= ($nChoice-1); --$nSubSet ) {
$total += CalcComb( $nSubSet, $nChoice - 1 );
}
return $total;
}
# caching
sub CalcComb {
my ( $nSet, $nChoice ) = @_;
if ( $isCached{$nSet,$nChoice} == 0 ) {
$value{$nSet,$nChoice} = CalcCombInternal( $nSet, $nChoice );
$isCached{$nSet,$nChoice} = 1;
}
return $value{$nSet,$nChoice};
}

$val = CalcComb( 90, 5 );
print "$val\n";
# 34826302
最初の関数だけでも解けるけど再帰ものの定石で結果を保存しておくのに2つめの関数を使用。

Perl,キャッシュなし版

遊び方: Perl処理系を用意しておいて、下記をsolve.pl にsaveして、
% perl solve.pl
とします.
----
# Sep. 11, 2008 zhuo
sub CalcComb {
my ( $nSet, $nChoice ) = @_;
if ( $nChoice == 1 ) { return $nSet; }
my $total = 0;
for ( my $nSubSet = $nSet - 2; $nSubSet >= ($nChoice-1); --$nSubSet ) {
$total += CalcComb( $nSubSet, $nChoice - 1 );
}
return $total;
}
$val = CalcComb( 90, 5 );
print "$val\n";
# 34826302

Emacs Lisp版

tabが出ないんだな… Emacs Lisp.
遊び方: Emacsのバッファに下記を入れて M-x eval-buffer
----
; by zhuo
(defun CalcComb (nSet nChoice)
; (print (format "c(%d %d)" nSet nChoice) (get-buffer "*scratch*"))
(if (eq nChoice 1) nSet
(let ((total 0) (nSubChoice (- nChoice 1)) (nSubSet (- nSet 2)))
(while (>= nSubSet nSubChoice)
(setq total (+ total (CalcComb nSubSet nSubChoice)))
(setq nSubSet (- nSubSet 1)))
total)))

(print (CalcComb 90 5) t)
; 34826302

Tcl/Tk版

遊び方: ActiveTcl など、tcltk処理系を入れておき、下記をsolve.tclとしてsaveしておいて
% tclsh solve.tcl
----
# by zhuo
proc CalcComb { nSet nChoice} {
if { $nChoice == 1 } { return $nSet }
set total 0
set nSubSet [expr $nSet - 2]
set nSubChoice [expr $nChoice - 1]
while { $nSubSet >= $nSubChoice } {
incr total [CalcComb $nSubSet $nSubChoice]
incr nSubSet -1
}
return $total
}

set val [CalcComb 90 5]
puts $val
# 34826302

sh

遊び方: sh の使える環境で 下記をsolve.shとしてsaveしておいて
% sh solve.sh
. 唯一、遅すぎて(90,5)の結果が出ないので、10,3で。
----
#by zhuo
total=0
function CalcComb {
echo "c($1 $2)"
if [ "$2" -eq "1" ] ; then
total=`expr $total + $1`
else
typeset -i nSubChoice=`expr $2 - 1`
typeset -i nSubSet=`expr $1 - 2`
while [ $nSubSet -ge $nSubChoice ]; do
CalcComb $nSubSet $nSubChoice
nSubSet=`expr $nSubSet - 1`
done
fi
}
CalcComb 10 3
echo "c(10 3)=$total"

AWK

遊び方:awk 処理系を入れておいて、下記をsolve.awkとしてsaveしておいて
% awk -f solve.awk
----
# by zhuo
function CalcComb ( nSet, nChoice, _total, _nSubSet, _nSubChoice ) {
if ( nChoice == 1 ) return nSet;
_total = 0;
_nSubSet = nSet - 2;
_nSubChoice = nChoice - 1;
while ( _nSubSet >= _nSubChoice ) {
_total += CalcComb( _nSubSet, _nSubChoice );
--_nSubSet;
}
return _total;
}
BEGIN {
print CalcComb( 90, 5 );
}

# 34826302

javascript

htmlにパックして。 awkに似てまつな。 遊び方:下記を solve.htmlとしてsaveしておいてweb browserで開く。
----
<html>
<head>
<script type="text/javascript">
<!--
// by zhuo
function CalcComb ( nSet, nChoice, _total, _nSubSet, _nSubChoice ) {
if ( nChoice == 1 ) return nSet;
_total = 0;
_nSubSet = nSet - 2;
_nSubChoice = nChoice - 1;
while ( _nSubSet >= _nSubChoice ) {
_total += CalcComb( _nSubSet, _nSubChoice );
--_nSubSet;
}
return _total;
}

document.write( CalcComb( 90, 5 ) );
// 34826302
</script>
</head>


<body>
</body>
</html>

postscript, name を使わずに。

遊び方: 下記を solve.ps として saveしておいて、 PSプリンタで印刷する。唯一、プリンタに計算させることになりますな。 あ、ghostscriptでひらくのでもおk.
----
% by zhuo
% nSet nChoice -> CalcComb -> result
/CalcComb {
dup 1 eq {
pop
} {
1 sub exch
2 sub
0 exch
-1
3 index {
2 index CalcComb add
} for
exch pop
} ifelse
} def

% to render the result on the graphic pane
%!
/Times-Roman findfont
128 scalefont setfont
32 640 moveto
90 5 CalcComb
=string cvs show
% 34826302

MSX-BASIC

Z80機械語使用(ハンドアセンブル)。 Windows環境でもエミュレータを使って実行できるよ。 遊び方:
  1. コードをclccmb02.bas というファイル名でsave.
  2. http://www.lexlechz.at/en/software.htmlでDisk-Manager, v0.12 (2008-07-02) を入手しinstall.
  3. Disk-Managerを起動して、上記clccmb02.basを入れた2DD floppy disk imageをclccmb.dsk などというファイル名でsave.
  4. http://nlmsx.generation-msx.nl/で MSXエミュレータ NLMSX v0.48 を入手しinstall.
  5. NLMSXを起動して、edit -> configuration で MSX2 Japaneseを選ぶ。
  6. edit-> Disk Drive A: でclccmb.dsk を選択。
  7. load "clccmb02.bas"としてロードしてrun.
コード:
10 rem Oct. 17, 2008 zhuo, in Z80 (MSX)
100 c
lear 200,&HD000:a=&HD006:def usr=a
110 read b$:if b$="" goto 130
120 for i=1to len(b$)step2:poke a,val("&H"+MID$(b$,i,2)):a=a+1:next:goto 110
130 poke &HD000,90:poke &Hd001,5:a=usr(0)
140 for a=&HD005 to &HD002 step -1:print right$(("0"+hex$(peek(a))),2);:next
500 data "F5E5D5C5ED4B00D0210000545DCD23D0ED6302D0ED5304D0C1D1E1F1C9100409"
510 data "D013C90D0D79B8D8C5CD23D0C118F5"
520 data ""

結果:
0213683E
この16進数を, perlの使える環境で
perl -e 'printf("%d\n", 0x0213683e);'
とやって10進数に換算してみると34826302であり、合っている。

余談:
native speed mode(3.58MHz)で約75秒で解が求まった。sh版に勝ったw