4

I am trying to create a function that returns a dictionary that describes a pascal triangle.

For example,

pascal(3)

would give me

{1: [1], 2: [1,1], 3: [1,2,1]} 

I currently know how to create a function that returns the list of elements in a certain row for n equal to or greater than 2

def pascal(n):
 if n == 0:
    return {}
 elif n == 1:
    return {1:[1]}
 else:
    row = [1] + [list(pascal(n-1))[i] + list(pascal(n-1))[i+1] for i in range(n-2)] + [1]
    return row

With this function,

pascal(3)

gives me

[1,2,1]

Is it possible to change my function in such a way that

pascal(3)

returns the desired result of

{1: [1], 2: [1,1], 3: [1,2,1]} 

Any help would be appreciated.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    are you open to having a list of lists instead of a dict? the keys of your dict are just line indices anyways – Srini Nov 06 '18 at 22:43

5 Answers5

2

You can use zip to pair the returning list from the recursive call with the same list but at one index apart, padded with 0:

def pascal(n):
    if n == 1:
        return {1: [1]}
    p = pascal(n - 1)
    p[n] = list(map(sum, zip([0] + p[n - 1], p[n - 1] + [0])))
    return p

so that:

for n in range(1, 6):
    print(pascal(n))

outputs:

{1: [1]}
{1: [1], 2: [1, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1], 4: [1, 3, 3, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1], 4: [1, 3, 3, 1], 5: [1, 4, 6, 4, 1]}
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • The OP's list comprehension seems just fine. So instead of using `zip`, they can also replace it with what they had before: `p[n] = [1] + [p[n - 1][i] + p[n - 1][i + 1] for i in range(n - 2)] + [1]` – slider Nov 06 '18 at 23:35
  • Yes, but I prefer `zip` because it is generally considered more Pythonic to use iterators rather than list indices. – blhsing Nov 06 '18 at 23:42
1

If you are open to an iterative solution, I cooked up up the following.

from itertools import chain 

def pascal(n):
    pad = (0,)
    result = {1: [1]}
    for i in range(2, n + 1):
        previous = list(chain(pad, result[i - 1], pad))
        result[i] = [sum(pair) for pair in zip(previous, previous[1:])]
    return result

Demo:

>>> for n in range(1, 6):
...:    print(pascal(n))
...:    
...:    
{1: [1]}
{1: [1], 2: [1, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1], 4: [1, 3, 3, 1]}
{1: [1], 2: [1, 1], 3: [1, 2, 1], 4: [1, 3, 3, 1], 5: [1, 4, 6, 4, 1]}

With a bit more lines, but also better memory efficiency:

from itertools import chain, tee

def pascal(n):
    pad = (0,)
    result = {1: [1]}
    for i in range(2, n + 1):
        previous = chain(pad, result[i - 1], pad)
        c1, c2 = tee(previous)
        next(c2)
        result[i] = [sum(pair) for pair in zip(c1, c2)]
    return result

Lastly, having a dict with consecutive integer keys is not very useful, you could just use a list into which you index starting at 0. Final solution:

def pascal(n):
    pad = (0,)
    result = [[1]]
    for i in range(1, n):
        previous = chain(pad, result[i - 1], pad)
        c1, c2 = tee(previous)
        next(c2)
        result.append([sum(pair) for pair in zip(c1, c2)])
    return result

Demo:

>>> for n in range(1, 6):
...:    print(pascal(n))
...:    
[[1]]
[[1], [1, 1]]
[[1], [1, 1], [1, 2, 1]]
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]

edit: improved efficiency by not creating two tuples per iteration, instantiating pad once is enough.

timgeb
  • 76,762
  • 20
  • 123
  • 145
0

I would be careful using recursion like that - it's very inefficient. You are calling the function twice in a loop in the function body. It's important to think about how many times the function will be called to evaluate certain values of n.

It's obvious that when n = 1, the function is called once.

When n = 2, the function is called once, and then the function calls itself twice for a total of 3 calls.

For n = 3 the function is called once, and then the functions calls itself twice, and then these two calls each call the function four times... So that's 11 calls.

So the number of calls is numCalls = 1 + 2 + 2*4 + 2*4*6 + ... + 2*4*6*...*2n)

This sequence grows extremely fast... When n is 20 that's 1308293051285742128434781 Calls

Recursion isn't always evil, you just have to be careful, this solution calls itself n times:

    def genPascalDict(nMax):
        if nMax < 2:
            return {1: [1]}
        else:
            pascalDict = genPascalDict(nMax - 1)
            lastRow = pascalDict[nMax - 1]
            pascalDict[nMax] = [1] + [lastRow[n + 1] + lastRow[nMax - n - 2] for n in range(nMax - 2)] + [1]
            return pascalDict
Theo Emms
  • 293
  • 1
  • 7
0

You can make it fast while building your dict as a side effect:

_cache = {}

def pascal(n):
    try:
        result = _cache[n]
    except KeyError:
        if n == 0:
            result = []
        elif n == 1:
            result = [1]
        else:
            previous = pascal(n - 1)
            result = [1] + [previous[i] + previous[i + 1] for i in range(n - 2)] + [1]
        _cache[n] = result
    return result

pascal(500)

print(_cache)

You don't need to compute pascal(n) more than once: it's not like it changes. So remember what your final answer was by storing it in a caching dictionary, said dictionary being what you actually wanted in the first place.

This takes about .08s to build the dictionary on my laptop.

Kirk Strauser
  • 30,189
  • 5
  • 49
  • 65
0

You can use a closure with recursion:

def pascal(n:int) -> dict:
  def _pascal(_s, _e, _last, _base={1:[1], 2:[1, 1]}):
    return _last if not _e-_s else _pascal(_s+1, _e, {**_last, **{_s:_base.get(_s, [1, *[_last[_s-1][i]+_last[_s-1][i+1] for i in range(len(_last)-1)], 1])}})
  return _pascal(1, n+1, {})

print(pascal(3))

Output:

{1: [1], 2: [1, 1], 3: [1, 2, 1]}
Ajax1234
  • 69,937
  • 8
  • 61
  • 102