Have fun with CodeJam, by using Perl!(1)

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 :=)
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);



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值