nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

巡回セールスマン問題をPerlで(4)

はじめに

前回に引き続き、巡回セールスマン問題の解法をPythonからPerlに移植します。
今回は、greed1関数内で既存ライブラリ(List::PriorityQueue, Graph::UnionFind)を使っていた部分を、元サイトのロジックどおりに実装し直して、draw_path関数の移植をミスっていたのを修正しました。
結果的に全サンプルデータで元サイトどおりの結果が出るようになったので、今回で終了にしたいと思います。

greed1で使っているモジュール

extlib/lib/perl5/My/Edge.pm
package My::Edge;

sub new {
    my $class = shift;
    my ($p1, $p2, $weight) = @_;
    return bless {
        p1     => $p1,
        p2     => $p2,
        weight => $weight,
    }, $class;
}

1;
extlib/lib/perl5/My/PQueue.pm
package My::PQueue;

use strict;
use warnings;

sub new {
    my $class = shift;
    my ($cmp) = @_;
    my $self = bless {
        buff => [],
        cmp  => $cmp || sub { $_[0] - $_[1] },
    }, $class;
    for my $n (0 .. $#{$self->{buff}}) {
        $self->downheap($n);
    }
    return $self;
}

sub downheap {
    my $self = shift;
    my ($n) = @_;
    my $size = scalar(@{$self->{buff}});
    while () {
        my $c = 2 * $n + 1;
        if ($c >= $size) {
            last;
        }
        if ($c + 1 < $size) {
            if ($self->{cmp}->($self->{buff}->[$c], $self->{buff}->[$c + 1]) > 0) {
                $c += 1;
            }
        }
        if ($self->{cmp}->($self->{buff}->[$n], $self->{buff}->[$c]) <= 0) {
            last;
        }
        my $temp = $self->{buff}->[$n];
        $self->{buff}->[$n] = $self->{buff}->[$c];
        $self->{buff}->[$c] = $temp;
        $n = $c;
    }
}

sub upheap {
    my $self = shift;
    my ($n) = @_;
    while () {
        my $p = ($n - 1) / 2;
        if ($p < 0 || $self->{cmp}->($self->{buff}->[$p], $self->{buff}->[$n]) <= 0) {
            last;
        }
        my $temp = $self->{buff}->[$n];
        $self->{buff}->[$n] = $self->{buff}->[$p];
        $self->{buff}->[$p] = $temp;
        $n = $p;
    }
}

sub push {
    my $self = shift;
    my ($data) = @_;
    push @{$self->{buff}}, $data;
    $self->upheap(scalar(@{$self->{buff}}) - 1);
}

sub pop {
    my $self = shift;
    if (scalar(@{$self->{buff}}) == 0) {
        die;
    }
    my $value = $self->{buff}->[0];
    my $last = pop @{$self->{buff}};
    if (scalar(@{$self->{buff}}) > 0) {
        $self->{buff}->[0] = $last;
        $self->downheap(0);
    }
    return $value;
}

1;
extlib/lib/perl5/My/UnionFind.pm
package My::UnionFind;

use strict;
use warnings;

sub new {
    my $class = shift;
    my ($size) = @_;
    my $table = [ map { -1 } (0 .. ($size - 1)) ];
    return bless {
        table => $table,
    }, $class;
}

sub find {
    my $self = shift;
    my ($x) = @_;
    if ($self->{table}->[$x] < 0) {
        return $x;
    }
    else {
        $self->{table}->[$x] = $self->find($self->{table}->[$x]);
        return $self->{table}->[$x];
    }
}

sub union {
    my $self = shift;
    my ($x, $y) = @_;
    my $s1 = $self->find($x);
    my $s2 = $self->find($y);
    if ($s1 != $s2) {
        if ($self->{table}->[$s1] <= $self->{table}->[$s2]) {
            $self->{table}->[$s1] += $self->{table}->[$s2];
            $self->{table}->[$s2] = $s1;
        }
        else {
            $self->{table}->[$s2] += $self->{table}->[$s1];
            $self->{table}->[$s1] = $s2;
        }
        return 1;
    }
    return 0;
}

1;

実行スクリプト

#!/usr/bin/perl
use strict;
use warnings;
use lib "./extlib/lib/perl5/";

use Time::HiRes qw(gettimeofday tv_interval);
use Data::Dumper;
use My::Edge;
use My::PQueue;
use My::UnionFind;
use Tk;
use List::Util qw(max);

sub read_data {
    my $buff = [];
    while (my $line = <STDIN>) {
        my @vals = split /\s+/, $line;
        push @$buff, \@vals;
    }
    return $buff;
}

sub distance {
    my $ps = shift; # [ [x, y], .. ]
    my $max = $#{$ps};
    my $table = [ map { [0] } @$ps ];
    for my $i (0..$max) {
        for my $j (0..$max) {
            if ($i != $j) {
                my $dx = $ps->[$i]->[0] - $ps->[$j]->[0];
                my $dy = $ps->[$i]->[1] - $ps->[$j]->[1];
                $table->[$i]->[$j] = sqrt($dx ** 2 + $dy ** 2);
            }
        }
    }
    return $table;
}

sub path_length {
    my ($path, $distance_table) = @_;
    my $n = 0;
    for my $i (1..$#{$path}) {
        $n += $distance_table->[$path->[$i - 1]]->[$path->[$i]];
    }
    $n += $distance_table->[$path->[0]]->[$path->[-1]];
    return $n;
}

sub greedy0 {
    my ($size, $distance_table) = @_;
    my @path = (0 .. $size-1);
    for my $i (0 .. $size-2) {
        my $min_len = 1000000;
        my $min_pos = 0;
        for my $j ($i+1 .. $size-1) {
            my $len = $distance_table->[$path[$i]]->[$path[$j]];
            if ($len < $min_len) {
                $min_len = $len;
                $min_pos = $j;
            }
        }
        ($path[$i + 1], $path[$min_pos]) = ($path[$min_pos], $path[$i + 1]);
    }
    return \@path;
}

sub make_edge {
    my ($size, $distance_table) = @_;
    my $edges = My::PQueue->new(sub { $_[0]->{weight} - $_[1]->{weight} });
    for my $i (0 .. $size-2) {
        for my $j ($i+1 .. $size-1) {
            my $priority = $distance_table->[$i]->[$j];
            my $edge = My::Edge->new($i, $j, $priority);
            $edges->push($edge);
        }
    }
    return $edges;
}

sub edge_to_path {
    my ($edges, $size) = @_;
    my $search_edge = sub {
        my $x = shift;
        my @r;
        for my $i (0 .. $size-1) {
            my $edge = $edges->[$i];
            my ($p1, $p2) = ($edge->{p1}, $edge->{p2});
            if ($p1 == $x) {
                push @r, $p2;
            }
            elsif ($p2 == $x) {
                push @r, $p1;
            }
        }
        return @r;
    };
    my @path = map { 0 } (0 .. $size-1);
    for my $i (0 .. $size-2) {
        my ($x, $y) = $search_edge->($path[$i]);
        if ($i == 0) {
            $path[$i + 1] = $x;
            $path[-1] = $y;
        }
        elsif ($path[$i - 1] == $x) {
            $path[$i + 1] = $y;
        }
        else {
            $path[$i + 1] = $x;
        }
    }
    return \@path;
}

sub greedy1 {
    my ($size, $edges) = @_;
    
    my @edge_count = map { 0 } (0 .. $size-1);
    
    my $u = My::UnionFind->new($size);
    
    my $i = 0;
    my @select_edge;
    while ($i < $size) {
        my $edge = $edges->pop();
        my ($p1, $p2) = ($edge->{p1}, $edge->{p2});
        if (
            $edge_count[$p1] < 2 &&
            $edge_count[$p2] < 2 &&
            ( $u->find($p1) != $u->find($p2) || $i == $size -1 )
        ) {
            $u->union($p1, $p2);
            $edge_count[$p1] += 1;
            $edge_count[$p2] += 1;
            push @select_edge, $edge;
            $i += 1;
        }
    }
    return edge_to_path(\@select_edge, $size);
}

sub opt_2 {
    my ($size, $path, $distance_table) = @_;
    my $total = 0;
    while () {
        my $count = 0;
        for my $i (0 .. $size-3) {
            my $i1 = $i + 1;
            for my $j ($i+2 .. $size-1) {
                my $j1;
                if ($j == $size-1) {
                    $j1 = 0;
                }
                else {
                    $j1 = $j + 1;
                }
                if ($i != 0 || $j1 != 0) {
                    my $l1 = $distance_table->[$path->[$i]]->[$path->[$i1]];
                    my $l2 = $distance_table->[$path->[$j]]->[$path->[$j1]];
                    my $l3 = $distance_table->[$path->[$i]]->[$path->[$j]];
                    my $l4 = $distance_table->[$path->[$i1]]->[$path->[$j1]];
                    if ($l1 + $l2 > $l3 + $l4) {
                        my @new_path = @{$path}[$i1 .. $j];
                        @{$path}[$i1 .. $j] = reverse @new_path;
                        $count += 1;
                    }
                }
            }
        }
        my $path_len = path_length($path, $distance_table);
        print "len:$path_len count:$count\n";
        $total += $count;
        if ($count == 0) {
            last;
        }
    }
    return $path, $total;
}

sub opt_or {
    my ($size, $path, $distance_table) = @_;
    my $total = 0;
    while () {
        my $count = 0;
        for my $i (0 .. $size-1) {
            my $i0 = $i - 1;
            my $i1 = $i + 1;
            $i0 = $size - 1 if $i0 < 0;
            $i1 = 0         if $i1 == $size;
            for my $j (0 .. $size-1) {
                my $j1 = $j + 1;
                $j1 = 0 if $j1 == $size;
                if ($j != $i and $j1 != $i) {
                    my $l1 = $distance_table->[$path->[$i0]]->[$path->[$i]];
                    my $l2 = $distance_table->[$path->[$i]]->[$path->[$i1]];
                    my $l3 = $distance_table->[$path->[$j]]->[$path->[$j1]];
                    my $l4 = $distance_table->[$path->[$i0]]->[$path->[$i1]];
                    my $l5 = $distance_table->[$path->[$j]]->[$path->[$i]];
                    my $l6 = $distance_table->[$path->[$i]]->[$path->[$j1]];
                    if ($l1 + $l2 + $l3 > $l4 + $l5 + $l6) {
                        my $p = splice(@$path, $i, 1);
                        if ($i < $j) {
                            splice(@$path, $j, 0, $p);
                        }
                        else {
                            splice(@$path, $j1, 0, $p);
                        }
                        $count += 1;
                    }
                }
            }
        }
        my $path_len = path_length($path, $distance_table);
        print "len:$path_len count:$count\n";
        $total += $count;
        if ($count == 0) {
            last;
        }
    }
    return $path, $total;
}

sub draw {
    my ($path, $point_table) = @_;
    my $top = new MainWindow;

    my $max_x = max map { $point_table->[$_]->[0] } @$path;
    my $max_y = max map { $point_table->[$_]->[1] } @$path;
    my $canvas = $top->Canvas( -width => $max_x + 20, -height => $max_y + 20 )->pack();
    
    draw_path($canvas, $path, $point_table);
 
    MainLoop;
}

sub draw_path {
    my ($canvas, $path, $point_table) = @_;
    my @_path = map { $point_table->[$_] } @$path;
    my ($x0, $y0) = @{$_path[0]};
    for my $i (1 .. $#_path) {
        my ($x1, $y1) = @{$_path[$i]};
        $canvas->createLine($x0, $y0, $x1, $y1);
        $x0 = $x1;
        $y0 = $y1;
    }
    my ($x1, $y1) = @{$_path[0]};
    $canvas->createLine($x0, $y0, $x1, $y1);
    for my $p (@$point_table) {
        my ($x, $y) = ($p->[0], $p->[1]);
        $canvas->createOval($x-4, $y-4, $x+4, $y+4, -fill => 'green' );
    }
}

sub main {

    if ($#ARGV < 1) {
        die "invalid args 1";
    }

    my $start = [gettimeofday];

    # 初期化
    my $point_table = read_data();
    my $point_size = scalar(@$point_table);
    my $distance_table = distance($point_table);

    # 貪欲法で最適化
    my $path;
    if ($ARGV[0] eq "g0") {
        $path = greedy0($point_size, $distance_table);
    }
    elsif ($ARGV[0] eq "g1") {
        my $edges = make_edge($point_size, $distance_table);
        $path = greedy1($point_size, $edges);
    }
    else {
        die "invalid args 2";
    }
    my $path_len = path_length($path, $distance_table);

    my $end1 = [gettimeofday];
    my $spend1 = tv_interval $start, $end1;

    printf "\nlen:%d\ntime1:%s\n", $path_len, $spend1;

    # 2-opt / or-opt で最適化
    if ($ARGV[1] eq "2opt") {
        ($path, undef) = opt_2($point_size, $path, $distance_table);
    }
    elsif ($ARGV[1] eq "oropt") {
        ($path, undef) = opt_or($point_size, $path, $distance_table);
    }
    elsif ($ARGV[1] eq "2or") {
        while () {
            my $flag;
            ($path, undef) = opt_2($point_size, $path, $distance_table);
            ($path, $flag) = opt_or($point_size, $path, $distance_table);
            last if $flag == 0;
        }
    }
    elsif ($ARGV[1] eq "or2") {
        while () {
            my $flag;
            ($path, undef) = opt_or($point_size, $path, $distance_table);
            ($path, $flag) = opt_2($point_size, $path, $distance_table);
            last if $flag == 0;
        }
    }
    else {
        #die "invalid args 3";
    }
    $path_len = path_length($path, $distance_table);

    my $end2 = [gettimeofday];
    my $spend2 = tv_interval $end1, $end2;

    printf "\nlen:%d\ntime2:%s\n\n", $path_len, $spend2;

    draw($path, $point_table);
}


main();

__END__

=head1 USAGE

 perl SCRIPT_NAME [g0 | g1] [2opt | oropt | 2or | or2] < DATA_FILE

=head1 DATA FILE FORMAT

 position1 [space] position2

=head1 DATA FILE EXAMPLE

 20  20
 120 20
 220 20
 70  120
 170 120
 270 120
 20  220
 120 220