I think you only need one hash, and you just have to loop over it as often as necessary, to eliminate intermediate links in the chains:
use strict;
use warnings;
my %parent_of;
<DATA>; # skip header
while (<DATA>) {
chomp;
my ( $child, $parent ) = split( /;/ );
$parent ||= $child;
$parent_of{$child} = $parent;
$parent_of{$parent} = $parent unless ( exists( $parent_of{$parent}
+ ));
}
my $changes;
do {
$changes = 0;
for my $child ( keys %parent_of ) {
my $parent = $parent_of{$child};
while ( $parent_of{$parent} != $parent ) {
my $next_parent = $parent_of{$parent};
$parent_of{$child} = $next_parent;
$parent = $next_parent;
$changes++;
}
}
} while ( $changes );
for my $child ( sort keys %parent_of ) {
print " $child => $parent_of{$child}\n";
}
__DATA__
NUM;NUMPRED
567;456
456;345
345;234
234;123
339;228
228;117
131;
435;324
324;213
372;
789;678
678;567
(I commend you for using
Text::CSV_XS, and you can certainly stick with that, but I assumed the input would be simple enough to do without it.) The DATA above includes some extra samples, to create a longer chain.
The point of the do {...} while ( $changes ) loop is simply to keep iterating over the "parent_of" hash, changing values for child keys until all the values represent "primary nodes" in all the chains.
Apart from that, I like the other suggestion given above: if possible, it would make more sense to create this form output as part of the same process that produces the sample input you've shown us here.