Home > Software design >  What's the mathematical formula for a nested loop?
What's the mathematical formula for a nested loop?

Time:07-06

Imagine this loop:

for i in range(3):
 for j in range(3):
  for k in range(3):
    print("{}{}{}".format(letter, number, symbol))

I want to generate this sequence numerically - without a nested for loop, by manipulating the i, j and k values. What's the formula to covering every permutation of these 3 values?

Given 3 numbers I, j, k how do I calculate the next position without repeats? I should add that i, j, k might have different sizes.

I've tried using python itertools permutations and I've used a counting algorithm.

I tried to generate it with a counting algorithm. But I think there's something obvious in my counting algorithm that I'm missing, but I'm not sure what it is.

Imagine a function called .tick that produces the next item in the sequence without repeats.

My counting algorithm looks like this. You can find it on Repl.it here.

class Tick:
  def __init__(self, collections, func):
    self.collections = collections
    self.indexes = []
    self.loops = 0
    self.reset = -1
    self.func = func
    self.started = False
    for collection in collections:
      self.indexes.append(0)

  def size(self):
    total = len(self.collections[0])
    for collection in self.collections[1:]:
      total = total * len(collection)
    return total


  def tick(self):
    if self.started:
      if self.reset != -1:
         self.indexes[self.reset   1] = 0
         for index in range(self.reset   1, len(self.indexes)):
           
           self.indexes[index] = 0
         self.reset = -1
       
      else:
       self.loop = len(self.indexes) - 1
       while self.loop != -1 and self.indexes[self.loop] == len(self.collections[self.loop]) - 1:
         self.loop = self.loop - 1

       if self.loop == -1:
         return
       
       self.indexes[self.loop] = self.indexes[self.loop]   1
       if self.loop < len(self.indexes) - 1:
         self.reset = self.loop
    else:
      self.started = True

    items = []
    for loop in range(len(self.indexes)):
      items.append(self.collections[loop][self.indexes[loop]])
    return self.func(items)
  
a = ["0", "1", "2"]      
b  = ["0", "1", "2"]
c = ["0", "1", "2"]

def printer(items):
  output = ""
  for item in items:
    output  = item
  return output

ticker = Tick([a, b, c], printer)
print(ticker.size())
for index in range(ticker.size()):
  print(ticker.tick())

Unfortunately it produces the following

27
000
001
002
012
010
011
012
022
020
021
022
122
100
101
102
112
110
111
112
122
120
121
122
222
200
201
202

Notice the following are duplicated. I used this duplicate line finder.

COUNT    | LINE
-----------------------------------------------------
       3 |     122
       2 |     012
       2 |     022
       2 |     112
       1 |     000
       1 |     001
       1 |     002
       1 |     010
       1 |     011
       1 |     020
       1 |     021
       1 |     100
       1 |     101
       1 |     102
       1 |     110
       1 |     111
       1 |     120
       1 |     121
       1 |     200
       1 |     201
       1 |     202
       1 |     222
-----------------------------------------------------
      27 | TOTAL LINES
      22 | DISTINCT LINES

CodePudding user response:

If I understand your question correctly, you are looking for a way to: Given a number n, find the n-th element in the product of the different lists, without generating all the elements before that. You can do that similar to converting a number to a different base, except there might be a different base for each digit, and instead of returning the digits themselves, you are getting those elements from the corresponding lists.

def nth_product(list_of_lists, n):
    res = []
    # reversed to match order of itertools.product
    for lst in reversed(list_of_lists): 
        n, r = divmod(n, len(lst))
        res.append(lst[r])
    return tuple(reversed(res))

Similarly, you can compute the position of a given product in the list of products, to e.g. get the product directly after some other product:

def position(list_of_lists, tup):
    t = 1
    res = 0
    for lst, x in zip(reversed(list_of_lists), reversed(tup)):
        res  = t * lst.index(x)
        t *= len(lst)
    return res

Comparing that with what itertools.product produces:

import itertools
list_of_lists = [[1,2], [3,4,5,6], [7,8,9]]
for n, prod in enumerate(itertools.product(*list_of_lists)):
    print(n, prod, nth_product(list_of_lists, n), prod_position(list_of_lists, prod))

Result:

0 (1, 3, 7) (1, 3, 7) 0
1 (1, 3, 8) (1, 3, 8) 1
...
23 (2, 6, 9) (2, 6, 9) 23

CodePudding user response:

This is the code I ended up using. I had an additional requirement that I wanted the cartesian product to be evenly spread out too! This uses tobias_k answer to produce the nth product of the nested loop. I also wanted to evenly spread the evaluation of nested loops: so rather than 001 002 003 010 011 100 100 101 111 I wanted 111 222 333. Please see this stackoverflow answer, Enumerating Cartesian product while minimizing repetition for the formula for evenly spreading out the cartesian product. Secret, use more modulo!

class Tick:
  def __init__(self, collections, func):
    self.collections = collections
    self.n = 0
    self.func = func
    
    self.current = []
    for item in range(len(self.collections)):
      
      self.current.append(0)
   
  def size(self):
    total = len(self.collections[0])
    for collection in self.collections[1:]:
      total = total * len(collection)
    return total

  def tick(self):
    
    items = []
    n = self.n
    self.indexes = []
    
    
    # reversed to match order of itertools.product
    for collection in reversed(self.collections): 
        n, r = divmod(n, len(collection))
        self.indexes.append(r)

    
    for loop in range(len(self.collections)):
      previous = 0
      for mod in range(0, len(self.collections)):
        previous = previous   self.indexes[mod]
      
      self.indexes[loop] = previous % len(self.collections[loop])
      

    for loop, item in enumerate(self.indexes):
      items.append(self.collections[loop][item])
    
    self.n = self.n   1
    return self.func(items)
  
a = ["a", "b", "c"]      
b  = ["1", "2", "3"]
c = ["÷", "×", "("]

def printer(items):
  output = ""
  for item in items:
    output  = item
  return output

ticker = Tick([a, b, c], printer)
print(ticker.size())
for index in range(ticker.size()):
  print(ticker.tick())

print("CORRECT")
for letter in a:
 for number in b:
  for symbol in c:
    print("{}{}{}".format(letter, number, symbol))

For the curious, this is what it produces:

a1÷
b2(
c3×
b3÷
c1(
a2×
c2÷
a3(
b1×
b3×
c1÷
a2(
c2×
a3÷
b1(
a1×
b2÷
c3(
c2(
a3×
b1÷
a1(
b2×
c3÷
b3(
c1×
a2÷

Notice after the obvious sequence

a1÷ 1 1 1
b2( 2 2 2
c3× 3 3 3

We get

b3÷ which is 2 3 1
c1( which is 3 1 3
a2× which is 1 2 2

And so on, evenly spread out!

  • Related