Permutations
Otrzymujesz listę nums zawierającą różne liczby całkowite. Zwróć wszystkie uporządkowania tych wartości, każde jako listę, która zawiera każdą wartość dokładnie raz, więc n wartości daje n! uporządkowań. Wypisz je w porządku leksykograficznym: porównaj dwa uporządkowania pozycja po pozycji i niech rozstrzygnie pierwsza różnica. Dla [1, 2, 3] oznacza to, że [1, 2, 3] jest pierwsze, a [3, 2, 1] ostatnie.
Funkcja
- numsinteger-array
- wartości, wszystkie różne, w dowolnej kolejności
- Zwracainteger-2d-array
- każde uporządkowanie wartości, wymienione w kolejności leksykograficznej
Ograniczenia
1 ≤ nums.length ≤ 6-10 ≤ nums[i] ≤ 10- Wszystkie wartości w
numssą różne. numsmoże występować w dowolnej kolejności.
Przykłady
- Wejście
- nums = [3, 1, 2]
- Wyjście
- [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
- Wyjaśnienie
- Trzy wartości mają 3! = 6 uporządkowań. Po posortowaniu wartości to 1, 2, 3, więc najpierw występują uporządkowania zaczynające się od 1, a
[1, 2, 3]występuje przed[1, 3, 2], ponieważ 2 jest mniejsze od 3 na drugiej pozycji. Kolejność danych wejściowych nie ma znaczenia.
- Wejście
- nums = [2, -1]
- Wyjście
- [[-1, 2], [2, -1]]
- Wyjaśnienie
- Dwie wartości można zapisać w dwóch kolejnościach.
[-1, 2]jest pierwsza, ponieważ -1 jest mniejsze niż 2.
- Wejście
- nums = [7]
- Wyjście
- [[7]]
- Wyjaśnienie
- Jedna wartość ma dokładnie jeden porządek: samą listę.
+13 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Mając jedno uporządkowanie, czy potrafisz wygenerować następne w porządku leksykograficznym w miejscu, w czasie O(n) i przy użyciu O(1) dodatkowej pamięci?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Twórz uporządkowanie po jednej pozycji naraz. Ile wartości może znaleźć się na pierwszej pozycji, ile na drugiej i co mówi to o łącznej liczbie?
Śledź, które wartości zostały już umieszczone. Na każdej pozycji próbuj każdej wartości, która jest jeszcze wolna, a gdy skończysz, zwolnij ją ponownie, aby następna próba zaczynała się od tego samego stanu.
Posortuj wartości, a następnie napisz funkcję pomocniczą rekurencyjną. Jeśli ścieżka zawiera wszystkie wartości
n, zapisz jej kopię. W przeciwnym razie przejdź w pętli przez wartości od najmniejszej do największej, pomijaj te już użyte, oznacz jedną jako używaną i dodaj ją, wywołaj rekurencję, a następnie ją usuń i oznacz jako nieużywaną. Rozpoczynanie od najmniejszej dostępnej wartości sprawia, że uporządkowania są od razu posortowane.
Rozwiązanie
Lista n różnych wartości ma n! uporządkowań, 720 dla sześciu wartości, a odpowiedź musi je wszystkie wymienić, więc nakład pracy wynosi co najmniej n × n!. Wyzwanie polega na tym, by zbudować każde uporządkowanie dokładnie raz i wypisać je w porządku leksykograficznym. Cofanie się po posortowanych wartościach, zawsze z próbą użycia najpierw najmniejszej niewykorzystanej wartości, pozwala osiągnąć oba cele jednocześnie.
Wstaw w każdą lukę, a następnie posortuj
Intuicja
Rozbudowuj uporządkowania po jednej wartości. Gdy nie ma żadnych wartości, istnieje jedno uporządkowanie: pusta lista. Aby dodać wartość 3 do uporządkowania [1, 2], umieść ją w każdej z jego trzech luk: [3, 1, 2], [1, 3, 2] i [1, 2, 3]. Zrób to dla każdego uporządkowania, które masz, a uporządkowania k wartości przekształcą się w uporządkowania k+1 wartości.
Każde uporządkowanie k+1 wartości powstaje dokładnie raz: usuń z niego najnowszą wartość, a otrzymasz jedyne uporządkowanie, z którego powstało, natomiast pozycja najnowszej wartości wskazuje lukę. Liczby te wynoszą więc 1, 2, 6, 24, a n wartości daje n! uporządkowań.
Nie powstają one w wymaganej kolejności. Dla [1, 2, 3] pierwszym utworzonym uporządkowaniem jest [3, 2, 1], więc na końcu trzeba je posortować, porównując pozycje jedna po drugiej. To sortowanie jest kosztowną częścią: n! uporządkowań wymaga około n! × log(n!) porównań, a każde z nich odczytuje do n wartości. Dla sześciu wartości to w przybliżeniu 720 × 9.5 × 6, czyli około 41 000 odczytów. Podczas tworzenia kolejnej generacji metoda przechowuje też w pamięci całą poprzednią generację uporządkowań.
Algorytm
- Zacznij od listy zawierającej jedno puste uporządkowanie.
- Dla każdej wartości w
numsutwórz nową listę: dla każdego dotychczasowego uporządkowania i każdej przerwy od 0 do jego długości skopiuj uporządkowanie z wartością wstawioną w tę przerwę. - Zastąp starą listę nową.
- Posortuj uporządkowania pozycja po pozycji i zwróć je.
def permute(nums):
perms = [[]]
for value in nums:
grown = []
for perm in perms:
# Put value into every gap of perm, both ends included.
for gap in range(len(perm) + 1):
grown.append(perm[:gap] + [value] + perm[gap:])
perms = grown
# Insertion order is not lexicographic, so sort at the end.
perms.sort()
return permsWycofywanie z użyciem tablicy oznaczającej użyte elementy
Intuicja
Wypełnij n miejsc od lewej do prawej. Pierwsze miejsce ma n kandydatów, drugie n-1 i tak dalej — stąd bierze się n!. Przedstaw te wybory jako drzewo: korzeń to pusta ścieżka, każda krawędź dodaje jedną wartość, a każdy liść na głębokości n to jedno gotowe uporządkowanie. Dla posortowanych wartości 1, 2, 3 korzeń ma dzieci [1], [2] i [3]; [1] ma dzieci [1, 2] i [1, 3]; każde z nich ma po jednym liściu.
Wycofywanie przechodzi przez to drzewo, korzystając z jednej współdzielonej ścieżki path i flagi used dla każdej wartości. W każdym węźle pętla przechodzi przez wartości i pomija te, które są już użyte. Dla każdej dostępnej wartości wykonuje wybór (oznacza ją jako używaną i dodaje do ścieżki), eksplorację (rekurencyjnie przechodzi poziom głębiej), a następnie cofa wybór (usuwa ją i oznacza jako dostępną). Cofnięcie wyboru przywraca dokładnie stan sprzed pętli, więc następna wartość jest sprawdzana w tym samym węźle. Ścieżka o długości n jest liściem: zapisz jej kopię i wróć.
Kolejność uzyskuje się sama. Pętla najpierw próbuje najmniejszą dostępną wartość, a przejście kończy wszystkie uporządkowania zaczynające się od danego prefiksu, zanim zmieni ten prefiks. Zatem wszystkie uporządkowania zaczynające się od 1 pojawiają się przed tymi zaczynającymi się od 2, a wśród nich [1, 2, ...] pojawia się przed [1, 3, ...]. To kolejność leksykograficzna. Dlatego najpierw sortujesz też nums: pętla przechodzi po indeksach, więc indeksy muszą być uporządkowane według wartości.
Drzewo ma około e × n! węzłów (e wynosi około 2.72), a każdy z nich wykonuje pętlę n razy, więc złożoność czasowa wynosi O(n × n!), czyli tyle samo co rozmiar wyniku. Poza wynikiem ścieżka, flagi i stos wywołań przechowują najwyżej po n elementów.
Algorytm
- Posortuj wartości i utwórz tablicę
usedznflagami o wartości false. - Napisz
explore(). Jeślipathzawieranwartości, dołącz kopię do wyniku i zwróć go. - W przeciwnym razie dla każdego indeksu
iod 0 do n-1, którego wartość jest dostępna: oznacz ją jako używaną i dołączvalues[i](wybór), wywołajexplore()(eksploracja), a następnie usuń ją i oznacz jako dostępną (cofnięcie wyboru). - Wywołaj
explore()raz i zwróć wynik.
def permute(nums):
values = sorted(nums)
n = len(values)
result = []
path = []
used = [False] * n
def explore():
# A full path is a leaf of the decision tree: one finished ordering.
if len(path) == n:
result.append(path[:])
return
# Smallest unused value first, so the leaves come out in lexicographic order.
for i in range(n):
if used[i]:
continue
used[i] = True
path.append(values[i]) # choose
explore() # explore
path.pop() # un-choose
used[i] = False
explore()
return result
Pułapki i przypadki brzegowe
Błędy związane z nawrotami niemal zawsze wynikają ze stanu, który nie został przywrócony, albo ze stanu współdzielonego przypadkowo.
- Zapisywanie
pathzamiast jego kopii. Wszystkie n! wpisów wskazują tę samą listę, która po zakończeniu przechodzenia jest pusta. - Cofanie tylko połowy wyboru. Jeśli usuniesz wartość, ale pozostawisz ustawione
used[i], ta wartość nie pojawi się ponownie w późniejszej gałęzi i zwrócisz mniej niż n! uporządkowań. - Nieposortowanie najpierw
nums. Przechodzenie nadal znajduje każde uporządkowanie, ale podąża za kolejnością danych wejściowych, więc dane wejściowe[3, 1, 2]zostaną wypisane jako pierwsze. - Używanie metody zamiany (zamień
nums[start]z każdą późniejszą pozycją, wykonaj rekurencję, zamień z powrotem) bez końcowego sortowania. Znajduje wszystkie n! uporządkowań, ale dla[1, 2, 3]wypisuje[3, 2, 1]przed[3, 1, 2]. - Sprawdzanie, czy wartość została już użyta, przez wyszukiwanie w
path. Działa to tutaj tylko dlatego, że wartości są różne, a na każdym kroku kosztuje n. Flaga dla każdego indeksu działa w czasie O(1) i nadal sprawdza się, gdy wartości się powtarzają.
Najczęstsze pytania4
Ile permutacji ma lista zawierająca n różnych elementów?
n!, czytaj jako n silnia: n możliwości na pierwszą pozycję, n-1 na drugą, aż do jednej na ostatnią, pomnożone przez siebie. Trzy wartości dają 6 uporządkowań, sześć daje 720, a dziesięć daje już 3,628,800, dlatego w zadaniach dotyczących permutacji n jest małe.
Jaka jest złożoność czasowa generowania wszystkich permutacji?
O(n × n!). Istnieje n! uporządkowań, a zapisanie każdego z nich wymaga n kroków, więc żadna metoda nie może działać szybciej, jeśli musi zwrócić je wszystkie. Backtracking osiąga tę granicę, a oprócz miejsca na wynik potrzebuje O(n) pamięci na bieżącą ścieżkę, flagi użycia i rekursję.
Dlaczego wycofywanie rekurencji generuje permutacje w kolejności leksykograficznej?
To przeszukiwanie w głąb, które najpierw próbuje najmniejszej dostępnej wartości. Kończy wszystkie uporządkowania zaczynające się od danego prefiksu, zanim przejdzie do następnego prefiksu, a prefiksy próbuje od najmniejszego do największego. Odpowiada to sposobowi, w jaki słownik porządkuje słowa, o ile dane wejściowe zostaną posortowane przed rozpoczęciem przeszukiwania.
Jak generować permutacje, gdy dane wejściowe zawierają duplikaty?
Posortuj wartości i na każdej pozycji pomiń wartość równą poprzedniej, jeśli ta wcześniejsza kopia nie jest używana: i > 0, values[i] == values[i-1] i !used[i-1]. Dzięki temu równe wartości są umieszczane w pierwotnej kolejności, więc każde różne uporządkowanie jest tworzone tylko raz.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def permute(nums):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [3, 1, 2]
Oczekiwane
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]