0

I went through similar posts on the forum but all of them suggest using itertools.product but I was wondering if it can be solved without using it.

I want to print all the combinations of outcomes for N flips of a coin. This can be done if N is known in advance. So the number of nested loops will be just N. But if N has to be determined dynamically (input() function) then I am stuck in implementing it in code. In plain English it is easy to imagine that the number of for loops is proportional to N, but how do I implement it? Do I have to use lambdas or recursion? Below is as example code for N = 4.

results = ["H", "T"]
outcomes = []
for l1 in results:
    for l2 in results:
        for l3 in results:
            for l4 in results:
                outcomes.append(l1+l2+l3+l4)

for o in outcomes:
    print(o)  

Thanks in advance.

Sandeep
  • 1,237
  • 1
  • 14
  • 29

2 Answers2

4

DIY with generators

Here's one way to calculate a product of lists without using the built-in

def product (*iters):
  def loop (prod, first = [], *rest):
    if not rest:
      for x in first:
        yield prod + (x,)
    else:
      for x in first:
        yield from loop (prod + (x,), *rest)
  yield from loop ((), *iters)

for prod in product ("ab", "xyz"):
  print (prod)

# ('a', 'x')
# ('a', 'y')
# ('a', 'z')
# ('b', 'x')
# ('b', 'y')
# ('b', 'z')

In python, we can collect the outputs of a generator in a list by using the list constructor. Note we can also calculate the product of more than two inputs as seen below

print (list (product ("+-", "ab", "xyz")))
# [ ('+', 'a', 'x')
# , ('+', 'a', 'y')
# , ('+', 'a', 'z')
# , ('+', 'b', 'x')
# , ('+', 'b', 'y')
# , ('+', 'b', 'z')
# , ('-', 'a', 'x')
# , ('-', 'a', 'y')
# , ('-', 'a', 'z')
# , ('-', 'b', 'x')
# , ('-', 'b', 'y')
# , ('-', 'b', 'z')
# ]

Because product accepts a a list of iterables, any iterable input can be used in the product. They can even be mixed as demonstrated below

print (list (product (['@', '%'], range (2), "xy")))
# [ ('@', 0, 'x')
# , ('@', 0, 'y')
# , ('@', 1, 'x')
# , ('@', 1, 'y')
# , ('%', 0, 'x')
# , ('%', 0, 'y')
# , ('%', 1, 'x')
# , ('%', 1, 'y')
# ]

Because product is defined as a generator, we are afforded much flexibility even when writing more complex programs. Consider this program that finds right triangles made up whole numbers, a Pythagorean triple. Also note that product allows you to repeat an iterable as input as see in product (r, r, r) below

def is_triple (a, b, c):
  return a * a + b * b == c * c

def solver (n):
  r = range (1, n)
  for p in product (r, r, r):
    if is_triple (*p):
      yield p

print (list (solver (20)))
# (3, 4, 5)
# (4, 3, 5)
# (5, 12, 13)
# (6, 8, 10)
# (8, 6, 10)
# (8, 15, 17)
# (9, 12, 15)
# (12, 5, 13)
# (12, 9, 15)
# (15, 8, 17)

Implementing your coin tossing program is easy now.

def toss_coins (n):
  sides = [ 'H', 'T' ]
  coins = [ sides ] * n
  yield from product (*coins)

print (list (toss_coins (2)))
# [ ('H', 'H'), ('H', 'T'), ('T', 'H'), ('T', 'T') ]

print (list (toss_coins (3)))
# [ ('H', 'H', 'H'), ('H', 'H', 'T'), ('H', 'T', 'H'), ('H', 'T', 'T'), ('T', 'H', 'H'), ('T', 'H', 'T'), ('T', 'T', 'H'), ('T', 'T', 'T') ]

Without generators

But generators are a very high-level language feature and we wonder how we could represent product using pure recursion. Below product is reimplemented without the use of generators and now returns a filled array with all calculated sub-products

def map (f, lst):
  if not lst:
    return []
  else:
    first, *rest = lst
    return [ f (first ) ] + map (f, rest)

def flat_map (f, lst):
  if not lst:
    return []
  else:
    first, *rest = lst
    return f (first) + flat_map (f, rest)

def product (*iters):
  def loop (acc, iters):
    if not iters:
      return acc
    else:
      first, *rest = iters
      return flat_map (lambda c: map (lambda x: [x] + c, first), loop (acc, rest))
  return loop ([[]], iters)

We can now skip the yield and list calls in your program

def toss_coins (n):
  sides = [ 'H', 'T' ]
  coins = [ sides ] * n
  return product (*coins)

print (toss_coins (2))
# [('H', 'H'), ('H', 'T'), ('T', 'H'), ('T', 'T')]

print (toss_coins (3))
# [('H', 'H', 'H'), ('H', 'H', 'T'), ('H', 'T', 'H'), ('H', 'T', 'T'), ('T', 'H', 'H'), ('T', 'H', 'T'), ('T', 'T', 'H'), ('T', 'T', 'T')]

Above, we define map and flat_map with as few dependencies as possible, however there is only one subtle distinction in each implementation. Below, we represent each as a fold (using reduce) allowing us to see the semantic difference more easily. Also note Python includes its own version of map and reduce (in functools) that differ slightly from the versions provided here.

def concat (xs, ys):
  return xs + ys

def append (xs, x):
  return xs + [ x ]

def reduce (f, init, lst):
  if not lst:
    return init
  else:
    first, *rest = lst
    return reduce (f, f (init, first), rest)

def map_reduce (m, r):
  return lambda acc, x: r (acc, m (x))

def map (f, lst):
  return reduce (map_reduce (f, append), [], lst)

def flat_map (f, lst):
  return reduce (map_reduce (f, concat), [], lst)

def product (*iters):
  # this stays the same
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • This works very well and I will mark as the answer, however my mind is stuck in finding a solution using recursion. Is there a way to use the suggested answer here https://stackoverflow.com/questions/7186518/function-with-varying-number-of-for-loops-python to adapt to my coin flipping problem. – Sandeep Apr 08 '18 at 15:21
  • 2
    Sandeep, I included an edit that shows how to write `product` without generators, but note that all solutions here are using recursion. – Mulan Apr 08 '18 at 15:41
-1
x=int(input("No. of coin: "))
range_lst=[[] for i in range(2**x)]
i=0
while i<x:
    div=2**(x-1-i)
    alt=0
    for j in range(2**x):
        if alt<div:    
            range_lst[j].append("H")
            alt+=1
        else:
            if alt==((div*2)-1):
                alt-=((div*2)-1)
            else:
                alt+=1
            range_lst[j].append("T")
    i+=1
print(range_lst)
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 27 '22 at 06:07