// Parse reactions recipes
$recipes = $input->lines->mapAssoc(function ($i, $line) {
[$res, $comp] = explode(" => ", $line);
[$qt, $mat] = explode(" ", $comp);
$res = set(explode(", ", $res))->mapAssoc(function ($i, $c) {
$c = explode(" ", $c);
return [$c[1] => (int) $c[0]];
});
return [$mat => [(int) $qt, $res]];
});
// Decompose a reciepe to have the number of ores and all the leftovers materials
function decompose($recipe, $recipes) {
$leftovers = [];
while (true) {
$decomp = [];
foreach ($recipe as $comp=>$qty) {
if ($comp == "ORE") {
$decomp["ORE"] ??= 0;
$decomp["ORE"] += $qty;
continue;
}
// Take from leftovers first
if (!empty($leftovers[$comp])) {
$available = min($leftovers[$comp], $qty);
$qty -= $available;
$leftovers[$comp] -= $available;
}
if (!$qty) continue;
// Decompose item
$times = ceil($qty / $recipes[$comp][0]);
$build = $recipes[$comp][1]->map(fn ($r) => $r * $times);
// Add build surpulus to leftovers
$leftovers[$comp] ??= 0;
$leftovers[$comp] += $recipes[$comp][0] * $times - $qty;
// Replace item with its composition
foreach ($build as $bc=>$bq) {
$decomp[$bc] ??= 0;
$decomp[$bc] += $bq;
}
}
if (count(array_keys($decomp)) == 1) return [$decomp["ORE"], $leftovers];
$recipe = $decomp;
}
}
// ==================================================
// > PART 1 : Decompose FUEL recipe
// ==================================================
[$solution_1, $_] = decompose($recipes["FUEL"][1], $recipes);
// ==================================================
// > PART 2 : Binary search the maximum number of fuel that decompose to the closest of 1_000_000_000_000 ORES
// ==================================================
$lb = floor(1_000_000_000_000 / $solution_1);
$ub = $lb * 2;
$solution_2 = binary_search($lb, $ub, function ($value) use ($recipes, $solution_1) {
$ores = decompose(["FUEL" => $value], $recipes)[0];
if (1_000_000_000_000 - $solution_1 < $ores && $ores < 1_000_000_000_000) {
return 0;
}
return 1_000_000_000_000 <=> $ores;
});