feat: use coin-change alongside subset-sum impl
This commit is contained in:
parent
2cb52a925d
commit
5de4e21b2e
2 changed files with 133 additions and 3 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const sumService = require('@haensl/subset-sum')
|
const sumService = require('@haensl/subset-sum')
|
||||||
|
|
||||||
|
const logger = require('./logger')
|
||||||
|
const cc = require('./coin-change')
|
||||||
|
|
||||||
const BILL_LIST_MODES = {
|
const BILL_LIST_MODES = {
|
||||||
LAST_UNIT_FIRST: 0,
|
LAST_UNIT_FIRST: 0,
|
||||||
FIRST_UNIT_FIRST: 1,
|
FIRST_UNIT_FIRST: 1,
|
||||||
|
|
@ -103,7 +106,7 @@ const buildBillList = (units, mode) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSolution = (units, amount, mode) => {
|
const getSolution_old = (units, amount, mode) => {
|
||||||
const billList = buildBillList(units, mode)
|
const billList = buildBillList(units, mode)
|
||||||
|
|
||||||
if (_.sum(billList) < amount.toNumber()) {
|
if (_.sum(billList) < amount.toNumber()) {
|
||||||
|
|
@ -122,6 +125,13 @@ const getSolution = (units, amount, mode) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSolution = (units, amount) => {
|
||||||
|
amount = amount.toNumber()
|
||||||
|
units = units.map(({ denomination, count }) => [denomination, count])
|
||||||
|
const model = cc.model(units)
|
||||||
|
return cc.solve(model, amount)
|
||||||
|
}
|
||||||
|
|
||||||
const solutionToOriginalUnits = (solution, units) => {
|
const solutionToOriginalUnits = (solution, units) => {
|
||||||
const billsToAssign = (count, left) => _.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left)
|
const billsToAssign = (count, left) => _.clamp(0, count)(_.isNaN(left) || _.isNil(left) ? 0 : left)
|
||||||
|
|
||||||
|
|
@ -141,8 +151,20 @@ const solutionToOriginalUnits = (solution, units) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeChange(outCassettes, amount) {
|
function makeChange(outCassettes, amount) {
|
||||||
const solution = getSolution(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN)
|
const ss_solution = getSolution_old(outCassettes, amount, BILL_LIST_MODES.VALUE_ROUND_ROBIN)
|
||||||
return solutionToOriginalUnits(solution, outCassettes)
|
const cc_solution = cc.solve(cc.model(outCassettes), amount.toNumber())
|
||||||
|
|
||||||
|
if (!!ss_solution === !!cc_solution) {
|
||||||
|
logger.error(new Error(`subset-sum and coin-change don't agree on solvability -- subset-sum:${!!ss_solution} coin-change:${!!cc_solution}`))
|
||||||
|
return solutionToOriginalUnits(ss_solution, outCassettes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cc.check(cc_solution, amount.toNumber())) {
|
||||||
|
logger.error(new Error("coin-change provided a bad solution"))
|
||||||
|
return solutionToOriginalUnits(ss_solution, outCassettes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return solutionToOriginalUnits(cc_solution, outCassettes)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { makeChange }
|
module.exports = { makeChange }
|
||||||
|
|
|
||||||
108
lib/coin-change.js
Normal file
108
lib/coin-change.js
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Greedy solver of the coin change problem, based on the following CHICKEN
|
||||||
|
* implementation: https://git.sr.ht/~siiky/coin-change
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prepare_denominations([[d0, count], [d1, count], ...])
|
||||||
|
* => [{ denom, count, csum }, ...]
|
||||||
|
*/
|
||||||
|
const prepare_denominations = denominations =>
|
||||||
|
JSON.parse(JSON.stringify(denominations))
|
||||||
|
.sort(([d1, c1], [d2, c2]) => d1 < d2)
|
||||||
|
.reduce(
|
||||||
|
([csum, denoms], [denom, count]) => {
|
||||||
|
csum += denom*count
|
||||||
|
return [
|
||||||
|
csum,
|
||||||
|
[{ denom, count, csum }].concat(denoms)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[0, []]
|
||||||
|
)[1] /* ([csum, denoms]) => denoms */
|
||||||
|
|
||||||
|
const max_denomination_multiplicity = (denom, count, target) =>
|
||||||
|
Math.min(count, Math.floor(target / denom))
|
||||||
|
|
||||||
|
const has_divisor = (didx, denominations, target) =>
|
||||||
|
denominations
|
||||||
|
.slice(didx)
|
||||||
|
.some(({ denom }) => (target % denom) === 0)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @returns null if there's no solution set;
|
||||||
|
* false if there's no solution;
|
||||||
|
* solution if there's a solution
|
||||||
|
*/
|
||||||
|
const memo_get = (memo, target, denom) => {
|
||||||
|
const denom_solutions = memo[target]
|
||||||
|
if (denom_solutions === undefined) return null
|
||||||
|
const solution = denom_solutions[denom]
|
||||||
|
return solution === undefined ? null : solution
|
||||||
|
}
|
||||||
|
|
||||||
|
const memo_set = (memo, target, denom, solution) => {
|
||||||
|
let denom_solutions = memo[target]
|
||||||
|
if (denom_solutions === undefined)
|
||||||
|
memo[target] = denom_solutions = {}
|
||||||
|
return denom_solutions[denom] = solution
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = (solution, target) =>
|
||||||
|
!solution
|
||||||
|
|| target === solution.reduce((sum, [denom, provisioned]) => sum + denom*provisioned, 0)
|
||||||
|
|
||||||
|
const model = denominations => ({
|
||||||
|
denominations: prepare_denominations(denominations),
|
||||||
|
memo: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* target :: Int
|
||||||
|
* denominations :: [[d0, count], [d1, count], ...]
|
||||||
|
*
|
||||||
|
* @returns [[d0, provisioned], [d1, provisioned], ... ];
|
||||||
|
* false if there's no solution.
|
||||||
|
*/
|
||||||
|
const solve = (model, target) => {
|
||||||
|
const { denominations, memo } = model
|
||||||
|
|
||||||
|
const coin_change = (didx, target) => {
|
||||||
|
if (target === 0) return []
|
||||||
|
|
||||||
|
for (; didx < denominations.length; didx++) {
|
||||||
|
const { denom, count, csum } = denominations[didx]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There's no solution if the target is greater than the cumulative sum
|
||||||
|
* of the denominations, or if the target is not divisible by any of the
|
||||||
|
* denominations
|
||||||
|
*/
|
||||||
|
if (target > csum || !has_divisor(didx, denominations, target))
|
||||||
|
return memo_set(memo, target, denom, false)
|
||||||
|
|
||||||
|
let solution = memo_get(memo, target, denom)
|
||||||
|
if (solution === false) continue /* not here, keep looking */
|
||||||
|
if (solution) return solution /* we've previously computed a solution */
|
||||||
|
|
||||||
|
/* solution === null */
|
||||||
|
for (let nd = max_denomination_multiplicity(denom, count, target); nd >= 0; nd--) {
|
||||||
|
solution = coin_change(didx+1, target - denom*nd)
|
||||||
|
if (solution)
|
||||||
|
return memo_set(memo, target, denom, [[denom, nd]].concat(solution))
|
||||||
|
}
|
||||||
|
|
||||||
|
memo_set(memo, target, denom, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return coin_change(0, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
check,
|
||||||
|
model,
|
||||||
|
solve,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue