Tower of Hanoi

尊敬する友人が「Higher Order Perl」を買って読んでる、という話を聞いた。僕はHOPは「なんとなくめんど草そー」というか難しそうだったので避けてたんだけど、いい機会だから勉強してみようと買ってみる。こないだとどいた。最初と2つ目はすげー単純な10進 2進変換とか、順列組み合わせの数を再帰でやる、というありがちケース。次のハノイの塔も非常に有名なので・・・と思ったんだけど、どうやって自分でやるかふと迷った。
ので、これは読まずに自分でやってみようとか考え始める。

準備

とりあえず板を動かして表示するクラスを書いてみる(それMooseで。。。)

## Tower of Hanoi  --  Tower behavior & representation
##
package Hanoi;

use strict;
use warnings;

sub new {
	my ($class, $n) = @_;
	my $ref = {};

	$ref->{n} = $n;
	$ref->{peg} = [[1..$n], [], []];

	bless $ref, $class;
}

sub move {
	my ($self, $from, $to) = @_;
	my ($picked, $dst) = (shift @{$self->{peg}->[$from-1]},
			      $self->{peg}->[$to-1]);
	die "illegal move" if $dst->[0] && $picked > $dst->[0];
	unshift @$dst, $picked;
}

sub pad_string {
	my ($ary, $n, $padding) = @_;
	[($padding) x ($n - $#$ary -1), @$ary];
}

sub pad_pegs {
	my ($self) = @_;
	[map {pad_string($_, $self->{n}, "|")} @{$self->{peg}}];
}

sub transpose {
	my $ary = shift;
	my $ret = [];

	for my $i (0..$#$ary){
		my $j = 0;

		for my $c (@{$ary->[$i]}) {
			$ret->[$j] //= [];
			push @{$ret->[$j++]} , $c;
		}
	}
	$ret;
}

sub disp {
	my $self = shift;

	[map {join("  ",@$_)} 
		@{transpose($self->pad_pegs)}];
}

sub str {
	my $self = shift;

	join "\n", @{$self->disp};
}

sub print {
	my $self = shift;

	print join("\n", $self->str). "\n";
}

1;

まあちょっと確認目的だけだしいいか......
ついでにTest::Exceptionの練習がてらにテストも書いてみる。

## test for hanoi tower class    -- t/hanoi.t
##
use strict;
use Test::More tests => 8;
use Test::Differences;
use Test::Exception;

use_ok("Hanoi") or exit;

my $h = Hanoi->new(4);

isa_ok($h, "Hanoi");

ok($h->{n} == 4, "check size of Hanoi");

eq_or_diff (
    $h->pad_pegs,
    [[1,2,3,4],
     ["|","|","|","|"],
     ["|","|","|","|"]],
    'fill peg array with "|"');


eq_or_diff(
    $h->disp,
    [
     "1  |  |",
     "2  |  |",
     "3  |  |",
     "4  |  |",
    ],
    'display array reference');


$h->move(1,2);

eq_or_diff(
    $h->disp,
    [
     "|  |  |",
     "2  |  |",
     "3  |  |",
     "4  1  |",
    ],
    'just moving peg 1 to peg 2');

$h->move(1,3);
$h->move(2,3);
$h->move(1,2);
$h->move(3,1);

eq_or_diff(
    $h->disp,
    [
     "|  |  |",
     "|  |  |",
     "1  |  |",
     "4  3  2",
    ],
    'some more movement');

dies_ok {$h->move(2,1)} "illegal movement";

テスト開始。

quartz:pts/1% prove -v t                                                                                                       /home/kuro/Lang/Perl/Hanoi
t/Hanoi....
1..8
ok 1 - use Hanoi;
ok 2 - The object isa Hanoi
ok 3 - check size of Hanoi
ok 4 - fill peg array with "|"
ok 5 - display array reference
ok 6 - just moving peg 1 to peg 2
ok 7 - some more movement
ok 8 - illegal movement
ok
All tests successful.
Files=1, Tests=8,  0 wallclock secs ( 0.04 usr  0.01 sys +  0.04 cusr  0.01 csys =  0.10 CPU)
Result: PASS

よさ気。

本番

要するに i→jにn個移したかったら、i,jじゃないもう一本をkとして, i→ kに n-1個移す必要があるのだよね。

  A     B     C
  |     |     |
  *     |     |
 *2*    |     |
**3**   |     |

↑を↓にするには

  A     B     C
  |     |     |  
  |     |     *  
  |     |    *2* 
  |     |   **3**


その前に
  A     B     C
  |     |     |
  |     |     |
  |     *     |
**3**  *2*    |
こうなってないといけない

こうなってから**3**を Cに動かせばよいわけで。
素直に書くとこうか

use strict;
use warnings;

use Hanoi;

my $height_of_tower = 3;

my $hanoi = Hanoi->new($height_of_tower);

$hanoi->print;

sub hanoi {
  my ($from, $to, $n) = @_;
  my $another = (1+2+3)-$from-$to;
  if ($n == 1) { 
    $hanoi->move($from, $to);
	 print "-------------------\n";
    $hanoi->print;
    return;
  }
  hanoi($from, $another, $n -1); # 一歩前の段階
  hanoi($from, $to, 1);           # 残った一枚(一番下)を目的のペグに移動
  hanoi($another, $to, $n -1);    # ↑にさっき動かした山を移動
}

hanoi(1,3, $height_of_tower);  # 全部を1→3に移動(スタート)

やってみる

|  |  |
2  |  |
3  |  1
-------------------
|  |  |
|  |  |
3  2  1
-------------------
|  |  |
|  1  |
3  2  |
-------------------
|  |  |
|  1  |
|  2  3
-------------------
|  |  |
|  |  |
1  2  3
-------------------
|  |  |
|  |  2
1  |  3
-------------------
|  |  1
|  |  2
|  |  3
-------------------

ありゃ、簡単にできてしまった。なんかつまらない。というかやはり再帰って考え方は恐ろしく強力だと改めて思った。