the big picture
This solution hopes to teach you that complex problems can be made by combining solutions to smaller sub-problems. After all, this is often the way we should be thinking about recursive problems. Recursion is a functional heritage and so using it with functional style will yield the best results. This means avoiding things like mutation, variable reassignment, and other side effects.
def powerset(t):
for k in range(len(t)):
yield from combinations(t, k)
yield t
for x in powerset((1,2,3)):
print(x)
The combinations will be ordered in the same way the original input is ordered. If you would like the output to be sorted, simply sort the input first -
()
(1,)
(2,)
(3,)
(1, 2)
(1, 3)
(2, 3)
(1, 2, 3)
Since you asked how to write it, here is combinations
as its own generic function -
def combinations(t, k):
if k <= 0:
yield ()
elif not t:
return
else:
for x in combinations(t[1:], k - 1):
yield (t[0], *x)
yield from combinations(t[1:], k)
itertools
Or you can use the built in itertools.combinations
function, provided by python -
from itertools import combinations # <- import
def powerset(t):
for k in range(len(t)):
yield from combinations(t, k) # <- provided by python
yield t
for x in powerset((3,2,1)): # <- let's try a different input order
print(x)
()
(3,)
(2,)
(1,)
(3, 2)
(3, 1)
(2, 1)
(3, 2, 1)
accept list as input
Notice above there's no need to "generate" the final subset as it is simply t
itself. If you want to accept a list or non-tuple as an input, we can slightly alter powerset
-
def powerset(t):
for k in range(len(t)):
yield from combinations(t, k)
yield tuple(t) # <- coerce tuple
for x in powerset([9,3,6]): # <- list input
print(x)
Before this change the program would output [9,3,6]
instead of the desired (9,3,6)
-
()
(9,)
(3,)
(6,)
(9, 3)
(9, 6)
(3, 6)
(9, 3, 6)
generators
Notice we use yield
to lazily generate the possible subsets. Generators are a good fit for combinatorics problems because often times we do not need to iterate the entire solution space before an answer can be determined. This way the caller can work with the results ad hoc, as they are generated, without necessarily computing every subset.
Show all orders containing fries -
for order in powerset(""): # <- input can be a string too!
if "" in order:
print(order)
('',)
('', '')
('', '')
('', '', '')
Sometimes however you will want all of the results, and that's perfectly okay. The natural way to turn an iterable generator to a list is to use the list
function -
print(list(powerset("")))
All of the possible orders are collected into a list now -
[(), ('',), ('',), ('',), ('', ''), ('', ''), ('', ''), ('', '', '')]
practice makes permanent
LeetCode (and other sites like it) provided exercises that will often start you with some boilerplate code to fill in -
class Solution:
def subsets(self, nums):
# implement the solution here
Writing code in the template sets you up for failure as it unnecessarily tangles your ordinary functions with self
context and makes it difficult to reuse your code elsewhere. As beginners practice over and over, they begin to see this as the correct way to write code. It's not.
Instead write ordinary functions as we have above and simply call them in the awkward Solution
class wrapper -
def combinations(...):
...
def powerset(...):
...
class Solution:
def subsets(self, nums):
return powerset(nums) # <- call your function in the wrapper