Palindrome Partitioning
Palindrom czyta się tak samo od lewej do prawej i od prawej do lewej. Podziel ciąg znaków s na co najmniej jedną część tak, aby każda część była palindromem, i zwróć wszystkie możliwe podziały.
Zapisz każdy podział jako jego części w kolejności, połączone znakiem |: podział aab na a, a i b daje a|a|b. Zwróć podziały posortowane rosnąco według kodów znaków, tak aby | znajdował się po każdej małej literze.
Funkcja
- sstring
- ciąg małych liter do wycięcia
- Zwracastring-array
- każdy podział s na palindromy, zapisany z | między częściami, w kolejności rosnącej
Ograniczenia
1 ≤ s.length ≤ 11szawiera wyłącznie małe litery alfabetu angielskiego.- Ciąg 11 jednakowych liter ma 1,024 podziałów — to największa odpowiedź.
Przykłady
- Wejście
- s = "aab"
- Wyjście
- ["aa|b", "a|a|b"]
- Wyjaśnienie
- Tylko
aa|bia|a|bmają wyłącznie palindromiczne części;a|abnie przechodzi dlaab.aa|bsortuje się jako pierwsze: na drugim znaku maa, podczas gdy drugi ciąg ma|, który sortuje się po każdej literze.
- Wejście
- s = "abba"
- Wyjście
- ["abba", "a|bb|a", "a|b|b|a"]
- Wyjaśnienie
abbasamo w sobie jest palindromem, więc ciąg bez cięć jest podziałem i sortuje się jako pierwszy: jego drugi znak to litera.a|bb|awystępuje przeda|b|b|a: na czwartym znaku mab, podczas gdy drugi ciąg ma|.
Pytanie dodatkowe
Czy potrafisz znaleźć najmniejszą liczbę cięć, która dzieli s na palindromy, dla ciągów o długości do 2 000?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Pojedyncza litera jest zawsze palindromem, więc każdy ciąg znaków ma co najmniej jedno rozcięcie. Przyjrzyj się przerwom między sąsiednimi literami: rozcięcie to wybór, czy w każdym miejscu przeciąć, czy nie.
Utwórz podział od lewej do prawej. Pierwszy fragment musi być palindromowym prefiksem; gdy go wybierzesz, pozostała część ciągu znów staje się tym samym problemem. Tablica określająca, czy
s[i..j]jest palindromem, sprawia, że każde sprawdzenie zajmuje O(1).Rekurencyjnie wywołaj funkcję dla indeksu, od którego zaczyna się następny fragment, i wypróbuj każdy indeks końcowy, dla którego fragment jest palindromem. Ponieważ
|sortuje się po każdej literze, wypróbowanie najdłuższego fragmentu jako pierwszego sprawia, że podziały są już posortowane.
Rozwiązanie
Każda z n-1 przerw między literami jest albo rozcinana, albo nie, więc istnieje 2^(n-1) podziałów i każdy z nich może być odpowiedzią. Nie da się zmniejszyć rozmiaru wyniku, ale można pomijać podziały, które nie spełniają warunku: buduj każdy podział od lewej do prawej, odcinaj tylko fragmenty będące palindromami i odczytuj palindromy z tabeli wypełnionej tylko raz.
Wypróbuj każdy zestaw cięć
Intuicja
Ponumeruj podziały liczbami całkowitymi od 0 do 2^(n-1)-1, przy czym każdy bit wskazuje, czy przecięto jedną przerwę. Zachowaj podział danej liczby, jeśli każdy fragment jest palindromem.
Jeśli pierwszej przerwie odpowiada najwyższy bit, liczenie w górę wylicza podziały w kolejności rosnącej. Dla dwóch liczb najwyższy bit, którym się różnią, odpowiada najwcześniejszej przerwie, którą jedna z nich przecina, a druga nie. Wcześniej ciągi są równe; w tym miejscu mniejsza liczba ma literę, a większa ma |, który sortuje się po każdej literze. Dla aab, 1 daje aa|b, a 3 daje a|a|b.
Dla 11 liter daje to 1,024 masek, każda po 11 liter. Marnotrawstwo wynika z odrzucanych podziałów: abcdefghijk tworzy wszystkie 1,024, aby znaleźć jedną odpowiedź.
Algorytm
- Iteruj po
maskod 0 do2^(n-1)-1. - Przejdź po literach; zakończ część po literze
i, gdyijest ostatnim indeksem lub bitn-2-iwmaskjest ustawiony. - Sprawdź każdą część, przesuwając dwa wskaźniki do środka.
- Jeśli każda część jest palindromem, połącz części za pomocą
|i dodaj ciąg do odpowiedzi. - Zwróć odpowiedź; jest już posortowana, ponieważ pierwsza przerwa odpowiada najwyższemu bitowi.
def partition(s):
n = len(s)
result = []
# Bit (n-2-i) of mask says whether to cut after s[i]. Counting the
# masks up from 0 lists the splits in sorted order.
for mask in range(1 << (n - 1)):
pieces = []
start = 0
for i in range(n):
if i == n - 1 or (mask >> (n - 2 - i)) & 1:
pieces.append(s[start:i + 1])
start = i + 1
if all(piece == piece[::-1] for piece in pieces):
result.append("|".join(pieces))
return resultWycofywanie z użyciem tablicy palindromów
Intuicja
Pierwszy fragment jest palindromicznym prefiksem s[0..end], a podział pozostałej części to ten sam problem zaczynający się od end+1. Dlatego backtrack(start) wypróbowuje każdą wartość end, dla której s[start..end] jest palindromem, dodaje fragment, wykonuje rekurencję i usuwa go. Każda gałąź kończy się odpowiedzią, ponieważ pojedyncza litera jest palindromem, więc nakład pracy zależy od rozmiaru wyniku: dla abcdefghijk wyszukiwanie przechodzi jedną ścieżkę.
To, czy s[start..end] jest palindromem, sprawdza się w wielu gałęziach, więc odczytuj tę informację z tabeli: s[i..j] ma wartość jeden, gdy s[i] == s[j] i wnętrze s[i+1..j-1] ma wartość jeden, albo gdy fragment ma co najwyżej 2 litery.
Wypróbowywanie wartości end od ostatniego indeksu w dół daje posortowany wynik. Dwa podziały z różnymi pierwszymi fragmentami są zgodne aż do końca krótszego fragmentu; w tym miejscu krótszy ma |, a dłuższy ma literę, więc dłuższy pierwszy fragment sortuje się wcześniej.
Algorytm
- Wypełnij
pal[i][j]dlaiodn-1w dół do 0:s[i] == s[j]oraz alboj-i < 2, albopal[i+1][j-1]. - Napisz
backtrack(start). Jeślistartjest równen, połącz bieżące części za pomocą|i zapisz ciąg. - W przeciwnym razie wykonuj pętlę po
endodn-1w dół dostarti pomiń tę wartość, jeślipal[start][end]nie jest prawdą. - Dodaj
s[start..end]do części, wywołajbacktrack(end+1), a następnie usuń tę część. - Wywołaj
backtrack(0)i zwróć zapisane podziały, już posortowane.
def partition(s):
n = len(s)
# pal[i][j] is True when s[i..j] reads the same both ways.
pal = [[False] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
for j in range(i, n):
pal[i][j] = s[i] == s[j] and (j - i < 2 or pal[i + 1][j - 1])
result = []
parts = []
def backtrack(start):
if start == n:
result.append("|".join(parts))
return
# Longest piece first: "|" sorts after every letter, so a longer
# first piece gives a smaller string and the output stays sorted.
for end in range(n - 1, start - 1, -1):
if pal[start][end]:
parts.append(s[start:end + 1])
backtrack(end + 1)
parts.pop()
backtrack(0)
return result
Pułapki i przypadki brzegowe
- Próbowanie najkrótszego fragmentu jako pierwszego. Podziały są poprawne, ale ich kolejność jest błędna:
a|a|bprzedaa|b. - Sortowanie na końcu za pomocą porównania uwzględniającego reguły języka lub ustawień regionalnych, które może umieścić
|przed literami. Porównuj kody znaków albo generuj podziały w odpowiedniej kolejności. - Wypełnianie tablicy palindromów przy rosnącym
i. Wtedypal[i+1][j-1]jest odczytywane, zanim zostanie obliczone; wypełniaj tablicę dlaiodn-1w dół. - Łączenie włącznie końcowego
endz wyłącznym końcem podciągu. Fragments[start..end]maend-start+1liter. - Zapisywanie współdzielonej listy fragmentów zamiast połączonego ciągu znaków. Wyszukiwanie ciągle zmienia tę listę, więc każdy zapisany wpis okazuje się taki sam.
Najczęstsze pytania3
Jaka jest złożoność czasowa problemu podziału na palindromy?
W ciągu jednakowych liter wszystkie 2^(n-1) podziały są odpowiedziami, każda o długości do 2n-1 znaków: O(n × 2^n). Żadna metoda nie jest od tego szybsza, ponieważ zapisanie odpowiedzi wymaga tyle samo pracy. Tablica palindromów wymaga O(n²) czasu i miejsca, a rekurencja O(n).
Ile podziałów na palindromy może mieć ciąg znaków?
Co najwyżej 2^(n-1), po jednym dla każdego sposobu wyboru, które z n-1 przerw przeciąć. Ciąg jednakowych liter osiąga tę granicę: 1,024 podziałów dla 11 liter. Ciąg bez powtarzających się liter ma dokładnie jeden podział — każda litera osobno.
Czy podział palindromu to programowanie dynamiczne czy wycofywanie?
Wykorzystuje oba podejścia. Wypisanie wszystkich podziałów, których liczba może wynieść do 2^(n-1), wymaga wyszukiwania, które buduje każdy podział kawałek po kawałku: nawrotów. Programowanie dynamiczne dostarcza tabelę z odpowiedziami tak lub nie dla podciągów będących palindromami, z której korzysta wyszukiwanie; tabela jest wypełniana raz w czasie O(n²).
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
def partition(s):
# Napisz kod tutajTest #1
Test #2
Argument
s = "aab"
Oczekiwany wynik
["aa|b", "a|a|b"]