From c84993fa6fe15a9f343572ae58bf87b306b7dd87 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 4 Feb 2026 13:35:34 -0600 Subject: [PATCH 1/7] Change the weight color used from red to FireBrick. This color has a contrast ratio of 6.68 against the white background. The red color has a contrast ration of 4.00 which is not sufficient for accessibility purposes. --- macros/math/SimpleGraph.pl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index d498a735a..9ed10f5b0 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -819,7 +819,7 @@ sub image { $u * $iVertex->[0] + $v * $jVertex->[0] + $perp[0] * 0.06, $u * $iVertex->[1] + $v * $jVertex->[1] + $perp[1] * 0.06, label => "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", - color => 'red', + color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / $main::PI - ($perp[1] < 0 ? 180 : 0) @@ -898,7 +898,7 @@ sub gridLayoutImage { $u * $iVertex->[0] + $v * $jVertex->[0] - $vector->[1] / $norm * 2, $u * $iVertex->[1] + $v * $jVertex->[1] + $vector->[0] / $norm * 2, label => "\\\\($self->{adjacencyMatrix}[$i][$j]\\\\)", - color => 'red' + color => 'FireBrick' ); } } @@ -1002,7 +1002,7 @@ sub bipartiteLayoutImage { $u * $point1->[0] + $v * $point2->[0] - $vector->[1] / $norm * 5 / 4, $u * $point1->[1] + $v * $point2->[1] + $vector->[0] / $norm * 5 / 4, label => "\\\\($self->{adjacencyMatrix}[ $top->[$i] ][ $bottom->[$j] ]\\\\)", - color => 'red' + color => 'FireBrick' ); } } @@ -1071,7 +1071,7 @@ sub wheelLayoutImage { 0.5 * $iVertex->[0] + $iVertex->[1] / $norm * 0.1, 0.5 * $iVertex->[1] - $iVertex->[0] / $norm * 0.1, label => "\\\\($self->{adjacencyMatrix}->[ $self->{wheelLayout} ][$i]\\\\)", - color => 'red', + color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / $main::PI - ($perp[1] < 0 ? 180 : 0) @@ -1096,7 +1096,7 @@ sub wheelLayoutImage { 0.5 * $iVertex->[0] + 0.5 * $jVertex->[0] + $vector[1] / $norm * 0.1, 0.5 * $iVertex->[1] + 0.5 * $jVertex->[1] - $vector[0] / $norm * 0.1, label => "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", - color => 'red', + color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / $main::PI - ($perp[1] < 0 ? 180 : 0) From cc7a6ca49607e22d731bbba8bfd6abfc8af23a62 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 2 Mar 2026 19:06:42 -0600 Subject: [PATCH 2/7] Add a `components` method that returns the components of the graph. The method returns an array containing references to arrays that form a partition the vertex indices into the connected components of the graph. For example, for the graph with vertices E, F, G, H, I, J, K, and L, and edge set {{{E, L}, {F, G}, {F, L}, {G, J}, {H, I}, {J, K}, {J, L}}}, the method will return ([E, F, G, J, K, L], [H, I]). --- macros/math/SimpleGraph.pl | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index 9ed10f5b0..acb5611c0 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -645,6 +645,37 @@ sub numComponents { return $result; } +sub components { + my $self = shift; + + my @adjacencyMatrix = map { [@$_] } @{ $self->{adjacencyMatrix} }; + + for my $i (0 .. $#adjacencyMatrix) { + for my $j ($i + 1 .. $#adjacencyMatrix) { + if ($adjacencyMatrix[$i][$j] != 0) { + for my $k (0 .. $#adjacencyMatrix) { + $adjacencyMatrix[$j][$k] += $adjacencyMatrix[$i][$k]; + $adjacencyMatrix[$k][$j] += $adjacencyMatrix[$k][$i]; + } + } + } + } + + my @components; + for my $i (reverse(0 .. $#adjacencyMatrix)) { + my $componentFound = 0; + for (@components) { + next unless $adjacencyMatrix[ $_->[-1] ][$i]; + $componentFound = 1; + unshift(@$_, $i); + last; + } + push(@components, [$i]) unless $componentFound; + } + + return main::PGsort(sub { $_[0][0] < $_[1][0] }, @components); +} + sub edgeWeight { my ($self, $i, $j, $weight) = @_; if (defined $weight) { @@ -2242,6 +2273,13 @@ =head2 numComponents This method returns the number of connected components in the graph. +=head2 components + + @c = $graph->components; + +This method returns an array containing references to arrays that form a +partition of the vertex indices into the connected components of the graph. + =head2 edgeWeight $c = $graph->edgeWeight($i, $j); From 207ae206eea9091378b569d6b403a008ae4e7515 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 3 Mar 2026 19:07:57 -0600 Subject: [PATCH 3/7] Improve label positioning. Instead of trying to place the labels using the coordinates, use the `anchor` and padding options to get better positioning. The primary advantage is now the labels don't float away when the image is enlarged. This was not done originally because at the time this macro was implemented the `anchor` and `padding` options didn't exist. The weights for the default layout and the wheel layout are still positioned along the perpendicular vector for now. The problem is that those labels are rotated, and that does not work well with the anchor. This is a TikZ issue (I implemented the anchor for JSXGraph to work the same as the TikZ anchor option). The problem is that the rotation is around the position of the anchor, and not around the center of the text. The usual solution for this in TikZ is to use a `\rotatebox` on the node contents. Perhaps another rotation option could be addded to the plots macro that would rotate the text instead of rotating around the anchor position. --- macros/math/SimpleGraph.pl | 71 ++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index acb5611c0..d9a126dea 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -135,7 +135,7 @@ sub randomGraphWithoutEulerTrail { my $graph; do { - $graph = simpleGraphWithDegreeSequence([ map { main::random(2, $size - 1, 1) } 0 .. $size - 1 ], %options); + $graph = simpleGraphWithDegreeSequence([ map { main::random(2, $size - 1) } 0 .. $size - 1 ], %options); } while !defined $graph || $graph->hasEulerTrail; return $graph->setRandomWeights( @@ -827,11 +827,10 @@ sub image { $plot->add_point(@$iVertex, color => 'blue', mark_size => 3); $plot->add_label( - 1.25 * $iVertex->[0], 1.25 * $iVertex->[1], - label => "\\\\($self->{labels}[$i]\\\\)", + $iVertex->[0], $iVertex->[1], "\\\\($self->{labels}[$i]\\\\)", color => 'blue', - h_align => 'center', - v_align => 'middle' + anchor => 180 + $i * $gap * 180 / $main::PI, + padding => 8 ) if $graphOptions{showLabels}; my $u = 0.275; @@ -849,7 +848,7 @@ sub image { $plot->add_label( $u * $iVertex->[0] + $v * $jVertex->[0] + $perp[0] * 0.06, $u * $iVertex->[1] + $v * $jVertex->[1] + $perp[1] * 0.06, - label => "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", + "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / @@ -898,11 +897,10 @@ sub gridLayoutImage { my $y = $gridGap * ($self->{gridLayout}[0] - $i - 1); $plot->add_point($x, $y, color => 'blue', mark_size => 3); $plot->add_label( - $x - $labelShift, $y + 2 * $labelShift, - label => "\\\\($self->{labels}[$i + $self->{gridLayout}[0] * $j]\\\\)", + $x, $y, "\\\\($self->{labels}[$i + $self->{gridLayout}[0] * $j]\\\\)", color => 'blue', - h_align => 'center', - v_align => 'middle' + anchor => -atan2(2, 1) * 180 / $main::PI, + padding => 8, ) if $graphOptions{showLabels}; } } @@ -922,14 +920,14 @@ sub gridLayoutImage { ($self->{gridLayout}[0] - ($j % $self->{gridLayout}[0]) - 1) * $gridGap ]; $plot->add_dataset($iVertex, $jVertex, color => 'black', width => 1); - my $vector = [ $jVertex->[0] - $iVertex->[0], $jVertex->[1] - $iVertex->[1] ]; if ($graphOptions{showWeights}) { - my $norm = sqrt($vector->[0]**2 + $vector->[1]**2); + my $vector = [ $jVertex->[0] - $iVertex->[0], $jVertex->[1] - $iVertex->[1] ]; $plot->add_label( - $u * $iVertex->[0] + $v * $jVertex->[0] - $vector->[1] / $norm * 2, - $u * $iVertex->[1] + $v * $jVertex->[1] + $vector->[0] / $norm * 2, - label => "\\\\($self->{adjacencyMatrix}[$i][$j]\\\\)", - color => 'FireBrick' + $u * $iVertex->[0] + $v * $jVertex->[0], + $u * $iVertex->[1] + $v * $jVertex->[1], + "\\\\($self->{adjacencyMatrix}[$i][$j]\\\\)", + color => 'FireBrick', + anchor => atan2(-$vector->[0], $vector->[1]) * 180 / $main::PI ); } } @@ -1000,21 +998,21 @@ sub bipartiteLayoutImage { for my $i (0 .. $#$top) { $plot->add_point($i * $width + $shift[0], $high, color => 'blue', mark_size => 3); $plot->add_label( - $i * $width + $shift[0], $high + 2 / 3, - label => "\\\\($self->{labels}[$top->[$i]]\\\\)", + $i * $width + $shift[0], $high, "\\\\($self->{labels}[$top->[$i]]\\\\)", color => 'blue', h_align => 'center', - v_align => 'bottom' + v_align => 'bottom', + padding => 8 ) if $graphOptions{showLabels}; } for my $j (0 .. $#$bottom) { $plot->add_point($j * $width + $shift[1], $low, color => 'blue', mark_size => 3); $plot->add_label( - $j * $width + $shift[1], $low - 2 / 3, - label => "\\\\($self->{labels}[$bottom->[$j]]\\\\)", + $j * $width + $shift[1], $low, "\\\\($self->{labels}[$bottom->[$j]]\\\\)", color => 'blue', h_align => 'center', - v_align => 'top' + v_align => 'top', + padding => 8 ) if $graphOptions{showLabels}; } @@ -1028,12 +1026,13 @@ sub bipartiteLayoutImage { $plot->add_dataset($point1, $point2, color => 'black'); if ($graphOptions{showWeights}) { my $vector = [ $point2->[0] - $point1->[0], $point2->[1] - $point1->[1] ]; - my $norm = sqrt($vector->[0]**2 + $vector->[1]**2); $plot->add_label( - $u * $point1->[0] + $v * $point2->[0] - $vector->[1] / $norm * 5 / 4, - $u * $point1->[1] + $v * $point2->[1] + $vector->[0] / $norm * 5 / 4, - label => "\\\\($self->{adjacencyMatrix}[ $top->[$i] ][ $bottom->[$j] ]\\\\)", - color => 'FireBrick' + $u * $point1->[0] + $v * $point2->[0], + $u * $point1->[1] + $v * $point2->[1], + "\\\\($self->{adjacencyMatrix}[ $top->[$i] ][ $bottom->[$j] ]\\\\)", + color => 'FireBrick', + anchor => atan2($vector->[0], -$vector->[1]) * 180 / $main::PI + 180, + padding => 2 ); } } @@ -1070,11 +1069,10 @@ sub wheelLayoutImage { $plot->add_point(0, 0, color => 'blue', mark_size => 3); $plot->add_label( - 0.1, 0.2, - label => "\\\\($self->{labels}[ $self->{wheelLayout} ]\\\\)", + 0, 0, "\\\\($self->{labels}[ $self->{wheelLayout} ]\\\\)", color => 'blue', - h_align => 'center', - v_align => 'middle' + anchor => 180 + $gap * 90 / $main::PI, + padding => 10 ) if $graphOptions{showLabels}; for my $i (0 .. $self->lastVertexIndex) { @@ -1086,11 +1084,10 @@ sub wheelLayoutImage { $plot->add_point(@$iVertex, color => 'blue', mark_size => 3); $plot->add_label( - 1.25 * $iVertex->[0], 1.25 * $iVertex->[1], - label => "\\\\($self->{labels}[$i]\\\\)", + $iVertex->[0], $iVertex->[1], "\\\\($self->{labels}[$i]\\\\)", color => 'blue', - h_align => 'center', - v_align => 'middle' + anchor => 180 + $iRel * $gap * 180 / $main::PI, + padding => 8 ) if $graphOptions{showLabels}; if ($self->hasEdge($self->{wheelLayout}, $i)) { @@ -1101,7 +1098,7 @@ sub wheelLayoutImage { $plot->add_label( 0.5 * $iVertex->[0] + $iVertex->[1] / $norm * 0.1, 0.5 * $iVertex->[1] - $iVertex->[0] / $norm * 0.1, - label => "\\\\($self->{adjacencyMatrix}->[ $self->{wheelLayout} ][$i]\\\\)", + "\\\\($self->{adjacencyMatrix}->[ $self->{wheelLayout} ][$i]\\\\)", color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / @@ -1126,7 +1123,7 @@ sub wheelLayoutImage { $plot->add_label( 0.5 * $iVertex->[0] + 0.5 * $jVertex->[0] + $vector[1] / $norm * 0.1, 0.5 * $iVertex->[1] + 0.5 * $jVertex->[1] - $vector[0] / $norm * 0.1, - label => "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", + "\\\\($self->{adjacencyMatrix}->[$i][$j]\\\\)", color => 'FireBrick', rotate => ($perp[0] < 0 ? 1 : -1) * atan2(sqrt(1 - $perp[1] * $perp[1]), $perp[1]) * 180 / From 0e7173040c05d47d13a644ad5c2ce209b973589f Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 4 Mar 2026 15:23:16 -0600 Subject: [PATCH 4/7] Fix a bug in the `edgeSet` method that results in the method returning an invalid edge set for a graph with a single edge. --- macros/math/SimpleGraph.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index d9a126dea..2b86c1b8b 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -543,7 +543,7 @@ sub edgeSet { } } - my $edgeSet = GraphTheory::SimpleGraph::Value::EdgeSet->new($context, @edgeSet); + my $edgeSet = GraphTheory::SimpleGraph::Value::EdgeSet->new($context, \@edgeSet); $edgeSet->{open} = '{'; $edgeSet->{close} = '}'; return $edgeSet; From f2116a4aa25e2c61be0404e38b7fe80fdae58b11 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 6 Mar 2026 07:27:39 -0600 Subject: [PATCH 5/7] Make several improvements to the `sortedEdgesPath` method. First, the method is now quite a bit more efficient. Rather than finding and sorting only the weights of the edges in the graph, and then searching through the graph to find those edges at each step in the algorithm, the edges with the weights are all listed and sorted by weight (more like the actual sorted edges algorithm works). So there is no need to find the edge later, you just follow the algorithm and process the edges in order. Second, the return value of the method is reordered and more data returned. See the updated POD for good documentation on what is returned. This makes the method return a lot more useful information that can be used for constructing a solution to problems using the sorted edges algorithm. --- macros/math/SimpleGraph.pl | 76 ++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index 2b86c1b8b..72cca30d1 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -1486,54 +1486,51 @@ sub dijkstraPath { sub sortedEdgesPath { my $self = shift; - my @weights; + my @sortedEdges; my $sortedGraph = GraphTheory::SimpleGraph->new($self->numVertices, labels => $self->labels); for my $i (0 .. $self->lastVertexIndex) { for my $j ($i + 1 .. $self->lastVertexIndex) { next unless $self->hasEdge($i, $j); - push @weights, $self->edgeWeight($i, $j); + push @sortedEdges, [ $i, $j, $self->edgeWeight($i, $j) ]; } } - @weights = main::num_sort(@weights); + @sortedEdges = main::PGsort(sub { $_[0][-1] < $_[1][-1] }, @sortedEdges); - # Returns 1 if an edge can be added to the sorted edges based graph and 0 otherwise. An edge can be added if it does - # not make a vertex have more than two edges connected to it, and it does not create a circuit in the graph (unless - # it is the last vertex in which case that is okay since it completes the circuit). - my $goodEdge = sub { + # Returns 0 if an edge can be added to the sorted edges based graph, 1 if adding the edge results in a vertex having + # more than two edges connected to it, and 2 if adding the edge results in the path having a circuit (unless it is + # the last vertex in which case that is okay since it completes the circuit). + my $edgeCheck = sub { my $graph = shift; my $sum = 0; for my $i (0 .. $graph->lastVertexIndex) { my $degree = $graph->vertexDegree($i); - return 0 if $degree > 2; + return 1 if $degree > 2; $sum += $degree; } - return $sum < 2 * $graph->numVertices && $graph->hasCircuit ? 0 : 1; + return $sum < 2 * $graph->numVertices && $graph->hasCircuit ? 2 : 0; }; my @pathWeights; + my @algorithmSteps; do { - my $weight = shift @weights; - for my $i (0 .. $sortedGraph->lastVertexIndex) { - for my $j ($i + 1 .. $sortedGraph->lastVertexIndex) { - if ($weight == $self->edgeWeight($i, $j)) { - $sortedGraph->addEdge($i, $j, $self->edgeWeight($i, $j)); - if ($goodEdge->($sortedGraph)) { - push @pathWeights, $weight; - } else { - $sortedGraph->removeEdge($i, $j); - } - } - } + my $edge = shift @sortedEdges; + $sortedGraph->addEdge(@$edge); + my $edgeCheckResult = $edgeCheck->($sortedGraph); + push @algorithmSteps, [ @$edge, $edgeCheckResult ]; + if ($edgeCheckResult) { + $sortedGraph->removeEdge(@$edge); + } else { + push @pathWeights, $edge->[-1]; } - } while @pathWeights < $sortedGraph->numVertices && @weights > 0; + } while @pathWeights < $sortedGraph->numVertices && @sortedEdges; - return ($sortedGraph, \@pathWeights); + return (\@pathWeights, \@algorithmSteps, $sortedGraph); } sub chromaticNumber { @@ -2658,16 +2655,33 @@ =head2 dijkstraPath =head2 sortedEdgesPath - ($sortedEdgesPath, $edgeWeights) = $graph->sortedEdgesPath; + ($pathWeights, $algorithmSteps, $sortedGraph) = $graph->sortedEdgesPath; -This is an implementation of the sorted edges algorithm for finding the shortest +This is an implementation of the sorted edges algorithm for finding a low cost Hamiltonian circuit in a graph. That is a path that visits each vertex in the -graph exactly once. The return value will be a list with two entries The first -entry is the resulting sorted edges graph, and the second entry is a reference -to an array containing the weights of the edges in the path in the order that -they are chosen by the algorithm. Note that the returned graph will contain a -Hamiltonian circuit from the original graph if one exists. In any case the graph -will contain all edges chosen in the algorithm. +graph exactly once. The return value will be a list with three entries. The +first entry is a reference to an array containing the weights of the edges in +the path in the order that they are chosen by the algorithm, the second entry is +a reference to an array of array references that represent the steps of the +sorted edges algorithm, and the third entry is the resulting sorted edges graph. + +The returned representation of the steps in the algorithm will be a reference to +an array of array references where each array reference is of the form +C<[$i, $j, $weight, $reason]>. This is where C<$i> and C<$j> are the indices of +the vertices connected by an edge in the graph, and C<$weight> is the weight of +that edge. These arrays will be sorted in ascending order of weight, i.e., the +order the edge is considered by the sorted edges algorithm. The C<$reason> will +be one of 0, 1, or 2. It will be 0 if the edge is chosen by the sorted edges +algorithm, 1 if the edge is rejected by the sorted edges algorithm because +adding it would have made a vertex in the path have more than two edges +connected to it, and 2 if adding the edge would have created a circuit in the +path (before the path is completed). Note that this list may not contain all +edges of the original graph if there are edges that are never considered by the +algorithm because the circuit is completed before those edges are reached. + +The returned sorted edges graph will contain a Hamiltonian circuit from the +original graph if one exists. In any case the graph will contain all edges +chosen in the algorithm. =head2 chromaticNumber From 8c0898d41bfbe4b96de9cc7d3845b42c690e3b57 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 6 Mar 2026 16:44:05 -0600 Subject: [PATCH 6/7] Make several improvements to the `kruskalGraph` method. First, the method is now quite a bit more efficient. Rather than finding and sorting only the weights of the edges in the graph, and then searching through the graph to find those edges at each step in the algorithm, the edges with the weights are all listed and sorted by weight (more like the actual sorted edges algorithm works). So there is no need to find the edge later, you just follow the algorithm and process the edges in order. Also make the algorithm terminate once the minimal spanning tree is complete as it should. Second, the return value of the method returns more data. See the updated POD for good documentation on what is returned. This makes the method return more useful information that can be used for constructing a solution to problems using the sorted edges algorithm. These is the basically the same changes that were made for the `sortedEdgesPath` method. --- macros/math/SimpleGraph.pl | 76 ++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index 72cca30d1..38d6b456a 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -1199,45 +1199,43 @@ sub nearestNeighborPath { sub kruskalGraph { my $self = shift; + my $numComponents = $self->numComponents; my $graph = $self->copy; my $tree = GraphTheory::SimpleGraph->new($graph->numVertices, labels => $graph->labels); - my $numTreeComponents = $tree->numComponents; + my $numTreeComponents = $tree->numVertices; - my $treeWeight = 0; - - my $weight = 0; my @treeWeights; + my $treeWeight = 0; + my @algorithmSteps; - my @weights; - for my $i (0 .. $graph->lastVertexIndex) { - for my $j ($i + 1 .. $graph->lastVertexIndex) { - push(@weights, $graph->edgeWeight($i, $j)) if $graph->hasEdge($i, $j); + my @sortedEdges; + for my $i (0 .. $self->lastVertexIndex) { + for my $j ($i + 1 .. $self->lastVertexIndex) { + next unless $self->hasEdge($i, $j); + push @sortedEdges, [ $i, $j, $self->edgeWeight($i, $j) ]; } } - @weights = main::num_sort(@weights); + @sortedEdges = main::PGsort(sub { $_[0][-1] < $_[1][-1] }, @sortedEdges); - while (@weights > 0) { - $weight = shift @weights; - for my $i (0 .. $graph->lastVertexIndex) { - for my $j ($i + 1 .. $graph->lastVertexIndex) { - if ($graph->edgeWeight($i, $j) == $weight) { - $graph->removeEdge($i, $j); - $tree->addEdge($i, $j, $weight); - my $currentTreeNumComponents = $tree->numComponents; - if ($currentTreeNumComponents < $numTreeComponents) { - $numTreeComponents = $currentTreeNumComponents; - $treeWeight += $weight; - push @treeWeights, $weight; - } else { - $tree->removeEdge($i, $j); - } - last; - } - } + while (@sortedEdges && $numTreeComponents > $numComponents) { + my $edge = shift @sortedEdges; + my $weight = $edge->[2]; + + $graph->removeEdge($edge->[0], $edge->[1]); + $tree->addEdge(@$edge); + my $currentTreeNumComponents = $tree->numComponents; + if ($currentTreeNumComponents < $numTreeComponents) { + push @algorithmSteps, [ @$edge, 1 ]; + $numTreeComponents = $currentTreeNumComponents; + $treeWeight += $weight; + push @treeWeights, $weight; + } else { + push @algorithmSteps, [ @$edge, 0 ]; + $tree->removeEdge($edge->[0], $edge->[1]); } } - return ($tree, $treeWeight, \@treeWeights); + return ($tree, $treeWeight, \@treeWeights, \@algorithmSteps); } sub hasEulerCircuit { @@ -2535,18 +2533,32 @@ =head2 nearestNeighborPath =head2 kruskalGraph - ($tree, $treeWeight, $treeWeights) = $graph->kruskalGraph($vertex); + ($tree, $treeWeight, $treeWeights, $algorithmSteps) = $graph->kruskalGraph($vertex); This is an implementation of Kruskal's algorithm. It attempts to find a minimum spanning tree or forest for the graph. Note that if the graph is connected, then the result will be a tree, and otherwise it will be a forest consisting of minimal spanning trees for each component. -The method returns a list with three entries. The first entry is a +The method returns a list with four entries. The first entry is a C object representing the tree or forest found. The -second entry is the total weight of that tree or forest. The last entry is a +second entry is the total weight of that tree or forest. The third entry is a reference to an array containing the weights of the edges in the tree or forest -in the order that they are added by the algorithm. +in the order that they are added by the algorithm. The fourth entry is a +reference to an array of array references that represent the steps of Kruskal's +algorithm. + +The returned representation of the steps in the algorithm will be a reference to +an array of array references where each array reference is of the form C<[$i, +$j, $weight, $accepted]>. This is where C<$i> and C<$j> are the indices of the +vertices connected by an edge in the graph, and C<$weight> is the weight of that +edge. These arrays will be sorted in ascending order of weight, i.e., the order +the edge is considered by Kruskal's algorithm. The last C<$accepted> will be +either 0 or 1. It will be 0 if the edge is rejected by the algorithm, and 1 if +it is accepted by the algorithm. Note that this list may not contain all edges +of the original graph if there are edges that are never considered by the +algorithm because the minimal spanning tree is completed before those edges are +reached. =head2 hasEulerCircuit From e1702285d0511e5e5b22e49f36c772a00c6dec88 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 24 Mar 2026 05:55:22 -0500 Subject: [PATCH 7/7] Add a `description` method. This returns a translated string description of the graph. The string that is returned is suitable for use as the "alt" text for the image returned by one of the image methods. --- macros/math/SimpleGraph.pl | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/macros/math/SimpleGraph.pl b/macros/math/SimpleGraph.pl index 38d6b456a..ca0582353 100644 --- a/macros/math/SimpleGraph.pl +++ b/macros/math/SimpleGraph.pl @@ -593,7 +593,7 @@ sub labels { sub labelsString { my $self = shift; - return join(', ', @{ $self->{labels} }); + return join($main::PG->maketext(', '), @{ $self->{labels} }); } sub vertexLabel { @@ -785,6 +785,43 @@ sub isIsomorphic { return 0; } +sub description { + my ($self, %options) = @_; + + my $description = $main::PG->maketext('A graph with vertices [_1].', $self->labelsString); + + my $comma = $main::PG->maketext(', '); + + my @edgeText; + for my $i (0 .. $self->lastVertexIndex) { + for my $j ($i + 1 .. $self->lastVertexIndex) { + next unless $self->hasEdge($i, $j); + push( + @edgeText, + $options{includeWeights} + ? $main::PG->maketext( + '[_1] and [_2] with weight [_3]', $self->vertexLabel($i), + $self->vertexLabel($j), $self->edgeWeight($i, $j) + ) + : $main::PG->maketext('[_1] and [_2]', $self->vertexLabel($i), $self->vertexLabel($j)) + ); + } + } + if (@edgeText == 1) { + $description .= $main::PG->maketext(" There is an edge between [_1].", $edgeText[0]); + } elsif (@edgeText == 2) { + $description .= $main::PG->maketext(" There are edges between [_1] and [_2].", $edgeText[0], $edgeText[1]); + } elsif (@edgeText) { + $description .= $main::PG->maketext( + ' There are edges between [_1][_2]and [_3].', + join($comma, @edgeText[ 0 .. $#edgeText - 1 ]), + $comma, $edgeText[-1] + ); + } + + return $description; +} + sub image { my ($self, %options) = @_; @@ -2381,6 +2418,21 @@ =head2 isIsomorphic all possible permutations of the other graph, and so should not be used for graphs with a large number of vertices (probably no more than 8). +=head2 description + + $graph->description(%options); + +Returns a textual description of the graph. The string that is returned is +translated and is suitable to be used as the C text for the graph image +returned by one of the following image methods. Note that the description just +lists the vertices and edges, and does not describe the layout for the +specialized layout image methods. + +At this point the only option that can be set via the C<%options> argument is +C. If C is set to 1, then the edge weights will +be included in the description. If this is 0, then edge weights will not be +included. Default is 0. + =head2 image $graph->image(%options);