7

Example: n=8, k=4 Answer: 5

[1,1,1,5], [1,1,2,4], [1,1,3,3], [1,2,2,3], [2,2,2,2]

I thought of applying dynamic programming to count the number of ways 8 objects can be divided into 4 groups, but can't understand how to keep track of the number of objects in the previous group.

DP approach:

for(int j=0;j<=n;j++)
{
    for(int i=1;i<=k;i++)
    {
        if(j<i)
            continue;
        if(j==i)
            dp[j]=1;
        for(int k=1;k<i;k++)
        {
            dp[j]+=dp[k]*dp[j-k];
        }
    }
}

Please help with the approach. I'm relatively new to DP.

narendra-choudhary
  • 4,582
  • 4
  • 38
  • 58
Double A
  • 83
  • 1
  • 2
  • 4

4 Answers4

7

These are known as partitions with restricted number of parts. The idea behind the recurrence, which is equal to the number of partitions whose largest part is k (proof is left as a short, interesting read) is that if the smallest part of the partition is 1, we've added 1 to all partitions of n - 1 into k - 1 parts (to guarantee the smallest part is 1); and if the smallest part is not 1, we've added 1 to each of k parts in all partitions of n - k into k parts (guaranteeing that each part is greater than 1).

Here's a straightforward memoization:

function f(n, k, memo={}){
  if (k == 0 && n == 0)
    return 1

  if (n <= 0 || k <= 0)
    return 0

  let key = String([n, k]) // Thanks to comment by user633183

  if (memo.hasOwnProperty(key))
    return memo[key]

  return memo[key] = f(n - k, k, memo) + f(n - 1, k - 1, memo)
}

console.time('time taken')
console.log(f(1000, 10))
console.timeEnd('time taken')

Here's bottom-up:

function f(n, k){
  let dp = new Array(n + 1)
  for (let i=0; i<n+1; i++)
    dp[i] = new Array(k + 1).fill(0)
  dp[0][0] = 1
  
  for (let i=1; i<=n; i++)
    for (let j=1; j<=Math.min(i, k); j++)
      dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1]

  return dp[n][k]
}

console.time('time taken')
console.log(f(1000, 10))
console.timeEnd('time taken')
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • 1
    You can significantly reduce runtime in your `memo` example by stringifying `[n, k]` only once - ie, `const key = \`${n},${k}\``, then using `memo.hasOwnProperty(key)`, `memo[key]` and `memo[key] = ...`. You could use `const key = String([n, k])` but there's really no point to allocating the array. – Mulan Oct 09 '19 at 05:21
  • Nice! That's a much cleaner algorithm than mine. I still prefer the external memoization, but that would make little difference in performance. – Scott Sauyet Oct 09 '19 at 13:07
  • I created [a version of this](https://ramdajs.com/repl/?v=0.26.1#?const%20f%20%3D%20%28n%20%2C%20k%29%20%3D%3E%20%0A%20%20n%20%3C%3D%200%20%7C%7C%20k%20%3C%3D%200%0A%20%20%20%20%3F%20%5B%5D%0A%20%20%3A%20n%20%3D%3D%20k%0A%20%20%20%20%3F%20%5BArray%20%28n%29%20.fill%20%281%29%5D%0A%20%20%3A%20%5B%0A%20%20%20%20%20%20...f%20%28n%20-%201%2C%20k%20-%201%29%20.map%20%28xs%20%3D%3E%20%5B1%2C%20...xs%5D%29%2C%0A%20%20%20%20%20%20...f%20%28n%20-%20k%2C%20k%29%20.map%20%28xs%20%3D%3E%20xs%20.map%20%28n%20%3D%3E%20n%20%2B%201%29%29%0A%20%20%20%20%5D%0A%0Af%20%288%2C%204%29%0A) showing the raw partitions – Scott Sauyet Oct 09 '19 at 15:29
  • dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1] I am not able to understand the logic behind this. – aman_41907 Apr 18 '20 at 09:54
  • @aman_41907 `i` and `j` correspond to `n` and `k` in the recursive formulation above it. – גלעד ברקן Apr 18 '20 at 12:23
1

Update

Although all the discussion below is still useful, the answer from גלעד ברקן provides a much nicer underlying algorithm that allows us to skip my min parameter. (I knew I should have looked this up!) That understanding allows for significant performance improvement over the algorithm used below.


Think of dynamic programming (DP) as a simple optimization technique that can speed up certain recursive procedures. If your recursive calls are repeated (as with the Fibonacci numnbers), then storing their results can drastically speed up your program. But the underlying logic is still a recursive call. So let's solve this program recursively first and see where we might apply DP optimization.

(8, 4) with only five solutions is small enough that, even if the time is algorithmically exponential, it's still probably manageable. Let's try a simple recursion. And at first, let's actually build the output rather than count it in order to double-check that we're doing things right.

This version is based on the idea that we can set the first number of the list, keeping track of that value as a minimum for the remaining elements, and then recurring for the remaining positions. Finally, we try again with a higher initial number. So as well as our n and k inputs, we will also need to keep a min parameter, which we start at 1.

Here is one version:

const f = (n, k, min = 1) => 
  k < 1 || n < k * min
    ? []
  : k == 1
    ? [[n]]
  : [
      ... f (n - min, k - 1, min) .map (xs => [min, ...xs]), 
      ... f (n, k, min + 1)
    ]

console .log (
  f (8, 4) //~> [[1, 1, 1, 5], [1, 1, 2, 4], [1, 1, 3, 3], [1, 2, 2, 3], [2, 2, 2, 2]]
)

(You didn't specify a language tag; if that Javascript ES6 syntax is not clear, we can rewrite in another style.)

Since that seems right, we can write a simpler version just to count the results:

const f = (n, k, min = 1) => 
  k < 1 || n < k * min
    ? 0
  : k == 1
    ? 1
  : f (n - min, k - 1, min) + f (n, k, min + 1)

console .log (
  f (8, 4) //~> 5
)

But if we are going to try a larger set, say f(1000, 10) (which, by inspection, should be 8867456966532531), it might take a bit of time to calculate. Our algorithm is likely exponential. So there are two ways we might go about dynamic programming for this. The most obvious is the bottom-up approach:

const f = (_n, _k, _min = 1) => {
  const cache = {}
  for (let n = 1; n <= _n; n ++) {
    for (let k = 1; k <= Math.min(_k, n); k++) {
      for (let min = n; min >= 0; min--) {
        cache [n] = cache[n] || {}
        cache [n] [k] = cache [n] [k] || {}
        cache [n] [k] [min] = 
          k < 1 || n < k * min
            ? 0
            : k == 1
               ? 1
               : cache [n - min] [k - 1] [min]  + cache [n] [k] [min + 1]
      }
    }
  }
  return cache [_n] [_k] [_min]
}

console.time('time taken')
console .log (
  f (1000, 10) //~> 886745696653253
)
console.timeEnd('time taken')

Figuring out the correct bounds here is tricky, if for no other reason, because the recursion is based on an increasing value of min. It's likely that we're calculating things we don't need along the way.

It's also ugly code, losing the elegance and readability of the original while gaining only performance.

We can still keep the elegance by memoizing our function; this is the top-down approach. By working with a reusable memoize function, we can use our recursive solution almost intact:

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

const makeKey = (n, k, min) => `${n}-${k}-${min}`        
        
const f = memoize(makeKey, (n, k, min = 1) => 
  k < 1 || n < k * min
    ? 0
  : k == 1
    ? 1
  : f (n - min, k - 1, min)  + f (n, k, min + 1)
)

console.time('time taken')
console .log (
  f (1000, 10) //~> 886745696653253
)
console.timeEnd('time taken')

memoize turns a function that calculates its results on every call into one that only calculates them the first time it sees a certain set of input. This version requires you to supply an additional function that turns the arguments into a unique key. There are other ways this could be written, but they are a bit uglier. Here we just turn (8, 4, 1) into "8-4-1", then store the result under that key. There's no ambiguity. The next time we call with (8, 4, 1), the already-calculated result will be returned instantly from the cache.

Note that there is a temptation to try

const f = (...args) => {...}
const g = memoize(createKey, f)

But this doesn't work, if the recursive calls in f point back to f. And if they point to g, we've already mixed up the implementation and f is no longer stand-alone, so there's little reason for it. Thus we write it as memomize(createKey, (...args) => {...}). Advanced techniques that offer alternatives are beyond the scope of discussion here.


Deciding between bottom-up DP and top-down DP is a complicated question. You will see in the case above that the bottom-up version runs faster for the given input. There is some recursive overhead to the additional function calls, and you might in some cases be subject to recursion depth limits. But this is sometimes entirely offset by the top-down technique only calculating what you need. Bottom-up will calculate all smaller inputs (for some definition of "smaller") in order to find your value. Top-down will only calculate those values necessary for solving your problem.


1 A joke! I only found the value after using dynamic programming.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Comprehensive! A small module I created, [`DeepMap`](https://stackoverflow.com/a/56894824/633183), is great for memoising compound data where stringifying such as `${n}-${k}-${min}` cannot be used. – Mulan Oct 08 '19 at 22:44
  • 1
    `console.time` and `console.timeEnd` help reduce noise in benchmark demos too ^^ – Mulan Oct 08 '19 at 22:50
  • @user633183: Thanks. Updated to use `console.time/.timeEnd`. – Scott Sauyet Oct 09 '19 at 01:46
  • @user633183: DeepMap is very nice. There was a much less sophisticated version of the same idea in the [initial version of `memoize`](https://github.com/CrossEye/eweda/commit/4cc4c3d23b4c8d02f504e50c8f8b23546f6cff30#diff-91452c4737e0e4f3830175572840f99cR288-R298) in (the predecessor of) Ramda. But using Objects instead of Maps, it was mostly limited to String and Number keys. – Scott Sauyet Oct 09 '19 at 01:48
  • (By the way, both my top-down and bottom-up [versions](https://stackoverflow.com/a/58296796/2034787) seem significantly faster.) – גלעד ברקן Oct 09 '19 at 04:08
  • @גלעדברקן: Yes, that is a much faster algorithm. Nicely done. Updated this answer to point to it. – Scott Sauyet Oct 09 '19 at 13:22
0

From the example, I am assuming no groups can be empty. Also assuming the value of n,k <= 1000.

The dp states will be remaining Objects and remaining Groups. f(remObject,remGroup) will be the number of ways to put remObject in remGroup where no group will have fewer objects than previously formed groups.

We will consider 2 cases.

If we want to put an object in the left most group we will also need to put an object to all other groups too. So we have to make sure remaining Objects >= remaining Groups. In this case we will add f(remObject - remGroup, remGroup) to our answer.

If we do not want to put any object to the left most group anymore we will add f(remObject,remGroup - 1) with our answer.

And the base case will be when no groups are left to consider and all the objects are placed.

As any groups can't be empty, before calling our dp we will put 1 object into all the k groups.

Look at the code for more details.

#define mxn 1003
#define i64 long long int
#define mod 1000000007

i64 dp[mxn][mxn];

i64 f(int remObject,int remGroup) {
        if(!remGroup) {
                if(!remObject)
                        return 1;
                return 0;
        }

        if(dp[remObject][remGroup] != -1)
                return dp[remObject][remGroup];

        i64 ans = 0;
        if(remObject >= remGroup)
                ans += f(remObject - remGroup, remGroup);
        ans += f(remObject,remGroup - 1);
        ans %= mod;

         return dp[remObject][remGroup] = ans;
}

int main()
{
        int t,n,k;
        memset(dp,-1,sizeof dp);
        cin >> t;
        while(t--) {
                cin >> n >> k;
                if(n < k)
                        cout << 0 << endl;
                else
                        cout << f(n-k,k) << endl;
        }
        return 0;
}
0

The memoized solution can be further improved by adding few checks like. If n,k are equal the answer is 1. We don't need to do recursion for 1000,1000. Also of k is 1, no matter what n is the answer is 1. 1000,1 is 1 so saves memory and time. Updated Code: Can't add this as comment to above solution due to low reputation, Sorry. Also You can find simple Explanation here : N into K groups recursion.

function f(n, k, memo = {}) {
    if (k == 0 && n == 0) return 1;
    if (k == 1 && n != 0) return 1; //when k is 1 no matter what n is
    if (n == k) return 1; // when k and n are equal.

    if (n <= 0 || k <= 0) return 0;

    let key = String([n, k]); // Thanks to comment by user633183

    if (memo.hasOwnProperty(key)) return memo[key];


    return (memo[key] = f(n - k, k, memo) + f(n - 1, k - 1, memo));
}
  • [N into K groups recursion](https://math.stackexchange.com/questions/1908701/integer-partition-of-n-into-k-parts-recurrence). You can find simple Explanation here. – Farrukh Taqveem Haider Mar 18 '20 at 10:41