Informed (Heuristic) Search

Iterative Deepening A*

The Iterative Deepening A* Algorithm (IDA*) is a heuristic search method that combines the memory efficiency of Depth-First Search (DFS) with the optimal path-finding capabilities of the A* algorithm. It is specifically designed to handle large search spaces while maintaining optimality and completeness. By limiting memory usage, IDA* enables effective exploration of complex networks or trees to find the shortest path from the start state to the goal state.

Example of class InformedSearchGraph (with Iterative Deepening A* 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;
    }
}