Итерация по нескольким индексам с i> j (> k) в питоническом виде

Мне нужно перебирать кортеж индексов. все индексы должны находиться в диапазоне [0, N) с условием i > j. Представленный здесь пример игрушки посвящен только два индекса; Мне нужно будет расширить это до трех (с i > j > k) или более.

Базовая версия такова:

N = 5
for i in range(N):
    for j in range(i):
        print(i, j)

и он работает отлично; выход

1 0
2 0
2 1
3 0
3 1
3 2
4 0
4 1
4 2
4 3

Я не хочу иметь еще один уровень отступов для каждого дополнительного индекса, поэтому я предпочитаю эту версию:

for i, j in ((i, j) for i in range(N) for j in range(i)):
    print(i, j)

это работает отлично, делает то, что нужно, и избавляется от лишних уровень отступов.

Я надеялся иметь что-то более элегантное (для двух индексов, которые не все, что уместно, но для трех или более это становится более актуальным). То, что я придумал, таково:

from itertools import combinations

for j, i in combinations(range(N), 2):
    print(i, j)

Это итерации по одной и той же паре индексов. Единственное, что разным является порядок, в котором появляются пары:

1 0
2 0
3 0
4 0
2 1
3 1
4 1
3 2
4 2
4 3

Поскольку порядок того, что я делаю с этими индексами, имеет значение, поэтому я не могу использовать это.

Есть ли элегантный, короткий, питонический способ перебора этих индексов в том же порядке, что и в первом примере? Имейте в виду, что N будет большим, поэтому сортировка - это не то, что я хотел бы сделать.

Ответы

Ответ 1

Вы можете решить это в целом следующим образом:

def indices(N, length=1):
    """Generate [length]-tuples of indices.

    Each tuple t = (i, j, ..., [x]) satisfies the conditions 
    len(t) == length, 0 <= i < N  and i > j > ... > [x].

    Arguments:
      N (int): The limit of the first index in each tuple.
      length (int, optional): The length of each tuple (defaults to 1).

    Yields:
      tuple: The next tuple of indices.

    """
    if length == 1:
       for x in range(N):
           yield (x,)
    else:
       for x in range(1, N):
            for t in indices(x, length - 1):
                yield (x,) + t

При использовании:

>>> list(indices(5, 2))
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2), (4, 3)]
>>> list(indices(5, 3))
[(2, 1, 0), (3, 1, 0), (3, 2, 0), (3, 2, 1), (4, 1, 0), (4, 2, 0), (4, 2, 1), (4, 3, 0), (4, 3, 1), (4, 3, 2)]

Ответ 2

Здесь существует подход с itertools.combinations, чтобы иметь общее количество уровней -

map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1])

Или немного перекрученный одним способом -

map(tuple,np.array(list(combinations(range(N-1,-1,-1),M)))[::-1])

где N: количество элементов и M: количество уровней.

Пример прогона -

In [446]: N = 5
     ...: for i in range(N):
     ...:     for j in range(i):
     ...:         for k in range(j):  # Three levels here
     ...:             print(i, j, k)
     ...:             
(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)
(4, 1, 0)
(4, 2, 0)
(4, 2, 1)
(4, 3, 0)
(4, 3, 1)
(4, 3, 2)

In [447]: N = 5; M = 3

In [448]: map(tuple,(N-1-np.array(list(combinations(range(N),M))))[::-1])
Out[448]: 
[(2, 1, 0),
 (3, 1, 0),
 (3, 2, 0),
 (3, 2, 1),
 (4, 1, 0),
 (4, 2, 0),
 (4, 2, 1),
 (4, 3, 0),
 (4, 3, 1),
 (4, 3, 2)]

Ответ 3

Вы можете использовать product из itertools, если вы не против неэффективности выкидывания большинства сгенерированных кортежей. (Неэффективность ухудшается по мере увеличения параметра repeat.)

>>> from itertools import product
>>> for p in ((i,j) for (i,j) in product(range(5), repeat=2) if i > j):
...   print p
...
(1, 0)
(2, 0)
(2, 1)
(3, 0)
(3, 1)
(3, 2)
(4, 0)
(4, 1)
(4, 2)
(4, 3)
>>> for p in ((i,j,k) for (i,j,k) in product(range(5), repeat=3) if i > j > k):
...   print p
...
(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)
(4, 1, 0)
(4, 2, 0)
(4, 2, 1)
(4, 3, 0)
(4, 3, 1)
(4, 3, 2)

Обновление: вместо распаковки кортежа, используя индексацию для фильтра. Это позволяет писать код немного компактнее. Для tuples разного размера необходимо изменить только my_filter.

from itertools import product, ifilter
def my_filter(p):
    return p[0] > p[1] > p[2]

for p in ifilter(my_filter, product(...)):
    print p

Ответ 4

Это подход, основанный на наблюдении, что легче генерировать отрицательные значения индексов в (обратном) желаемом порядке. Он похож на подход @Дивакара и, как и тот, имеет недостаток в требовании списка для создания в памяти:

def decreasingTuples(N,k):
    for t in reversed(list(itertools.combinations(range(1-N,1),k))):
        yield tuple(-i for i in t)

>>> for t in decreasingTuples(4,2): print(t)

(1, 0)
(2, 0)
(2, 1)
(3, 0)
(3, 1)
(3, 2)
>>> for t in decreasingTuples(4,3): print(t)

(2, 1, 0)
(3, 1, 0)
(3, 2, 0)
(3, 2, 1)

Ответ 5

несколько "хакерская" попытка с использованием eval (просто добавив это для полноты. здесь более приятные ответы!).

Идея состоит в том, чтобы построить строку типа

'((a, b, c) for a in range(5) for b in range(a) for c in range(b))'

и верните eval из этого:

def ijk_eval(n, depth):
    '''
    construct a string representation of the genexpr and return eval of it...
    '''

    var = string.ascii_lowercase
    assert len(var) >= depth > 1  # returns int and not tuple if depth=1

    for_str = ('for {} in range({}) '.format(var[0], n) +
               ' '.join('for {} in range({})'.format(nxt, cur)
                        for cur, nxt in zip(var[:depth-1], var[1:depth])))
    return eval('(({}) {})'.format(', '.join(var[:depth]), for_str))

может использоваться таким образом и дает правильные результаты.

for i, j in ijk_eval(n=5, depth=2):
    print(i, j)

конструкция не очень приятная, но результат: это регулярный genexpr и такой же эффективный, как и те, что есть.