| blib/lib/Graph/Undirected/Hamiltonicity.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % | 
| statement | 112 | 112 | 100.0 | 
| branch | 29 | 32 | 90.6 | 
| condition | 21 | 21 | 100.0 | 
| subroutine | 10 | 10 | 100.0 | 
| pod | 1 | 4 | 25.0 | 
| total | 173 | 179 | 96.6 | 
| line | stmt | bran | cond | sub | pod | time | code | 
|---|---|---|---|---|---|---|---|
| 1 | |||||||
| 2 | =encoding utf-8 | ||||||
| 3 | |||||||
| 4 | =head1 NAME | ||||||
| 5 | |||||||
| 6 | Graph::Undirected::Hamiltonicity - decide whether a given Graph::Undirected | ||||||
| 7 | contains a Hamiltonian Cycle. | ||||||
| 8 | |||||||
| 9 | =head1 VERSION | ||||||
| 10 | |||||||
| 11 | Version 0.16 | ||||||
| 12 | |||||||
| 13 | =head1 LICENSE | ||||||
| 14 | |||||||
| 15 | Copyright (C) Ashwin Dixit. | ||||||
| 16 | |||||||
| 17 | This library is free software; you can redistribute it and/or modify | ||||||
| 18 | it under the same terms as Perl itself. | ||||||
| 19 | |||||||
| 20 | =head1 AUTHOR | ||||||
| 21 | |||||||
| 22 | Ashwin Dixit, C<< | ||||||
| 23 | |||||||
| 24 | =cut | ||||||
| 25 | |||||||
| 26 | |||||||
| 27 | 4 | 4 | 108519 | use Modern::Perl; | |||
| 4 | 29 | ||||||
| 4 | 25 | ||||||
| 28 | 4 | 4 | 2116 | use lib 'local/lib/perl5'; | |||
| 4 | 2386 | ||||||
| 4 | 22 | ||||||
| 29 | |||||||
| 30 | package Graph::Undirected::Hamiltonicity; | ||||||
| 31 | |||||||
| 32 | # ABSTRACT: decide whether a given Graph::Undirected contains a Hamiltonian Cycle. | ||||||
| 33 | |||||||
| 34 | # You can get documentation for this module with this command: | ||||||
| 35 | # perldoc Graph::Undirected::Hamiltonicity | ||||||
| 36 | |||||||
| 37 | 4 | 4 | 2141 | use Graph::Undirected::Hamiltonicity::Output qw(&output); | |||
| 4 | 9 | ||||||
| 4 | 581 | ||||||
| 38 | 4 | 4 | 1834 | use Graph::Undirected::Hamiltonicity::Tests qw(:all); | |||
| 4 | 12 | ||||||
| 4 | 694 | ||||||
| 39 | 4 | 4 | 32 | use Graph::Undirected::Hamiltonicity::Transforms qw(:all); | |||
| 4 | 9 | ||||||
| 4 | 423 | ||||||
| 40 | |||||||
| 41 | 4 | 4 | 26 | use Exporter qw(import); | |||
| 4 | 50 | ||||||
| 4 | 5396 | ||||||
| 42 | |||||||
| 43 | our $VERSION = '0.16'; | ||||||
| 44 | our @EXPORT = qw(graph_is_hamiltonian); # exported by default | ||||||
| 45 | our @EXPORT_OK = qw(graph_is_hamiltonian); | ||||||
| 46 | our %EXPORT_TAGS = ( all => \@EXPORT_OK ); | ||||||
| 47 | |||||||
| 48 | our $calls = 0; ### Number of calls to is_hamiltonian() | ||||||
| 49 | |||||||
| 50 | ########################################################################## | ||||||
| 51 | |||||||
| 52 | # graph_is_hamiltonian() | ||||||
| 53 | # | ||||||
| 54 | # Takes a Graph::Undirected object. | ||||||
| 55 | # | ||||||
| 56 | # Returns | ||||||
| 57 | # 1 if the given graph contains a Hamiltonian Cycle. | ||||||
| 58 | # 0 otherwise. | ||||||
| 59 | # | ||||||
| 60 | |||||||
| 61 | sub graph_is_hamiltonian { | ||||||
| 62 | 40 | 40 | 1 | 56938 | my ($g) = @_; | ||
| 63 | |||||||
| 64 | 40 | 88 | $calls = 0; | ||||
| 65 | 40 | 90 | my ( $is_hamiltonian, $reason ); | ||||
| 66 | 40 | 96 | my $time_begin = time; | ||||
| 67 | 40 | 159 | my @once_only_tests = ( \&test_trivial, \&test_dirac ); | ||||
| 68 | 40 | 112 | foreach my $test_sub (@once_only_tests) { | ||||
| 69 | 76 | 291 | ( $is_hamiltonian, $reason ) = &$test_sub($g); | ||||
| 70 | 76 | 100 | 274 | last unless $is_hamiltonian == $DONT_KNOW; | |||
| 71 | } | ||||||
| 72 | |||||||
| 73 | 40 | 178 | my $params = { | ||||
| 74 | transformed => 0, | ||||||
| 75 | tentative => 0, | ||||||
| 76 | }; | ||||||
| 77 | |||||||
| 78 | 40 | 100 | 126 | if ( $is_hamiltonian == $DONT_KNOW ) { | |||
| 79 | 35 | 148 | ( $is_hamiltonian, $reason, $params ) = is_hamiltonian($g, $params); | ||||
| 80 | } else { | ||||||
| 81 | 5 | 18 | my $spaced_string = $g->stringify(); | ||||
| 82 | 5 | 9121 | $spaced_string =~ s/\,/, /g; | ||||
| 83 | 5 | 17 | output(" "); | ||||
| 84 | 5 | 19 | output("In graph_is_hamiltonian($spaced_string)"); | ||||
| 85 | 5 | 11 | output($g); | ||||
| 86 | } | ||||||
| 87 | 40 | 138 | my $time_end = time; | ||||
| 88 | |||||||
| 89 | 40 | 162 | $params->{time_elapsed} = int($time_end - $time_begin); | ||||
| 90 | 40 | 120 | $params->{calls} = $calls; | ||||
| 91 | |||||||
| 92 | 40 | 100 | 133 | my $final_bit = ( $is_hamiltonian == $GRAPH_IS_HAMILTONIAN ) ? 1 : 0; | |||
| 93 | 40 | 50 | 236 | return wantarray ? ( $final_bit, $reason, $params ) : $final_bit; | |||
| 94 | } | ||||||
| 95 | |||||||
| 96 | ########################################################################## | ||||||
| 97 | |||||||
| 98 | # is_hamiltonian() | ||||||
| 99 | # | ||||||
| 100 | # Takes a Graph::Undirected object. | ||||||
| 101 | # | ||||||
| 102 | # Returns a result ( $is_hamiltonian, $reason ) | ||||||
| 103 | # indicating whether the given graph contains a Hamiltonian Cycle. | ||||||
| 104 | # | ||||||
| 105 | # | ||||||
| 106 | |||||||
| 107 | sub is_hamiltonian { | ||||||
| 108 | 215 | 215 | 0 | 686 | my ($g, $params) = @_; | ||
| 109 | 215 | 441 | $calls++; | ||||
| 110 | |||||||
| 111 | 215 | 878 | my $spaced_string = $g->stringify(); | ||||
| 112 | 215 | 180637 | $spaced_string =~ s/\,/, /g; | ||||
| 113 | 215 | 990 | output(" "); | ||||
| 114 | 215 | 947 | output("Calling is_hamiltonian($spaced_string)"); | ||||
| 115 | 215 | 692 | output($g); | ||||
| 116 | |||||||
| 117 | 215 | 443 | my ( $is_hamiltonian, $reason ); | ||||
| 118 | 215 | 1026 | my @tests_1 = ( | ||||
| 119 | \&test_ore, | ||||||
| 120 | \&test_min_degree, | ||||||
| 121 | \&test_articulation_vertex, | ||||||
| 122 | \&test_graph_bridge, | ||||||
| 123 | ); | ||||||
| 124 | |||||||
| 125 | 215 | 584 | foreach my $test_sub (@tests_1) { | ||||
| 126 | 844 | 3051 | ( $is_hamiltonian, $reason ) = &$test_sub($g, $params); | ||||
| 127 | 844 | 100 | 336620 | return ( $is_hamiltonian, $reason, $params ) | |||
| 128 | unless $is_hamiltonian == $DONT_KNOW; | ||||||
| 129 | } | ||||||
| 130 | |||||||
| 131 | ### Create a graph made of only required edges. | ||||||
| 132 | 204 | 377 | my $required_graph; | ||||
| 133 | 204 | 970 | ( $required_graph, $g ) = get_required_graph($g); | ||||
| 134 | |||||||
| 135 | 204 | 100 | 626 | if ( $required_graph->edges() ) { | |||
| 136 | 194 | 5924 | my @tests_2 = ( | ||||
| 137 | \&test_required_max_degree, | ||||||
| 138 | \&test_required_connected, | ||||||
| 139 | \&test_required_cyclic ); | ||||||
| 140 | 194 | 484 | foreach my $test_sub (@tests_2) { | ||||
| 141 | 488 | 1946 | ( $is_hamiltonian, $reason, $params ) = &$test_sub($required_graph, $g, $params); | ||||
| 142 | 488 | 100 | 6758 | return ( $is_hamiltonian, $reason, $params ) | |||
| 143 | unless $is_hamiltonian == $DONT_KNOW; | ||||||
| 144 | } | ||||||
| 145 | |||||||
| 146 | ### Delete edges that can be safely eliminated so far. | ||||||
| 147 | 116 | 716 | my ( $deleted_edges , $g1 ) = delete_cycle_closing_edges($g, $required_graph); | ||||
| 148 | 116 | 522 | my ( $deleted_edges2, $g2 ) = delete_non_required_neighbors($g1, $required_graph); | ||||
| 149 | 116 | 100 | 100 | 735 | if ($deleted_edges || $deleted_edges2) { | ||
| 150 | 73 | 329 | $params->{transformed} = 1; | ||||
| 151 | 73 | 1209 | @_ = ($g2, $params); | ||||
| 152 | 73 | 5074 | goto &is_hamiltonian; | ||||
| 153 | } | ||||||
| 154 | } | ||||||
| 155 | |||||||
| 156 | ### If there are undecided vrtices, choose between them recursively. | ||||||
| 157 | 53 | 425 | my @undecided_vertices = grep { $g->degree($_) > 2 } $g->vertices(); | ||||
| 689 | 167087 | ||||||
| 158 | 53 | 50 | 13959 | if (@undecided_vertices) { | |||
| 159 | 53 | 50 | 227 | unless ( $params->{tentative} ) { | |||
| 160 | 53 | 268 | output( "Now running an exhaustive, recursive," | ||||
| 161 | . " and conclusive search," | ||||||
| 162 | . " only slightly better than brute force. " ); | ||||||
| 163 | } | ||||||
| 164 | |||||||
| 165 | 53 | 318 | my $vertex = | ||||
| 166 | get_chosen_vertex( $g, $required_graph, \@undecided_vertices ); | ||||||
| 167 | |||||||
| 168 | 53 | 217 | my $tentative_combinations = | ||||
| 169 | get_tentative_combinations( $g, $required_graph, $vertex ); | ||||||
| 170 | |||||||
| 171 | 53 | 170 | foreach my $tentative_edge_pair (@$tentative_combinations) { | ||||
| 172 | 107 | 430 | my $g1 = $g->deep_copy_graph(); | ||||
| 173 | output("For vertex: $vertex, protecting " . | ||||||
| 174 | 107 | 134428 | ( join ',', map {"$vertex=$_"} @$tentative_edge_pair ) . | ||||
| 214 | 1067 | ||||||
| 175 | " " ); | ||||||
| 176 | 107 | 490 | foreach my $neighbor ( $g1->neighbors($vertex) ) { | ||||
| 177 | 483 | 100 | 32611 | next if $neighbor == $tentative_edge_pair->[0]; | |||
| 178 | 376 | 100 | 870 | next if $neighbor == $tentative_edge_pair->[1]; | |||
| 179 | 269 | 1093 | output("Deleting edge: $vertex=$neighbor "); | ||||
| 180 | 269 | 744 | $g1->delete_edge( $vertex, $neighbor ); | ||||
| 181 | } | ||||||
| 182 | |||||||
| 183 | 107 | 5318 | output( "The Graph with $vertex=" . $tentative_edge_pair->[0] | ||||
| 184 | . ", $vertex=" . $tentative_edge_pair->[1] | ||||||
| 185 | . " protected: " ); | ||||||
| 186 | 107 | 317 | output($g1); | ||||
| 187 | |||||||
| 188 | 107 | 282 | $params->{tentative} = 1; | ||||
| 189 | 107 | 465 | ( $is_hamiltonian, $reason, $params ) = is_hamiltonian($g1, $params); | ||||
| 190 | 107 | 100 | 404 | if ( $is_hamiltonian == $GRAPH_IS_HAMILTONIAN ) { | |||
| 191 | 39 | 5303 | return ( $is_hamiltonian, $reason, $params ); | ||||
| 192 | } | ||||||
| 193 | 68 | 266 | output("...backtracking. "); | ||||
| 194 | } | ||||||
| 195 | } | ||||||
| 196 | |||||||
| 197 | 14 | 783 | return ( $GRAPH_IS_NOT_HAMILTONIAN, | ||||
| 198 | "The graph passed through an exhaustive search " . | ||||||
| 199 | "for Hamiltonian Cycles.", $params ); | ||||||
| 200 | |||||||
| 201 | } | ||||||
| 202 | |||||||
| 203 | ########################################################################## | ||||||
| 204 | |||||||
| 205 | sub get_tentative_combinations { | ||||||
| 206 | |||||||
| 207 | # Generate all allowable combinations of 2 edges, | ||||||
| 208 | # incident on a given vertex. | ||||||
| 209 | |||||||
| 210 | 53 | 53 | 0 | 164 | my ( $g, $required_graph, $vertex ) = @_; | ||
| 211 | 53 | 102 | my @tentative_combinations; | ||||
| 212 | 53 | 206 | my @neighbors = sort { $a <=> $b } $g->neighbors($vertex); | ||||
| 355 | 4669 | ||||||
| 213 | 53 | 100 | 269 | if ( $required_graph->degree($vertex) == 1 ) { | |||
| 214 | 26 | 4747 | my ($fixed_neighbor) = $required_graph->neighbors($vertex); | ||||
| 215 | 26 | 1687 | foreach my $tentative_neighbor (@neighbors) { | ||||
| 216 | 102 | 100 | 228 | next if $fixed_neighbor == $tentative_neighbor; | |||
| 217 | 76 | 181 | push @tentative_combinations, | ||||
| 218 | [ $fixed_neighbor, $tentative_neighbor ]; | ||||||
| 219 | } | ||||||
| 220 | } else { | ||||||
| 221 | 27 | 3379 | for ( my $i = 0; $i < scalar(@neighbors) - 1; $i++ ) { | ||||
| 222 | 115 | 263 | for ( my $j = $i + 1; $j < scalar(@neighbors); $j++ ) { | ||||
| 223 | 334 | 943 | push @tentative_combinations, | ||||
| 224 | [ $neighbors[$i], $neighbors[$j] ]; | ||||||
| 225 | } | ||||||
| 226 | } | ||||||
| 227 | } | ||||||
| 228 | |||||||
| 229 | 53 | 180 | return \@tentative_combinations; | ||||
| 230 | } | ||||||
| 231 | |||||||
| 232 | ########################################################################## | ||||||
| 233 | |||||||
| 234 | sub get_chosen_vertex { | ||||||
| 235 | 53 | 53 | 0 | 178 | my ( $g, $required_graph, $undecided_vertices ) = @_; | ||
| 236 | |||||||
| 237 | # 1. Choose the vertex with the highest degree first. | ||||||
| 238 | # | ||||||
| 239 | # 2. If degrees are equal, prefer vertices which already have | ||||||
| 240 | # a required edge incident on them. | ||||||
| 241 | # | ||||||
| 242 | # 3. Break a tie from rules 1 & 2, by picking the lowest | ||||||
| 243 | # numbered vertex first. | ||||||
| 244 | |||||||
| 245 | 53 | 171 | my $chosen_vertex; | ||||
| 246 | my $chosen_vertex_degree; | ||||||
| 247 | 53 | 0 | my $chosen_vertex_required_degree; | ||||
| 248 | 53 | 154 | foreach my $vertex (@$undecided_vertices) { | ||||
| 249 | 530 | 1158 | my $degree = $g->degree($vertex); | ||||
| 250 | 530 | 146625 | my $required_degree = $required_graph->degree($vertex); | ||||
| 251 | 530 | 100 | 100 | 75981 | if ( ( !defined $chosen_vertex_degree ) | ||
| 100 | |||||||
| 100 | |||||||
| 100 | |||||||
| 100 | |||||||
| 100 | |||||||
| 252 | or ( $degree > $chosen_vertex_degree ) | ||||||
| 253 | or ( ( $degree == $chosen_vertex_degree ) | ||||||
| 254 | and ( $required_degree > $chosen_vertex_required_degree ) ) | ||||||
| 255 | or ( ( $degree == $chosen_vertex_degree ) | ||||||
| 256 | and ( $required_degree == $chosen_vertex_required_degree ) | ||||||
| 257 | and ( $vertex < $chosen_vertex ) ) | ||||||
| 258 | ) | ||||||
| 259 | { | ||||||
| 260 | 135 | 289 | $chosen_vertex = $vertex; | ||||
| 261 | 135 | 224 | $chosen_vertex_degree = $degree; | ||||
| 262 | 135 | 254 | $chosen_vertex_required_degree = $required_degree; | ||||
| 263 | } | ||||||
| 264 | } | ||||||
| 265 | |||||||
| 266 | 53 | 195 | return $chosen_vertex; | ||||
| 267 | } | ||||||
| 268 | |||||||
| 269 | ########################################################################## | ||||||
| 270 | |||||||
| 271 | 1; # End of Graph::Undirected::Hamiltonicity |