Informed (Heuristic) Search

A* Tree Search

A* Tree Search, commonly referred to as A* Search, is a widely used pathfinding and graph traversal algorithm. It builds on the strengths of uniform-cost search and greedy search, offering a robust mechanism for finding the most cost-effective path from a starting node to a goal node.

A* uses a heuristic function, $f(x) = g(x) + h(x)$, where is the cumulative cost to reach the current node, and is an estimated cost to reach the goal from the current node. This balance between actual cost and estimated cost makes A* one of the most efficient search algorithms in many applications, including game development, robotics, and network optimization.

Example of class InformedSearchGraph (with A* Tree search):

 
<?php

namespace app\classes\search;

use 
InvalidArgumentException;

class 
InformedSearchGraph {
    private array 
$adjacencyList;
    private array 
$vertexLabels;
    private array 
$levels;
    private array 
$heuristics;
    private array 
$edgeCosts;

    public function 
__construct() {
        
$this->adjacencyList = [];
        
$this->vertexLabels = [];
        
$this->levels = [];
        
$this->heuristics = [];
        
$this->edgeCosts = [];
    }

    public function 
addVertex(string $vertexint $level = -1float $heuristic 0.0string $label ''): void {
        if (!isset(
$this->adjacencyList[$vertex])) {
            
$this->adjacencyList[$vertex] = [];
            
$this->vertexLabels[$vertex] = $label;
            
$this->levels[$vertex] = $level;
            
$this->heuristics[$vertex] = $heuristic;
            
$this->edgeCosts[$vertex] = [];
        }
    }

    public function 
getVertexLabel(string $vertex ''): string {
        return 
$this->vertexLabels[$vertex] ?? $vertex;
    }

    public function 
addEdge(string $fromstring $tofloat $cost 1.0): void {
        if (!isset(
$this->adjacencyList[$from]) || !isset($this->adjacencyList[$to])) {
            throw new 
InvalidArgumentException('Both vertices must exist in the graph.');
        }

        if (!isset(
$this->edgeCosts[$from])) {
            
$this->edgeCosts[$from] = [];
        }

        if (!
in_array($to$this->adjacencyList[$from])) {
            
$this->adjacencyList[$from][] = $to;
            
$this->edgeCosts[$from][$to] = $cost;
        }
    }

    public function 
getEdgeCost(string $fromstring $to): float {
        if (!isset(
$this->edgeCosts[$from][$to])) {
            throw new 
InvalidArgumentException("No edge exists between $from and $to");
        }
        return 
$this->edgeCosts[$from][$to];
    }

    public function 
greedySearch(string $startstring $goal$maxSteps 100): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException('Both start and goal vertices must exist in the graph.');
        }

        
$path = [];
        
$currentVertex $start;

        
// Keep going until we reach the goal
        
$count 0;
        while (
$currentVertex !== $goal && $count $maxSteps) {
            
// Add current vertex to path
            
$path[] = [
                
'vertex' => $currentVertex,
                
'level' => $this->levels[$currentVertex],
                
'heuristic' => $this->heuristics[$currentVertex]
            ];

            
// Get all neighbors of current vertex
            
$neighbors $this->adjacencyList[$currentVertex];

            if (empty(
$neighbors)) {
                return 
null// Dead end
            
}

            
// Find neighbor with lowest heuristic value
            
$bestNeighbor null;
            
$bestHeuristic PHP_FLOAT_MAX;

            foreach (
$neighbors as $neighbor) {
                
$h $this->heuristics[$neighbor];
                if (
$h $bestHeuristic) {
                    
$bestHeuristic $h;
                    
$bestNeighbor $neighbor;
                }
            }

            
// If we can't find a better neighbor, we're stuck
            
if ($bestNeighbor === null) {
                return 
null;
            }

            
// Move to the best neighbor
            
$currentVertex $bestNeighbor;
            
$count++;
        }

        
// No path found within reasonable time
        
if ($count >= $maxSteps) {
            return 
null;
        }

        
// Add the goal vertex to complete the path
        
$path[] = [
            
'vertex' => $goal,
            
'level' => $this->levels[$goal],
            
'heuristic' => $this->heuristics[$goal]
        ];

        return 
$path;
    }

    public function 
aStarTreeSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
// Priority queue implemented as array with state information
        
$openSet = [[
            
'vertex' => $start,
            
'gScore' => 0.0,
            
'fScore' => $this->heuristics[$start],
            
'path' => [
                [
                    
'vertex' => $start,
                    
'level' => $this->levels[$start],
                    
'heuristic' => $this->heuristics[$start]
                ]
            ]
        ]];

        while (!empty(
$openSet)) {
            
// Find node in openSet with lowest fScore
            
$currentIndex 0;
            
$currentFScore $openSet[0]['fScore'];

            for (
$i 1$i count($openSet); $i++) {
                if (
$openSet[$i]['fScore'] < $currentFScore) {
                    
$currentIndex $i;
                    
$currentFScore $openSet[$i]['fScore'];
                }
            }

            
$current $openSet[$currentIndex];
            
$currentVertex $current['vertex'];

            
// Check if we've reached the goal
            
if ($currentVertex === $goal) {
                return 
$current['path'];
            }

            
// Remove current from openSet
            
array_splice($openSet$currentIndex1);

            
// Explore all neighbors
            
foreach ($this->adjacencyList[$currentVertex] as $neighbor) {
                
// Calculate g score for this path
                
$tentativeGScore $current['gScore'] + $this->getEdgeCost($currentVertex$neighbor);

                
// Calculate f score (g score + heuristic)
                
$fScore $tentativeGScore $this->heuristics[$neighbor];

                
// Create new path
                
$newPath $current['path'];
                
$newPath[] = [
                    
'vertex' => $neighbor,
                    
'level' => $this->levels[$neighbor],
                    
'heuristic' => $this->heuristics[$neighbor]
                ];

                
// Add new state to openSet
                
$openSet[] = [
                    
'vertex' => $neighbor,
                    
'gScore' => $tentativeGScore,
                    
'fScore' => $fScore,
                    
'path' => $newPath
                
];
            }
        }

        return 
null// No path found
    
}

    public function 
aStarGroupSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
$openSet = [$start];
        
$cameFrom = [];
        
$gScore = [$start => 0.0];
        
$fScore = [$start => $this->heuristics[$start]];

        while (!empty(
$openSet)) {
            
// Find node in openSet with lowest fScore
            
$current $openSet[0];
            
$currentFScore $fScore[$current];
            foreach (
$openSet as $node) {
                if (
$fScore[$node] < $currentFScore) {
                    
$current $node;
                    
$currentFScore $fScore[$node];
                }
            }

            if (
$current === $goal) {
                
// Reconstruct path
                
$path = [$current];
                while (isset(
$cameFrom[$current])) {
                    
$current $cameFrom[$current];
                    
array_unshift($path$current);
                }
                return 
$path;
            }

            
// Remove current from openSet
            
$openSet array_values(array_diff($openSet, [$current]));

            
// Explore neighbors
            
foreach ($this->adjacencyList[$current] as $neighbor) {
                
$tentativeGScore $gScore[$current] + $this->getEdgeCost($current$neighbor);

                if (!isset(
$gScore[$neighbor]) || $tentativeGScore $gScore[$neighbor]) {
                    
$cameFrom[$neighbor] = $current;
                    
$gScore[$neighbor] = $tentativeGScore;
                    
$fScore[$neighbor] = $tentativeGScore $this->heuristics[$neighbor];

                    if (!
in_array($neighbor$openSet)) {
                        
$openSet[] = $neighbor;
                    }
                }
            }
        }

        return 
null// No path found
    
}

    public function 
beamSearch(string $startstring $goalint $beamWidth 2): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
// Initialize beam with start vertex
        
$beam = [[
            
'vertex' => $start,
            
'path' => [
                [
                    
'vertex' => $start,
                    
'level' => $this->levels[$start],
                    
'heuristic' => $this->heuristics[$start]
                ]
            ]
        ]];

        
$visited = [$start => true];

        while (!empty(
$beam)) {
            
$candidates = [];

            
// Generate all possible next states from current beam
            
foreach ($beam as $state) {
                
$currentVertex $state['vertex'];

                
// Check if we've reached the goal
                
if ($currentVertex === $goal) {
                    return 
$state['path'];
                }

                
// Get all neighbors of current vertex
                
foreach ($this->adjacencyList[$currentVertex] as $neighbor) {
                    
// Skip if we've already visited this vertex
                    
if (isset($visited[$neighbor])) {
                        continue;
                    }

                    
$newPath $state['path'];
                    
$newPath[] = [
                        
'vertex' => $neighbor,
                        
'level' => $this->levels[$neighbor],
                        
'heuristic' => $this->heuristics[$neighbor]
                    ];

                    
$candidates[] = [
                        
'vertex' => $neighbor,
                        
'path' => $newPath,
                        
'heuristic' => $this->heuristics[$neighbor]
                    ];

                    
$visited[$neighbor] = true;
                }
            }

            if (empty(
$candidates)) {
                return 
null// No path found
            
}

            
// Sort candidates by heuristic value (ascending)
            
usort($candidates, function($a$b) {
                return 
$a['heuristic'] <=> $b['heuristic'];
            });

            
// Keep only the k best candidates (beam width)
            
$beam array_slice($candidates0$beamWidth);
        }

        return 
null// No path found
    
}

    public function 
idaStarSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
// Initialize the bound as the heuristic value of the start node
        
$bound $this->heuristics[$start];

        
// Initial path contains only the start node
        
$path = [
            [
                
'vertex' => $start,
                
'level' => $this->levels[$start],
                
'heuristic' => $this->heuristics[$start]
            ]
        ];

        while (
true) {
            
// Search with current bound
            
$result $this->idaStarRecursive($path0$bound$goal);

            if (
is_array($result)) {
                
// Path found
                
return $result;
            }

            if (
$result === PHP_FLOAT_MAX) {
                
// No path exists
                
return null;
            }

            
// Update bound to the minimum f-value that exceeded current bound
            
$bound $result;
        }
    }

    private function 
idaStarRecursive(array $pathfloat $gfloat $boundstring $goal): array|float {
        
$current $path[count($path) - 1]['vertex'];
        
$f $g $this->heuristics[$current];

        
// If f exceeds bound, return f as the new minimum bound
        
if ($f $bound) {
            return 
$f;
        }

        
// If goal is reached, return the path
        
if ($current === $goal) {
            return 
$path;
        }

        
$min PHP_FLOAT_MAX;

        
// Explore all neighbors
        
foreach ($this->adjacencyList[$current] as $neighbor) {
            
// Check if neighbor is already in path (avoid cycles)
            
$inPath false;
            foreach (
$path as $node) {
                if (
$node['vertex'] === $neighbor) {
                    
$inPath true;
                    break;
                }
            }
            if (
$inPath) {
                continue;
            }

            
// Add neighbor to path
            
$path[] = [
                
'vertex' => $neighbor,
                
'level' => $this->levels[$neighbor],
                
'heuristic' => $this->heuristics[$neighbor]
            ];

            
// Recursively search from neighbor
            
$result $this->idaStarRecursive(
                
$path,
                
$g $this->getEdgeCost($current$neighbor),
                
$bound,
                
$goal
            
);

            
// Remove neighbor from path (backtrack)
            
array_pop($path);

            
// Process result
            
if (is_array($result)) {
                
// Path to goal found
                
return $result;
            }

            
// Update minimum bound if needed
            
if ($result $min) {
                
$min $result;
            }
        }

        return 
$min;
    }

    
/**
     * Simple Hill Climbing: Takes the first neighbor that improves the current state.
     * Faster but may miss better solutions.
     */
    
public function simpleHillClimbing(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
$currentVertex $start;
        
$path = [[
            
'vertex' => $start,
            
'level' => $this->levels[$start],
            
'heuristic' => $this->heuristics[$start]
        ]];

        
$visited = [$start => true];

        while (
$currentVertex !== $goal) {
            
$neighbors $this->adjacencyList[$currentVertex];
            if (empty(
$neighbors)) {
                return 
null// Dead end
            
}

            
// Find first better neighbor
            
$betterFound false;
            
$currentHeuristic $this->heuristics[$currentVertex];

            foreach (
$neighbors as $neighbor) {
                if (isset(
$visited[$neighbor])) {
                    continue;
                }

                if (
$this->heuristics[$neighbor] < $currentHeuristic) {
                    
$currentVertex $neighbor;
                    
$visited[$neighbor] = true;
                    
$path[] = [
                        
'vertex' => $neighbor,
                        
'level' => $this->levels[$neighbor],
                        
'heuristic' => $this->heuristics[$neighbor]
                    ];
                    
$betterFound true;
                    break;
                }
            }

            if (!
$betterFound) {
                return 
null// Local maximum reached
            
}
        }

        return 
$path;
    }

    
/**
     * Steepest Ascent Hill Climbing: Evaluates all neighbors and selects the one
     * that provides the maximum improvement. More thorough but slower.
     */
    
public function steepestAscentHillClimbing(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
$currentVertex $start;
        
$path = [[
            
'vertex' => $start,
            
'level' => $this->levels[$start],
            
'heuristic' => $this->heuristics[$start]
        ]];

        
$visited = [$start => true];

        while (
$currentVertex !== $goal) {
            
$neighbors $this->adjacencyList[$currentVertex];
            if (empty(
$neighbors)) {
                return 
null// Dead end
            
}

            
// Find the neighbor with maximum improvement
            
$bestNeighbor null;
            
$bestImprovement 0;
            
$currentHeuristic $this->heuristics[$currentVertex];

            foreach (
$neighbors as $neighbor) {
                if (isset(
$visited[$neighbor])) {
                    continue;
                }

                
$improvement $currentHeuristic $this->heuristics[$neighbor];
                if (
$improvement $bestImprovement) {
                    
$bestImprovement $improvement;
                    
$bestNeighbor $neighbor;
                }
            }

            if (
$bestNeighbor === null) {
                return 
null// Local maximum reached
            
}

            
$currentVertex $bestNeighbor;
            
$visited[$currentVertex] = true;
            
$path[] = [
                
'vertex' => $currentVertex,
                
'level' => $this->levels[$currentVertex],
                
'heuristic' => $this->heuristics[$currentVertex]
            ];
        }

        return 
$path;
    }

    
/**
     * Stochastic Hill Climbing: Randomly selects among the better neighbors,
     * with probability proportional to the amount of improvement.
     * Can escape some local maxima but may take longer to converge.
     */
    
public function stochasticHillClimbing(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        
$currentVertex $start;
        
$path = [[
            
'vertex' => $start,
            
'level' => $this->levels[$start],
            
'heuristic' => $this->heuristics[$start]
        ]];

        
$visited = [$start => true];
        
$maxAttempts 100// Prevent infinite loops
        
$attempts 0;

        while (
$currentVertex !== $goal && $attempts $maxAttempts) {
            
$neighbors $this->adjacencyList[$currentVertex];
            if (empty(
$neighbors)) {
                return 
null// Dead end
            
}

            
// Collect better neighbors and their improvements
            
$candidates = [];
            
$totalImprovement 0;
            
$currentHeuristic $this->heuristics[$currentVertex];

            foreach (
$neighbors as $neighbor) {
                if (isset(
$visited[$neighbor])) {
                    continue;
                }

                
$improvement $currentHeuristic $this->heuristics[$neighbor];
                if (
$improvement 0) {
                    
$candidates[] = [
                        
'vertex' => $neighbor,
                        
'improvement' => $improvement
                    
];
                    
$totalImprovement += $improvement;
                }
            }

            if (empty(
$candidates)) {
                return 
null// Local maximum reached
            
}

            
// Randomly select a neighbor, weighted by improvement
            
$random mt_rand() / mt_getrandmax() * $totalImprovement;
            
$sum 0;
            
$selectedNeighbor null;

            foreach (
$candidates as $candidate) {
                
$sum += $candidate['improvement'];
                if (
$sum >= $random) {
                    
$selectedNeighbor $candidate['vertex'];
                    break;
                }
            }

            if (
$selectedNeighbor === null) {
                
$selectedNeighbor $candidates[array_key_last($candidates)]['vertex'];
            }

            
$currentVertex $selectedNeighbor;
            
$visited[$currentVertex] = true;
            
$path[] = [
                
'vertex' => $currentVertex,
                
'level' => $this->levels[$currentVertex],
                
'heuristic' => $this->heuristics[$currentVertex]
            ];

            
$attempts++;
        }

        return 
$attempts $maxAttempts $path null;
    }

    
/*
     | ----------------------------------------------------------------------
     | DEBUG methods
     | ----------------------------------------------------------------------
     */

    /**
     * Debug output for Beam Search showing the beam selection process
     * and path finding decisions
     */
    
public function debugBeamSearch(string $startstring $goalint $beamWidth 2): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== Beam Search Debug (Beam Width: {$beamWidth}) ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
// Initialize beam with start vertex
        
$beam = [[
            
'vertex' => $start,
            
'path' => [
                [
                    
'vertex' => $start,
                    
'level' => $this->levels[$start],
                    
'heuristic' => $this->heuristics[$start]
                ]
            ]
        ]];

        
$visited = [$start => true];
        
$iteration 0;

        while (!empty(
$beam)) {
            
$iteration++;
            echo 
"\n=== Iteration {$iteration} ===\n";
            echo 
"Current beam states:\n";
            foreach (
$beam as $index => $state) {
                echo 
sprintf("  %d. %s (h=%.2f)\n",
                    
$index 1,
                    
$state['vertex'],
                    
$this->heuristics[$state['vertex']]
                );
            }

            
$candidates = [];

            
// Generate all possible next states from current beam
            
foreach ($beam as $state) {
                
$currentVertex $state['vertex'];
                echo 
"\nExpanding vertex: {$currentVertex}\n";

                
// Check if we've reached the goal
                
if ($currentVertex === $goal) {
                    echo 
"\nGoal state reached!\n";
                    echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($state['path'], 'vertex'))) . "\n\n";
                    return 
$state['path'];
                }

                
// Get all neighbors of current vertex
                
echo "Evaluating neighbors:\n";
                foreach (
$this->adjacencyList[$currentVertex] as $neighbor) {
                    if (isset(
$visited[$neighbor])) {
                        echo 
"  {$neighbor}: Skipped (already visited)\n";
                        continue;
                    }

                    
$newPath $state['path'];
                    
$newPath[] = [
                        
'vertex' => $neighbor,
                        
'level' => $this->levels[$neighbor],
                        
'heuristic' => $this->heuristics[$neighbor]
                    ];

                    
$candidates[] = [
                        
'vertex' => $neighbor,
                        
'path' => $newPath,
                        
'heuristic' => $this->heuristics[$neighbor]
                    ];

                    echo 
sprintf("  %s: Added as candidate (h=%.2f)\n",
                        
$neighbor,
                        
$this->heuristics[$neighbor]
                    );

                    
$visited[$neighbor] = true;
                }
            }

            if (empty(
$candidates)) {
                echo 
"\nNo candidates available - search failed\n";
                return 
null;
            }

            
// Sort candidates by heuristic value (ascending)
            
usort($candidates, function($a$b) {
                return 
$a['heuristic'] <=> $b['heuristic'];
            });

            echo 
"\nCandidate ranking:\n";
            foreach (
$candidates as $index => $candidate) {
                echo 
sprintf("  %d. %s (h=%.2f)%s\n",
                    
$index 1,
                    
$candidate['vertex'],
                    
$candidate['heuristic'],
                    
$index $beamWidth " ← Selected for beam" ""
                
);
            }

            
// Keep only the k best candidates (beam width)
            
$beam array_slice($candidates0$beamWidth);
            echo 
"\nNew beam width: " count($beam) . " states\n";
        }

        echo 
"\nSearch completed without finding goal\n";
        return 
null;
    }

    
/**
     * Debug output for A* Tree Search showing the search process
     * and decision making at each step
     */
    
public function debugAStarTreeSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== A* Tree Search Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
// Priority queue implemented as array with state information
        
$openSet = [[
            
'vertex' => $start,
            
'gScore' => 0.0,
            
'fScore' => $this->heuristics[$start],
            
'path' => [
                [
                    
'vertex' => $start,
                    
'level' => $this->levels[$start],
                    
'heuristic' => $this->heuristics[$start]
                ]
            ]
        ]];

        
$iteration 0;
        
$totalCost 0;

        while (!empty(
$openSet)) {
            
$iteration++;
            echo 
"\n=== Iteration {$iteration} ===\n";

            
// Find node in openSet with lowest fScore
            
$currentIndex 0;
            
$currentFScore $openSet[0]['fScore'];

            echo 
"Current open set states:\n";
            foreach (
$openSet as $index => $state) {
                echo 
sprintf("  %d. %s (g=%.2f, f=%.2f)\n",
                    
$index 1,
                    
$state['vertex'],
                    
$state['gScore'],
                    
$state['fScore']
                );
                if (
$state['fScore'] < $currentFScore) {
                    
$currentIndex $index;
                    
$currentFScore $state['fScore'];
                }
            }

            
$current $openSet[$currentIndex];
            
$currentVertex $current['vertex'];

            echo 
"\nSelected node {$currentVertex} with lowest f-score: {$currentFScore}\n";
            echo 
"Current path: " implode(" -> "array_column($current['path'], 'vertex')) . "\n";

            
// Check if we've reached the goal
            
if ($currentVertex === $goal) {
                echo 
"\nGoal reached!\n";
                echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($current['path'], 'vertex'))) . "\n";
                echo 
"Total cost: {$current['gScore']}\n\n";
                return 
$current['path'];
            }

            
// Remove current from openSet
            
array_splice($openSet$currentIndex1);
            echo 
"\nExploring neighbors of {$currentVertex}:\n";

            
// Explore all neighbors
            
foreach ($this->adjacencyList[$currentVertex] as $neighbor) {
                
// Calculate g score for this path
                
$tentativeGScore $current['gScore'] + $this->getEdgeCost($currentVertex$neighbor);

                
// Calculate f score (g score + heuristic)
                
$fScore $tentativeGScore $this->heuristics[$neighbor];

                echo 
sprintf("  %s: g=%.2f, h=%.2f, f=%.2f\n",
                    
$neighbor,
                    
$tentativeGScore,
                    
$this->heuristics[$neighbor],
                    
$fScore
                
);

                
// Create new path
                
$newPath $current['path'];
                
$newPath[] = [
                    
'vertex' => $neighbor,
                    
'level' => $this->levels[$neighbor],
                    
'heuristic' => $this->heuristics[$neighbor]
                ];

                
// Add new state to openSet
                
$openSet[] = [
                    
'vertex' => $neighbor,
                    
'gScore' => $tentativeGScore,
                    
'fScore' => $fScore,
                    
'path' => $newPath
                
];

                echo 
"  → Added to open set\n";
            }
        }

        echo 
"\nNo path found to goal\n";
        return 
null;
    }

    
/**
     * Debug output for A* Group Search showing the search process
     * and decision making at each step
     */
    
public function debugAStarGroupSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== A* Group Search Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
$openSet = [$start];
        
$cameFrom = [];
        
$gScore = [$start => 0.0];
        
$fScore = [$start => $this->heuristics[$start]];
        
$iteration 0;

        echo 
"\nInitial state:";
        echo 
"\nVertex: {$start}";
        echo 
"\nLevel: {$this->levels[$start]}";
        echo 
"\nHeuristic: {$this->heuristics[$start]}\n";

        while (!empty(
$openSet)) {
            
$iteration++;
            echo 
"\n=== Iteration {$iteration} ===\n";

            
// Find node in openSet with lowest fScore
            
$current $openSet[0];
            
$currentFScore $fScore[$current];

            echo 
"Current open set:\n";
            foreach (
$openSet as $vertex) {
                echo 
sprintf("  %s (g=%.2f, f=%.2f)\n",
                    
$vertex,
                    
$gScore[$vertex],
                    
$fScore[$vertex]
                );
                if (
$fScore[$vertex] < $currentFScore) {
                    
$current $vertex;
                    
$currentFScore $fScore[$vertex];
                }
            }

            echo 
"\nSelected vertex {$current} with lowest f-score: {$currentFScore}\n";

            if (
$current === $goal) {
                
// Reconstruct path
                
$path = [$current];
                
$totalCost $gScore[$current];

                while (isset(
$cameFrom[$current])) {
                    
$current $cameFrom[$current];
                    
array_unshift($path$current);
                }

                echo 
"\nGoal reached!\n";
                echo 
"Final path: \n" implode(" -> "$this->getSequenceNames($path)) . "\n";
                echo 
"Total cost: {$totalCost}\n\n";
                return 
$path;
            }

            
// Remove current from openSet
            
$openSet array_values(array_diff($openSet, [$current]));
            echo 
"\nExploring neighbors of {$current}:\n";

            
// Explore neighbors
            
foreach ($this->adjacencyList[$current] as $neighbor) {
                
$tentativeGScore $gScore[$current] + $this->getEdgeCost($current$neighbor);
                
$neighborFScore $tentativeGScore $this->heuristics[$neighbor];

                echo 
sprintf("  %s: g=%.2f, h=%.2f, f=%.2f",
                    
$neighbor,
                    
$tentativeGScore,
                    
$this->heuristics[$neighbor],
                    
$neighborFScore
                
);

                if (!isset(
$gScore[$neighbor]) || $tentativeGScore $gScore[$neighbor]) {
                    echo 
" → Better path found!\n";
                    
$cameFrom[$neighbor] = $current;
                    
$gScore[$neighbor] = $tentativeGScore;
                    
$fScore[$neighbor] = $neighborFScore;

                    if (!
in_array($neighbor$openSet)) {
                        
$openSet[] = $neighbor;
                        echo 
"    Added to open set\n";
                    }
                } else {
                    echo 
" → Not better than existing path\n";
                }
            }
        }

        echo 
"\nNo path found to goal\n";
        return 
null;
    }

    
/**
     * Debug output for IDA* Search showing iterative deepening process
     * and the search decisions at each step
     */
    
public function debugIdaStarSearch(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== IDA* Search Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
// Initialize the bound as the heuristic value of the start node
        
$bound $this->heuristics[$start];
        
$iteration 0;

        echo 
"\nInitial state:";
        echo 
"\nVertex: {$start}";
        echo 
"\nLevel: {$this->levels[$start]}";
        echo 
"\nHeuristic: {$this->heuristics[$start]}";
        echo 
"\nInitial bound: {$bound}\n";

        
// Initial path contains only the start node
        
$path = [
            [
                
'vertex' => $start,
                
'level' => $this->levels[$start],
                
'heuristic' => $this->heuristics[$start]
            ]
        ];

        while (
true) {
            
$iteration++;
            echo 
"\n=== Iteration {$iteration} ===\n";
            echo 
"Current bound: {$bound}\n";

            
// Search with current bound
            
$result $this->debugIdaStarRecursive($path0$bound$goal1);

            if (
is_array($result)) {
                echo 
"\nGoal reached!\n";
                echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($result'vertex'))) . "\n\n";
                return 
$result;
            }

            if (
$result === PHP_FLOAT_MAX) {
                echo 
"\nNo path exists to goal\n";
                return 
null;
            }

            echo 
"\nMinimum f-value exceeding current bound: {$result}\n";
            echo 
"Increasing bound for next iteration\n";

            
// Update bound to the minimum f-value that exceeded current bound
            
$bound $result;
        }
    }

    
/**
     * Debug recursive helper function for IDA* search
     */
    
private function debugIdaStarRecursive(array $pathfloat $gfloat $boundstring $goalint $depth): array|float {
        
$current $path[count($path) - 1]['vertex'];
        
$f $g $this->heuristics[$current];

        
$indent str_repeat("  "$depth);
        echo 
"{$indent}Examining vertex: {$current}\n";
        echo 
"{$indent}g-value: {$g}, h-value: {$this->heuristics[$current]}, f-value: {$f}\n";
        echo 
"{$indent}Current path: " implode(" -> "array_column($path'vertex')) . "\n";

        
// If f exceeds bound, return f as the new minimum bound
        
if ($f $bound) {
            echo 
"{$indent}f-value {$f} exceeds bound {$bound}, backtracking\n";
            return 
$f;
        }

        
// If goal is reached, return the path
        
if ($current === $goal) {
            echo 
"{$indent}Goal reached!\n";
            return 
$path;
        }

        
$min PHP_FLOAT_MAX;
        echo 
"{$indent}Exploring neighbors:\n";

        
// Explore all neighbors
        
foreach ($this->adjacencyList[$current] as $neighbor) {
            
// Check if neighbor is already in path (avoid cycles)
            
$inPath false;
            foreach (
$path as $node) {
                if (
$node['vertex'] === $neighbor) {
                    
$inPath true;
                    break;
                }
            }

            if (
$inPath) {
                echo 
"{$indent}  {$neighbor}: Skipped (already in path)\n";
                continue;
            }

            echo 
"{$indent}  {$neighbor}:\n";
            echo 
"{$indent}    Edge cost: {$this->getEdgeCost($current$neighbor)}\n";
            echo 
"{$indent}    Heuristic: {$this->heuristics[$neighbor]}\n";

            
// Add neighbor to path
            
$path[] = [
                
'vertex' => $neighbor,
                
'level' => $this->levels[$neighbor],
                
'heuristic' => $this->heuristics[$neighbor]
            ];

            
// Recursively search from neighbor
            
$result $this->debugIdaStarRecursive(
                
$path,
                
$g $this->getEdgeCost($current$neighbor),
                
$bound,
                
$goal,
                
$depth 1
            
);

            
// Remove neighbor from path (backtrack)
            
array_pop($path);

            
// Process result
            
if (is_array($result)) {
                
// Path to goal found
                
return $result;
            }

            
// Update minimum bound if needed
            
if ($result $min) {
                
$min $result;
                echo 
"{$indent}  New minimum f-value found: {$min}\n";
            }
        }

        if (
$min === PHP_FLOAT_MAX) {
            echo 
"{$indent}No valid neighbors found, backtracking\n";
        } else {
            echo 
"{$indent}All neighbors explored, minimum f-value: {$min}\n";
        }

        return 
$min;
    }

    
/**
     * Debug the Simple Hill Climbing algorithm showing decision process
     */
    
public function debugSimpleHillClimbing(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== Simple Hill Climbing Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
$currentVertex $start;
        
$path = [[
            
'vertex' => $start,
            
'level' => $this->levels[$start],
            
'heuristic' => $this->heuristics[$start]
        ]];
        
$visited = [$start => true];
        
$totalCost 0;

        echo 
"\nInitial state:";
        echo 
"\nVertex: {$start}";
        echo 
"\nLevel: {$this->levels[$start]}";
        echo 
"\nHeuristic: {$this->heuristics[$start]}\n";

        while (
$currentVertex !== $goal) {
            echo 
"\n----------------------------\n";
            echo 
"Current path: " implode(" -> "array_column($path'vertex'));
            echo 
"\nAt vertex: {$currentVertex} (h={$this->heuristics[$currentVertex]})\n";

            
$neighbors $this->adjacencyList[$currentVertex];
            if (empty(
$neighbors)) {
                echo 
"Dead end reached - no neighbors available\n";
                return 
null;
            }

            
$betterFound false;
            
$currentHeuristic $this->heuristics[$currentVertex];

            foreach (
$neighbors as $neighbor) {
                if (isset(
$visited[$neighbor])) {
                    echo 
"  Skipping {$neighbor}: already visited\n";
                    continue;
                }

                
$neighborHeuristic $this->heuristics[$neighbor];
                
$edgeCost $this->getEdgeCost($currentVertex$neighbor);
                echo 
"  Evaluating {$neighbor}:";
                echo 
"\n    Heuristic: {$neighborHeuristic}";
                echo 
"\n    Edge cost: {$edgeCost}";
                echo 
"\n    Current best: {$currentHeuristic}\n";

                if (
$neighborHeuristic $currentHeuristic) {
                    echo 
"  → Better neighbor found: {$neighbor}\n";
                    
$totalCost += $edgeCost;
                    
$currentVertex $neighbor;
                    
$visited[$neighbor] = true;
                    
$path[] = [
                        
'vertex' => $neighbor,
                        
'level' => $this->levels[$neighbor],
                        
'heuristic' => $neighborHeuristic
                    
];
                    
$betterFound true;
                    break;
                } else {
                    echo 
"  → Not better than current state\n";
                }
            }

            if (!
$betterFound) {
                echo 
"\nLocal maximum reached - no improving neighbors found\n";
                echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($path'vertex'))) . "\n";
                echo 
"Total cost: {$totalCost}\n\n";
                return 
null;
            }
        }

        echo 
"\nGoal reached!\n";
        echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($path'vertex'))) . "\n";
        echo 
"Total cost: {$totalCost}\n\n";
        return 
$path;
    }

    
/**
     * Debug the Steepest Ascent Hill Climbing algorithm showing evaluation process
     */
    
public function debugSteepestAscentHillClimbing(string $startstring $goal): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException("Both start and goal vertices must exist in the graph.");
        }

        echo 
"\n=== Steepest Ascent Hill Climbing Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
$currentVertex $start;
        
$path = [[
            
'vertex' => $start,
            
'level' => $this->levels[$start],
            
'heuristic' => $this->heuristics[$start]
        ]];
        
$visited = [$start => true];
        
$totalCost 0;

        echo 
"\nInitial state:";
        echo 
"\nVertex: {$start}";
        echo 
"\nLevel: {$this->levels[$start]}";
        echo 
"\nHeuristic: {$this->heuristics[$start]}\n";

        while (
$currentVertex !== $goal) {
            echo 
"\n----------------------------\n";
            echo 
"Current path: " implode(" -> "array_column($path'vertex'));
            echo 
"\nAt vertex: {$currentVertex} (h={$this->heuristics[$currentVertex]})\n";

            
$neighbors $this->adjacencyList[$currentVertex];
            if (empty(
$neighbors)) {
                echo 
"Dead end reached - no neighbors available\n";
                return 
null;
            }

            
$bestNeighbor null;
            
$bestImprovement 0;
            
$bestCost 0;
            
$currentHeuristic $this->heuristics[$currentVertex];

            echo 
"\nEvaluating all neighbors:\n";
            foreach (
$neighbors as $neighbor) {
                if (isset(
$visited[$neighbor])) {
                    echo 
"  Skipping {$neighbor}: already visited\n";
                    continue;
                }

                
$neighborHeuristic $this->heuristics[$neighbor];
                
$edgeCost $this->getEdgeCost($currentVertex$neighbor);
                
$improvement $currentHeuristic $neighborHeuristic;

                echo 
"  {$neighbor}:";
                echo 
"\n    Heuristic: {$neighborHeuristic}";
                echo 
"\n    Edge cost: {$edgeCost}";
                echo 
"\n    Improvement: {$improvement}\n";

                if (
$improvement $bestImprovement) {
                    
$bestImprovement $improvement;
                    
$bestNeighbor $neighbor;
                    
$bestCost $edgeCost;
                    echo 
"  → New best neighbor!\n";
                }
            }

            if (
$bestNeighbor === null) {
                echo 
"\nLocal maximum reached - no improving neighbors found\n";
                echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($path'vertex'))) . "\n";
                echo 
"Total cost: {$totalCost}\n\n";
                return 
null;
            }

            echo 
"\nSelected best neighbor: {$bestNeighbor}";
            echo 
"\nImprovement: {$bestImprovement}";
            echo 
"\nEdge cost: {$bestCost}\n";

            
$totalCost += $bestCost;
            
$currentVertex $bestNeighbor;
            
$visited[$currentVertex] = true;
            
$path[] = [
                
'vertex' => $currentVertex,
                
'level' => $this->levels[$currentVertex],
                
'heuristic' => $this->heuristics[$currentVertex]
            ];
        }

        echo 
"\nGoal reached!\n";
        echo 
"Final path: \n" implode(" -> "$this->getSequenceNames(array_column($path'vertex'))) . "\n";
        echo 
"Total cost: {$totalCost}\n\n";
        return 
$path;
    }

    
/**
     * Debug output for Stochastic Hill Climbing showing the probabilistic decisions
     * made during the search process
     */
    
public function debugStochasticHillClimbing(?array $searchResultstring $startstring $goal): ?array {
        
$start $this->vertexLabels[$start] ?? $start;
        
$goal $this->vertexLabels[$goal] ?? $goal;

        if (
$searchResult === null) {
            echo 
"\n=== Stochastic Hill Climbing Debug ===\n";
            echo 
"No path found from {$start} to {$goal}!\n";
            return 
null;
        }

        echo 
"\n=== Stochastic Hill Climbing Debug ===\n";
        echo 
"Path found from {$start} to {$goal}\n";

        
$totalCost 0;
        
$pathLength count($searchResult);

        echo 
"\nPath sequence analysis:\n";
        for (
$i 0$i $pathLength$i++) {
            
$currentNode $searchResult[$i];
            
$currentVertex $currentNode['vertex'];

            echo 
"\nStep {$i}:";
            echo 
"\nCurrent vertex: {$currentVertex}";
            echo 
"\nLevel: {$currentNode['level']}";
            echo 
"\nHeuristic: {$currentNode['heuristic']}\n";

            
// If not the last node, analyze the transition to the next node
            
if ($i $pathLength 1) {
                
$nextNode $searchResult[$i 1];
                
$nextVertex $nextNode['vertex'];

                
// Get all neighbors of current vertex
                
$neighbors $this->adjacencyList[$currentVertex];

                
// Calculate improvements and probabilities for all valid neighbors
                
$candidates = [];
                
$totalImprovement 0;

                echo 
"\nNeighbor analysis:";
                foreach (
$neighbors as $neighbor) {
                    
$neighborHeuristic $this->heuristics[$neighbor];
                    
$improvement $this->heuristics[$currentVertex] - $neighborHeuristic;

                    if (
$improvement 0) {
                        
$candidates[$neighbor] = $improvement;
                        
$totalImprovement += $improvement;
                    }

                    echo 
sprintf("\n  %s:"$neighbor);
                    echo 
sprintf("\n    Heuristic: %.2f"$neighborHeuristic);
                    echo 
sprintf("\n    Improvement: %.2f"$improvement);
                    if (
$improvement 0) {
                        echo 
"\n    → Eligible for selection";
                    } else {
                        echo 
"\n    → Not eligible (no improvement)";
                    }
                }

                
// Show selection probabilities
                
if (!empty($candidates)) {
                    echo 
"\n\nSelection probabilities:";
                    foreach (
$candidates as $neighbor => $improvement) {
                        
$probability = ($improvement $totalImprovement) * 100;
                        echo 
sprintf("\n  %s: %.2f%%"$neighbor$probability);
                        if (
$neighbor === $nextVertex) {
                            echo 
" ← Selected";
                        }
                    }
                }

                
// Show transition details
                
$edgeCost $this->getEdgeCost($currentVertex$nextVertex);
                
$totalCost += $edgeCost;

                echo 
sprintf("\n\nTransition: %s -> %s"$currentVertex$nextVertex);
                echo 
sprintf("\nEdge cost: %.2f"$edgeCost);
                echo 
sprintf("\nRunning total cost: %.2f"$totalCost);
            }

            echo 
"\n----------------------------";
        }

        echo 
sprintf("\n\nFinal path: \n%s"implode(" -> "$this->getSequenceNames(array_column($searchResult'vertex'))));
        echo 
sprintf("\nTotal path cost: %.2f\n\n"$totalCost);

        return 
$searchResult;
    }

    
/**
     * Debug output for Greedy Search showing decision process
     * and neighbor evaluations at each step
     */
    
public function debugGreedySearch(string $startstring $goal$maxSteps 100): ?array {
        if (!isset(
$this->adjacencyList[$start]) || !isset($this->adjacencyList[$goal])) {
            throw new 
InvalidArgumentException('Both start and goal vertices must exist in the graph.');
        }

        echo 
"\n=== Greedy Search Debug ===\n";
        echo 
"Starting from {$start} to reach {$goal}\n";

        
$path = [];
        
$currentVertex $start;
        
$totalCost 0;
        
$iteration 0;

        echo 
"\nInitial state:";
        echo 
"\nVertex: {$start}";
        echo 
"\nLevel: {$this->levels[$start]}";
        echo 
"\nHeuristic: {$this->heuristics[$start]}\n";

        
// Keep going until we reach the goal
        
$count 0;
        while (
$currentVertex !== $goal && $count $maxSteps) {
            
$iteration++;
            echo 
"\n=== Iteration {$iteration} ===\n";

            
// Add current vertex to path
            
$path[] = [
                
'vertex' => $currentVertex,
                
'level' => $this->levels[$currentVertex],
                
'heuristic' => $this->heuristics[$currentVertex]
            ];

            echo 
"Current path: " implode(" -> "array_column($path'vertex')) . "\n";
            echo 
"At vertex: {$currentVertex} (h={$this->heuristics[$currentVertex]})\n";

            
// Get all neighbors of current vertex
            
$neighbors $this->adjacencyList[$currentVertex];

            if (empty(
$neighbors)) {
                echo 
"Dead end reached - no neighbors available\n";
                return 
null;
            }

            echo 
"\nEvaluating neighbors:\n";

            
// Find neighbor with lowest heuristic value
            
$bestNeighbor null;
            
$bestHeuristic PHP_FLOAT_MAX;
            
$bestEdgeCost 0;

            foreach (
$neighbors as $neighbor) {
                
$h $this->heuristics[$neighbor];
                
$edgeCost $this->getEdgeCost($currentVertex$neighbor);

                echo 
sprintf("  %s:\n"$neighbor);
                echo 
sprintf("    Heuristic: %.2f\n"$h);
                echo 
sprintf("    Edge cost: %.2f\n"$edgeCost);

                if (
$h $bestHeuristic) {
                    
$bestHeuristic $h;
                    
$bestNeighbor $neighbor;
                    
$bestEdgeCost $edgeCost;
                    echo 
"    → New best neighbor!\n";
                } else {
                    echo 
"    → Not better than current best\n";
                }
            }

            
// If we can't find a better neighbor, we're stuck
            
if ($bestNeighbor === null) {
                echo 
"\nNo improving neighbor found - search failed\n";
                return 
null;
            }

            echo 
"\nSelected best neighbor: {$bestNeighbor}";
            echo 
"\nHeuristic value: {$bestHeuristic}";
            echo 
"\nEdge cost: {$bestEdgeCost}\n";

            
// Move to the best neighbor
            
$totalCost += $bestEdgeCost;
            
$currentVertex $bestNeighbor;
            
$count++;
        }

        
// No path found within reasonable time
        
if ($count >= $maxSteps) {
            return 
null;
        }

        
// Add the goal vertex to complete the path
        
$path[] = [
            
'vertex' => $goal,
            
'level' => $this->levels[$goal],
            
'heuristic' => $this->heuristics[$goal]
        ];

        echo 
"\nGoal reached!\n";
        echo 
"Final path: " implode(" -> "array_column($path'vertex')) . "\n";
        echo 
"Total cost: {$totalCost}\n";

        return 
$path;
    }

    public function 
searchAnalysis($searchResultbool $showCost true): void {
        if (
$searchResult === null) {
            echo 
"No path found!\n";
            return;
        }

        
$totalCost 0;

        echo 
"Path sequence:\n";
        
$pathSequenceNames array_map(function ($node) {
            
$vertex $node['vertex'] ?? $node;
            return 
$this->vertexLabels[$vertex] ?? $vertex;
        }, 
$searchResult);
        echo 
" -> " implode("\n -> "$pathSequenceNames) . "\n";

        echo 
"\nPath analysis:\n";
        
$lastIndex 0;
        
$lastVertex null;
        
$pathSequence array_map(function ($node) {
            
$vertex $node['vertex'] ?? $node;
            return 
$node['vertex'] ?? $node;
        }, 
$searchResult);

        foreach (
$pathSequence as $index => $vertex) {
            if (
$index 0) {
                
$prevVertex $pathSequence[$index 1];
                
$cost $this->getEdgeCost($prevVertex$vertex);
                
$totalCost += $cost;
                echo 
sprintf("Step %d: %s (level %d, h=%.1f) -> %s (level %d, h=%.1f): cost: %.1f\n",
                    
$index,
                    
$prevVertex,
                    
$this->levels[$prevVertex],
                    
$this->heuristics[$prevVertex],
                    
$vertex,
                    
$this->levels[$vertex],
                    
$this->heuristics[$vertex],
                    
$cost,
                
//$searchResult[$index]['heuristic']
                
);
            }
            
$lastIndex++;
            
$lastVertex $vertex;
        }

        echo 
sprintf("Step %d: %s (level %d, h=%.1f)\n",
            
$lastIndex,
            
$lastVertex,
            
$this->levels[$vertex],
            
$this->heuristics[$vertex]
        );

        if (
$showCost) {
            echo 
sprintf("\nTotal path cost: %.1f\n"$totalCost);
        }
    }

    public function 
getAdjacencyList(): array {
        return 
$this->adjacencyList;
    }

    public function 
printPath(array $pathbool $showCost true): void {
        
$totalCost 0;
        
$previousVertex null;

        foreach (
$path as $step) {
            
// Get current vertex name depending on path format
            
$currentVertex is_array($step) ? $step['vertex'] : $step;

            if (
$showCost) {
                if (
$previousVertex !== null) {
                    
$edgeCost $this->getEdgeCost($previousVertex$currentVertex);
                    
$totalCost += $edgeCost;
                    echo 
sprintf("Edge cost from %s to %s: %.1f\n",
                        
$previousVertex,
                        
$currentVertex,
                        
$edgeCost
                    
);
                }
            }

            echo 
sprintf("Node: %s (Level %d, h=%.1f)\n",
                
$currentVertex,
                
$this->levels[$currentVertex],
                
$this->heuristics[$currentVertex]
            );

            
$previousVertex $currentVertex;
        }

        if (
$showCost) {
            echo 
sprintf("\nTotal path cost: %.1f\n"$totalCost);
        }
    }

    public function 
printGraph(): void {
        foreach (
$this->adjacencyList as $vertex => $neighbors) {
            
$costs array_map(function($neighbor) use ($vertex) {
                return 
sprintf("%s(%.1f)"$neighbor$this->edgeCosts[$vertex][$neighbor]);
            }, 
$neighbors);

            echo 
sprintf("%s (Level %d, h=%.1f) -> %s\n",
                
$this->vertexLabels[$vertex] ?? $vertex,
                
$this->levels[$vertex],
                
$this->heuristics[$vertex],
                
implode(', '$costs)
            );
        }
    }

    
/**
     * @param array $path
     * @return array
     */
    
protected function getSequenceNames(array $path): array {
        
$pathSequenceNames array_map(function ($node) {
            
$vertex $node['vertex'] ?? $node;
            return 
$this->vertexLabels[$vertex] ?? $vertex;
        }, 
$path);

        return 
$pathSequenceNames;
    }
}