{"id":"dsa-backtracking-subsets","slug":"subsets","title":"Subsets: take it or leave it","text":"Which toppings could go on a pizza? Which of your songs fit on a 60-minute playlist? In what order could five friends sit at a table? Questions like these have no clever formula: a program has to go through the options one by one. For two toppings, two nested loops would do, and for three toppings, three loops. But how many toppings there are is only known when the program runs, and you can't write a loop for each one. A recursive function can do it: one call settles **one** decision, and a call one level deeper settles the next.\n\nWhen a call has seen everything that can follow from its decision, it takes the decision back and tries its next option. That is **backtracking**, and every function in this module is made of the same three moves: **choose** an option, **explore** what follows from it with a recursive call, **un-choose** it.\n\n### Take it or leave it\n\nA **subset** of a list is some of its items, anything from none of them to all of them. Each item is either in or out, so three items give 2 × 2 × 2 = 8 subsets. Drawn as a tree, each level settles one item (the question on the right): taking the item goes left, leaving it out goes right. The bottom row has the 8 subsets:\n\n```text\n                    []                      a?\n              /            \\\n           [a]              []              b?\n         /     \\          /     \\\n     [a, b]    [a]      [b]      []         c?\n     /   \\     /  \\     /  \\     /  \\\n   abc   ab   ac   a   bc   b   c   (none)\n```\n\nA recursive function walks this tree from left to right:\n\n```python\ndef explore(items, i, path):\n    if i == len(items):\n        print(path)\n        return\n    path.append(items[i])         # choose: take items[i]\n    explore(items, i + 1, path)   # explore every subset with it\n    path.pop()                    # un-choose\n    explore(items, i + 1, path)   # explore every subset without it\n\n\nexplore([\"a\", \"b\", \"c\"], 0, [])\n```\n\n```output\n['a', 'b', 'c']\n['a', 'b']\n['a', 'c']\n['a']\n['b', 'c']\n['b']\n['c']\n[]\n```\n\n### How it works\n\n- The parameter `i` is the position of the item this call settles. Once `i` reaches `len(items)`, nothing is left to settle and `path` is one finished subset, so the call prints it: that's the base case.\n- There is only **one** list, `path`, shared by all the calls. Taking an item appends it, and `path.pop()` takes it off again, so the \"leave it out\" call starts from exactly the path this call was given. That undo is the heart of backtracking: without it, `a` would still be in the path while the search explores the subsets without `a`.\n- The first four lines all hold `a`: the search finishes everything below \"take `a`\" before it starts on \"leave `a`\". The output is the bottom row of the tree, read from left to right.\n\n### Saving the subsets: copy the path\n\nTo return the subsets instead of printing them, append each complete one to a result list. The obvious line has a trap:\n\n```python\ndef collect(items, i, path, result):\n    if i == len(items):\n        result.append(path)\n        return\n    path.append(items[i])\n    collect(items, i + 1, path, result)\n    path.pop()\n    collect(items, i + 1, path, result)\n\n\nresult = []\ncollect([1, 2, 3], 0, [], result)\nprint(result)\n```\n\n```output\n[[], [], [], [], [], [], [], []]\n```\n\nEight empty lists. `result.append(path)` doesn't store the numbers the path holds right now: it stores a reference to the list `path` itself, the same list eight times. Every `append` on `path` was undone by a `pop`, so at the end `path` is empty, and so is every \"subset\" in the result. Store a copy instead, `result.append(path[:])` (or `list(path)`), and the result is `[[1, 2, 3], [1, 2], [1, 3], [1], [2, 3], [2], [3], []]`.\n\n### What it costs\n\nEvery item you add doubles the number of subsets: n items have 2ⁿ of them. The playlist question over 15 songs means 32,768 subsets, over 25 songs about 33 million, and over 40 songs about a trillion. The tree has 2ⁿ⁺¹ - 1 calls in all, and copying a subset takes up to n steps, so collecting every subset is O(n · 2ⁿ) time, and the result needs as much memory. The search itself only keeps the calls on the current path: n + 1 calls deep, O(n) extra memory, far from Python's limit of about 1000 calls. Going through every option like this is called **exhaustive search**. It's fine for 20 items and hopeless for 50, unless whole branches of the tree can be skipped, as step 3 shows.\n","task":"This version leaves each item out before it takes it, so it starts from the empty subset. Fill in the choose and the un-choose so that it prints all 8 subsets of `[1, 2, 3]`:\n\n```\n[]\n[3]\n[2]\n[2, 3]\n[1]\n[1, 3]\n[1, 2]\n[1, 2, 3]\n```\n","kind":"fill","seed":"def show(items, i, path):\n    if i == len(items):\n        print(path)\n        return\n    show(items, i + 1, path)\n    [[path.append(items[i])]]\n    show(items, i + 1, path)\n    [[path.pop()‖del path[-1]‖path.pop(-1)]]\n\n\nshow([1, 2, 3], 0, [])\n","solution":"def show(items, i, path):\n    if i == len(items):\n        print(path)\n        return\n    show(items, i + 1, path)\n    path.append(items[i])\n    show(items, i + 1, path)\n    path.pop()\n\n\nshow([1, 2, 3], 0, [])\n","hints":["The first call leaves `items[i]` out. Before the second call, take it: put it on the end of the path.","After the second call, undo the take so the caller gets its path back as it was: the first blank is `path.append(items[i])`, and the second takes the last item off again."],"tests":[{"name":"Output","stdin":"","expect":"[]\n[3]\n[2]\n[2, 3]\n[1]\n[1, 3]\n[1, 2]\n[1, 2, 3]","hidden":false}],"require":[],"forbid":[],"more":[{"task":"`subsets_with_sum(numbers, target)` should return every subset of `numbers` whose numbers add up to `target`, as lists, in the order the search finds them. It finds the right number of subsets, but every one of them comes back empty. Fix it.\n\nFor example, `subsets_with_sum([3, 1, 4, 2], 5)` should return:\n\n```\n[[3, 2], [1, 4]]\n```\n","kind":"code","seed":"def search(numbers, i, path, target, found):\n    if i == len(numbers):\n        if sum(path) == target:\n            found.append(path)\n        return\n    path.append(numbers[i])\n    search(numbers, i + 1, path, target, found)\n    path.pop()\n    search(numbers, i + 1, path, target, found)\n\n\ndef subsets_with_sum(numbers, target):\n    found = []\n    search(numbers, 0, [], target, found)\n    return found\n\n\nif __name__ == \"__main__\":\n    print(subsets_with_sum([3, 1, 4, 2], 5))\n","solution":"def search(numbers, i, path, target, found):\n    if i == len(numbers):\n        if sum(path) == target:\n            found.append(path[:])\n        return\n    path.append(numbers[i])\n    search(numbers, i + 1, path, target, found)\n    path.pop()\n    search(numbers, i + 1, path, target, found)\n\n\ndef subsets_with_sum(numbers, target):\n    found = []\n    search(numbers, 0, [], target, found)\n    return found\n\n\nif __name__ == \"__main__\":\n    print(subsets_with_sum([3, 1, 4, 2], 5))\n","hints":["Add `print(found)` right after the `append` in the base case. It shows the right subset when it's found. What has happened to it by the end?","`found.append(path)` puts the list `path` itself into `found`, not the numbers it holds at that moment. The `pop` calls later empty that one list, and every entry of `found` is that list.","Append a copy of the path instead: `path[:]` or `list(path)`."],"tests":[{"name":"subsets_with_sum([3, 1, 4, 2], 5)","stdin":"","call":"subsets_with_sum([3, 1, 4, 2], 5)\n","expect":"[[3, 2], [1, 4]]","hidden":false,"returns":true},{"name":"subsets_with_sum([1, 2, 3], 3)","stdin":"","call":"subsets_with_sum([1, 2, 3], 3)\n","expect":"[[1, 2], [3]]","hidden":true,"returns":true},{"name":"subsets_with_sum([5, 5], 5)","stdin":"","call":"subsets_with_sum([5, 5], 5)\n","expect":"[[5], [5]]","hidden":true,"returns":true},{"name":"subsets_with_sum([2, 4], 7)","stdin":"","call":"subsets_with_sum([2, 4], 7)\n","expect":"[]","hidden":true,"returns":true},{"name":"subsets_with_sum([], 0)","stdin":"","call":"subsets_with_sum([], 0)\n","expect":"[[]]","hidden":true,"returns":true},{"name":"subsets_with_sum([-1, 1, 2, 0], 1)","stdin":"","call":"subsets_with_sum([-1, 1, 2, 0], 1)\n","expect":"[[-1, 2, 0], [-1, 2], [1, 0], [1]]","hidden":true,"returns":true},{"name":"Test 7","stdin":"","call":"found = subsets_with_sum([1] * 10, 5)\nprint(len(found), found[0], found[-1])\n","expect":"252 [1, 1, 1, 1, 1] [1, 1, 1, 1, 1]","hidden":true}],"require":[],"forbid":[]},{"task":"Write `count_with_sum(numbers, target)`, which returns how many subsets of `numbers` add up to exactly `target`. The numbers can be negative or zero, and the empty subset adds up to 0. You don't need the subsets themselves: pass down how much of the target is still missing, and add up the counts of the two branches.\n\nFor example, `count_with_sum([7, 2, 5, 4, 9], 11)` returns 3, for 7 + 4, 2 + 9 and 2 + 5 + 4:\n\n```\n3\n```\n","kind":"code","seed":"def count_with_sum(numbers, target):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(count_with_sum([7, 2, 5, 4, 9], 11))\n","solution":"def count_from(numbers, i, remaining):\n    if i == len(numbers):\n        return 1 if remaining == 0 else 0\n    take = count_from(numbers, i + 1, remaining - numbers[i])\n    leave = count_from(numbers, i + 1, remaining)\n    return take + leave\n\n\ndef count_with_sum(numbers, target):\n    return count_from(numbers, 0, target)\n\n\nif __name__ == \"__main__\":\n    print(count_with_sum([7, 2, 5, 4, 9], 11))\n","hints":["Write a helper, `count_from(numbers, i, remaining)`: in how many ways can the items from `numbers[i]` on make up `remaining`?","The base case is `i == len(numbers)`: every item is decided. That subset counts 1 when `remaining == 0`, and 0 otherwise.","Otherwise add two counts: taking `numbers[i]` (pass `remaining - numbers[i]`) and leaving it (pass `remaining`). Don't stop early when `remaining` reaches 0: a 0 or a -1 and a 1 later on can still make more subsets."],"tests":[{"name":"count_with_sum([7, 2, 5, 4, 9], 11)","stdin":"","call":"count_with_sum([7, 2, 5, 4, 9], 11)\n","expect":"3","hidden":false,"returns":true},{"name":"count_with_sum([6, 2], 0)","stdin":"","call":"count_with_sum([6, 2], 0)\n","expect":"1","hidden":true,"returns":true},{"name":"count_with_sum([6, 2], 5)","stdin":"","call":"count_with_sum([6, 2], 5)\n","expect":"0","hidden":true,"returns":true},{"name":"count_with_sum([-2, 3, 1, 2], 1)","stdin":"","call":"count_with_sum([-2, 3, 1, 2], 1)\n","expect":"3","hidden":true,"returns":true},{"name":"count_with_sum([4, -4, 6], 0)","stdin":"","call":"count_with_sum([4, -4, 6], 0)\n","expect":"2","hidden":true,"returns":true},{"name":"count_with_sum([0, 0, 5], 5)","stdin":"","call":"count_with_sum([0, 0, 5], 5)\n","expect":"4","hidden":true,"returns":true},{"name":"count_with_sum([], 0)","stdin":"","call":"count_with_sum([], 0)\n","expect":"1","hidden":true,"returns":true},{"name":"count_with_sum([7, -3, 4, 2, -6, 5, 1], 3)","stdin":"","call":"count_with_sum([7, -3, 4, 2, -6, 5, 1], 3)\n","expect":"8","hidden":true,"returns":true},{"name":"count_with_sum([1] * 16, 8)","stdin":"","call":"count_with_sum([1] * 16, 8)\n","expect":"12870","hidden":true,"returns":true}],"require":[],"forbid":[]}]}