math-tasks/tasks/collecting-asteroids/scripts/verify.mjs

121 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Asteroid partition solver.
* Verifies that disjoint subsets of asteroid values can sum to each ship capacity.
*
* Usage:
* node verify-asteroids.mjs --ships 10,10,14 --asteroids 3,2,1,4,5,2,3,6,5,4,1,6
*
* Output: prints whether a valid partition exists and one example solution.
*/
const args = process.argv.slice(2);
function parseArg(flag) {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return null;
return args[idx + 1].split(',').map(Number);
}
const ships = parseArg('--ships');
const asteroids = parseArg('--asteroids');
if (!ships || !asteroids) {
console.log('Usage: node verify-asteroids.mjs --ships 10,10,14 --asteroids 3,2,1,4,5,2,3,6,5,4,1,6');
process.exit(1);
}
console.log(`Ships: [${ships.join(', ')}] (sum=${ships.reduce((a, b) => a + b, 0)})`);
console.log(`Asteroids: [${asteroids.join(', ')}] (count=${asteroids.length}, sum=${asteroids.reduce((a, b) => a + b, 0)})`);
/**
* Find disjoint subsets of `available` (by index) that sum to each capacity in `capacities`.
* Returns array of index arrays, or null if no solution.
*/
function findPartition(available, capacities, usedSet = new Set()) {
if (capacities.length === 0) return [];
const target = capacities[0];
const remaining = capacities.slice(1);
const remainingSum = remaining.reduce((a, b) => a + b, 0);
// Available sum check
let availSum = 0;
for (let i = 0; i < available.length; i++) {
if (!usedSet.has(i)) availSum += available[i];
}
if (availSum < target + remainingSum) return null;
// Find all subsets summing to target using DFS
const subset = [];
function dfs(startIdx, currentSum) {
if (currentSum === target) {
// Try solving remaining capacities
const newUsed = new Set(usedSet);
for (const idx of subset) newUsed.add(idx);
const result = findPartition(available, remaining, newUsed);
if (result !== null) {
return [subset.slice(), ...result];
}
return null;
}
if (currentSum > target) return null;
for (let i = startIdx; i < available.length; i++) {
if (usedSet.has(i)) continue;
if (currentSum + available[i] > target) continue;
// Pruning: check if remaining available sum can reach target
let futureSum = 0;
for (let j = i; j < available.length; j++) {
if (!usedSet.has(j) && !subset.includes(j)) futureSum += available[j];
}
if (currentSum + futureSum < target) return null;
subset.push(i);
const result = dfs(i + 1, currentSum + available[i]);
if (result) return result;
subset.pop();
}
return null;
}
return dfs(0, 0);
}
// Sort ships descending for better pruning (hardest to fill first)
const sortedShips = [...ships].sort((a, b) => b - a);
console.log(`\nSolving for ships (sorted desc): [${sortedShips.join(', ')}]`);
const solution = findPartition(asteroids, sortedShips);
if (solution) {
console.log('\n✓ SOLVABLE! Solution found:');
// Map back to original ship order
const shipOrder = ships.map((cap, idx) => ({ cap, idx }))
.sort((a, b) => b.cap - a.cap);
for (let i = 0; i < sortedShips.length; i++) {
const indices = solution[i];
const values = indices.map(j => asteroids[j]);
const origShipIdx = shipOrder[i].idx;
console.log(` Ship ${origShipIdx + 1} (capacity ${sortedShips[i]}): asteroids [${values.join(' + ')}] = ${values.reduce((a, b) => a + b, 0)} (indices: ${indices.join(',')})`);
}
const usedIndices = new Set(solution.flat());
const unused = asteroids.map((v, i) => ({ v, i })).filter(x => !usedIndices.has(x.i));
if (unused.length > 0) {
console.log(` Unused asteroids: [${unused.map(x => `${x.v}(idx ${x.i})`).join(', ')}]`);
}
} else {
console.log('\n✗ NO SOLUTION found.');
console.log(` Ship sum: ${ships.reduce((a, b) => a + b, 0)}, Asteroid sum: ${asteroids.reduce((a, b) => a + b, 0)}`);
if (asteroids.reduce((a, b) => a + b, 0) < ships.reduce((a, b) => a + b, 0)) {
console.log(' Reason: asteroid sum < ship sum (mathematically impossible)');
}
}