перестановки з унікальними значеннями


76

itertools.permutations генерує, коли його елементи розглядаються як унікальні залежно від їхнього положення, а не від їх вартості. Тому в основному я хочу уникати таких дублікатів:

>>> list(itertools.permutations([1, 1, 1]))
[(1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1), (1, 1, 1)]

Потім фільтрація неможлива, оскільки в моєму випадку кількість перестановок занадто велика.

Хтось знає відповідний алгоритм для цього?

Дуже дякую!

РЕДАГУВАТИ:

В основному я хочу наступного:

x = itertools.product((0, 1, 'x'), repeat=X)
x = sorted(x, key=functools.partial(count_elements, elem='x'))

що неможливо, оскільки sortedстворює список, а результат itertools.product занадто великий.

Вибачте, я повинен був описати фактичну проблему.


Що занадто велике? ВСЬОГО перестановки або УНІКАЛЬНІ перестановки чи обидва?
FogleBird

4
Є навіть швидше рішення, ніж прийнята відповідь (реалізація Алгоритму Кнута L), подана тут
Геррат,

Ви шукаєте перестановки мультимножин . Дивіться відповідь Білла Белла нижче.
Джозеф Вуд,

Ви пробували for x in permutation() set.add(x)?
NotAnAmbiTurner

Можливо, кращою назвою цього питання буде "чіткі перестановки". А ще краще, "чіткі перестановки списку з дублікатами".
Дон Хетч,

Відповіді:


58
class unique_element:
    def __init__(self,value,occurrences):
        self.value = value
        self.occurrences = occurrences

def perm_unique(elements):
    eset=set(elements)
    listunique = [unique_element(i,elements.count(i)) for i in eset]
    u=len(elements)
    return perm_unique_helper(listunique,[0]*u,u-1)

def perm_unique_helper(listunique,result_list,d):
    if d < 0:
        yield tuple(result_list)
    else:
        for i in listunique:
            if i.occurrences > 0:
                result_list[d]=i.value
                i.occurrences-=1
                for g in  perm_unique_helper(listunique,result_list,d-1):
                    yield g
                i.occurrences+=1




a = list(perm_unique([1,1,2]))
print(a)

результат:

[(2, 1, 1), (1, 2, 1), (1, 1, 2)]

РЕДАКТУЙ (як це працює):

Я переписав вищезазначену програму, щоб вона була довшою, але читабельнішою.

Зазвичай мені важко пояснити, як щось працює, але дозвольте спробувати. Для того, щоб зрозуміти, як це працює, ви повинні зрозуміти подібну, але простішу програму, яка дасть усі перестановки з повтореннями.

def permutations_with_replacement(elements,n):
    return permutations_helper(elements,[0]*n,n-1)#this is generator

def permutations_helper(elements,result_list,d):
    if d<0:
        yield tuple(result_list)
    else:
        for i in elements:
            result_list[d]=i
            all_permutations = permutations_helper(elements,result_list,d-1)#this is generator
            for g in all_permutations:
                yield g

Ця програма, очевидно, набагато простіша: d означає глибину в permutations_helper і має дві функції. Одна функція - умова зупинки нашого рекурсивного алгоритму, а інша - для списку результатів, який передається навколо.

Замість того, щоб повертати кожен результат, ми даємо його. Якби не було функції / оператора, yieldнам довелося б висунути результат у якусь чергу в точці умови зупинки. Але таким чином, як тільки умова зупинки досягнута, результат поширюється по всіх стеках аж до абонента. Це мета
for g in perm_unique_helper(listunique,result_list,d-1): yield g тому кожен результат поширюється до абонента.

Повернімось до оригінальної програми: ми маємо список унікальних елементів. Перш ніж ми зможемо використовувати кожен елемент, ми повинні перевірити, скільки з них ще доступні для натискання на result_list. Робота з цією програмою дуже схожа на permutations_with_replacement. Різниця полягає в тому, що кожен елемент не можна повторити більше разів, ніж у perm_unique_helper.


3
Я намагаюся зрозуміти, як це працює, але я в тупі. Не могли б ви надати якийсь коментар?
Натан

@Nathan Я відредагував відповідь та вдосконалив код. Не соромтеся розміщувати додаткові запитання, які у вас є.
Luka Rahne

1
Гарний шматок коду. Ви повторно впровадили itertools.Counter, так?
Ерік Думініл

Я не знайомий з лічильником itertools. Цей код є більше прикладом і для навчальних цілей, але менше для виробництва через проблеми з продуктивністю. Якщо потрібно краще рішення , яке я хотів би запропонувати ітераційне / без рекурсії відбувається рішення від Нараяна Пандіта , а також пояснюється Donad Кнут в області комп'ютерних програм з можливістю реалізацією пітона в stackoverflow.com/a/12837695/429982
Luka Rahne

Я відтворив це за допомогою itertools.Counter, але, здається, ваш код швидший :)
Roelant

46

Оскільки іноді нові запитання позначаються як дублікати і їх автори посилаються на це питання, може бути важливим згадати, що для цього у symy є ітератор.

>>> from sympy.utilities.iterables import multiset_permutations
>>> list(multiset_permutations([1,1,1]))
[[1, 1, 1]]
>>> list(multiset_permutations([1,1,2]))
[[1, 1, 2], [1, 2, 1], [2, 1, 1]]

8
Це єдина відповідь, яка чітко визначає, що дійсно шукає OP (тобто перестановки мультимножин ).
Джозеф Вуд,

25

Це спирається на детальну реалізацію, що будь-яка перестановка відсортованого ітерабелу знаходиться у відсортованому порядку, якщо це не дублікати попередніх перестановок.

from itertools import permutations

def unique_permutations(iterable, r=None):
    previous = tuple()
    for p in permutations(sorted(iterable), r):
        if p > previous:
            previous = p
            yield p

for p in unique_permutations('cabcab', 2):
    print p

дає

('a', 'a')
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'b')
('b', 'c')
('c', 'a')
('c', 'b')
('c', 'c')

працює чудово, але повільніше, ніж прийняте рішення. Дякую!
xyz-123

Це не відповідає дійсності в нових версіях Python. Наприклад, у Python 3.7.1 list(itertools.permutations([1,2,2], 3))повертає [(1, 2, 2), (1, 2, 2), (2, 1, 2), (2, 2, 1), (2, 1, 2), (2, 2, 1)].
Кірк Штраузер,

@KirkStrauser: Ви маєте рацію. Твердження "будь-яка перестановка відсортованого ітеративного файлу розташовані в порядку сортування" навіть не відповідало дійсності старіших версій Python. Я протестував версії Python ще через 2.7 і визнав ваш результат точним. Цікаво, що це не робить алгоритм недійсним. Він виробляє такі перестановки, що оригінальними є лише максимальні перестановки в будь-якій точці.
Стівен Румбальський

@KirkStrauser: Я повинен внести зміни. Ви неправильні. Я пішов редагувати свою відповідь і уважніше прочитав те, що написав. У моєму викладі був кваліфікувач, який зробив це правильним: "будь-які перестановки відсортованого ітерабелу розташовані в порядку сортування, якщо вони не є дублікатами попередніх перестановок ".
Стівен Румбальський

15

Приблизно так швидко, як відповідь Луки Ране, але коротше та простіше, IMHO.

def unique_permutations(elements):
    if len(elements) == 1:
        yield (elements[0],)
    else:
        unique_elements = set(elements)
        for first_element in unique_elements:
            remaining_elements = list(elements)
            remaining_elements.remove(first_element)
            for sub_permutation in unique_permutations(remaining_elements):
                yield (first_element,) + sub_permutation

>>> list(unique_permutations((1,2,3,1)))
[(1, 1, 2, 3), (1, 1, 3, 2), (1, 2, 1, 3), ... , (3, 1, 2, 1), (3, 2, 1, 1)]

Він працює рекурсивно, встановлюючи перший елемент (перебираючи всі унікальні елементи) та перебираючи перестановки для всіх інших елементів.

Давайте пройдемося через unique_permutations(1,2,3,1), щоб побачити, як це працює:

  • unique_elements складають 1,2,3
  • Давайте переглянемо їх: first_elementпочинається з 1.
    • remaining_elements складають [2,3,1] (тобто 1,2,3,1 мінус перший 1)
    • Ми повторюємо (рекурсивно) через перестановки решти елементів: (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)
    • Для кожного sub_permutationми вставляємо first_element: ( 1 , 1,2,3), ( 1 , 1,3,2), ... і отримуємо результат.
  • Тепер ми робимо ітерацію до first_element= 2 і робимо те саме, що вище.
    • remaining_elements складають [1,3,1] (тобто 1,2,3,1 мінус перші 2)
    • Перебираємо перестановки решти елементів: (1, 1, 3), (1, 3, 1), (3, 1, 1)
    • Для кожного sub_permutationми вставляємо first_element: ( 2 , 1, 1, 3), ( 2 , 1, 3, 1), ( 2 , 3, 1, 1) ... і отримуємо результат.
  • Нарешті, ми робимо те саме з first_element= 3.

13

Ви можете спробувати використовувати set:

>>> list(itertools.permutations(set([1,1,2,2])))
[(1, 2), (2, 1)]

Виклик для встановлення видалених дублікатів


9
Йому може знадобитися список (встановити (itertools.permutations ([1,1,2,2])))
Лука Ране

2
Або list(itertools.permutations({1,1,2,2}))в Python 3+ або Python 2.7, через існування встановлених літералів. Хоча, якщо він не використовує буквальні значення, він все set()одно просто буде використовувати . І @ralu: ще раз подивіться на питання, фільтрування після цього буде дорогим.
JAB,

32
set (permutations (somelist))! = permutations (set (somelist))
Luka Rahne

1
проблема з цим полягає в тому, що мені потрібен вихід, щоб мати довжину вводу. Наприклад, list(itertools.permutations([1, 1, 0, 'x']))але без дублікатів, де вони міняються місцями.
xyz-123

2
@JAB: хм, це займає дуже багато часу для більш ніж 12 значень ... те, що я насправді хочу, це щось на зразок, itertools.product((0, 1, 'x'), repeat=X)але мені потрібно обробляти значення з кількома символами "х" (сортування не підходить, оскільки це генерує список і використовує занадто багато пам'яті).
xyz-123

9

Це моє рішення з 10 рядків:

class Solution(object):
    def permute_unique(self, nums):
        perms = [[]]
        for n in nums:
            new_perm = []
            for perm in perms:
                for i in range(len(perm) + 1):
                    new_perm.append(perm[:i] + [n] + perm[i:])
                    # handle duplication
                    if i < len(perm) and perm[i] == n: break
            perms = new_perm
        return perms


if __name__ == '__main__':
    s = Solution()
    print s.permute_unique([1, 1, 1])
    print s.permute_unique([1, 2, 1])
    print s.permute_unique([1, 2, 3])

--- результат ----

[[1, 1, 1]]
[[1, 2, 1], [2, 1, 1], [1, 1, 2]]
[[3, 2, 1], [2, 3, 1], [2, 1, 3], [3, 1, 2], [1, 3, 2], [1, 2, 3]]

Мені подобається це рішення
jef

Я радий, що вам сподобався цей метод
Little Roys

Привіт @LittleRoys. Я використав трохи змінену версію вашого коду для PR вmore-itertools . Ви добре з цим?
jferard

1
Мені цікаво, чи додає клас якесь значення? Чому це не просто функція?
Дон Хетч

9

Наївним підходом може бути прийняття набору перестановок:

list(set(it.permutations([1, 1, 1])))
# [(1, 1, 1)]

Однак ця методика марно обчислює повторювані перестановки та відкидає їх. Більш ефективний підхід був би more_itertools.distinct_permutations, інструмент третьою стороною .

Код

import itertools as it

import more_itertools as mit


list(mit.distinct_permutations([1, 1, 1]))
# [(1, 1, 1)]

Продуктивність

Використовуючи більший ітератор, ми порівняємо виступи між наївними та сторонніми техніками.

iterable = [1, 1, 1, 1, 1, 1]
len(list(it.permutations(iterable)))
# 720

%timeit -n 10000 list(set(it.permutations(iterable)))
# 10000 loops, best of 3: 111 µs per loop

%timeit -n 10000 list(mit.distinct_permutations(iterable))
# 10000 loops, best of 3: 16.7 µs per loop

Ми бачимо, more_itertools.distinct_permutationsце на порядок швидше.


Деталі

З джерела використовується алгоритм рекурсії (як видно з прийнятої відповіді) для обчислення різних перестановок, тим самим уникаючи марних обчислень. Докладніше див. У вихідному коді .


Прихильний. list(mit.distinct_permutations([1]*12+[0]*12))також виявилося ~ в 5,5 рази швидше, ніж list(multiset_permutations([1]*12+[0]*12))з відповіді @Bill Bell.
Дарконаут

3

Ось рекурсивне рішення проблеми.

def permutation(num_array):
    res=[]
    if len(num_array) <= 1:
        return [num_array]
    for num in set(num_array):
        temp_array = num_array.copy()
        temp_array.remove(num)
        res += [[num] + perm for perm in permutation(temp_array)]
    return res

arr=[1,2,2]
print(permutation(arr))

2

Здається, ви шукаєте itertools.combinations () docs.python.org

list(itertools.combinations([1, 1, 1],3))
[(1, 1, 1)]

8
Ні, комбінації мали б однакову проблему.
JAB

лише впорядковує, наприклад, [1, 2, 3] дасть [1, 2, 3], але не [3, 2, 1] чи [2, 3, 1] тощо
Jk

1

Наткнувся на це питання, шукаючи щось сам!

Ось що я зробив:

def dont_repeat(x=[0,1,1,2]): # Pass a list
    from itertools import permutations as per
    uniq_set = set()
    for byt_grp in per(x, 4):
        if byt_grp not in uniq_set:
            yield byt_grp
            uniq_set.update([byt_grp])
    print uniq_set

for i in dont_repeat(): print i
(0, 1, 1, 2)
(0, 1, 2, 1)
(0, 2, 1, 1)
(1, 0, 1, 2)
(1, 0, 2, 1)
(1, 1, 0, 2)
(1, 1, 2, 0)
(1, 2, 0, 1)
(1, 2, 1, 0)
(2, 0, 1, 1)
(2, 1, 0, 1)
(2, 1, 1, 0)
set([(0, 1, 1, 2), (1, 0, 1, 2), (2, 1, 0, 1), (1, 2, 0, 1), (0, 1, 2, 1), (0, 2, 1, 1), (1, 1, 2, 0), (1, 2, 1, 0), (2, 1, 1, 0), (1, 0, 2, 1), (2, 0, 1, 1), (1, 1, 0, 2)])

В основному, робіть набір і продовжуйте додавати до нього. Краще, ніж складати списки тощо, які забирають занадто багато пам’яті .. Сподіваюся, це допоможе наступній людині, яка звертає увагу :-) Прокоментуйте встановлене «оновлення» у функції, щоб побачити різницю.


Їх , 4слід видалити, щоб він працював на речах будь-якої довжини. Навіть якщо це виправлено, це не є чудовим рішенням. З одного боку, він зберігає всі елементи в пам'яті одночасно, перемагаючи деякі переваги генератора. З іншого боку, це все ще надзвичайно неефективно з точки зору часу, в деяких випадках, коли воно має бути миттєвим. Спробуйте for i in dont_repeat([1]*20+[2]): print i; це займе вічно.
Дон Хетч,

1

Найкраще рішення цієї проблеми, яку я бачив, використовує "Алгоритм L" Кнута (як зазначав раніше Геррат у коментарях до початкової публікації):
http://stackoverflow.com/questions/12836385/how-can-i-interleave- або створіть-унікальні-перестановки-двох укусів-без-повторень / 12837695

Деякі терміни:

Сортування [1]*12+[0]*12(2 704 156 унікальних перестановок):
Алгоритм L → 2,43 с
Розв’язання Люка Ране → 8,56 с
scipy.multiset_permutations()→ 16,8 с


1

Ви можете створити функцію, яка використовує collections.Counterдля отримання унікальних елементів та їх підрахунків із заданої послідовності, а також використовує itertools.combinationsдля вибору комбінацій індексів для кожного унікального елемента в кожному рекурсивному виклику та відображення індексів назад у список, коли всі індекси вибрані:

from collections import Counter
from itertools import combinations
def unique_permutations(seq):
    def index_permutations(counts, index_pool):
        if not counts:
            yield {}
            return
        (item, count), *rest = counts.items()
        rest = dict(rest)
        for indices in combinations(index_pool, count):
            mapping = dict.fromkeys(indices, item)
            for others in index_permutations(rest, index_pool.difference(indices)):
                yield {**mapping, **others}
    indices = set(range(len(seq)))
    for mapping in index_permutations(Counter(seq), indices):
        yield [mapping[i] for i in indices]

так що [''.join(i) for i in unique_permutations('moon')]повертає:

['moon', 'mono', 'mnoo', 'omon', 'omno', 'nmoo', 'oomn', 'onmo', 'nomo', 'oonm', 'onom', 'noom']

1

Для створення унікальних перестановок ["A","B","C","D"]я використовую наступне:

from itertools import combinations,chain

l = ["A","B","C","D"]
combs = (combinations(l, r) for r in range(1, len(l) + 1))
list_combinations = list(chain.from_iterable(combs))

Що породжує:

[('A',),
 ('B',),
 ('C',),
 ('D',),
 ('A', 'B'),
 ('A', 'C'),
 ('A', 'D'),
 ('B', 'C'),
 ('B', 'D'),
 ('C', 'D'),
 ('A', 'B', 'C'),
 ('A', 'B', 'D'),
 ('A', 'C', 'D'),
 ('B', 'C', 'D'),
 ('A', 'B', 'C', 'D')]

Зверніть увагу, що дублікати не створюються (наприклад, елементи в поєднанні з D не створюються, оскільки вони вже існують).

Приклад: Потім це можна використовувати для створення термінів вищого або нижчого порядку для моделей OLS за допомогою даних у фреймі даних Pandas.

import statsmodels.formula.api as smf
import pandas as pd

# create some data
pd_dataframe = pd.Dataframe(somedata)
response_column = "Y"

# generate combinations of column/variable names
l = [col for col in pd_dataframe.columns if col!=response_column]
combs = (combinations(l, r) for r in range(1, len(l) + 1))
list_combinations = list(chain.from_iterable(combs))

# generate OLS input string
formula_base = '{} ~ '.format(response_column)
list_for_ols = [":".join(list(item)) for item in list_combinations]
string_for_ols = formula_base + ' + '.join(list_for_ols)

Створює ...

Y ~ A + B + C + D + A:B + A:C + A:D + B:C + B:D + C:D + A:B:C + A:B:D + A:C:D + B:C:D + A:B:C:D'

Які потім можна спрямувати до вашої регресії OLS

model = smf.ols(string_for_ols, pd_dataframe).fit()
model.summary()

0

Натрапив на це днями під час роботи над власною проблемою. Мені подобається підхід Луки Ране, але я вважав, що використання класу Counter у бібліотеці колекцій здавалося скромним покращенням. Ось мій код:

def unique_permutations(elements):
    "Returns a list of lists; each sublist is a unique permutations of elements."
    ctr = collections.Counter(elements)

    # Base case with one element: just return the element
    if len(ctr.keys())==1 and ctr[ctr.keys()[0]] == 1:
        return [[ctr.keys()[0]]]

    perms = []

    # For each counter key, find the unique permutations of the set with
    # one member of that key removed, and append the key to the front of
    # each of those permutations.
    for k in ctr.keys():
        ctr_k = ctr.copy()
        ctr_k[k] -= 1
        if ctr_k[k]==0: 
            ctr_k.pop(k)
        perms_k = [[k] + p for p in unique_permutations(ctr_k)]
        perms.extend(perms_k)

    return perms

Цей код повертає кожну перестановку у вигляді списку. Якщо ви подасте йому рядок, він дасть вам список перестановок, де кожна з них є списком символів. Якщо ви хочете отримати результат як список рядків (наприклад, якщо ви жахлива людина і хочете зловживати моїм кодом, щоб допомогти вам обдурити в Scrabble), просто виконайте наступне:

[''.join(perm) for perm in unique_permutations('abunchofletters')]

0

Я придумав дуже підходящу реалізацію, використовуючи itertools.product у цьому випадку (це реалізація, де ви хочете всі комбінації

unique_perm_list = [''.join(p) for p in itertools.product(['0', '1'], repeat = X) if ''.join(p).count() == somenumber]

це, по суті, комбінація (n над k) з n = X і кількістю = k itertools.product () ітерації від k = 0 до k = X, подальша фільтрація з підрахунком гарантує, що лише перестановки з потрібною кількістю одиниць список. Ви можете легко переконатися, що це працює, коли обчислюєте n по k і порівнюєте з len (unique_perm_list)


0

Пристосований для видалення рекурсії, використовуйте словник та numba для високої продуктивності, але не використовуючи стиль yield / generator, тому використання пам'яті не обмежується:

import numba

@numba.njit
def perm_unique_fast(elements): #memory usage too high for large permutations
    eset = set(elements)
    dictunique = dict()
    for i in eset: dictunique[i] = elements.count(i)
    result_list = numba.typed.List()
    u = len(elements)
    for _ in range(u): result_list.append(0)
    s = numba.typed.List()
    results = numba.typed.List()
    d = u
    while True:
        if d > 0:
            for i in dictunique:
                if dictunique[i] > 0: s.append((i, d - 1))
        i, d = s.pop()
        if d == -1:
            dictunique[i] += 1
            if len(s) == 0: break
            continue
        result_list[d] = i
        if d == 0: results.append(result_list[:])
        dictunique[i] -= 1
        s.append((i, -1))
    return results
import timeit
l = [2, 2, 3, 3, 4, 4, 5, 5, 6, 6]
%timeit list(perm_unique(l))
#377 ms ± 26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

ltyp = numba.typed.List()
for x in l: ltyp.append(x)
%timeit perm_unique_fast(ltyp)
#293 ms ± 3.37 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

assert list(sorted(perm_unique(l))) == list(sorted([tuple(x) for x in perm_unique_fast(ltyp)]))

Приблизно на 30% швидше, але все одно трохи страждає через копіювання списків та керування ними.

В якості альтернативи без numba, але все-таки без рекурсії та використання генератора, щоб уникнути проблем із пам’яттю:

def perm_unique_fast_gen(elements):
    eset = set(elements)
    dictunique = dict()
    for i in eset: dictunique[i] = elements.count(i)
    result_list = list() #numba.typed.List()
    u = len(elements)
    for _ in range(u): result_list.append(0)
    s = list()
    d = u
    while True:
        if d > 0:
            for i in dictunique:
                if dictunique[i] > 0: s.append((i, d - 1))
        i, d = s.pop()
        if d == -1:
            dictunique[i] += 1
            if len(s) == 0: break
            continue
        result_list[d] = i
        if d == 0: yield result_list
        dictunique[i] -= 1
        s.append((i, -1))

0

Це моя спроба, не вдаючись до set / dict, як генератора, що використовує рекурсію, але використовуючи рядок як вхід. Вихід також упорядковується в натуральному порядку:

def perm_helper(head: str, tail: str):
    if len(tail) == 0:
        yield head
    else:
        last_c = None
        for index, c in enumerate(tail):
            if last_c != c:
                last_c = c
                yield from perm_helper(
                    head + c, tail[:index] + tail[index + 1:]
                )


def perm_generator(word):
    yield from perm_helper("", sorted(word))

приклад:

from itertools import takewhile
word = "POOL"
list(takewhile(lambda w: w != word, (x for x in perm_generator(word))))
# output
# ['LOOP', 'LOPO', 'LPOO', 'OLOP', 'OLPO', 'OOLP', 'OOPL', 'OPLO', 'OPOL', 'PLOO', 'POLO']

-1

Що стосовно

np.unique(itertools.permutations([1, 1, 1]))

Проблема полягає в тому, що перестановки тепер є рядками масиву Numpy, таким чином, використовуючи більше пам'яті, але ви можете циклічно перебирати їх, як і раніше

perms = np.unique(itertools.permutations([1, 1, 1]))
for p in perms:
    print p

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.