Files
sudoku-tools/src/solver/killer.ts
T

80 lines
2.5 KiB
TypeScript

export interface KillerCombinationOptions {
readonly size: number;
readonly count: number;
readonly sum: number;
readonly noRepeat?: boolean;
readonly usedDigits?: readonly number[];
readonly maxResults?: number;
}
export interface KillerCombinationResult {
readonly combinations: readonly (readonly number[])[];
readonly truncated: boolean;
}
export function killerDigitCombinations(
options: KillerCombinationOptions,
): KillerCombinationResult {
const { size, count, sum } = options;
if (!Number.isInteger(size) || size < 4 || size > 16) {
throw new RangeError("size must be an integer from 4 to 16");
}
if (!Number.isInteger(count) || count < 1 || count > size) {
throw new RangeError("count must be an integer from 1 to size");
}
if (!Number.isInteger(sum) || sum < count || sum > count * size) {
throw new RangeError("sum is outside the possible range");
}
const maxResults = options.maxResults ?? 10_000;
if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 100_000) {
throw new RangeError("maxResults must be an integer from 1 to 100000");
}
const noRepeat = options.noRepeat !== false;
const usedDigits = new Set(options.usedDigits ?? []);
for (const digit of usedDigits) {
if (!Number.isInteger(digit) || digit < 1 || digit > size) {
throw new RangeError("usedDigits contains an out-of-range digit");
}
}
const combinations: number[][] = [];
let truncated = false;
const current: number[] = [];
const visit = (
remainingCount: number,
remainingSum: number,
minimum: number,
): void => {
if (combinations.length >= maxResults) {
truncated = true;
return;
}
if (remainingCount === 0) {
if (remainingSum === 0) combinations.push([...current]);
return;
}
const lowestPossible = noRepeat
? (remainingCount * (2 * minimum + remainingCount - 1)) / 2
: remainingCount * minimum;
const highestPossible = noRepeat
? (remainingCount * (2 * size - remainingCount + 1)) / 2
: remainingCount * size;
if (remainingSum < lowestPossible || remainingSum > highestPossible) return;
for (let digit = minimum; digit <= size; digit += 1) {
if (usedDigits.has(digit)) continue;
if (digit > remainingSum) break;
current.push(digit);
visit(
remainingCount - 1,
remainingSum - digit,
noRepeat ? digit + 1 : digit,
);
current.pop();
if (truncated) return;
}
};
visit(count, sum, 1);
return { combinations, truncated };
}