Рандомизировать матрицу в Perl, сохраняя итоги строк и столбцов - PullRequest
11 голосов
/ 25 января 2010

У меня есть матрица, которую я хочу рандомизировать пару тысяч раз, при этом общее количество строк и столбцов остается неизменным:

     1 2 3 
   A 0 0 1 
   B 1 1 0 
   C 1 0 0      

Примером правильной случайной матрицы будет:

     1 2 3
   A 1 0 0
   B 1 1 0
   C 0 0 1

Моя фактическая матрица намного больше (около 600х600 элементов), поэтому мне действительно нужен подход, эффективный в вычислительном отношении.

Мой первоначальный (неэффективный) подход состоял в перестановке массивов с использованием Perl Cookbook shuffle

Я вставил свой текущий код ниже. У меня есть дополнительный код, чтобы начать с нового перемешанного списка чисел, если в цикле while не найдено решение. Алгоритм отлично работает для небольшой матрицы, но как только я начинаю увеличивать масштаб, мне нужно вечно, чтобы найти случайную матрицу, которая соответствует требованиям.

Есть ли более эффективный способ выполнить то, что я ищу? Большое спасибо!

#!/usr/bin/perl -w
use strict;

my %matrix = ( 'A' => {'3'  => 1 },
           'B' => {'1'  => 1,
               '2'  => 1 },
           'C' => {'1'  => 1 }
    );

my @letters = ();
my @numbers = ();

foreach my $letter (keys %matrix){
    foreach my $number (keys %{$matrix{$letter}}){
    push (@letters, $letter);
    push (@numbers, $number);
    }
}

my %random_matrix = ();

&shuffle(\@numbers);
foreach my $letter (@letters){
    while (exists($random_matrix{$letter}{$numbers[0]})){
    &shuffle (\@numbers);
    }
    my $chosen_number = shift (@numbers);
    $random_matrix{$letter}{$chosen_number} = 1;
}

sub shuffle {
    my $array = shift;
    my $i = scalar(@$array);
    my $j;
    foreach my $item (@$array )
    {
        --$i;
        $j = int rand ($i+1);
        next if $i == $j;
        @$array [$i,$j] = @$array[$j,$i];
    }
    return @$array;
}

Ответы [ 5 ]

9 голосов
/ 26 января 2010

Проблема с вашим текущим алгоритмом заключается в том, что вы пытаетесь перетасовать свой путь из тупиковых ситуаций, в частности, когда ваши массивы @letters и @numbers (после начального перемешивания @numbers) дают одну и ту же ячейку больше чем единожды. Этот подход работает, когда матрица мала, потому что не требуется слишком много попыток найти подходящую перестановку. Тем не менее, это убийца, когда списки большие. Даже если бы вы могли более эффективно искать альтернативы - например, пытаясь перестановок, а не случайных перемешиваний - такой подход, вероятно, обречен.

Вместо того, чтобы перетасовывать целые списки, вы можете решить эту проблему, внеся небольшие изменения в существующую матрицу.

Например, давайте начнем с вашего примера матрицы (назовите ее M1). Случайно выберите одну ячейку для изменения (скажем, A1). На данный момент матрица находится в недопустимом состоянии. Наша цель состоит в том, чтобы исправить это в минимальном количестве правок - в частности, еще 3 правки. Вы реализуете эти 3 дополнительных правки, «обходя» матрицу, при этом каждый ремонт строки или столбца приводит к решению еще одной проблемы, пока вы не пройдете полный круг (ошибка ... полный прямоугольник).

Например, после изменения А1 с 0 на 1, есть 3 пути для следующего ремонта: А3, В1 и С1. Давайте решим, что первое редактирование должно исправить строки. Итак, мы выбираем А3. При втором редактировании мы исправим столбец, поэтому у нас есть выбор: B3 или C3 (скажем, C3). Окончательный ремонт предлагает только один выбор (C1), потому что нам нужно вернуться к столбцу нашего исходного редактирования. Конечный результат - новая действительная матрица.

    Orig         Change A1     Change A3     Change C3     Change C1
    M1                                                     M2

    1 2 3        1 2 3         1 2 3         1 2 3         1 2 3
    -----        -----         -----         -----         -----
A | 0 0 1        1 0 1         1 0 0         1 0 0         1 0 0
B | 1 1 0        1 1 0         1 1 0         1 1 0         1 1 0
C | 1 0 0        1 0 0         1 0 0         1 0 1         0 0 1

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

Этот подход будет быстро генерировать новые действительные матрицы. Это не обязательно приведет к случайным результатам: M1 и M2 будут по-прежнему тесно связаны друг с другом, и эта точка станет более очевидной при увеличении размера матрицы.

Как вы увеличиваете случайность? Вы упомянули, что большинство ячеек (99% и более) - это нули. Одна идея состояла бы в следующем: для каждого 1 в матрице установите его значение равным 0, а затем восстановите матрицу, используя метод 4-edit, описанный выше. По сути, вы будете перемещать все из них в новые случайные места.

Вот иллюстрация. Возможно, здесь есть дальнейшая оптимизация скорости, но этот подход позволил получить 10 новых матриц 600x600 с плотностью 0,5% за 30 секунд на моем компьютере с Windows. Не знаю, достаточно ли это быстро.

use strict;
use warnings;

# Args: N rows, N columns, density, N iterations.
main(@ARGV);

sub main {
    my $n_iter = pop;
    my $matrix = init_matrix(@_);
    print_matrix($matrix);
    for my $n (1 .. $n_iter){
        warn $n, "\n"; # Show progress.
        edit_matrix($matrix);
        print_matrix($matrix);
    }
}

sub init_matrix {
    # Generate initial matrix, given N of rows, N of cols, and density.
    my ($rows, $cols, $density) = @_;
    my @matrix;
    for my $r (1 .. $rows){
        push @matrix, [ map { rand() < $density ? 1 : 0  } 1 .. $cols ];
    }
    return \@matrix;
}

sub print_matrix {
    # Dump out a matrix for checking.
    my $matrix = shift;
    print "\n";
    for my $row (@$matrix){
        my @vals = map { $_ ? 1 : ''} @$row;
        print join("\t", @vals), "\n";
    }
}

sub edit_matrix {
    # Takes a matrix and moves all of the non-empty cells somewhere else.
    my $matrix = shift;
    my $move_these = cells_to_move($matrix);
    for my $cell (@$move_these){
        my ($i, $j) = @$cell;
        # Move the cell, provided that the cell hasn't been moved
        # already and the subsequent edits don't lead to a dead end.
        $matrix->[$i][$j] = 0
            if $matrix->[$i][$j]
            and other_edits($matrix, $cell, 0, $j);
    }
}

sub cells_to_move {
    # Returns a list of non-empty cells.
    my $matrix = shift;
    my $i = -1;
    my @cells = ();
    for my $row (@$matrix){
        $i ++;
        for my $j (0 .. @$row - 1){
            push @cells, [$i, $j] if $matrix->[$i][$j];
        }
    }
    return \@cells;
}

sub other_edits {
    my ($matrix, $cell, $step, $last_j) = @_;

    # We have succeeded if we've already made 3 edits.
    $step ++;
    return 1 if $step > 3;

    # Determine the roster of next edits to fix the row or
    # column total upset by our prior edit.
    my ($i, $j) = @$cell;
    my @fixes;
    if ($step == 1){
        @fixes = 
            map  { [$i, $_] }
            grep { $_ != $j and not $matrix->[$i][$_] }
            0 .. @{$matrix->[0]} - 1
        ;
        shuffle(\@fixes);
    }
    elsif ($step == 2) {
        @fixes = 
            map  { [$_, $j] }
            grep { $_ != $i and $matrix->[$_][$j] }
            0 .. @$matrix - 1
        ;
        shuffle(\@fixes);
    }
    else {
        # On the last edit, the column of the fix must be
        # the same as the column of the initial edit.
        @fixes = ([$i, $last_j]) unless $matrix->[$i][$last_j];
    }

    for my $f (@fixes){
        # If all subsequent fixes succeed, we are golden: make
        # the current fix and return true.
        if ( other_edits($matrix, [@$f], $step, $last_j) ){
            $matrix->[$f->[0]][$f->[1]] = $step == 2 ? 0 : 1;
            return 1;
        }
    }

    # Failure if we get here.
    return;
}

sub shuffle {
    my $array = shift;
    my $i = scalar(@$array);
    my $j;
    for (@$array ){
        $i --;
        $j = int rand($i + 1);
        @$array[$i, $j] = @$array[$j, $i] unless $i == $j;
    }
}
5 голосов
/ 25 января 2010

Шаг 1: Сначала я инициализирую матрицу нулями и вычисляю требуемые итоги по строкам и столбцам.

Шаг 2: Теперь выберите случайную строку, взвешенную по количеству единиц, которые должны быть в этой строке (таким образом, строка со счетом 300 будет выбрана с большей вероятностью, чем строка с весом 5).

Шаг 3: В этой строке выберите случайный столбец, взвешенный по количеству 1 в этом столбце (за исключением игнорирования любых ячеек, которые могут уже содержать 1 - подробнее об этом позже).

Шаг 4. Поместите единицу в эту ячейку и уменьшите количество строк и столбцов для соответствующей строки и столбца.

Шаг 5: Вернитесь к шагу 2, пока ни в одной строке не будет нуля.

Проблема в том, что этот алгоритм может не завершиться, потому что у вас может быть строка, в которой вам нужно разместить единицу, и столбец, в котором она нужна, но вы уже поместили единицу в эту ячейку, так что вы застрять'. Я не уверен, насколько вероятно, что это произойдет, но я не удивлюсь, если это произошло очень часто - достаточно, чтобы сделать алгоритм непригодным для использования. Если это проблема, я могу подумать о двух способах ее решения:

a) Рекурсивно сконструируйте вышеуказанный алгоритм и разрешите возврат в случае сбоя.

b) Разрешить ячейке содержать значение больше 1, если другой опции нет, и продолжать работу. Затем в конце вы получите правильное количество строк и столбцов, но некоторые ячейки могут содержать числа больше 1. Вы можете исправить это, найдя группировку, которая выглядит следующим образом:

2 . . . . 0
. . . . . .
. . . . . .
0 . . . . 1

и изменив его на:

1 . . . . 1
. . . . . .
. . . . . .
1 . . . . 0

Должно быть легко найти такую ​​группу, если у вас много нулей. Я думаю б) скорее всего будет быстрее.

Я не уверен, что это лучший способ, но он, вероятно, быстрее, чем тасование массивов. Я буду отслеживать этот вопрос, чтобы увидеть, что придут другие люди.

1 голос
/ 25 января 2010

Я не математик, но я полагаю, что если вам нужно сохранить одинаковые итоги столбцов и строк, то случайные версии матрицы будут иметь одинаковое количество единиц и нулей.

Поправьте меня, если я ошибаюсь, но это будет означать, что для создания последующих версий матрицы потребуется только перемешивание строк и столбцов.

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

Это должно быть довольно быстро.

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

Как @Gabriel, я не программист на Perl, поэтому вполне возможно, что это то, что ваш код уже делает ...

Вы опубликовали только один пример. Не ясно, хотите ли вы, чтобы случайная матрица имела такое же число единиц в каждой строке и столбце, что и ваша стартовая матрица, или матрица, которая имеет те же строки и столбцы, но перемешана. Если последний достаточно хорош, вы можете создать массив индексов строк (или столбцов, это не имеет значения) и случайным образом переставить это. Затем вы можете прочитать исходный массив в порядке, указанном рандомизированным индексом. Нет необходимости изменять исходный массив или создавать копию.

Конечно, это может не соответствовать аспектам ваших требований, которые не являются явными.

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

Не уверен, поможет ли это, но вы можете попробовать перейти из одного угла, и для каждого столбца и строки вы должны отслеживать общую и фактическую сумму. Вместо того, чтобы пытаться попасть в хорошую матрицу, попробуйте увидеть сумму как сумму и разделить ее. Для каждого элемента найдите меньшее количество итогов строки - фактическая сумма строки и итога столбца - фактическая сумма столбца. Теперь у вас есть верхняя граница для вашего случайного числа. Это понятно? Извините, я не знаю Perl, поэтому я не могу показать какой-либо код.

...