1

我有以下数据集

 A       C
 A       S
 B       F
 B       Q
 C       A
 C       I
 D       K
 E       Y
 F       B
 F       R
 I       Y
 K       P

第一列中的每个值在第二列中都有一个关联值。第 1 行中的值“A”具有关联的值“C”。在第二行中,值“A”与值“S”相关联。

使用 Perl,我想找到所有关联值的集合。使用上面的规则,我会得到集合(ACEISY)、(BFQR)和(DKP)。

我正在寻找有关如何解决此问题的算法或示例的建议。我不确定哈希表是否是为此使用的正确数据结构。任何帮助,将不胜感激。

下面是我的实现:

while<INPUT>{
    my ($c1, $c2) = split;
    my %clusterhash = ();
    if (exists $clusterhash{$c1}){
        if (exists $clusterhash{$c1}{$c2}){
            #do nothing
        }
        else {
            $clusterhash{$c1}{$c2} = $c2;
        }
    }
    else{
        foreach my $key ( keys %clusterhash ) {
            if (exists $clusterhash{$key}{$c1}{
                $clusterhash{$c1}{$key} = $key;
            }
        }
        $clusterhash{$c1}{$c2} = $c2;
    }
}
4

2 回答 2

5

您的数据集可以被视为可能断开的有向图。在我看来,您想要为每个弱连接子图设置节点。自己写这个并不难:

  • 我们认为该图是无向的。
  • 我们将边存储在哈希中,因此条目$edge{$a}{$b}是从顶点$a到的有向边$b
  • 现在我们只需要一个迭代搜索,删除所有访问过的边。

示例代码:

use strict; use warnings; use feature qw/say/;

# build the graph
my %edge;
while (<>) {
  my ($from, $to) = split;
  $edge{$from}{$to} = $edge{$to}{$from} = undef;
}

while (my ($start) = keys %edge) {
  my @seen  = ($start);
  my @stack = ($start);

  while (@stack) {
    my $vertex = pop @stack;

    # delete edges from and to this vertex
    # NB: any connections to seen vertices are already removed.
    my @reachable = keys %{ delete($edge{$vertex}) // {} };
    delete $edge{$_}{$vertex} for @reachable;

    # mark new vertices as seen, and enqueue them
    push @seen, @reachable;
    push @stack, @reachable;
  }

  my $nodes = join ', ', sort @seen;
  say "node set: {$nodes}";
}

数据的输出:

node set: {B, F, Q, R}
node set: {D, K, P}
node set: {A, C, E, I, S, Y}

该算法已经相当优化,并且在O ( n · k ) 时间和空间中运行(其中k是邻居的平均数量)。

当然,已经有一个实现图算法的模块。不出所料,它被称为Graph. 上面的代码等价于:

use strict; use warnings; use feature qw/say/;
use Graph;

my $graph = Graph::Undirected->new;
while (<>) {
  my ($from, $to) = split;
  $graph->add_edge($from, $to);
}

for my $nodes_array ($graph->connected_components) {
  my $nodes = join ', ', sort @$nodes_array;
  say "node set: {$nodes}";
}

通过在构建图形时跟踪连接的组件,可以在更少的内存和更少的时间中执行这些计算。为此,我们有一个将顶点映射到其子图的哈希。

  1. 如果边的两个顶点都是未知的,则它们创建一个新的子图。
  2. 如果确切地知道一个顶点,则另一个节点映射到第一个节点的子图,并在那里列为成员。
  3. 如果两个顶点都是已知的,那么
    1. 如果它们指向同一个子图,则不会发生任何事情。
    2. 如果它们指向不同的子图,则所有条目都将更新为指向同一个子图,该子图现在包含先前子图的组合节点。

编码:

use strict; use warnings; use feature qw/say/;

my %subgraph_by_id;
my %subgraph_by_vertex;
while(<>) {
  my ($x, $y) = split;
  # case 1:
  # If an both vertices of an edge are unknown, they create a new subgraph.
  if (not exists $subgraph_by_vertex{$x} and not exists $subgraph_by_vertex{$y}) {
    my $new = [$x, $y];
    $subgraph_by_id{0+ $new} = $new;
    $subgraph_by_vertex{$_} = $new for $x, $y;
  }
  # case 2:
  # If exactly one vertex is known, the other node maps to the subgraph of the
  # first node, and is listed there as a member.
  elsif (not exists $subgraph_by_vertex{$x} or not exists $subgraph_by_vertex{$y}) {
    my ($known, $unknown) = (exists $subgraph_by_vertex{$x}) ? ($x, $y) : ($y, $x);
    my $subgraph = $subgraph_by_vertex{$unknown} = $subgraph_by_vertex{$known};
    push @$subgraph, $unknown;
  }
  # case 3:
  # both vertices are known. If they point to different subgraphs, all entries
  # are updated to point to the same subgraph which now contains the combined
  # nodes of the previous subgraphs.
  # Except all that copying would make for a horrible worst case.
  # Instead, we just add a reference to the smaller list, flattening it later.
  else {
    my $prev_x = $subgraph_by_vertex{$x};
    my $prev_y = $subgraph_by_vertex{$y};
    # don't test for inequality directly to allow subgraph nesting
    if ($subgraph_by_id{0+ $prev_x} != $subgraph_by_id{0+ $prev_y}) {
      my ($new, $old) = (@$prev_x > @$prev_y) ? ($prev_x, $prev_y) : ($prev_y, $prev_x);
      push @$new, $old;
      # $old not needed on top level any longer – associate it with $new by id
      $subgraph_by_id{0+ $old} = 0+ $new;
    }
  }
}

# skip symbolic IDs
for my $nodes_array (grep ref, values %subgraph_by_id) {
  my $nodes = join ', ', flatten($nodes_array);
  say "node set: {$nodes}";
}

sub flatten {
  return map { ref $_ ? flatten($_) : $_ } @{ shift() };
}

这仅使用O ( n ) 空间和时间,使用了很多尴尬的技巧。在构建子图期间,我不会合并两个连接的子图,而是将其推迟到以后。否则,边缘情况(自下而上构建的平衡树——对于每个非叶节点,将复制树的一半)可能需要指数时间——我没有进行全面分析。“ 0+venus”伪操作符将其参数进行了数字化,在这里用于获取数组引用的 ID。

于 2013-10-23T11:02:23.227 回答
1

这实际上不应该成为答案,而是评论,但它变得太长并且不适合评论字段:

速度有问题吗?例如,是否有太多数据以至于经常循环遍历它可能是个坏主意?因为如果重复循环没有问题,那么散列将是一个简单的解决方案:取第 1 列中尚未包含在散列中的第一个元素;将其作为键添加到散列中,并带有新的集合号;遍历所有行,将所有 ist 关联值也作为键添加到哈希中,并具有相同的集合编号;如果您在上一次迭代中添加了新键,请对这些键再次执行此操作,直到您没有添加新键;取下一个尚未包含在哈希中的元素,并使用下一个集合索引重复;一旦没有未分配的元素,您就可以将哈希中的所有元素作为键,并将其设置为值。您可能需要最后格式化它。

编辑:好的,如果速度是一个问题,那么缩放值的数量而不是行数如何?

有一个外部散列,其中集合 indizes 作为键,内部散列作为值。这些内部哈希将元素作为键,将“1”作为值。遍历行。在每一行中,检查值是否已经是一个或两个内部哈希中的键。如果它们在不同的散列中,则合并这些散列并删除外部散列的键之一。如果一个在一个散列中而另一个不在,则将新值添加到第一个散列中,如果它们在同一个散列中,则什么也不做,如果两者都不是散列,则为外部散列创建一个新键并将这两个值添加到相应的内部哈希中。

如果内部散列可能会变大或者可能有很多集合,这可能会变得非常缓慢。但是,如果与行数相比,可能值的集合很小,这可能会非常快。

最佳编辑:我刚刚有了另一个想法。这个最多每行看三次(假设随机关联更可能是两次),我认为这相当快,但需要更多内存。使用两个大哈希遍历行。在每一行中,您将 cell2 添加到存储在键 cell1 的 hash1 中的数组中,并将 cell1 添加到存储在键 cell2 的 hash2 中的数组中。基本上,您将所有信息读入这两个哈希中。现在您获取 hash1 的随机密钥并将该密钥以及相应数组中的所有元素添加到您希望存储最终集合的任何结构中(我假设为密钥到第三个哈希值,设置数字为值)并从 hash1 中删除键。现在,您还将所有这些元素作为 hash2 中的键查找,并将这些数组中的所有内容添加到集合中,并从 hash2 中删除键。现在,您将已添加到集合中的所有内容作为 hash1 的键,并再次将数组中的所有内容添加到集合中,依此类推。您必须继续这样做,直到 hash1 和 hash2 都连续没有要添加到集合中。然后你拿另一个随机密钥开始下一组。删除所有使用过的键可以保证您没有得到任何东西两次,并且您不会经常检查同一行。那是假设查找哈希中是否存在密钥实际上与我认为的一样快。删除所有使用过的键可以保证您没有得到任何东西两次,并且您不会经常检查同一行。那是假设查找哈希中是否存在密钥实际上与我认为的一样快。删除所有使用过的键可以保证您没有得到任何东西两次,并且您不会经常检查同一行。那是假设查找哈希中是否存在密钥实际上与我认为的一样快。

于 2013-10-23T08:34:56.910 回答