1

Given a set of integers 1,2, and 3, find the number of ways that these can add up to n. (The order matters, i.e. say n is 5. 1+2+1+1 and 2+1+1+1 are two distinct solutions)

My solution involves splitting n into a list of 1s so if n = 5, A = [1,1,1,1,1]. And I will generate more sublists recursively from each list by adding adjacent numbers. So A will generate 4 more lists: [2,1,1,1], [1,2,1,1], [1,1,2,1],[1,1,1,2], and each of these lists will generate further sublists until it reaches a terminating case like [3,2] or [2,3]

Here is my proposed solution (in Python)

ways = []
def check_terminating(A,n):
    # check for terminating case
    for i in range(len(A)-1):
        if A[i] + A[i+1] <= 3:
            return False # means still can compute
    return True

def count_ways(n,A=[]):
    if A in ways:
       # check if alr computed if yes then don't compute
       return True
    if A not in ways: # check for duplicates
        ways.append(A) # global ways
    if check_terminating(A,n):
        return True # end of the tree
    for i in range(len(A)-1):
        # for each index i,
        # combine with the next element and form a new list
        total = A[i] + A[i+1]
        print(total)
        if total <= 3:
            # form new list and compute
            newA = A[:i] + [total] + A[i+2:]
            count_ways(A,newA)
            # recursive call

# main            
n = 5
A = [1 for _ in range(n)]

count_ways(5,A)
print("No. of ways for n = {} is {}".format(n,len(ways)))

May I know if I'm on the right track, and if so, is there any way to make this code more efficient?

Please note that this is not a coin change problem. In coin change, order of occurrence is not important. In my problem, 1+2+1+1 is different from 1+1+1+2 but in coin change, both are same. Please don't post coin change solutions for this answer.

Edit: My code is working but I would like to know if there are better solutions. Thank you for all your help :)

chowsai
  • 565
  • 3
  • 15
  • Have you run this through a compiler? Are you getting errors? If not, then try that first before asking questions on SO. If you want someone to tell you how _good_ your code is, try Code Review. – jmoon Aug 16 '17 at 18:06
  • 2
    Btw, the problem you're solving is called "partitioning". – m69's been on strike for years Aug 16 '17 at 18:09
  • Does this set of numbers have to be 1, 2, 3? – bendl Aug 16 '17 at 18:09
  • I would ask this question on math.stackexchange.com, and tag it combinatorics. They'll likely give you a good response. – bendl Aug 16 '17 at 18:10
  • I'm voting to close this question as off-topic because there is no functional problem with the code; improvement of working code belongs in CodeReview.StackExchange.com – Prune Aug 16 '17 at 18:11
  • Welcome to StackOverflow. Please read and follow the posting guidelines in the help documentation. [on topic](http://stackoverflow.com/help/on-topic) applies. – Prune Aug 16 '17 at 18:11
  • 2
    @Prune: I don't think that this question is not suitable for this portal. It is a normal algorithmic problem and deserves attention. Why would you close it? – CodeHunter Aug 16 '17 at 18:26
  • @m69 It is not partitioning. In a partition, order does not matter. – btilly Aug 16 '17 at 19:05
  • @bendl Actually math.stackexchange would be worse for this. There they can talk about recurrence relations, the characteristic polynomial, and give you an exact formula. But the programming techniques to calculate this in reasonable time are unlikely to be discussed there. – btilly Aug 16 '17 at 19:08
  • @btilly You're right, they have a specific name which escapes me now; looking up partitioning should get you there, though. – m69's been on strike for years Aug 16 '17 at 19:18
  • 2
    FWIW, the formal name for this kind of combination is [composition](https://en.wikipedia.org/wiki/Composition_%28combinatorics%29); if order is _not_ significant, then it's a partition. – PM 2Ring Aug 16 '17 at 19:25

3 Answers3

5

The recurrence relation is F(n+3)=F(n+2)+F(n+1)+F(n) with F(0)=1, F(-1)=F(-2)=0. These are the tribonacci numbers (a variant of the Fibonacci numbers):

It's possible to write an easy O(n) solution:

def count_ways(n):
    a, b, c = 1, 0, 0
    for _ in xrange(n):
        a, b, c = a+b+c, a, b
    return a

It's harder, but possible to compute the result in relatively few arithmetic operations:

def count_ways(n):
    A = 3**(n+3)
    P = A**3-A**2-A-1
    return pow(A, n+3, P) % A

for i in xrange(20):
    print i, count_ways(i)
Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • Nice answer Paul. It does tell the correct number of ways possible in `O(n)` time. I am thinking if we can also print those solutions, now that we know what would be the total number of solutions actually possible. – CodeHunter Aug 16 '17 at 18:37
  • Can you explain where that amazing second formula comes from? I don't see it mentioned on [OEIS A000073](http://oeis.org/A000073) and I am interested to learn how that approach generalizes to other recurrence relations. – Peter de Rivaz Aug 16 '17 at 20:53
  • 2
    @PeterdeRivaz if you consider the polynomial quotient ring: Z[X]/(X^3-X^2-X-1), compute X^n in that ring. Then note you can approximate that computation in Z/(A^3-A^2-A-1) as long as "A" is large enough that the coefficients don't overflow. `3**(n+3)` is the A here -- and is just a number that grows faster than the tribonacci numbers. There's some useful (fibonacci rather than tribonacci) background here: http://fare.tunes.org/files/fun/fibonacci.lisp – Paul Hankin Aug 17 '17 at 18:24
  • That is a fascinating link! My understanding is that it is roughly equivalent in efficiency to a fast matrix exponentiation by packing the 3 relevant numbers into a single number (and exploiting the special structure of the matrix to further accelerate each matrix multiplication) – Peter de Rivaz Aug 17 '17 at 19:09
  • @PeterdeRivaz yes, that's right. And it works for any fibonacci variant -- for example the hexonacci numbers here: https://stackoverflow.com/questions/45657365/how-many-possibilities-to-get-to-n-spaces-away-from-starting-point-by-rolling-6/45659692?noredirect=1#comment78329633_45659692 – Paul Hankin Aug 17 '17 at 20:17
3

The idea that you describe sounds right. It is easy to write a recursive function that produces the correct answer..slowly.

You can then make it faster by memoizing the answer. Just keep a dictionary of answers that you've already calculated. In your recursive function look at whether you have a precalculated answer. If so, return it. If not, calculate it, save that answer in the dictionary, then return the answer.

That version should run quickly.

btilly
  • 43,296
  • 3
  • 59
  • 88
  • For a detailed example of the impact of memoization on a partitioning algorithm, see e.g. this answer: https://stackoverflow.com/questions/32907406/is-there-an-efficient-algorithm-for-integer-partitioning-with-restricted-number/32918426#32918426 – m69's been on strike for years Aug 16 '17 at 18:17
  • Memoization using the way you told won't keep track of order of appearance. This method would be ideal if order was not important here. I don't memoization using this way could help here. – CodeHunter Aug 16 '17 at 18:22
  • @CodeHunter As long as the recursive function is correct, the memoized function is as well. Keeping track of the order of appearance in the recursive function is easy - it is actually harder to NOT keep track of the order of appearance. – btilly Aug 16 '17 at 19:03
2

An O(n) method is possible:

def countways(n):
    A=[1,1,2]
    while len(A)<=n:
        A.append(A[-1]+A[-2]+A[-3])
    return A[n]

The idea is that we can work out how many ways of making a sequence with n by considering each choice (1,2,3) for the last partition size.

e.g. to count choices for (1,1,1,1) consider:

  1. choices for (1,1,1) followed by a 1
  2. choices for (1,1) followed by a 2
  3. choices for (1) followed by a 3

If you need the results (instead of just the count) you can adapt this approach as follows:

cache = {}
def countwaysb(n):
    if n < 0:
        return []
    if n == 0:
        return [[]]
    if n in cache:
        return cache[n]
    A = []
    for last in range(1,4):
        for B in countwaysb(n-last):
            A.append(B+[last])
    cache[n] = A   
    return A
Peter de Rivaz
  • 33,126
  • 4
  • 46
  • 75