80 lines
2.5 KiB
TypeScript
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 };
|
|
}
|