The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
# $SIG{__DIE__ } = \&Graph::__carp_confess;
# $SIG{__WARN__} = \&Graph::__carp_confess;
sub reset {
my $self = shift;
require Set::Object;
$self->{ unseen } = Set::Object->new($self->{ graph }->vertices);
$self->{ seen } = Set::Object->new;
$self->{ order } = [ ];
$self->{ preorder } = [ ];
$self->{ postorder } = [ ];
$self->{ roots } = [ ];
$self->{ tree } = Graph->new(directed => $self->{ graph }->directed);
delete $self->{ terminate };
}
sub _see {
my $self = shift;
$self->see;
}
sub has_a_cycle {
my ($u, $v, $t, $s) = @_;
$s->{ has_a_cycle } = 1;
$t->terminate;
}
sub find_a_cycle {
my ($u, $v, $t, $s) = @_;
my @cycle = ( $u );
push @cycle, $v unless $u eq $v;
my $path = $t->{ order };
if (@$path) {
my $i = $#$path;
while ($i >= 0 && $path->[ $i ] ne $v) { $i-- }
if ($i >= 0) {
unshift @cycle, @{ $path }[ $i+1 .. $#$path ];
}
}
$s->{ a_cycle } = \@cycle;
$t->terminate;
}
my @KNOWN_CONFIG = qw(
tree_edge seen_edge
next_alphabetic next_numeric next_random
has_a_cycle find_a_cycle
);
my @EXTRACT_CONFIG = qw(
pre post pre_vertex post_vertex
pre_edge post_edge back_edge down_edge cross_edge non_tree_edge
first_root next_root next_successor
);
sub new {
my ($class, $g, %attr) = @_;
Graph::__carp_confess("Graph::Traversal: first argument is not a Graph")
unless ref $g && $g->isa('Graph');
my $self = bless { graph => $g, state => { } }, $class;
$self->reset;
if (exists $attr{ start }) {
$attr{ first_root } = delete $attr{ start };
$attr{ next_root } = undef;
}
my @found_known = grep exists $attr{$_}, @EXTRACT_CONFIG;
@$self{@found_known} = delete @attr{@found_known};
$self->{ seen_edge } = $attr{ seen_edge }
if exists $attr{ seen_edge } and ($g->multiedged || $g->countedged);
$self->{ pre_edge } = $attr{ tree_edge } if exists $attr{ tree_edge };
my $default_next =
$attr{ next_alphabetic } ? \&Graph::_next_alphabetic :
$attr{ next_numeric } ? \&Graph::_next_numeric :
\&Graph::_next_random;
$self->{ next_root } = $default_next if !exists $self->{ next_root };
$self->{ first_root } = $self->{ next_root }
if !exists $self->{ first_root };
$self->{ next_successor } = $default_next if !exists $self->{ next_successor };
if (exists $attr{ has_a_cycle }) {
$self->{ back_edge } = my $has_a_cycle =
ref $attr{ has_a_cycle } eq 'CODE' ?
$attr{ has_a_cycle } : \&has_a_cycle;
$self->{ down_edge } = $has_a_cycle if $g->is_undirected;
}
if (exists $attr{ find_a_cycle }) {
$self->{ back_edge } = my $find_a_cycle =
ref $attr{ find_a_cycle } eq 'CODE' ?
$attr{ find_a_cycle } : \&find_a_cycle;
$self->{ down_edge } = $find_a_cycle if $g->is_undirected;
}
$self->{ add } = \&add_order;
$self->{ see } = \&_see;
delete @attr{@KNOWN_CONFIG};
Graph::_opt_unknown(\%attr);
return $self;
}
sub terminate {
my $self = shift;
$self->{ terminate } = 1;
}
sub add_order {
my ($self, @next) = @_;
push @{ $self->{ order } }, @next;
}
sub visit {
my ($self, @next) = @_;
$self->{ unseen }->remove(@next);
$self->{ seen }->insert(@next);
$self->{ add }->( $self, @next );
return unless my $p = $self->{ pre };
$p->( $_, $self ) for @next;
}
sub visit_preorder {
my ($self, @next) = @_;
push @{ $self->{ preorder } }, @next;
$self->{ preordern }->{ $_ } = $self->{ preorderi }++ for @next;
$self->visit( @next );
}
sub visit_postorder {
my ($self) = @_;
my @post = reverse $self->{ see }->( $self );
push @{ $self->{ postorder } }, @post;
$self->{ postordern }->{ $_ } = $self->{ postorderi }++ for @post;
if (my $p = $self->{ post }) {
$p->( $_, $self ) for @post;
}
return unless (my $p = $self->{ post_edge }) and defined(my $u = $self->current);
$p->( $u, $_, $self, $self->{ state }) for @post;
}
sub _callbacks {
my ($self, $current, @all) = @_;
return unless @all;
my $nontree = $self->{ non_tree_edge };
my $back = $self->{ back_edge };
my $down = $self->{ down_edge };
my $cross = $self->{ cross_edge };
my $seen = $self->{ seen_edge };
my $bdc = defined $back || defined $down || defined $cross;
return unless (defined $nontree || $bdc || defined $seen);
my $u = $current;
my $preu = $self->{ preordern }->{ $u };
my $postu = $self->{ postordern }->{ $u };
for my $v ( @all ) {
if (!$self->{tree}->has_edge($u, $v) && (defined $nontree || $bdc) &&
exists $self->{ seen }->{ $v }) {
$nontree->( $u, $v, $self, $self->{ state }) if $nontree;
if ($bdc) {
my $postv = $self->{ postordern }->{ $v };
if ($back &&
(!defined $postv || $postv >= $postu)) {
$back ->( $u, $v, $self, $self->{ state });
} else {
my $prev = $self->{ preordern }->{ $v };
if ($down && $prev > $preu) {
$down ->( $u, $v, $self, $self->{ state });
} elsif ($cross && $prev < $preu) {
$cross->( $u, $v, $self, $self->{ state });
}
}
}
}
next if !$seen;
my $c = $self->graph->get_edge_count($u, $v);
$seen->( $u, $v, $self, $self->{ state } ) while $c-- > 1;
}
}
sub next {
my $self = shift;
return undef if $self->{ terminate };
my @next;
while ($self->seeing) {
my $current = $self->current;
my $next = Set::Object->new($self->{ graph }->successors($current));
my @all = $next->members;
$next = $next->difference($self->{seen});
if ($next->size) {
@next = $self->{ next_successor }->( $self, { map +($_=>$_), $next->members } );
$self->{ tree }->add_edges(map [$current, $_], @next);
last unless my $p = $self->{ pre_edge };
$p->($current, $_, $self, $self->{ state }) for @next;
last;
} else {
$self->visit_postorder;
}
return undef if $self->{ terminate };
$self->_callbacks($current, @all);
}
unless (@next) {
if (!@{ $self->{ roots } } and defined(my $first = $self->{ first_root })) {
return unless @next = ref $first eq 'CODE'
? $first->( $self, { map +($_=>$_), $self->unseen } )
: $first;
}
return if !@next and !$self->{ next_root };
return if !@next and !(@next = $self->{ next_root }->( $self, { map +($_=>$_), $self->unseen } ));
return if !defined $next[0] or $self->{ seen }->contains($next[0]); # Sanity check.
push @{ $self->{ roots } }, $next[0];
}
$self->visit_preorder( @next ) if @next;
return $next[0];
}
sub _order {
my ($self, $order) = @_;
1 while defined $self->next;
@{ $self->{ $order } };
}
sub preorder {
my $self = shift;
$self->_order( 'preorder' );
}
sub postorder {
my $self = shift;
$self->_order( 'postorder' );
}
sub unseen {
my $self = shift;
$self->{ unseen }->${ wantarray ? \'members' : \'size' };
}
sub seen {
my $self = shift;
$self->{ seen }->${ wantarray ? \'members' : \'size' };
}
sub seeing {
my $self = shift;
@{ $self->{ order } };
}
sub roots {
my $self = shift;
@{ $self->{ roots } };
}
sub is_root {
my ($self, $v) = @_;
for my $u (@{ $self->{ roots } }) {
return 1 if $u eq $v;
}
return 0;
}
sub tree {
my $self = shift;
$self->{ tree };
}
sub graph {
my $self = shift;
$self->{ graph };
}
sub vertex_by_postorder {
my ($self, $i) = @_;
exists $self->{ postorder } && $self->{ postorder }->[ $i ];
}
sub postorder_by_vertex {
my ($self, $v) = @_;
exists $self->{ postordern } && $self->{ postordern }->{ $v };
}
sub postorder_vertices {
my ($self, $v) = @_;
exists $self->{ postordern } ? %{ $self->{ postordern } } : ();
}
sub vertex_by_preorder {
my ($self, $i) = @_;
exists $self->{ preorder } && $self->{ preorder }->[ $i ];
}
sub preorder_by_vertex {
my ($self, $v) = @_;
exists $self->{ preordern } && $self->{ preordern }->{ $v };
}
sub preorder_vertices {
my ($self, $v) = @_;
exists $self->{ preordern } ? %{ $self->{ preordern } } : ();
}
sub has_state {
my ($self, $var) = @_;
exists $self->{ state } && exists $self->{ state }->{ $var };
}
sub get_state {
my ($self, $var) = @_;
exists $self->{ state } ? $self->{ state }->{ $var } : undef;
}
sub set_state {
my ($self, $var, $val) = @_;
$self->{ state }->{ $var } = $val;
return 1;
}
sub delete_state {
my ($self, $var) = @_;
delete $self->{ state }->{ $var };
delete $self->{ state } unless keys %{ $self->{ state } };
return 1;
}
1;
__END__
=pod
=head1 NAME
Graph::Traversal - traverse graphs
=head1 SYNOPSIS
Don't use Graph::Traversal directly, use Graph::Traversal::DFS
or Graph::Traversal::BFS instead.
use Graph;
my $g = Graph->new;
$g->add_edge(...);
use Graph::Traversal::...;
my $t = Graph::Traversal::...->new($g, %opt);
$t->...
=head1 DESCRIPTION
You can control how the graph is traversed by the various callback
parameters in the C<%opt>. In the parameters descriptions below the
$u and $v are vertices, and the $self is the traversal object itself.
=head2 Callback parameters
The following callback parameters are available:
=over 4
=item tree_edge
Called when traversing an edge that belongs to the traversal tree.
Called with arguments ($u, $v, $self).
=item non_tree_edge
Called when an edge is met which either leads back to the traversal tree
(either a C<back_edge>, a C<down_edge>, or a C<cross_edge>).
Called with arguments ($u, $v, $self).
=item pre_edge
Called for edges in preorder.
Called with arguments ($u, $v, $self).
=item post_edge
Called for edges in postorder.
Called with arguments ($u, $v, $self).
=item back_edge
Called for back edges.
Called with arguments ($u, $v, $self).
=item down_edge
Called for down edges.
Called with arguments ($u, $v, $self).
=item cross_edge
Called for cross edges.
Called with arguments ($u, $v, $self).
=item pre
=item pre_vertex
Called for vertices in preorder.
Called with arguments ($v, $self).
=item post
=item post_vertex
Called for vertices in postorder.
Called with arguments ($v, $self).
=item first_root
Called when choosing the first root (start) vertex for traversal.
Called with arguments ($self, $unseen) where $unseen is a hash
reference with the unseen vertices as keys.
If not supplied, defaults to the same as C<next_root>.
=item next_root
Called when choosing the next root (after the first one) vertex for
traversal (useful when the graph is not connected). Called with
arguments ($self, $unseen) where $unseen is a hash reference with
the unseen vertices as keys. If you want only the first reachable
subgraph to be processed, set the next_root to C<undef>.
=item start
Identical to defining C<first_root> and undefining C<next_root>.
=item next_alphabetic
Set this to true if you want the vertices to be processed in
alphabetic order (and leave first_root/next_root undefined).
=item next_numeric
Set this to true if you want the vertices to be processed in
numeric order (and leave first_root/next_root undefined).
=item next_successor
Called when choosing the next vertex to visit. Called with arguments
($self, $next) where $next is a hash reference with the possible
next vertices as keys. Use this to provide a custom ordering for
choosing vertices, as opposed to C<next_numeric> or C<next_alphabetic>.
=back
The parameters C<first_root> and C<next_successor> have a 'hierarchy'
of how they are determined: if they have been explicitly defined, use
that value. If not, use the value of C<next_alphabetic>, if that has
been defined. If not, use the value of C<next_numeric>, if that has
been defined. If not, the next vertex to be visited is chosen randomly.
=head2 Methods
The following methods are available:
=over 4
=item unseen
Return the unseen vertices in random order.
=item seen
Return the seen vertices in random order.
=item seeing
Return the active fringe vertices in random order.
=item preorder
Return the vertices in preorder traversal order.
=item postorder
Return the vertices in postorder traversal order.
=item vertex_by_preorder
$v = $t->vertex_by_preorder($i)
Return the ith (0..$V-1) vertex by preorder.
=item preorder_by_vertex
$i = $t->preorder_by_vertex($v)
Return the preorder index (0..$V-1) by vertex.
=item vertex_by_postorder
$v = $t->vertex_by_postorder($i)
Return the ith (0..$V-1) vertex by postorder.
=item postorder_by_vertex
$i = $t->postorder_by_vertex($v)
Return the postorder index (0..$V-1) by vertex.
=item preorder_vertices
Return a hash with the vertices as the keys and their preorder indices
as the values.
=item postorder_vertices
Return a hash with the vertices as the keys and their postorder
indices as the values.
=item tree
Return the traversal tree as a graph.
=item has_state
$t->has_state('s')
Test whether the traversal has state 's' attached to it.
=item get_state
$t->get_state('s')
Get the state 's' attached to the traversal (C<undef> if none).
=item set_state
$t->set_state('s', $s)
Set the state 's' attached to the traversal.
=item delete_state
$t->delete_state('s')
Delete the state 's' from the traversal.
=back
=head2 Special callbacks
If in a callback you call the special C<terminate> method,
the traversal is terminated, no more vertices are traversed.
=head1 SEE ALSO
L<Graph::Traversal::DFS>, L<Graph::Traversal::BFS>
=head1 AUTHOR AND COPYRIGHT
Jarkko Hietaniemi F<jhi@iki.fi>
=head1 LICENSE
This module is licensed under the same terms as Perl itself.
=cut