问题 在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项),所以我真的需要一种计算效率高的方法。

我的初始(低效)方法包括使用the来改组数组 Perl Cookbook  拖曳

我粘贴了下面的当前代码。如果在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;
}

7927
2018-01-25 15:26


起源

它总是二进制矩阵(矩阵元素是0还是1)? - mob
是的,矩阵总是二进制的。此外,0的实例数量也远远超过1的实例! - Lucas
不要发布链接到未经授权的书籍副本!被标记为垃圾邮件。 - Sinan Ünür
@Lucas:在这个过程中你覆盖了我的链接 shuffle 在Perl FAQ列表中仅用a替换它 404 链接到一些网络托管公司。我会说你在这里继续垃圾邮件。 - Sinan Ünür
@Sinan + Lucas:即使没有这本书的链接,问题仍然没有意义吗?我相信这里的大多数人都能理解shuffle是如何工作的,如果没有,那么总是有Google。那么你不必争论哪个链接最好。 :) - Mark Byers


答案:


你当前算法的问题在于你试图将你的方式摆脱死胡同 - 特别是当你的时候 @letters 和 @numbers 数组(在最初的shuffle之后) @numbers)不止一次产生相同的细胞。当矩阵很小时,这种方法很有效,因为它不需要太多的尝试来找到可行的重新洗牌。然而,当名单很大时,这是一个杀手。即使您可以更有效地寻找替代方案 - 例如,尝试排列而不是随机改组 - 这种方法可能注定失败。

您可以通过对现有矩阵进行少量修改来解决问题,而不是改变整个列表。

例如,让我们从您的示例矩阵开始(称之为M1)。随机选择一个单元格进行更改(例如,A1)。此时矩阵处于非法状态。我们的目标是将其修改为最少的编辑次数 - 特别是3次编辑。您通过在矩阵周围“行走”来实现这3个额外的编辑,每行修复一行或列会产生另一个需要解决的问题,直到您走完整圆(错误...全矩形)。

例如,在将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编辑方法修复矩阵。实际上,您将把所有这些移动到新的随机位置。

这是一个例子。这里可能还有进一步的速度优化,但这种方法在我的Windows机器上30秒左右产生了10个新的600x600矩阵,密度为0.5%。不知道那是否足够快。

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;
    }
}

9
2018-01-26 01:30



算法绝对够快!我有点担心非随机性,我确实需要尽可能随机的矩阵,给定约束条件。我将尝试根据您的算法生成矩阵,并检查它们与原始矩阵的相关性。问题是,我无法将它们与真正的随机矩阵进行比较;当然;)。 - Lucas
@Lucas在代码中实现的算法非常随机。原始矩阵中的每1个都关闭,其他3个单元格被切换以保持行/列总和不变。这3个修复程序尽可能随机,受问题的限制。如果存在非随机方面,则它来自这样的事实:原始矩阵中的1在新矩阵中具有高于0的随机概率。纠正这种偏差的一种方法可能是在原始矩阵中跳过一些1的比例 - 换句话说,将它们保持原样。比例是多少?也许使用密度。 - FMc
@Lucas:如果选择这种方法,改善随机性的另一种方法是多次重复随机化过程(随机次数)。然后我认为与原始矩阵的相关性可以忽略不计。它会让它慢一点,但它的实现比我的建议更简单。 - Mark Byers
是的,我昨晚在各种随机矩阵上运行了几千次算法,并且看到每次迭代时相关性稳定下降。很好的解决FM!你能解释一下如何在init矩阵中使用问号映射函数吗?你正在生成一个数组数组作为矩阵,但我不熟悉这种符号。感谢十亿为我的问题提供了一个很好的解决方案:)。 - Lucas
@Lucas很高兴得到帮助。这是一个有趣的问题,有点与我最近一直在研究的副项目有关。关于 map { rand() < $density ? 1 : 0 } 1 .. $cols 我们正在生成一个0和1的列表。如果 rand() 小于 $density,细胞将是1;否则为0.搜索 perldoc perlop 有关条件运算符的更多详细信息 ? : 句法。它基本上是一个迷你的IF-THEN结构,可以在更大的表达式中方便地使用。 - FMc


第1步:首先,我将矩阵初始化为零,并计算所需的行和列总数。

第2步:现在选择一个随机行,加权必须在该行中的1的计数(因此,与具有权重5的行相比,更有可能选择具有计数300的行)。

步骤3:对于此行,选择一个随机列,按该列中的1s计数加权(除了忽略任何可能已经包含1的单元格 - 稍后再说明)。

步骤4:在此单元格中放置一个并减少相应行和列的行数和列数。

步骤5:返回步骤2,直到没有行具有非零计数。

但问题是这个算法可能无法终止,因为你可能有一行你需要放一个,而一个列需要一个,但是你已经在那个单元中放了一个,所以你得到'卡住了”。我不确定这种情况发生的可能性有多大,但如果频繁发生我也不会感到惊讶 - 足以让算法无法使用。如果这是一个问题,我可以想到两种方法来解决它:

a)递归地构造上述算法并允许回溯失败。

b)如果没有其他选项,允许单元格包含大于1的值并继续。然后在最后你有一个正确的行和列数,但有些单元格可能包含大于1的数字。你可以通过找到如下所示的分组来解决这个问题:

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

并将其更改为:

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

如果你有很多零,应该很容易找到这样的分组。我认为b)可能会更快。

我不确定这是最好的方法,但它可能比洗牌更快。我会跟踪这个问题,看看其他人想出了什么。


5
2018-01-25 16:01



谢谢你的建议!它看起来像一个声音的方法,我一定会尝试实现。我将尝试查看算法填充已填充的单元格的次数。我不认为它会经常发生,因为我的表中1的频率很低,所以简单地再次运行算法也可行(如果它不经常发生)! - Lucas
我根据你的建议实现了算法,但我遇到的问题是1s不是随机分布在矩阵上的。有些列可以包含多达45-65的1,而平均值只有1.5。因此,当试图选择b)时,我不仅会遇到2,而且会陷入3,4,5,6,7,这些都不是那么容易修复的。我将试图找到一种方法来在算法中构建回溯功能,但我担心这需要比我更好的编程技能;)。 - Lucas
卢卡斯:你还记得给随机选择的栏目加权吗?您必须先选择最多1s的列,否则会增加阻止自己的机会。你能在某处发布一个示例输入数据吗? SO是不合适的,但也许是另一个网站,并链接到它? - Mark Byers
如果使用算法b),7也不是真正的问题。您只需要重复重组足够多次,每次选择不同的块。 - Mark Byers
还记得 不 在选择随机列时包含已包含1的列。 - Mark Byers


我不是数学家,但我认为如果你需要保持相同的列和行总数,那么矩阵的随机版本将具有相同数量的1和0。

如果我错了,请纠正我,但这意味着制作矩阵的后续版本只需要在行和列周围进行随机播放。

随机改组列不会更改行和列的总计,也不会随机改变行。所以,我会做的,首先是洗牌行,然后洗牌。

那应该很快。


1
2018-01-25 16:29



我对该问题的理解是行(和列)总数必须保持不变并且顺序相同。如果是这样,可以交换具有相同行总数的两行,但如果它们有不同则不行。在问题中给出的3x3示例中,交换行A和C是有效的,因为它们都具有行总数1,而不是行A和B,因为行B总共为2。 - Mark Byers
随机改组列不会影响行总计,但它确实会影响列总计(反之亦然)。如果我将所有列在我的初始矩阵中向右移动一个位置,则第1列的总数不再等于2 ..编辑:@Mark你是对的+你的例子比我的更好:) - Lucas
此外,海报可能想要生成的一些可能的解决方案仅通过进行行和列交换是不可达的。例如。如果你有一个4x4矩阵,行和列总数全部为2,并且初始配置 1100,1100,0011,0011 我不确定你怎么能改变 1100,1010,0101,0011 只是行和列交换。 - Mark Byers
是的,我认为问题的描述有点令人困惑。我的印象是单个列和行总数需要保持不变,而不是总计的位置。再看一下,随机随机播放的列和行将无法正常工作。 - Tim Rupe


不确定它是否会有所帮助,但您可以尝试从一个角落开始,对于每个列和行,您应该跟踪总和和实际总和。不要试图找到一个好的矩阵,而是尝试将总量视为金额并将其拆分。对于每个元素,找到较小的行总数 - 实际行总数和列总数 - 实际列总数。现在你有了随机数的上限。 清楚吗?对不起,我不知道Perl,所以我无法显示任何代码。


0
2018-01-25 15:46



你的答案不是很清楚,但我想我现在明白了。我认为你错过了其中一个要求:每个单元格只能包含0或1.这个问题并不明确,但在对问题的评论中提到了这一点。 - Mark Byers


就像@Gabriel一样,我不是Perl程序员,所以这可能是你的代码已经做的......

你只发了一个例子。目前尚不清楚是否需要一个随机矩阵,每个行和列中的1和1的数量与起始矩阵相同,或者是一个具有相同行和列但是混洗的矩阵。如果后者足够好,你可以创建一个行(或列,无关紧要)索引并随机置换它。然后,您可以按随机索引指定的顺序读取原始数组。无需修改原始数组或创建副本。

当然,这可能无法满足您的要求方面,而这些方面并不明确。


0
2018-01-25 16:08



谢谢马克!但是你可能已经在其他评论中读过,我正在寻找你所描述的前一个问题的解决方案:)。 - Lucas