2

Assuming a simplified football scoring system where you can score {2, 3, 7} points, what are all the possible ways that you can score given a number of points?

I'm just working problems in the EPI book, and he solves this problem using DP. I wanted to practice a recursive technique, but I'm getting tripped up trying to save all the different ways in to a results list.

def find_scoring(points, scores):

    ways_to_score = [2, 3, 7]

    def score_finder(p, scores):
        print(scores, p)
        if p == 0:
            print('here')
            results.append(scores)
            return True
        elif p <= 1:
            return False

        for i in ways_to_score:
            scores.append(i)
            if not score_finder(p-i, scores):
                scores.pop()
                return False

    results = []
    score_finder(points, scores)
    return results

print(find_scoring(12, []))

For the above, I would expect to see [[2,2,2,2,2,2], [2,2,2,3,3], ....]

I cannot figure out how to pass the scores variable properly. Appreciate the assist!

Sean Mahoney
  • 131
  • 7

3 Answers3

2
  • results.append(scores) only adds a reference to scores when we need a copy. We can do this with a slice: scores[:].
  • There is no need for if not score_finder(p-i, scores):; we want to pop regardless of whether we had a successful result generated in a child node or not.
  • It's a bit of a burden on the caller to pass the temporary scores list into the score_finder function.
  • In contrast, it's nice to parameterize the ways_to_score so that the caller can specify it, but use a default argument so they don't have to.

Here's a possible rewrite which addresses these issues and cleans up the logic a bit:

def find_scoring(points, ways_to_score=[2, 3, 7]):
    def score_finder(points, scores, result):
        if points == 0:
            result.append(scores[:])
        elif points > 0:
            for val in ways_to_score:
                scores.append(val)
                score_finder(points - val, scores, result)
                scores.pop()

        return result

    return score_finder(points, [], [])

We can also return a generator:

def find_scoring(points, ways_to_score=[2, 3, 7]):
    def score_finder(points, scores, result):
        if points == 0:
            yield scores[:]
        elif points > 0:
            for val in ways_to_score:
                scores.append(val)
                yield from score_finder(points - val, scores, result)
                scores.pop()

    yield from score_finder(points, [], [])

if __name__ == "__main__":
    print(list(find_scoring(12)))
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Thanks ggorlen. Exactly what I was looking for. So the yield resets the scores list? – Sean Mahoney Sep 04 '19 at 00:34
  • No--`yield` just means the function returns a [generator](https://wiki.python.org/moin/Generators) which enables lazy iteration. It's just a nice way to keep the memory footprint a bit lower, and is a common pattern in libraries that deal in similar functions like [`itertools`](https://docs.python.org/3/library/itertools.html). The important stuff is all in the top version. The reason the generator version doesn't require `scores[:]` is mainly because of `scores + [val]`, so a new list is made for each recursive call (`append`/`pop` is slightly more memory-friendly). – ggorlen Sep 04 '19 at 00:54
  • `append` is also much faster; `scores + [val]` has to copy the contents of `scores` into the new list created by `+`. – chepner Sep 04 '19 at 12:07
  • `chain(scores, [val])` would be better, as it essentially creates a new iterator (not a new list) that simply has references to two other iterables; when the first is exhausted, it moves to the second. – chepner Sep 04 '19 at 12:09
  • 1
    You're right. I never liked the concat version so I rolled it back--looks good but too wasteful. Thanks. – ggorlen Sep 04 '19 at 15:52
2

You can use a function that iterates through the possible scores and deduct the score from the given target points before making a recursive call, until the target points reaches 0:

def find_scoring(points, ways_to_score):
    if points:
        for score in ways_to_score:
            if points - score >= 0:
                for way_to_score in find_scoring(points - score, ways_to_score):
                    yield [score, *way_to_score]
    else:
        yield []

So that list(find_scoring(12, {2, 3, 7})) returns:

[[2, 2, 2, 2, 2, 2],
 [2, 2, 2, 3, 3],
 [2, 2, 3, 2, 3],
 [2, 2, 3, 3, 2],
 [2, 3, 2, 2, 3],
 [2, 3, 2, 3, 2],
 [2, 3, 3, 2, 2],
 [2, 3, 7],
 [2, 7, 3],
 [3, 2, 2, 2, 3],
 [3, 2, 2, 3, 2],
 [3, 2, 3, 2, 2],
 [3, 2, 7],
 [3, 3, 2, 2, 2],
 [3, 3, 3, 3],
 [3, 7, 2],
 [7, 2, 3],
 [7, 3, 2]]
blhsing
  • 91,368
  • 6
  • 71
  • 106
1

You're having trouble because you've tried to write the entire program in one fell swoop, without testing the pieces as you go. You now have several problems facing you at once.

First of all, you've convoluted your coding blocks and variables: you define score_finder inside find_scoring, create global variables (almost always a bad idea), and initialize those somewhat arbitrarily. I've unraveled your code a bit here:

ways_to_score = [2, 3, 7]
results = []

def score_finder(p, scores):
    print(scores, p)
    if p == 0:
        print('here')
        results.append(scores)
        return True
    elif p <= 1:
        return False

    for i in ways_to_score:
        scores.append(i)
        if not score_finder(p-i, scores):
            scores.pop()
            return False


def find_scoring(points, scores):

    score_finder(points, scores)
    return results


print(find_scoring(12, []))

With that done, you're trying to use scores for more than one purpose, which is where you lose your accounting. Also, you haven't defined well in your mind what score_finder does, as shown by (a) sometimes using the return value, other times not; (b) both returning a value and employing a side effect on a global variable.


To clean your code, back up to the general approach to this sort of problem. When your routine is called, it gets a reference list of scores (2, 3, 7) and a target sum. You have a loop to go through those scores and try each one in turn. If you found a solution, then add it to the list of solutions.

You have two ways to handle the addition: (a) pass the partial solution as part of the recursive call; when you hit your base case, add the full solution to your list; (b) return that "success" Boolean; as you crawl back up to the top level, build the solution, one "return" at a time. Only when you get back to the top level do you add the full solution to that master list.

You're a bit confused; you're trying to do both.

Is that enough to get you moving?

Prune
  • 76,765
  • 14
  • 60
  • 81
  • 1
    Thanks Prune. I'm definitely not trying to argue with you about the style of code, but this particular author uses it extensively in his book (the function within a function). I was merely trying to emulate his work. I'll give your approach a shot with the suggestions you made. Thank you! – Sean Mahoney Sep 03 '19 at 23:02
  • 1
    I think function within a function is best here. The inner func is single-purpose to take the place of a loop, and it needs very specific parameters, so there's no need to expose it publicly. The scoped variables are really nonlocal rather than global, so they're also pretty reasonable but they're easy enough to eliminate. – ggorlen Sep 03 '19 at 23:20