Re: [QUIZ] Perl 'Medium' Quiz: Graph Connected Components (#2006-12-29)

qotwdiscuss-rAR/[email protected] (Ton Hospel) Thu, 18 Jan 2007 16:51:18 +0000 (UTC)
Newsgroups gmane.comp.lang.perl.qotw.discuss
Organization lunix confusion services
Message-ID <[email protected]>
In article <9b18b3110701180634q2885bfa6ja4e337ffaed06cc-JsoAwUIsXosN+BqQ9rBEUg@public.gmane.org>,
	demerphq <[email protected]> writes:
> I think you missed the requirement that the arrays be returned in the
> same order to which their first element was encountered in the input.
>

Right, I missed that, I only kept edges inside a component in the
original order. The fixed version below returns the components in the
order that their first edge is seen. Gives me an excuse to use the
rarely used return value of push.

sub connected {
   my ($edges) = @_;

   my (%uf, %components, @components);
   $uf{root(\%uf, $uf{$_->[0]} ||= $_->[0])} = $uf{$_->[1]} ||= $_->[1] for
       @$edges;
   for my $edge (@$edges) {
       my $root = root(\%uf, $edge->[1]);
       push @components, $components{$root} if
           1 == push @{$components{$root}}, $edge;
   }
   return @components;
}

sub root {
   my ($uf, $u) = @_;
   $u = $uf->{$u} = $uf->{$uf->{$u}} while $uf->{$u} ne $u;
   return $u;
}

> Also it seems this class of algorithm cant handle multiple edeges
> between two nodes. Is that right?
>

No, multiple edges will work just fine. It will return an edge as many
times as it appears in the input.

I think (variations on) fast union find is the proper algorithm for this
problem. At least it's the only one that I know that is essentially
O(edges) (Ignoring the practically irrelevant inverse ackerman,
assuming perl hash lookup is linear and assuming array extension has an 
amortized linear cost). It also matches quite well with perl 
datastructures, though not perfectly. The %uf hash values could
have been made into real perl references, but the self reference at 
the end will not automatically get cleaned up on program exit. If we knew the 
integers assigned to the edges are the consecutive numbers starting 
from 0 (or 1), it would also probably be better to use an array instead
of a hash for the union pointers.

I'd be very interested to hear if there are other (essentially) linear
algorithms.

Here is the same code using real perl references, assuming node numbers
without holes (does not impact correctness, but does impact speed and
memory usage) and with sub root unrolled:

sub connected {
   my ($edges) = @_;

   my (@uf, %components, @components);

   # Create an array of self references
   my $max = -1;
   for my $edge (@$edges) {
       $max = $edge->[0] if $edge->[0] > $max;
       $max = $edge->[1] if $edge->[1] > $max;
   }
   $#uf = $max;
   $_ = \$_ for @uf;
       
   # Do standard unioning
   for my $edge (@$edges) {
       my $u = $uf[$edge->[0]];
       $u = $$u = $$$u while $$u != $u;
       $$u = $uf[$edge->[1]];
   }

   # Collect components
   for my $edge (@$edges) {
       my $u = $uf[$edge->[1]];
       $u = $$u = $$$u while $$u != $u;
       # or maybe use pack on refaddr instead....
       my $root = "$u";
       push @components, $components{$root} if 
           1 == push @{$components{$root}}, $edge;
   }

   # Force garbage collection
   $_ = undef for @uf;

   return @components;
}

Or, less tricky, the same but without perl references:

sub connected {
   my ($edges) = @_;

   my (@uf, @components, @order);

   # Create an array of self references
   my $max = -1;
   for my $edge (@$edges) {
       $max = $edge->[0] if $edge->[0] > $max;
       $max = $edge->[1] if $edge->[1] > $max;
   }
   @uf = 0..$max;
       
   # Do standard unioning
   for my $edge (@$edges) {
       my $u = $edge->[0];
       $u = $uf[$u] = $uf[$uf[$u]] while $uf[$u] != $u;
       $uf[$u] = $edge->[1];
   }

   # Collect components
   $#components = $max;
   for my $edge (@$edges) {
       my $u = $edge->[1];
       $u = $uf[$u] = $uf[$uf[$u]] while $uf[$u] != $u;
       push @order, $components[$u] if 
           1 == push @{$components[$u]}, $edge;
   }

   return @order;
}