Как вы вращаете один слой матрицы на n мест? - PullRequest
0 голосов
/ 25 января 2019

Я прочитал все вопросы о переполнении стека относительно вращающихся матриц и ни один из них не затрагивает ничего, кроме перемещения или поворота на 90 или 180 градусов в направлении как отдельных слоев, так и целых матриц.

Я уже ответил на вопрос относительно его выполнения, но я ищу быстрые и / или «на месте» методы, предпочтительно, но любые методы кроме того, что я дал, было бы неплохо увидеть, хотя бы для образовательные цели. Мой пример на Perl 5, но, если ваши намерения ясны, почти любой язык должен быть приемлемым.

Матрица, использованная в моем примере, выглядит следующим образом

# 2D array @arr has 3 layers.
# @arr is 
#         0 1 2 3 4 5
#         a b c d e f
#         5 4 3 2 1 0
#         9 8 7 6 5 4
#         f e d c b a
#         4 5 6 7 8 9

Цель-> Повернуть средний слой на 2 места по часовой стрелке, чтобы получить следующее ...

Обратите внимание, что верхний левый угол среднего слоя, 'b c', переместился на 2 позиции вправо, а '8 4' с средней левой стороны теперь там, где был 'b c'.

# Rotated @arr is 
#         0 1 2 3 4 5
#         a 8 4 b c f
#         5 e 3 2 d 0
#         9 d 7 6 e 4 
#         f c b 5 1 a
#         4 5 6 7 8 9

В настоящее время я делаю что-то вроде этого, что, правда, ужасно медленно. Особенно на многих больших массивах.

#!/usr/bin/env perl
use strict; use warnings;

# Coordinates are $arr[$row][$col]
my @arr;
$arr[0][0]='0'; $arr[0][1]='1'; $arr[0][2]='2'; $arr[0][3]='3'; $arr[0][4]='4'; $arr[0][5]='5';
$arr[1][0]='a'; $arr[1][1]='b'; $arr[1][2]='c'; $arr[1][3]='d'; $arr[1][4]='e'; $arr[1][5]='f';
$arr[2][0]='5'; $arr[2][1]='4'; $arr[2][2]='3'; $arr[2][3]='2'; $arr[2][4]='1'; $arr[2][5]='0';
$arr[3][0]='9'; $arr[3][1]='8'; $arr[3][2]='7'; $arr[3][3]='6'; $arr[3][4]='5'; $arr[3][5]='4';
$arr[4][0]='f'; $arr[4][1]='e'; $arr[4][2]='d'; $arr[4][3]='c'; $arr[4][4]='b'; $arr[4][5]='a';
$arr[5][0]='4'; $arr[5][1]='5'; $arr[5][2]='6'; $arr[5][3]='7'; $arr[5][4]='8'; $arr[5][5]='9';

# Print matrix
print_matrix(@arr);

# Extract layer 2
my $target_layer=2;
my $layer_two=extract_layer($target_layer,@arr);

# From the top left corner of the layer, it is as follows
# bcde15bcde84
print "\n$layer_two\n";

# Rotate layer 2 clockwise 2 places
$layer_two=rotate_layer_cl($layer_two,2);

# 84bcde15bcde
print "$layer_two\n\n";

# Replace layer 2 in the same way
@arr=replace_layer($target_layer,$layer_two,@arr);

# Print again
print_matrix(@arr);

### Sub functions ###
# Extract layer by walking around it's coordinates like so
# [1,1]-[1,4]  Top(left->right)
# [2,4]-[4,4]  Right(top->bottom)
# [4,3]-[4,1]  Bottom(right->left)
# [3,1]-[2,1]  Left(bottom->top)
sub extract_layer {
    my ($layer_cnt,@matrix)=@_;
    my $width=scalar(@matrix);
    my $layer_width=$width-$layer_cnt;
    # layer_cnt=2
    # width=6
    # layer_width=4
    my $layer;
    for my $col ( $layer_cnt-1..$layer_width ) {
        $layer.=$matrix[$layer_cnt-1][$col];
    }
    for my $row ( $layer_cnt..$layer_width ) {
        $layer.=$matrix[$row][$layer_width];
    }
    my $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt-1 ) {
        $layer.=$matrix[$layer_width][$cnt];
        $cnt--;
    }
    $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt ) {
        $layer.=$matrix[$cnt][$layer_cnt-1];
        $cnt--;
    }
    return $layer;
}

# Shift input to the right by $n places, wrapping around.
sub rotate_layer_cl {
    my $n=$_[1];
    my $buf=substr($_[0],length($_[0])-$n,$n);
    return $buf.substr($_[0],0,length($_[0])-$n);
}

# Replace each char from the rotated layer.
sub replace_layer {
    my ($layer_cnt,$layer,@matrix)=@_;
    my $width=scalar(@matrix);
    my $layer_width=$width-$layer_cnt;
    # layer_cnt=2
    # width=6
    # layer_width=4
    my $slot=0;
    for my $col ( $layer_cnt-1..$layer_width ) {
        $matrix[$layer_cnt-1][$col]=substr($layer,$slot,1);
        $slot++;
    }
    for my $row ( $layer_cnt..$layer_width ) {
        $matrix[$row][$layer_width]=substr($layer,$slot,1);
        $slot++;
    }
    my $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt-1 ) {
        $matrix[$layer_width][$cnt]=substr($layer,$slot,1);
        $slot++;
        $cnt--;
    }
    $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt ) {
        $matrix[$cnt][$layer_cnt-1]=substr($layer,$slot,1);
        $slot++;
        $cnt--;
    }
    return @matrix;
}

# Prints given matrix
sub print_matrix {
    foreach my $row (@_) {
        my $cnt=0;
        foreach my $char (@$row) {
            print $char; $cnt++;
            if ( $cnt == scalar(@_) ) {
                print "\n";
            } else {
                print " ";
            }
        }
    }
}

Вышеприведенный код выводит следующее,

0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

bcde15bcde84
84bcde15bcde

0 1 2 3 4 5
a 8 4 b c f
5 e 3 2 d 0
9 d 7 6 e 4
f c b 5 1 a
4 5 6 7 8 9

, который показывает массив перед вращением среднего слоя, средний слой как строку, средний слой смещен как строку и окончательный результирующий массив после того, как средний слой был повернут на 2 места вправо.

----- EDIT -----

На данный момент самый быстрый способ, который я нашел, состоит в том, чтобы на самом деле не использовать полный 2D-массив, а поместить блок в обычный массив, например ...

my @hex_ary=('1234', '5678', '90ab', 'cdef');

Затем создайте строку слоя и сдвиньте ее так же, как показывает мой оригинальный пример.

Однако, поскольку они являются струнами, мне нужно только "ходить" вверх и вниз по сторонам. Верх и низ просто обозначены как ...

my $top=substr($hex_ary[0],0,3);

и

my $bot=reverse(substr($hex_ary[3],0,3));

для внешнего слоя в этом небольшом массиве 4x4, а стороны перебираются с помощью

my $right_side_char=substr($hex_ary[$top_row-$i],-1);
my $left_side_char=substr($hex_ary[$bot_row+$i],0,1);

Это увеличивает производительность ок. 100% от метода 2D, потому что количество срезов, взятых из массива, равно половине + 2.

Может быть, кто-то с лучшим пониманием C мог бы создать что-то более эффективное. Я чувствую, что Perl способен на такое только иногда.

Ответы [ 2 ]

0 голосов
/ 25 января 2019

Идея моего метода состоит в том, чтобы использовать очередь (ну, здесь я использую массив Perl, потому что я не смог найти подходящий модуль, но он далек от идеала), содержащий n элементы (где n - это число мест, на которые вы хотите повернуть слой), и для итерации по каждому элементу слоя и каждый раз push его значение в очереди и замена его на первый элемент очереди.

Это то, что делает саб rotate_matrix.
- $i и $j представляют индекс текущего элемента слоя. Их значения находятся между $target_layer и $max_i / $max_j; sub next_ij заботится о вычислении индекса следующего элемента слоя.
- Интересные вещи происходят внутри цикла while(1) (который на самом деле do...while; но циклы do while немного особенные в Perl): значение текущего элемента помещается в очередь и, если их больше чем $n элементов в очереди (что означает, что мы уже добавили $n элементов, представляющих тот факт, что мы хотим повернуть на $n), мы заменяем его значение первым элементом очередь. Блок continue заботится о приращении $i и $j и останавливает цикл, как только мы возвращаемся к первому элементу слоя.
- Как только этот цикл завершен, первые n элементы слоя не были обновлены (из-за этого next unless @queue > $n), поэтому нам нужно позаботиться об этом: это цикл while (@queue)).

По сложности, нет необходимости копировать массив. Использование памяти составляет O(n) (размер очереди). Со временем вы зацикливаетесь один раз на каждом элементе слоя, и каждый раз вы push и shift из своей очереди (используя лучшую структуру данных, такую ​​как связанный список, это будет O(1), но с Массив Perl, вероятно, в строках O(n), но, вероятно, амортизируется до O(1)), и вычисляется следующий индекс (O(1)). Таким образом, вы получите сложность времени, в итоге вы получите O(size of the layer to rotate).

Несколько предостережений: если $n больше, чем количество элементов в слое, это не сработает (но это можно исправить с помощью простого модуля). Если вы попросите слой, которого нет в матрице (например, 4-й слой матрицы 2x2), произойдут странные вещи (но, опять же, это легко исправить).

#!/usr/bin/perl

use strict;
use warnings;
use v5.14;


my @arr = ( [qw(0 1 2 3 4 5)],
            [qw(a b c d e f)],
            [qw(5 4 3 2 1 0)],
            [qw(9 8 7 6 5 4)],
            [qw(f e d c b a)],
            [qw(4 5 6 7 8 9)] );

print_matrix(@arr);

rotate_matrix(\@arr, 2, 2);
say "";

print_matrix(@arr);


sub rotate_matrix {
    my ($matrix, $target_layer, $n) = @_;
    --$target_layer; # I prefer a 0-indexed value
    # TODO: check that $target_layer < @$matrix/2
    # TODO: do something like $n = $n % (num of elements in layer)

    my @queue;
    my ($i, $j) = ($target_layer, $target_layer);
    my ($max_i, $max_j) = (@{$matrix->[0]}-$target_layer-1, @$matrix-$target_layer-1);

    while (1) { # Actually a do...while loop (see 'continue' block)
        push @queue, $matrix->[$i][$j];
        next unless @queue > $n; # Waiting to reach n-th element
        $matrix->[$i][$j] = shift @queue;
    } continue {
        ($i, $j) = next_ij($target_layer,$max_i,$max_j,$i,$j);
        # Stopping if we are back at the begining
        last if $i == $target_layer && $j == $target_layer;
    }

    # Emptying queue
    while (@queue) {
        $matrix->[$i][$j] = shift @queue;
        ($i, $j) = next_ij($target_layer,$max_i,$max_j,$i,$j);
    }


}

# Computes the index of the next element of the layer
sub next_ij {
    my ($target_layer, $max_i, $max_j, $i, $j) = @_;
    if ($j == $max_j) { # Last column
        if ($i == $max_i) { # Last row (bottom right -> going left)
            return ($i, $j-1);
        } else { # Not last row (somewhere on the right col -> going down)
            return ($i+1, $j);
        }
    } elsif ($j == $target_layer) { # First column
        if ($i == $target_layer) { # First row (top left -> going right)
            return ($i, $j+1);
        } else { # Not top row (somewhere on the left col -> going up)
            return ($i-1, $j);
        }
    } else { # Neither first nor last column
        if ($i == $target_layer) { # First row (somewhere on the top row -> going right)
            return ($i, $j+1);
        } else { # Last row (somewhere on the last row -> going left)
            return ($i, $j-1);
        }
    }
}



# Prints given matrix
sub print_matrix {
    foreach my $row (@_) {
        my $cnt=0;
        foreach my $char (@$row) {
            print $char; $cnt++;
            if ( $cnt == scalar(@_) ) {
                print "\n";
            } else {
                print " ";
            }
        }
    }
}
0 голосов
/ 25 января 2019

Подход в двух словах:

  • копировать массив
  • вычислить список членов (строка, столбец) для кольца
  • цикл по спискуindex

    • чтение из элемента [index] в исходном массиве
    • запись в элемент [(index + shift)% длины кольца] в массиве копирования
  • Сдвиг вправо на N мест: смещение == N

  • Сдвиг влево на N мест: смещение == -N
#!/usr/bin/perl
use strict;
use warnings;

sub dump_array($$) {
    my($label, $array) = @_;
    print "${label}:\n";
    foreach my $row (@{ $array }) {
        print join(" ", @{ $row }), "\n";
    }
    print "\n";
}

sub copy_array($) {
    my($array) = @_;
    my @copy;
    foreach my $row (@{ $array }) {
        push(@copy, [ @{ $row } ]);
    }
    return(\@copy);
}

sub ring_index_to_row_col($$$$$) {
    my($N, $ring, $total, $length, $index) = @_;
    my($row, $col);

    if ($index < $length) {
        # top row, left to right
        $row = 0;
        $col = $index;
    } elsif ($index < 2 * $length - 2) {
        # top to bottom, right row
        $row = $index - $length + 1;
        $col = $N - 1 - 2 * $ring;
    } elsif ($index < 3 * $length - 2) {
        # bottom row, right to left
        $row = $N - 1 - 2 * $ring;
        #$col = $length - 1 - ($index - 2 * $length + 2);
        $col = -$index + 3 * $length - 3;
    } else {
        # bottom to top, left row
        #$row = $total - 1 - $index + 1;
        $row = $total - $index;
        $col = 0;
    }

    #print "${index}\t of ${total}\t-> ${row}, ${col}\n";

    # shift $length x length array to offset ($ring, $ring)
    return([$row + $ring, $col + $ring]);
}

sub ring_members($$) {
    my($N, $ring) = @_;
    my @list;

    # @TODO: odd N?
    #
    # Examples for N == 6
    #   0   -> 2*6 + 2*4 = 20
    #   1   -> 2*4 + 2*2 = 12
    #   2   -> 2*2 + 2*0 =  4
    #
    # Examples for N == 5
    #   0   -> 2*5 + 2*3
    #   1   -> 2*3 + 2*1
    #   2   -> 1
    #
    # Examples for N == 4
    #   0   -> 2*4 + 2*2 = 12
    #   1   -> 2*2 + 2*0 =  4
    my $length  = $N - 2 * $ring;
    my $members = 4 *  $N - 8 * $ring - 4;

    foreach my $index (0..$members-1) {
        push(@list, ring_index_to_row_col($N, $ring, $members, $length, $index));
    }

    return(\@list);
}

sub rotate_array_ring(\@$$) {
    my($source, $ring, $shift) = @_;

    # Sanity check. @TODO is the check correct for odd N?
    my $N              = @{ $source };
    die "ERROR: invalid ring '${ring}' for 2D array of size $N!\n"
        unless $ring < ($N / 2);

    my $copy   = copy_array($source);

    my $list   = ring_members($N, $ring);
    my $length = @{ $list };

    foreach my $index (0..$length-1) {
        my($row,   $col)   = @{ $list->[ $index                     ] };
        my($s_row, $s_col) = @{ $list->[($index + $shift) % $length ] };

        $copy->[$s_row]->[$s_col] = $source->[$row]->[$col];
    }

    return($copy);
}

my @source;
while (<DATA>) {
    chomp;
    push(@source, [ split(/\s+/, $_) ]);
}
dump_array('SOURCE', \@source);

dump_array('SHIFT 1 RING 0', rotate_array_ring(@source, 0, 1));
dump_array('SHIFT 1 RING 1', rotate_array_ring(@source, 1, 1));
dump_array('SHIFT 1 RING 2', rotate_array_ring(@source, 2, 1));
dump_array('SHIFT 2 RING 0', rotate_array_ring(@source, 0, 2));
dump_array('SHIFT 2 RING 1', rotate_array_ring(@source, 1, 2));
dump_array('SHIFT 2 RING 2', rotate_array_ring(@source, 2, 2));

exit 0;

__DATA__
0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

Пример вывода:

$ perl dummy.pl
SOURCE:
0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

SHIFT 1 RING 0:
a 0 1 2 3 4
5 b c d e 5
9 4 3 2 1 f
f 8 7 6 5 0
4 e d c b 4
5 6 7 8 9 a

SHIFT 1 RING 1:
0 1 2 3 4 5
a 4 b c d f
5 8 3 2 e 0
9 e 7 6 1 4
f d c b 5 a
4 5 6 7 8 9

SHIFT 1 RING 2:
0 1 2 3 4 5
a b c d e f
5 4 7 3 1 0
9 8 6 2 5 4
f e d c b a
4 5 6 7 8 9

SHIFT 2 RING 0:
5 a 0 1 2 3
9 b c d e 4
f 4 3 2 1 5
4 8 7 6 5 f
5 e d c b 0
6 7 8 9 a 4

SHIFT 2 RING 1:
0 1 2 3 4 5
a 8 4 b c f
5 e 3 2 d 0
9 d 7 6 e 4
f c b 5 1 a
4 5 6 7 8 9

SHIFT 2 RING 2:
0 1 2 3 4 5
a b c d e f
5 4 6 7 1 0
9 8 2 3 5 4
f e d c b a
4 5 6 7 8 9

ОПТИМИЗАЦИЯ: не копировать весь массив, но

  1. использовать $list, чтобы скопировать членов кольца в список изатем
  2. зациклите этот список, чтобы переместить их обратно в массив.

Т.е. два цикла вместо одного:

my $list   = ring_members($N, $ring);
my $length = @{ $list };

# in-place approach
my @members;
foreach my $index (0..$length-1) {
    my($row, $col) = @{ $list->[ $index ] };
    push(@members, $source->[$row]->[$col]);
}
foreach my $index (0..$length-1) {
    my($row, $col) = @{ $list->[ ($index + $shift) % $length ] };
    $source->[$row]->[$col] = $members[$index];
}

return($source);

ОПТИМИЗАЦИЯ 2: если у вас есть последовательность операций, которую необходимо применить к набору двумерных массивов одинакового размера, вы можете предварительно сгенерировать их и обрабатывать их снова и снова, используя функцию:

my $N     = 6; # all arrays are of same size
my @rings = (
    ring_members($N, 0),
    ring_members($N, 1),
    ring_members($N, 2),
);
my @operations = (
    { ring => $rings[0], shift =>  1 },
    { ring => $rings[1], shift => -1 },
    { ring => $rings[2], shift =>  2 },
    # ...
)

# apply one operation in-place on array
sub apply_operation($$) {
    my($array, $operation) = @_;
    my $list               = $operation->{ring};
    my $shift              = $operation->{shift};
    my $length             = @{ $list };

    my @members;
    foreach my $index (0..$length-1) {
        my($row, $col) = @{ $list->[ $index ] };
        push(@members, $array->[$row]->[$col]);
    }
    foreach my $index (0..$length-1) {
        my($row, $col) = @{ $list->[ ($index + $shift) % $length ] };
        $array->[$row]->[$col] = $members[$index];
    }
}

# apply operation sequence in-place on array
sub apply_operations($$) {
    my($array, $operations) = @_;

    foreach my $operation (@{ $operations }) {
        apply_operation($array, $operation);    
    }
}

apply_operations(\@array1, \@operations);
apply_operations(\@array2, \@operations);
apply_operations(\@array3, \@operations);
...

ОПТИМИЗАЦИЯ 3: не делайте копию всей группы при использовании на месте, а только тех участников, которые будут перезаписаны при ротации.Т.е. использовать 3 цикла:

  1. скопировать первый или последний $shift членов (в зависимости от направления вращения) в массив @cache.
  2. сделать прямую копию для другого $length - abs($shift) членов.
  3. скопировать @cache членов в их повернутые места.

ОПТИМИЗАЦИЯ 4: не использовать двумерный массив (AoA), но1D массив с $N * $N записями.Т.е.

  • вместо ring_index_to_row_col() с возвратом [$row, $col] вы получите ring_index_to_array_index() с возвратом $row * $N + $col.
  • доступ к массиву ->[$row]->[$col] будет заменен на ->[$array_index].
...