2

How many ways can you make the sum of a number?

From wikipedia: https://en.wikipedia.org/wiki/Partition_(number_theory)#

In number theory and combinatorics, a partition of a positive integer n, also called an integer partition, is a way of writing n as a sum of positive integers. Two sums that differ only in the order of their summands are considered the same partition. If order matters, the sum becomes a composition. For example, 4 can be partitioned in five distinct ways:

4
3 + 1
2 + 2
2 + 1 + 1
1 + 1 + 1 + 1
Examples
Basic
sum(1) // 1
sum(2) // 2  -> 1+1 , 2
sum(3) // 3 -> 1+1+1, 1+2, 3
sum(4) // 5 -> 1+1+1+1, 1+1+2, 1+3, 2+2, 4
sum(5) // 7 -> 1+1+1+1+1, 1+1+1+2, 1+1+3, 1+2+2, 1+4, 5, 2+3

sum(10) // 42
Explosive
sum(50) // 204226
sum(80) // 15796476
sum(100) // 190569292

My Attempt

I tried to loop through two arrays simultaneously and test them against eachother. This doesn't work (at least in the way I did it) for a few reasons.

My Code:

function sum(num, arr = []) {
  if(num == 0){
    return testNumbers(arr, num);
}
  arr.push(num);
  return sum(num - 1, arr);


function testNumbers(arrr, n){
  let arr2 = [...arrr];
  let count = 0; 

  let calculations = arrr.filter((item)=>{
  return item + arr2.map((a)=>{
     return a;
   }) == n;
    
  })


console.log(calculations);

}


}


console.log(sum(10));

You don't need to fix my code, as I don't think its salvageable, but how do you solve the problem?

Dave Newton
  • 156,572
  • 25
  • 250
  • 300
AndrewNeedsHelp
  • 365
  • 2
  • 13
  • 1
    It's a classic dynamic programming problem, you can read about it here: https://www.geeksforgeeks.org/coin-change-dp-7/ – Photon Jun 23 '20 at 11:11
  • 1
    @Photon: While you *could* use the change-counting algorithm (by including every denomination up to the target), this would be a more appropriate page: https://www.geeksforgeeks.org/generate-unique-partitions-of-an-integer/ – Scott Sauyet Jun 23 '20 at 14:03

2 Answers2

6

This is in fact a fairly simple recursion, if we think of the problem as counting the partitions with a given lower bound. We have two simple bases cases of 0 and 1 if the lower bound is greater than our target number or equal to it. Otherwise we recur in two ways, one for when we use that lower bound, and one for when we don't. Here's one version, where lb is the lower bound of the numbers to use:

const count = (n, lb = 1) =>
  lb > n
    ? 0
  : lb == n
    ? 1
  : count (n - lb, lb) + count (n, lb + 1)

This counts the number of partitions with the given lower bound. So count(10, 3) would yield 5, one for each array in [[3, 3, 4], [3, 7], [4, 6], [5, 5], [10]]. Although the default value for the lower bound means that we can call it with just our target number, there are potential issues with this if we tried, say, to map over it. So it might be best to wrap this in a separate function, const countPartitions = (n) => count (n, 1)

const count = (n, lb) =>
  lb > n
    ? 0
  : lb == n
    ? 1
  : count (n - lb, lb) + count (n, lb + 1)

const countPartitions = (n) => count (n, 1)

console .log ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] .map (countPartitions))

But this will be quite slow for larger input. My test for 100 took 56.3 seconds.) If we memoize the intermediate results, we should speed things up a great deal. We can do this manually or, as I'd prefer, with a memoization helper:

const memoize = (makeKey, fn) => {
  const memo = {}
  return (...args) => {
    const key = makeKey (...args)
    return memo [key] || (memo [key] = fn (...args))
  }
}

const count = memoize (
  (n, lb) => `${n}~${lb}`, 
  (n, lb) => 
    lb > n
      ? 0
    : lb == n
      ? 1
    : count (n - lb, lb) + count (n, lb + 1)
)

const countPartitions = (n) => count (n, 1)

console .log (countPartitions (100))

And this now takes 20 milliseconds in my test.

Scott Sauyet
  • 44,568
  • 4
  • 43
  • 95
2

We can get a speed up over Scott's solution by using the recurrence relation for the partition function that uses pentagonal numbers:

function _p(k, memo){
  if (k == 0)
    return 1;

  if (memo[k])
    return memo[k];

  let result = 0;
  let i = 1;
  const ms = [1, 1, -1, -1];
  
  while (true){
    const n = i & 1 ? (i + 1) / 2 : -i / 2;
    const pentagonal = (3*n*n - n) / 2;

    if (pentagonal > k)
      break;

    result = result + ms[(i-1) % 4] * _p(k - pentagonal, memo);
    i = i + 1;
  }
  
  return memo[k] = result;
}

function p(k){
  return _p(k, {});
}

var ks = [1, 2, 3, 4, 5, 6, 10, 50, 80, 100];
for (let k of ks){
  const start = new Date;
  console.log(`${ k }: ${ p(k) }`);
  console.log(`${ new Date - start } ms`);
  console.log('');
}
גלעד ברקן
  • 22,586
  • 3
  • 21
  • 58
  • That's very nice. I didn't know that recurrence. – Scott Sauyet Jun 23 '20 at 16:01
  • Do note, though that the default parameter does allow for some mischief. `ks .map (k => p (k))` works fine. But `ks .map (p)` will fail because of it. – Scott Sauyet Jun 23 '20 at 17:57
  • @ScottSauyet interesting. Why is that? – גלעד ברקן Jun 23 '20 at 18:12
  • Because `Array.prototype.map` passes not just the value of the element but also the index and the entire array when it calls your callback. So the first `map` callback will look like `p(1, 0, [1, 2, 3,..., 50, 80, 100])` and `p` won't default your `memo` property to `{}` because it already has value `0`. See also https://stackoverflow.com/a/262511. – Scott Sauyet Jun 23 '20 at 19:45
  • @ScottSauyet makes sense. That's user error. User being the caller of `map` in this instance. – גלעד ברקן Jun 23 '20 at 20:44
  • I disagree. How would you document this function? `p :: Int -> Int`, perhaps? You probably wouldn't mention the `memo` parameter at all, as it's really an implementation detail. If the user queried the arity of the function, the result would be `1`. The only way to know to avoid this is to dig into your implementation. That feels like a design error. It's one of the serious issues with using default parameters that aren't part of the public API. I've been guilty of it a lot lately, I'm afraid. – Scott Sauyet Jun 23 '20 at 21:28
  • @ScottSauyet the arity is 2. It has two parameters. Default parameters don't invalidate their existence. – גלעד ברקן Jun 23 '20 at 21:29
  • `p.length //=> 1`. I don't like that fact, but that's how it is. – Scott Sauyet Jun 23 '20 at 21:30
  • @ScottSauyet huh, ok. I didn't know functions have length, cool. How about now? I made it explicit. – גלעד ברקן Jun 23 '20 at 21:32
  • @ScottSauyet alternatively, we could wrap it, of course. – גלעד ברקן Jun 23 '20 at 21:34
  • Wrapping it is what I tend to do. I've also used `bind` or `call` functions that make this easier to deal with. I don't think there's any great solution. – Scott Sauyet Jun 23 '20 at 21:38
  • I think that change just degrades the API, whereas wrapping it will work fine. Or you could use a helper function like in my version. BTW: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length – Scott Sauyet Jun 23 '20 at 21:40
  • 1
    @ScottSauyet that's fair. Wrapped. – גלעד ברקן Jun 23 '20 at 21:45