Рандомизированная матрица в perl, сохраняя значения строк и столбцов одинаковыми

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

     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

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

Мой первоначальный (неэффективный) подход состоял из перетасовки массивов с помощью 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;
}

Ответ 1

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

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

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

Например, после изменения A1 от 0 до 1 существует 3 способа перехода для следующего ремонта: A3, B1 и C1. Позвольте решить, что первое редактирование должно исправить строки. Итак, мы выбираем A3. Во втором редактировании мы исправим столбец, поэтому у нас есть выбор: 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;
    }
}

Ответ 2

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

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

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

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

Шаг 5: вернитесь к шагу 2, пока строки не имеют ненулевого отсчета.

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

Ответ 5

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

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

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