1

please can somebody help?

If i have a total or a sum for instance 91

How can I create an array of the least amount of elements needed to get to the total value?

[50, 20, 10 , 5, 3, 2, 1] totaling this array will provide 91.

I know how to perform the opposite function using reduce or like so:

<script>
var numbers = [65, 44, 12, 4];

function getSum(total, num) {
    return total + num;
}
function myFunction(item) {
    document.getElementById("demo").innerHTML = numbers.reduce(getSum);
}
</script>
Iwan Ross
  • 196
  • 2
  • 10
  • 5
    If you have a total of 91, then you can create an array whose sum is 91 with the fewest number of elements by putting that total as the only element of the array. `[91]`. (clarify your question?) – CertainPerformance May 26 '18 at 08:53
  • 3
    Let me make an assumption, you can only use certain values right? In that case it is not so trivial and you need to check something called dynamic programming to achieve that. If my assumption is wrong then see the previous comment. – Matus Dubrava May 26 '18 at 08:56
  • Do you *have* a list of values and need to take a subset of it that adds up to X or do you just have X and you need to generate a list of values that add up to it? If the former, you can use a simple(-ish) greedy algorithm by starting with the biggest numbers first, if the latter - what are the rules for generating the values? – VLAZ May 26 '18 at 09:04
  • @vlaz If the former, you can use a simple(-ish) greedy algorithm - that is really not a problem that can be solved by greedy algorithm in general. – Matus Dubrava May 26 '18 at 09:10
  • Thank you, no, you do not need to use only certain values, what I need is the least amount of numbers that can make up a total. So, you can use the same amount more than once. – Iwan Ross May 26 '18 at 09:15
  • @MatusDubrava I mean, you have, say `[1, 2, 3, 4, 5, 6]` and you want to find the smallest subset that adds up to `10`. Granted, that's assuming it's *possible*, but using greedy, you'd go `6 - yes` - > `5 - no` -> `4 - yes` and get two numbers. You might need to do backtracking and you also need a dataset that contains numbers that can add up to your target but it's not exactly clear if OP is asking for that or not. – VLAZ May 26 '18 at 09:17
  • 1
    @nawissor in that case the least amount of numbers that make up the total is a single number - just the total. – VLAZ May 26 '18 at 09:19
  • @vlaz Assume total must be `14` and you have `[10, 7, 7, 1, 1, 1, 1]`, greedy algorithm will take `10` -> yes, `7` -> no, `7` -> no, `1` -> yes, `1` -> yes, `1` -> yes, `1` -> yes and you end up with `[10, 1, 1, 1, 1]` while the correct answer is obviously `[7, 7]`. And this was just a simple case. – Matus Dubrava May 26 '18 at 09:22
  • @nawissor Ok, but can you pick number `91`? Because do much better as [`91`] adds up to `91`, as does `[90, 1]` and `[89, 2]` and `[88, 3]`... you can see where this is going. So, what numbers can you use to build your result? – Matus Dubrava May 26 '18 at 09:28
  • So, you can get a total from an array, but not an array from a total? How would you turn for instance the number 73 into an array, using the least number of elements? – Iwan Ross May 26 '18 at 09:29
  • @nawissor `[73]`, `[72, 1]`, `[71, 2]` ... pick whichever you like the most. – Matus Dubrava May 26 '18 at 09:30
  • This is the question as it was posted to me: 2. Write a function to determine the least amount of elements from an integer array that can be used to get to a total value. When the following input is given - Total = 73 - Elements = [50, 25, 10, 5, 2, 1] The output should be this: [50, 10, 10, 2, 1] (50 + 10 + 10 + 2 + 1) Have in mind that the input might be out of order as well. – Iwan Ross May 26 '18 at 09:32
  • @nawissor Then see my first comment, I made the right assumption and you need a dynamic programming. – Matus Dubrava May 26 '18 at 09:35
  • Thanks @MatusDubrava! – Iwan Ross May 26 '18 at 09:39
  • @nawissor I posted solution to that problem. – Matus Dubrava May 26 '18 at 09:59
  • @nawissor Consider posting the exact problem statement in the question itself when you ask questions, else it can easily result in lots of unnecessary confusion and requests for clarification – CertainPerformance May 26 '18 at 11:29

1 Answers1

2

Greedy algorithm

Here is a solution using greedy algorithm. Note that this solution will work correctly in case when all the smaller numbers are divisors of all the bigger numbers such as in case [50, 10, 5, 1]. (see dynamic algorithm below this one for solution that can handle any input)

50 mod 10 = 0
50 mod 5  = 0
50 mod 1  = 0
10 mod 5  = 0
10 mod 1  = 0
5  mod 1  = 0

const sum = xs => xs.reduce((acc, v) => acc + v, 0);

function pickSubset(options, total, currentPick) {

  if (sum(currentPick) === total) { return currentPick; }
  if (options.length === 0) { return null; }

  const firstVal = options[0];
  let res = null;

  if (sum(currentPick) + firstVal > total) {
    res = pickSubset(options.slice(1), total, currentPick);
  } else {
    let opt1 = pickSubset(options, total, currentPick.concat(options[0]));
    let opt2 = pickSubset(options.slice(1), total, currentPick.concat(options[0]));

    if (opt1 && opt2) {
      opt1.length < opt2.length ? res = opt1 : res = opt2
    } else if (opt1) {
      res = opt1;
    } else {
      res = opt2;
    }
  }

  return res;
}

const total = 73;
const options = [50, 25, 10, 5, 2, 1];

console.log(pickSubset(options, total, []));

To handle unsorted input you can wrap it in another function and sort it prior to passing it to the main function.

const sum = xs => xs.reduce((acc, v) => acc + v, 0);

function pickSubset(options, total, currentPick) {

  const sortedOptions = options.sort((a, b) => b - a);

  function _pickSubset(options, total, currentPick) {

    if (sum(currentPick) === total) { return currentPick; }
    if (options.length === 0) { return null; }

    const firstVal = options[0];
    let res = null;

    if (sum(currentPick) + firstVal > total) {
      res = pickSubset(options.slice(1), total, currentPick);
    } else {
      let opt1 = pickSubset(options, total, currentPick.concat(options[0]));
      let opt2 = pickSubset(options.slice(1), total, currentPick.concat(options[0]));

      if (opt1 && opt2) {
        opt1.length < opt2.length ? res = opt1 : res = opt2
      } else if (opt1) {
        res = opt1;
      } else {
        res = opt2;
      }
    }

    return res;
  }

  return _pickSubset(sortedOptions, total, currentPick);
}



const total = 73;
const options = [50, 25, 10, 5, 2, 1].reverse();

console.log(pickSubset(options, total, []));

Dynamic programming (bottom-up natural ordering approach)

This solution works correctly for any type of input.

function pickSubset(options, total) {

  function _pickSubset(options, change, minNums, numsUsed) {
    for (let i = 0; i < change + 1; i++) {
      let count = i;
      let newNum = 1;
      let arr = options.filter(v => v <= i);

      for (let j of arr) {
        if (minNums[i - j] + 1 < count) {
          count = minNums[i - j] + 1;
          newNum = j;
        }
      }

      minNums[i] = count;
      numsUsed[i] = newNum;
    }

    return minNums[change];
  }

  function printNums(numsUsed, change) {
    const res = [];
    let num = change;
    while (num > 0) {
      let thisNum = numsUsed[num];
      res.push(thisNum);
      num = num - thisNum;
    }
    return res;
  }

  const numsUsed = [];
  const numsCount = [];

  _pickSubset(options, total, numsCount, numsUsed);
  return printNums(numsUsed, total);
}

const options = [50, 10, 5, 2, 1];
console.log(pickSubset(options, 73));

Dynamic programming (top-down memoization approach)

// helper function that generates all the possible solutions
// meaning, all the possible ways in which we can pay the provided amount
// and caches those solutions;
// returns the number of possible solutions but that is not neccessary
// in this case
const _pickSubset = (toPay, options, currentPick, cache) => {
  if (toPay < 0) { return 0; }
  if (toPay === 0) {
    cache.add(currentPick);
    return 1;
  }
  if (options.length === 0) { return 0; }

  return _pickSubset(toPay - options[0], options, currentPick.concat(options[0]), cache)
    + _pickSubset(toPay, options.slice(1), currentPick, cache);
};

// memoize only with respect to the first two arguments - toPay, bills
// the other two are not necessary in this case
const memoizeFirstTwoArgs = fn => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args.slice(0, 2));
    if (cache.has(key)) { return cache.get(key); }
    const res = fn(...args);
    cache.set(key, res);
    return res;
  };
};

// uses memoized version of makeChange and provides cache to that function;
// after cache has been populated, by executing memoized version of makeChange,
// find the option with smallest length and return it
const pickSubset = (toPay, options) => {
  const cache = new Set();
  const memoizedPickSubset = memoizeFirstTwoArgs(_pickSubset);
  memoizedPickSubset(toPay, options, [], cache);

  let minLength = Infinity;
  let resValues;

  for (const value of cache) {
    if (value.length < minLength) {
      minLength = value.length;
      resValues = value;
    }
  }

  return resValues;
}

const options = [50, 25, 10, 5, 2, 1];
const toPay = 73;

console.log(pickSubset(toPay, options));
Matus Dubrava
  • 13,637
  • 2
  • 38
  • 54