【问题标题】:Randomize matrix in perl, keeping row and column totals the same在perl中随机化矩阵,保持行和列总数相同
【发布时间】:2010-01-25 15:26:16
【问题描述】:

我有一个矩阵,我想随机化几千次,同时保持行和列的总数相同:

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

【问题讨论】:

  • 总是二进制矩阵(矩阵元素不是0就是1)吗?
  • 是的,矩阵总是二进制的。此外,0 的实例数量也远远超过 1 的实例!
  • 请勿发布未经授权的书籍副本的链接!标记为垃圾邮件。
  • @Lucas:在此过程中,您在 Perl 常见问题列表中覆盖了我指向 shuffle 的链接,只是为了将其替换为指向某些网络托管公司的 404 链接。我会说你在这里继续发送垃圾邮件。
  • @Sinan + Lucas:即使没有本书的链接,这个问题是否仍然有意义?我敢肯定,这里的大多数人都能理解 shuffle 的工作原理,如果没有的话,总是有谷歌。这样您就不必争论哪个链接最好。 :)

标签: perl random matrix


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

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

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

【讨论】:

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

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

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

第 3 步:对于这一行,选择一个随机列,按该列中 1 的计数加权(忽略任何可能已经包含 1 的单元格 - 稍后会详细介绍)。

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

第 5 步:返回第 2 步,直到没有行的计数不为零。

但问题是该算法可能无法终止,因为您可能有一行需要放置一个,而一列需要一个,但您已经在该单元格中放置了一个,所以您进度停止了'。我不确定这种情况发生的可能性有多大,但如果它发生得非常频繁,我不会感到惊讶——足以使算法无法使用。如果这是一个问题,我可以考虑两种方法来解决它:

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

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

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

并将其更改为:

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

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

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

【讨论】:

  • 感谢您的建议!它看起来像是一种我一定会尝试实施的合理方法。我将尝试查看该算法必须填充多少次已填充的单元格。我不认为它会经常发生,因为我的表中 1 的频率很低,所以简单地再次运行算法也可能是可行的(如果它不经常发生的话)!
  • 我按照你的建议实现了算法,但我遇到了 1 不是随机分布在矩阵上的问题。有些列可以包含多达 45-65 个 1,而平均值仅为 1.5。因此,当尝试选择选项 b) 时,我不仅遇到了 2,而且还遇到了不容易修复的 3、4、5、6、7。我会尝试想办法将回溯功能内置到算法中,但恐怕这需要比我更好的编程技能;)。
  • Lucas:你记得对随机选择的列进行加权吗?你必须先挑出最多1的列,否则你会增加自己阻塞的机会。您可以在某处发布示例输入数据吗? SO 不合适,但也许是另一个网站,并链接到它?
  • 如果您使用算法 b),7 也不是问题。您只需要重复重组足够多次,每次选择不同的块。
  • 还记得 not 在选择随机列时包含已经包含 1 的列。
【解决方案3】:

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

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

随机打乱列不会改变行和列的总数,随机打乱行也不会。所以,我要做的就是先洗牌,然后洗牌。

这应该很快。

【讨论】:

  • 我对这个问题的理解是行(和列)总计必须保持不变并且顺序相同。如果是这样,则可以交换具有相同行总数的两行,但如果它们不同,则不能。在问题中给出的 3x3 示例中,交换行 A 和 C 是有效的,因为它们的行总数为 1,但不是行 A 和 B,因为行 B 的总数为 2。
  • 随机洗牌不会影响行总数,但会影响列总数(反之亦然)。如果我在初始矩阵中将所有列向右移动一个位置,则第 1 列的总数不再等于 2.. 编辑:@Mark 你是对的 + 你的例子比我的更好:)
  • 此外,发布者可能想要生成的一些可能的解决方案仅通过进行行和列交换是无法实现的。例如。如果您有一个 4x4 矩阵,其中行和列总计为 2,并且初始配置 1100,1100,0011,0011 我不确定您如何仅通过行和列交换更改为 1100,1010,0101,0011
  • 是的,我认为问题的描述有点混乱。我的印象是各个列和行的总计需要保持不变,而不是总计的位置。再一看,列和行的随机洗牌是行不通的。
【解决方案4】:

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

【讨论】:

  • 你的答案不是很清楚,但我想我现在明白了。我认为您错过了其中一个要求:每个单元格只能包含 0 或 1。问题中没有明确说明,但在问题的评论中提到了。
【解决方案5】:

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

您只发布了一个示例。目前尚不清楚您是否想要一个在每行和每列中具有与起始矩阵相同数量的 1 的随机矩阵,或者是具有相同行和列但被打乱的随机矩阵。如果后者足够好,您可以创建一个行(或列,没关系)索引数组并随机排列。然后,您可以按照随机索引指定的顺序读取原始数组。无需修改原始数组或创建副本。

当然,这可能无法满足您不明确的要求。

【讨论】:

  • 谢谢马克!但正如您可能已经在其他 cmets 中读到的那样,我正在为您描述的前一个问题寻找解决方案:)。
【解决方案6】:

感谢FMc 的 Perl 代码。基于这个解决方案,我用Python重写了它(供我自己使用并在这里分享以更清楚),如下所示:

matrix = numpy.array( 
    [[0, 0, 1], 
     [1, 1, 0], 
     [1, 0, 0]]
)

def shuffle(array):
    i = len(array)
    j = 0
    for _ in (array):
        i -= 1;
        j = random.randrange(0, i+1) #int rand($i + 1);
        #print('arrary:', array)
        #print(f'len(array)={len(array)}, (i, j)=({i}, {j})')
        if i != j: 
            tmp = array[i]
            array[i] = array[j]
            array[j] = tmp
    return array

def other_edits(matrix, cell, step, last_j):
    # We have succeeded if we've already made 3 edits.
    step += 1
    if step > 3: 
        return True

    # Determine the roster of next edits to fix the row or
    # column total upset by our prior edit.
    (i, j) = cell
    fixes = []
    if (step == 1):
        fixes = [[i, x] for x in range(len(matrix[0])) if x != j and not matrix[i][x] ]
        fixes = shuffle(fixes)
    elif (step == 2):
        fixes = [[x, j] for x in range(len(matrix)) if x != i and matrix[x][j]]
        fixes = shuffle(fixes)
    else:
        # On the last edit, the column of the fix must be
        # the same as the column of the initial edit.
        if not matrix[i][last_j]: fixes = [[i, last_j]]

    for f in (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]] = 0 if step == 2 else 1
            return True

    # Failure if we get here.
    return False # return False

def cells_to_move(matrix):
    # Returns a list of non-empty cells.
    i = -1
    cells = []
    for row in matrix:
        i += 1;
        for j in range(len(row)):
            if matrix[i][j]: cells.append([i, j])
    return cells

def edit_matrix(matrix):
    # Takes a matrix and moves all of the non-empty cells somewhere else.
    move_these = cells_to_move(matrix)
    for cell in move_these:
        (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.
        if matrix[i][j] and other_edits(matrix, cell, 0, j):
            matrix[i][j] = 0
    return matrix

def Shuffle_Matrix(matrix, N, M, n_iter):
    for n in range(n_iter):
        print(f'iteration: {n+1}') # Show progress.
        matrix = edit_matrix(matrix)
        #print('matrix:\n', matrix)
    return matrix

print(matrix.shape[0], matrix.shape[1]) 

# Args: N rows, N columns, N iterations.
matrix2 = Shuffle_Matrix(matrix, matrix.shape[0], matrix.shape[1], 1) 

print("The resulting matrix:\n", matrix2)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-02-10
    • 1970-01-01
    • 1970-01-01
    • 2022-08-08
    • 1970-01-01
    • 1970-01-01
    • 2020-02-17
    • 2020-11-22
    相关资源
    最近更新 更多