Have fun with CodeJam, by using Perl!
--基础训练题《迷宫绘制》
题目链接:http://code.google.com/codejam/contest/32003/dashboard#s=p1
最近一直在尝试用perl来解Code Jam的题,一方面Code Jam的题目很有趣,各种难度等级都有覆盖;同时用算法题目保持perl的练手也是一个不错的选择。比如今天提到的这道题(Code Jam题目名为always turn left,"总是左转",WTF is that?), 是一道中低难度的基础训练题,正好帮助我熟悉perl OO的方法(via Moose)。
首先分析这道题目,看上去是一道迷宫题,但不同的是,我们不是要解出迷宫的路径,而是要根据已知的路径走法,推算出整个迷宫的平面细节(Code Jam非常喜欢逆向出题,以后我们将会注意到这点)。为了达到这个目的,迷宫本身有所限制:
1. 迷宫是一个R x C大小的矩阵形状,也即是由R x C个同样大小的小房间组成;
2. 迷宫只有一个出口和一个入口;入口只能在北边的墙上,出口可以在任意墙上;
3. 迷宫中任意两个房间之间只有唯一一条通路;
题目提出你要用摸墙法(摸左手墙)来走出迷宫,走出之后,你会再从出口进入迷宫,反过来重走一遍,直到从入口走出;如此来回后,你得到两条类似“WRWWLWWLWWLWLWRRWRWWWRWWRWLW”的字符串,代表的是迷宫走法,据此你反推出迷宫的内部细节(每个房间的连通情况,每个房间的四面是缺口还是墙,用一个由东西南北组合成的四位比特掩码值来表示,最终题目要求输出该掩码值的16进制表示)。
读题至此,我头脑里浮现出这样一副画面:一位迷宫玩家,姑且称之为mazer,身上怀揣一份地图,进入迷宫后,一边摸墙一边前行,每走出一个房间,就标注一次地图...
显然,迷宫的连通情况由mazer的动作序列决定,mazer身上的数据(地图、坐标、方向),和mazer的行为(WLR代表的动作)高度聚合,让我不得不想到OO的方法。
Moose框架是Perl OO的一种选择,据说极为简单,于是决定以此练手。在CPAN上看了Moose::Manual后,大概得到的思路如下:
1. mazer在迷宫中的位置是二维坐标,再加上mazer的方向,一共三维(x,y,z),是mazer的三个attribute;
2. 地图是一个attribute,为哈希引用,其键为迷宫的二维坐标(x_y),值为四位比特掩码的组合;
3. 为了回溯整个地图,我们假设入口的坐标就是(0,0),往南是y方向的递增,往东是x方向的递增,这样我们要维护x的最大、最小值,以及y的最大值(为什么不需要y的最小值,请读者自行思考),这样也是三个attribute;回溯时,我们只需要从北到南,从西向东遍历x和y即可;
4. 每走出一个房间,x或y其中必有一个值发生变化,根据这个变化情况,我们可以更新地图。当属性发生变化时,需要触发一个动作,对此Moose提供了什么机制?---Trigger! That's what I'm looking for :=)
首先分析这道题目,看上去是一道迷宫题,但不同的是,我们不是要解出迷宫的路径,而是要根据已知的路径走法,推算出整个迷宫的平面细节(Code Jam非常喜欢逆向出题,以后我们将会注意到这点)。为了达到这个目的,迷宫本身有所限制:
1. 迷宫是一个R x C大小的矩阵形状,也即是由R x C个同样大小的小房间组成;
2. 迷宫只有一个出口和一个入口;入口只能在北边的墙上,出口可以在任意墙上;
3. 迷宫中任意两个房间之间只有唯一一条通路;
题目提出你要用摸墙法(摸左手墙)来走出迷宫,走出之后,你会再从出口进入迷宫,反过来重走一遍,直到从入口走出;如此来回后,你得到两条类似“WRWWLWWLWWLWLWRRWRWWWRWWRWLW”的字符串,代表的是迷宫走法,据此你反推出迷宫的内部细节(每个房间的连通情况,每个房间的四面是缺口还是墙,用一个由东西南北组合成的四位比特掩码值来表示,最终题目要求输出该掩码值的16进制表示)。
读题至此,我头脑里浮现出这样一副画面:一位迷宫玩家,姑且称之为mazer,身上怀揣一份地图,进入迷宫后,一边摸墙一边前行,每走出一个房间,就标注一次地图...
显然,迷宫的连通情况由mazer的动作序列决定,mazer身上的数据(地图、坐标、方向),和mazer的行为(WLR代表的动作)高度聚合,让我不得不想到OO的方法。
Moose框架是Perl OO的一种选择,据说极为简单,于是决定以此练手。在CPAN上看了Moose::Manual后,大概得到的思路如下:
1. mazer在迷宫中的位置是二维坐标,再加上mazer的方向,一共三维(x,y,z),是mazer的三个attribute;
2. 地图是一个attribute,为哈希引用,其键为迷宫的二维坐标(x_y),值为四位比特掩码的组合;
3. 为了回溯整个地图,我们假设入口的坐标就是(0,0),往南是y方向的递增,往东是x方向的递增,这样我们要维护x的最大、最小值,以及y的最大值(为什么不需要y的最小值,请读者自行思考),这样也是三个attribute;回溯时,我们只需要从北到南,从西向东遍历x和y即可;
4. 每走出一个房间,x或y其中必有一个值发生变化,根据这个变化情况,我们可以更新地图。当属性发生变化时,需要触发一个动作,对此Moose提供了什么机制?---Trigger! That's what I'm looking for :=)
package Mazer;
use 5.18.1;
use Moose;
has 'x' => (is => 'rw',
isa => 'Int',
trigger => \&OnMoveX,
default => 0);
has 'y' => (is => 'rw',
isa => 'Int',
trigger => \&OnMoveY,
default => 0);
#z is the direction mazer facing at, default to south
has 'z' => (is => 'rw',
isa => 'Str',
default => 'S');
has ['max_x', 'max_y', 'min_x'] => (is => 'rw',
isa => 'Int',
default => 0);
#the map, key is x_y, value is the bitmask of 'EWSN'
has 'map' => (is => 'rw',
isa => 'HashRef[Int]',
default => sub {{}});
在设计trigger前,我们先来看看怎么解析mazer的行为:
no warnings 'experimental::smartmatch';
sub OnAction {
my ($self, $action) = @_;
for($action){
when (/W/){
$self->y($self->y - 1) if $self->z eq 'N';
$self->y($self->y + 1) if $self->z eq 'S';
$self->x($self->x - 1) if $self->z eq 'W';
$self->x($self->x + 1) if $self->z eq 'E';
};
when (/L|R/){
$self->z($self->GetNextZ($self->z, $action));
};
default { die "die on unexepcted action: $action"; };
}
}
sub GetNextZ {
my ($self, $in, $turn) = @_;
my $dir_order = $turn eq 'L' ? 'WSEN' : 'WNES';
my $step = $turn eq 'O' ? 2 : 1;
return substr($dir_order, (index($dir_order, $in) + $step) % 4, 1);
}
其中GetNextZ是根据当前左转或右转的动作来获取mazer的朝向,另外'O'代表反方向,后面回头重走maze的时候会用到;可以看到,只有当前动作为W(alk)时,才会引起x或y值的变化,于是trigger闪亮登场:
sub OnMoveX {
my ($self, $new_x, $old_x) = @_;
return unless @_ > 2;
if($new_x > $old_x){
$self->set_maze($new_x, $self->y, 'W');
$self->set_maze($old_x, $self->y, 'E');
$self->max_x($new_x) if $new_x > $self->max_x;
}
else{
$self->set_maze($new_x, $self->y, 'E');
$self->set_maze($old_x, $self->y, 'W');
$self->min_x($new_x) if $new_x < $self->min_x;
}
}
sub OnMoveY {
my ($self, $new_y, $old_y) = @_;
return unless @_ > 2;
if($new_y > $old_y){
$self->set_maze($self->x, $new_y, 'N');
$self->set_maze($self->x, $old_y, 'S');
$self->max_y($new_y) if $new_y > $self->max_y;
}
else{
$self->set_maze($self->x, $new_y, 'S');
$self->set_maze($self->x, $old_y, 'N');
}
}
逻辑非常简单,x值增大,说明当前房间的西面可行,以及上一个房间的东面可行;x值减小,说明当前房间的东面可行,以及上一个房间的西面可行...以此类推。
同时trigger还负责min_x, max_x, max_y这三个值的更新。
很明显,set_maze函数的作用就是更新map属性引用的哈希数据:
sub set_maze {
my ($self, $in_x, $in_y, $dir) = @_;
my $room = $in_x.'_'.$in_y;
if (not exists $self->map->{$room}){
#assume all directions are not movable
$self->map->{$room} = 0;
}
my $index = index 'NSWE', $dir;
$self->map->{$room} |= (1 << $index);
}
主要逻辑基本上就是这些,需要注意一个特殊情况,也即mazer从迷宫出口出来时,不必根据当前坐标更新min_x, max_x, max_y这三个值,也即我们需要回退一下:
sub AdjustMaxMin {
my ($self, $exit_x, $exit_y) = @_;
if($self->min_x eq $exit_x && $self->z eq 'W'){
$self->min_x($self->min_x + 1);
}
elsif($self->max_x eq $exit_x && $self->z eq 'E'){
$self->max_x($self->max_x - 1);
}
elsif($self->max_y eq $exit_y && $self->z eq 'S'){
$self->max_y($self->max_y - 1);
}
}
走完一幅地图,将所有属性值恢复默认值:
sub reset_mazer {
my $self = shift;
$self->x(0);
$self->y(0);
$self->z('S');
$self->max_x(0);
$self->min_x(0);
$self->max_y(1);
%{$self->map} = ();
}
不要忘了在packge末尾使用:
__PACKAGE__->meta->make_immutable;
no Moose;
1;
以保持类的命名空间不被Moose本身导入的符号“污染”。
OK,主程序现在变得非常简单:
use Mazer;
my $input = "B-large-practice.in";
my $output = "output.txt";
open IN, '<', $input or die "Can't open file $input: $!";
open OUT, '>', $output or die "Can't open file $output: $!";
my $mazer = Mazer->new();
for my $tc_cnt (1..<IN>){
my $line = <IN>;
chomp($line);
my ($enter2exit, $exit2enter) = split " ", $line;
for my $act (split "", $enter2exit){
$mazer->OnAction($act);
};
$mazer->AdjustMaxMin($mazer->x, $mazer->y);
#turn around, re-enter the exit, explore again
$mazer->z($mazer->GetNextZ($mazer->z, 'O'));
for my $act (split "", $exit2enter){
$mazer->OnAction($act);
};
#print the map, reset all for next case
print OUT "Case #$tc_cnt:\n";
for my $y (1..$mazer->max_y){
for my $x ($mazer->min_x..$mazer->max_x){
my $room = $x.'_'.$y;
print OUT sprintf("%x", $mazer->map->{$room}) if exists $mazer->map->{$room};
}
print OUT "\n";
}
$mazer->reset_mazer();
}
close(IN);
close(OUT);