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 }; }