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.17 | ||||||
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 | 107789 | use Modern::Perl; | |||
4 | 29 | ||||||
4 | 24 | ||||||
28 | 4 | 4 | 2216 | use lib 'local/lib/perl5'; | |||
4 | 2519 | ||||||
4 | 21 | ||||||
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 | 2158 | use Graph::Undirected::Hamiltonicity::Output qw(&output); | |||
4 | 11 | ||||||
4 | 602 | ||||||
38 | 4 | 4 | 1884 | use Graph::Undirected::Hamiltonicity::Tests qw(:all); | |||
4 | 15 | ||||||
4 | 732 | ||||||
39 | 4 | 4 | 31 | use Graph::Undirected::Hamiltonicity::Transforms qw(:all); | |||
4 | 9 | ||||||
4 | 438 | ||||||
40 | |||||||
41 | 4 | 4 | 25 | use Exporter qw(import); | |||
4 | 50 | ||||||
4 | 5344 | ||||||
42 | |||||||
43 | our $VERSION = '0.17'; | ||||||
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 | 55760 | my ($g) = @_; | ||
63 | |||||||
64 | 40 | 100 | $calls = 0; | ||||
65 | 40 | 96 | my ( $is_hamiltonian, $reason ); | ||||
66 | 40 | 101 | my $time_begin = time; | ||||
67 | 40 | 169 | my @once_only_tests = ( \&test_trivial, \&test_dirac ); | ||||
68 | 40 | 102 | foreach my $test_sub (@once_only_tests) { | ||||
69 | 76 | 294 | ( $is_hamiltonian, $reason ) = &$test_sub($g); | ||||
70 | 76 | 100 | 261 | last unless $is_hamiltonian == $DONT_KNOW; | |||
71 | } | ||||||
72 | |||||||
73 | 40 | 188 | my $params = { | ||||
74 | transformed => 0, | ||||||
75 | tentative => 0, | ||||||
76 | }; | ||||||
77 | |||||||
78 | 40 | 100 | 143 | if ( $is_hamiltonian == $DONT_KNOW ) { | |||
79 | 33 | 125 | ( $is_hamiltonian, $reason, $params ) = is_hamiltonian($g, $params); | ||||
80 | } else { | ||||||
81 | 7 | 39 | my $spaced_string = $g->stringify(); | ||||
82 | 7 | 13022 | $spaced_string =~ s/\,/, /g; | ||||
83 | 7 | 43 | output(" "); |
||||
84 | 7 | 34 | output("In graph_is_hamiltonian($spaced_string)"); | ||||
85 | 7 | 18 | output($g); | ||||
86 | } | ||||||
87 | 40 | 135 | my $time_end = time; | ||||
88 | |||||||
89 | 40 | 152 | $params->{time_elapsed} = int($time_end - $time_begin); | ||||
90 | 40 | 119 | $params->{calls} = $calls; | ||||
91 | |||||||
92 | 40 | 100 | 154 | my $final_bit = ( $is_hamiltonian == $GRAPH_IS_HAMILTONIAN ) ? 1 : 0; | |||
93 | 40 | 50 | 281 | 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 | 200 | 200 | 0 | 530 | my ($g, $params) = @_; | ||
109 | 200 | 458 | $calls++; | ||||
110 | |||||||
111 | 200 | 803 | my $spaced_string = $g->stringify(); | ||||
112 | 200 | 162518 | $spaced_string =~ s/\,/, /g; | ||||
113 | 200 | 791 | output(" "); |
||||
114 | 200 | 881 | output("Calling is_hamiltonian($spaced_string)"); | ||||
115 | 200 | 625 | output($g); | ||||
116 | |||||||
117 | 200 | 440 | my ( $is_hamiltonian, $reason ); | ||||
118 | 200 | 878 | my @tests_1 = ( | ||||
119 | \&test_ore, | ||||||
120 | \&test_min_degree, | ||||||
121 | \&test_articulation_vertex, | ||||||
122 | \&test_graph_bridge, | ||||||
123 | ); | ||||||
124 | |||||||
125 | 200 | 526 | foreach my $test_sub (@tests_1) { | ||||
126 | 795 | 2664 | ( $is_hamiltonian, $reason ) = &$test_sub($g, $params); | ||||
127 | 795 | 100 | 312306 | return ( $is_hamiltonian, $reason, $params ) | |||
128 | unless $is_hamiltonian == $DONT_KNOW; | ||||||
129 | } | ||||||
130 | |||||||
131 | ### Create a graph made of only required edges. | ||||||
132 | 197 | 364 | my $required_graph; | ||||
133 | 197 | 996 | ( $required_graph, $g ) = get_required_graph($g); | ||||
134 | |||||||
135 | 197 | 100 | 644 | if ( $required_graph->edges() ) { | |||
136 | 187 | 5754 | my @tests_2 = ( | ||||
137 | \&test_required_max_degree, | ||||||
138 | \&test_required_connected, | ||||||
139 | \&test_required_cyclic ); | ||||||
140 | 187 | 436 | foreach my $test_sub (@tests_2) { | ||||
141 | 481 | 1738 | ( $is_hamiltonian, $reason, $params ) = &$test_sub($required_graph, $g, $params); | ||||
142 | 481 | 100 | 6259 | return ( $is_hamiltonian, $reason, $params ) | |||
143 | unless $is_hamiltonian == $DONT_KNOW; | ||||||
144 | } | ||||||
145 | |||||||
146 | ### Delete edges that can be safely eliminated so far. | ||||||
147 | 118 | 683 | my ( $deleted_edges , $g1 ) = delete_cycle_closing_edges($g, $required_graph); | ||||
148 | 118 | 534 | my ( $deleted_edges2, $g2 ) = delete_non_required_neighbors($g1, $required_graph); | ||||
149 | 118 | 100 | 100 | 676 | if ($deleted_edges || $deleted_edges2) { | ||
150 | 73 | 257 | $params->{transformed} = 1; | ||||
151 | 73 | 942 | @_ = ($g2, $params); | ||||
152 | 73 | 4973 | goto &is_hamiltonian; | ||||
153 | } | ||||||
154 | } | ||||||
155 | |||||||
156 | ### If there are undecided vrtices, choose between them recursively. | ||||||
157 | 55 | 404 | my @undecided_vertices = grep { $g->degree($_) > 2 } $g->vertices(); | ||||
698 | 166580 | ||||||
158 | 55 | 50 | 14644 | if (@undecided_vertices) { | |||
159 | 55 | 50 | 231 | unless ( $params->{tentative} ) { | |||
160 | 55 | 293 | output( "Now running an exhaustive, recursive," | ||||
161 | . " and conclusive search," | ||||||
162 | . " only slightly better than brute force. " ); |
||||||
163 | } | ||||||
164 | |||||||
165 | 55 | 263 | my $vertex = | ||||
166 | get_chosen_vertex( $g, $required_graph, \@undecided_vertices ); | ||||||
167 | |||||||
168 | 55 | 240 | my $tentative_combinations = | ||||
169 | get_tentative_combinations( $g, $required_graph, $vertex ); | ||||||
170 | |||||||
171 | 55 | 164 | foreach my $tentative_edge_pair (@$tentative_combinations) { | ||||
172 | 94 | 405 | my $g1 = $g->deep_copy_graph(); | ||||
173 | output("For vertex: $vertex, protecting " . | ||||||
174 | 94 | 118659 | ( join ',', map {"$vertex=$_"} @$tentative_edge_pair ) . | ||||
188 | 898 | ||||||
175 | " " ); |
||||||
176 | 94 | 403 | foreach my $neighbor ( $g1->neighbors($vertex) ) { | ||||
177 | 393 | 100 | 25167 | next if $neighbor == $tentative_edge_pair->[0]; | |||
178 | 299 | 100 | 694 | next if $neighbor == $tentative_edge_pair->[1]; | |||
179 | 205 | 773 | output("Deleting edge: $vertex=$neighbor "); |
||||
180 | 205 | 582 | $g1->delete_edge( $vertex, $neighbor ); | ||||
181 | } | ||||||
182 | |||||||
183 | 94 | 4816 | output( "The Graph with $vertex=" . $tentative_edge_pair->[0] | ||||
184 | . ", $vertex=" . $tentative_edge_pair->[1] | ||||||
185 | . " protected: " ); |
||||||
186 | 94 | 277 | output($g1); | ||||
187 | |||||||
188 | 94 | 253 | $params->{tentative} = 1; | ||||
189 | 94 | 367 | ( $is_hamiltonian, $reason, $params ) = is_hamiltonian($g1, $params); | ||||
190 | 94 | 100 | 333 | if ( $is_hamiltonian == $GRAPH_IS_HAMILTONIAN ) { | |||
191 | 41 | 4459 | return ( $is_hamiltonian, $reason, $params ); | ||||
192 | } | ||||||
193 | 53 | 176 | output("...backtracking. "); |
||||
194 | } | ||||||
195 | } | ||||||
196 | |||||||
197 | 14 | 616 | 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 | 55 | 55 | 0 | 197 | my ( $g, $required_graph, $vertex ) = @_; | ||
211 | 55 | 116 | my @tentative_combinations; | ||||
212 | 55 | 233 | my @neighbors = sort { $a <=> $b } $g->neighbors($vertex); | ||||
318 | 5180 | ||||||
213 | 55 | 100 | 214 | if ( $required_graph->degree($vertex) == 1 ) { | |||
214 | 32 | 5940 | my ($fixed_neighbor) = $required_graph->neighbors($vertex); | ||||
215 | 32 | 2026 | foreach my $tentative_neighbor (@neighbors) { | ||||
216 | 127 | 100 | 282 | next if $fixed_neighbor == $tentative_neighbor; | |||
217 | 95 | 238 | push @tentative_combinations, | ||||
218 | [ $fixed_neighbor, $tentative_neighbor ]; | ||||||
219 | } | ||||||
220 | } else { | ||||||
221 | 23 | 2859 | for ( my $i = 0; $i < scalar(@neighbors) - 1; $i++ ) { | ||||
222 | 91 | 222 | for ( my $j = $i + 1; $j < scalar(@neighbors); $j++ ) { | ||||
223 | 246 | 858 | push @tentative_combinations, | ||||
224 | [ $neighbors[$i], $neighbors[$j] ]; | ||||||
225 | } | ||||||
226 | } | ||||||
227 | } | ||||||
228 | |||||||
229 | 55 | 195 | return \@tentative_combinations; | ||||
230 | } | ||||||
231 | |||||||
232 | ########################################################################## | ||||||
233 | |||||||
234 | sub get_chosen_vertex { | ||||||
235 | 55 | 55 | 0 | 151 | 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 | 55 | 169 | my $chosen_vertex; | ||||
246 | my $chosen_vertex_degree; | ||||||
247 | 55 | 0 | my $chosen_vertex_required_degree; | ||||
248 | 55 | 154 | foreach my $vertex (@$undecided_vertices) { | ||||
249 | 528 | 1218 | my $degree = $g->degree($vertex); | ||||
250 | 528 | 142779 | my $required_degree = $required_graph->degree($vertex); | ||||
251 | 528 | 100 | 100 | 75686 | 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 | 126 | 229 | $chosen_vertex = $vertex; | ||||
261 | 126 | 198 | $chosen_vertex_degree = $degree; | ||||
262 | 126 | 221 | $chosen_vertex_required_degree = $required_degree; | ||||
263 | } | ||||||
264 | } | ||||||
265 | |||||||
266 | 55 | 208 | return $chosen_vertex; | ||||
267 | } | ||||||
268 | |||||||
269 | ########################################################################## | ||||||
270 | |||||||
271 | 1; # End of Graph::Undirected::Hamiltonicity |