Szukam sposobu, aby sprawdzić, czy dany ciąg powtarza się dla całego ciągu, czy nie.
Przykłady:
[
'0045662100456621004566210045662100456621', # '00456621'
'0072992700729927007299270072992700729927', # '00729927'
'001443001443001443001443001443001443001443', # '001443'
'037037037037037037037037037037037037037037037', # '037'
'047619047619047619047619047619047619047619', # '047619'
'002457002457002457002457002457002457002457', # '002457'
'001221001221001221001221001221001221001221', # '001221'
'001230012300123001230012300123001230012300123', # '00123'
'0013947001394700139470013947001394700139470013947', # '0013947'
'001001001001001001001001001001001001001001001001001', # '001'
'001406469760900140646976090014064697609', # '0014064697609'
]
są ciągami, które się powtarzają, i
[
'004608294930875576036866359447',
'00469483568075117370892018779342723',
'004739336492890995260663507109',
'001508295625942684766214177978883861236802413273',
'007518796992481203',
'0071942446043165467625899280575539568345323741',
'0434782608695652173913',
'0344827586206896551724137931',
'002481389578163771712158808933',
'002932551319648093841642228739',
'0035587188612099644128113879',
'003484320557491289198606271777',
'00115074798619102416570771',
]
to przykłady takich, które tego nie robią.
Powtarzane sekcje ciągów, które otrzymałem, mogą być dość długie, a same ciągi mogą mieć co najmniej 500 znaków, więc zapętlanie każdego znaku, próbując zbudować wzór, a następnie sprawdzanie wzoru względem reszty ciągu wydaje się okropnie wolne. Pomnóż to przez potencjalnie setki ciągów, a nie widzę żadnego intuicyjnego rozwiązania.
Przeszukałem trochę wyrażeń regularnych i wydają się one przydatne, gdy wiesz, czego szukasz, lub przynajmniej długość szukanego wzoru. Niestety nie znam żadnego z nich.
Jak mogę stwierdzić, czy ciąg się powtarza, a jeśli tak, to jaka jest najkrótsza powtarzalna podsekwencja?
Odpowiedzi:
Oto zwięzłe rozwiązanie, które pozwala uniknąć wyrażeń regularnych i wolnych pętli w Pythonie:
Zobacz odpowiedź Wiki Wiki rozpoczętą przez @davidism, aby uzyskać wyniki testów porównawczych. W podsumowaniu,
(Słowa tej odpowiedzi, nie moje).
Jest to oparte na spostrzeżeniu, że łańcuch jest okresowy wtedy i tylko wtedy, gdy jest równy nietrywialnej rotacji samego siebie. Wyrazy uznania dla @AleksiTorhamo za uświadomienie sobie, że możemy odzyskać okres główny z indeksu pierwszego wystąpienia
s
in(s+s)[1:-1]
, oraz za poinformowanie mnie o opcjonalnościstart
iend
argumentach Pythonastring.find
.źródło
.find()
lub.index()
zamiastin
, np.(s+s).find(s, 1, -1)
.(s+s).find(s, 1, -1)
będzie (bardzo nieznacznie) szybszy niż(s+s)[1:-1].find(s)
, przynajmniej w przypadku większych ciągów, ponieważ przecinanie oznacza, że musisz utworzyć kolejną kopię (prawie) całego ciągu."abcd"
postać z prawej strony i przyklej ją z powrotem w lewo, aby uzyskać"dabc"
. Ta procedura nazywa się obracaniem łańcucha w prawo o 1 znak . Powtarzajn
czasy, aby obracać ciąg znaków w prawo on
znaki. Teraz zauważ, że jeśli mamy ciągk
znaków, obrót w prawo o dowolną wielokrotnośćk
pozostawia ciąg bez zmian. Nietrywialna obrót ciąg jest jednym których liczba znaków nie jest wielokrotnością długości struny.Oto rozwiązanie wykorzystujące wyrażenia regularne.
Iterowanie po przykładach w pytaniu:
... tworzy ten wynik:
Wyrażenie regularne
(.+?)\1+$
dzieli się na trzy części:(.+?)
to dopasowana grupa zawierająca co najmniej jedną (ale jak najmniej) jakąkolwiek postać (ponieważ+?
jest nie chciwa ).\1+
sprawdza co najmniej jedno powtórzenie pasującej grupy w pierwszej części.$
sprawdza koniec łańcucha, aby upewnić się, że po powtórzonych podciągach nie ma dodatkowej, niepowtarzalnej treści (i użyciere.match()
zapewnia, że przed powtarzającymi się podciągami nie będzie żadnego powtarzającego się tekstu ).W Pythonie 3.4 i nowszych możesz zamiast tego
$
użyć ire.fullmatch()
lub (w każdym Pythonie co najmniej aż do wersji 2.3) przejść w drugą stronę i użyćre.search()
wyrażenia regularnego^(.+?)\1+$
, z których wszystkie są bardziej zależne od osobistego gustu niż cokolwiek innego.źródło
Możesz dokonać obserwacji, że aby łańcuch mógł być uważany za powtarzalny, jego długość musi być podzielna przez długość jego powtarzanej sekwencji. Biorąc to pod uwagę, oto rozwiązanie, które generuje dzielniki długości od
1
don / 2
włącznie, dzieli oryginalny ciąg na podciągi o długości dzielników i testuje równość zestawu wyników:EDYCJA: W Pythonie 3
/
operator zmienił się domyślnie na podział zmiennoprzecinkowy. Aby uzyskaćint
podział z Python 2, możesz//
zamiast tego użyć operatora. Dziękuję @ TigerhawkT3 za zwrócenie mojej uwagi na to.Że
//
operator wykonuje całkowite podziału zarówno Python 2 i Python 3, więc już zaktualizowane odpowiedź na poparcie obu wersji. Część, w której testujemy, czy wszystkie podciągi są równe, jest teraz operacją zwarciaall
i wyrażeniem generatora.AKTUALIZACJA: W odpowiedzi na zmianę w pierwotnym pytaniu kod został teraz zaktualizowany, aby zwracał najmniejsze powtarzające się podciągi, jeśli istnieje, a
None
jeśli nie. @godlygeek zasugerował użycie wdivmod
celu zmniejszenia liczby iteracji wdivisors
generatorze, a także kod został zaktualizowany, aby pasował do tego. Teraz zwraca wszystkie pozytywne dzielniki zn
w porządku rosnącym, z wyłączeniemn
siebie.Dalsza aktualizacja w celu uzyskania wysokiej wydajności: po wielu testach doszedłem do wniosku, że po prostu testowanie pod kątem równości łańcuchów ma najlepszą wydajność z dowolnego rozwiązania krojenia lub iteratora w Pythonie. Dlatego wyciągnąłem liść z książki @ TigerhawkT3 i zaktualizowałem moje rozwiązanie. Jest teraz ponad 6 razy szybszy niż wcześniej, zauważalnie szybszy niż rozwiązanie Tigerhawk, ale wolniejszy niż David.
źródło
(n/2)
.n / 2
wdivisors()
byćn // 2
?Oto kilka punktów odniesienia dla różnych odpowiedzi na to pytanie. Było kilka zaskakujących wyników, w tym bardzo różna wydajność w zależności od testowanego ciągu.
Niektóre funkcje modyfikowano do pracy z Pythonie 3 (głównie przez zastąpienie
/
w//
celu zapewnienia podziału całkowita). Jeśli widzisz coś nie tak, chcesz dodać swoją funkcję lub chcesz dodać kolejny ciąg testowy, ping @ ZeroPiraeus w czacie Python .Podsumowując: istnieje około 50-krotna różnica między najlepiej i najgorzej działającymi rozwiązaniami dla dużego zestawu przykładowych danych dostarczonych tutaj przez OP (poprzez ten komentarz). Rozwiązanie Davida Zhanga jest wyraźnym zwycięzcą, przewyższając wszystkich innych o około 5 razy w przypadku dużego zestawu przykładów.
Kilka odpowiedzi jest bardzo wolnych w bardzo dużych przypadkach „bez dopasowania”. W przeciwnym razie funkcje wydają się być jednakowo dopasowane lub jasne zwycięzcy w zależności od testu.
Oto wyniki, w tym wykresy wykonane przy użyciu matplotlib i seaborn, aby pokazać różne rozkłady:
Corpus 1 (dostarczone przykłady - mały zestaw)
Corpus 2 (dostarczone przykłady - duży zestaw)
Corpus 3 (przypadki na krawędziach)
Testy i surowe wyniki są dostępne tutaj .
źródło
Rozwiązanie inne niż wyrażenia regularne:
Szybsze rozwiązanie bez wyrażenia regularnego, dzięki @ThatWeirdo (patrz komentarze):
Powyższe rozwiązanie jest bardzo rzadko wolniejsze od oryginału o kilka procent, ale zwykle jest nieco szybsze - czasem o wiele szybsze. Nadal nie jest szybszy niż dawidyzm dla dłuższych łańcuchów, a rozwiązanie wyrażenia regularnego zera jest lepsze dla krótkich łańcuchów. Najszybszy (zgodnie z testem dawidyzmu na githubie - patrz jego odpowiedź) z ciągami około 1000-1500 znaków. Niezależnie od tego, jest niezawodnie drugi najszybszy (lub lepszy) we wszystkich testowanych przeze mnie przypadkach. Dzięki, ThatWeirdo.
Test:
Wyniki:
źródło
repeat('aa')
powracaNone
len(string[0:i])
jest zawsze równai
(przynajmniej w tym przypadku). Zastąpienie ich, a także zapisanielen(string)
istring[0:i]
zmienne mogą przyspieszyć. Również IMO to świetne rozwiązanie, niesamowite;)Najpierw podziel ciąg o połowę, o ile jest to duplikat „2 części”. Zmniejsza to przestrzeń wyszukiwania, jeśli jest parzysta liczba powtórzeń. Następnie, pracując naprzód, aby znaleźć najmniejszy powtarzający się ciąg, sprawdź, czy podzielenie pełnego ciągu na coraz większe podciąg powoduje tylko puste wartości.
length // 2
Konieczne jest przetestowanie tylko podłańcuchów , które nie będą się powtarzać.Zwraca najkrótsze dopasowanie lub Brak, jeśli nie ma żadnego dopasowania.
źródło
Problem można również rozwiązać
O(n)
w najgorszym przypadku dzięki funkcji prefiksu.Zauważ, że może być wolniejszy w ogólnym przypadku (UPD: i jest znacznie wolniejszy) niż inne rozwiązania, które zależą od liczby dzielników
n
, ale zwykle nie udaje się wcześniej, myślę, że jednym ze złych przypadków będzieaaa....aab
, jeśli sąn - 1 = 2 * 3 * 5 * 7 ... *p_n - 1
a
„sPrzede wszystkim musisz obliczyć funkcję prefiksu
wtedy albo nie ma odpowiedzi, albo jest najkrótszy okres
i musisz tylko sprawdzić, czy
k != n and n % k == 0
(jeśli tak,k != n and n % k == 0
to odpowiedź brzmis[:k]
, inaczej nie ma odpowiedziMożesz sprawdzić dowód tutaj (po rosyjsku, ale tłumacz online prawdopodobnie załatwi sprawę)
źródło
prefix_function()
niepoprawny Python: brakuje dwukropków na twoimwhile
iif
wyciągu, a&&
zamiastand
. Po ich naprawieniu nie udaje się zUnboundLocalError: local variable 'i' referenced before assignment
powodu liniifor i in range(i, n):
.prefix_function()
aby zwrócić podobne wyniki do innych odpowiedzi - albo najkrótszy podciąg, alboNone
- uwzględnię go w poprawionym teście porównawczym, który zestawiam.Ta wersja próbuje tylko tych długości sekwencji kandydujących, które są czynnikami długości łańcucha; i używa
*
operatora do zbudowania ciągu o pełnej długości z sekwencji kandydującej:Dzięki TigerhawkT3 za zauważenie, że
length // 2
bez+ 1
nie pasowałoby doabab
przypadku.źródło
range
limitlength//2
, tak jak ja - musisz to zmienić,length//2+1
jeśli chcesz złapać dokładnie podwojone łańcuchy (np'aabaab'
.).Oto proste rozwiązanie, bez wyrażeń regularnych.
W przypadku podciągów
s
zaczynających się od indeksu zerowego, o długości od 1 dolen(s)
, sprawdź, czy podciągsubstr
jest powtórzeniem wzoru. To sprawdzenie można wykonać, łącząc sięsubstr
ze sobąratio
czasów, tak aby długość utworzonego w ten sposób łańcucha była równa długościs
. W związku z tymratio=len(s)/len(substr)
.Wróć po znalezieniu pierwszego takiego podciągu. Zapewniłoby to możliwie najmniejszy podciąg, jeśli taki istnieje.
źródło
Zacząłem od ponad ośmiu rozwiązań tego problemu. Niektóre opierały się na wyrażeniach regularnych (dopasowywanie, znajdowanie, dzielenie), niektóre na cięciach i testowaniu ciągów, a niektóre na metodach ciągowych (znajdź, policz, podziel). Każdy z nich miał zalety w zakresie przejrzystości kodu, rozmiaru kodu, szybkości i zużycia pamięci. Zamierzałem zamieścić tutaj swoją odpowiedź, gdy zauważyłem, że szybkość wykonania została sklasyfikowana jako ważna, dlatego wykonałem więcej testów i ulepszeń, aby to osiągnąć:
Ta odpowiedź wydaje się podobna do kilku innych odpowiedzi tutaj, ale ma kilka optymalizacji prędkości, których inni nie używali:
xrange
jest nieco szybszy w tej aplikacji,s[:n]
bezpośrednio, unikamy tworzenia zmiennej w każdej pętli.Byłbym zainteresowany, aby zobaczyć, jak to działa w standardowych testach na zwykłym sprzęcie. Sądzę, że w większości testów nie będzie to doskonały algorytm Davida Zhanga, ale w przeciwnym razie powinien być dość szybki.
Uważam, że ten problem jest bardzo sprzeczny z intuicją. Rozwiązania, które moim zdaniem będą szybkie, były powolne. Rozwiązania, które wyglądały powoli, były szybkie! Wygląda na to, że tworzenie napisów w Pythonie za pomocą operatora mnożenia i porównywania napisów jest wysoce zoptymalizowane.
źródło
statistics
modułu), więc musiałem zmienić twoje/
s na//
s i wymienićxrange()
zrange()
(który zachowuje się jak 2.x użytkownikaxrange()
w wersji 3.x).//
w 3.x jest dzieleniem całkowitym (podobnie jak zachowanie 2.x/
), podczas gdy 3.x/
to dzielenie zmiennoprzecinkowe (co na pewno byłoby znacznie wolniejsze, nawet gdyby nie złamało twojego rozwiązania powodując próbę użycia liczba zmiennoprzecinkowa jako indeks). Jak wspomniano, 3.xrange()
jest tym samym, co 2.xxrange()
; nie ma odpowiednika 2.xrange()
w 3.x. Więc nie sądzę, że jest to przyczyną jakiejkolwiek rozbieżności między testem porównawczym a ustalonymi czasami. Prawdopodobnie po prostu 3.x jest wolniejszy niż 2.x (a może twoja maszyna jest szybsza niż moja).Ta funkcja działa bardzo szybko (przetestowana i jest ponad 3 razy szybsza niż najszybsze rozwiązanie tutaj na ciągach o ponad 100 000 znaków, a różnica staje się większa, im dłuższy jest powtarzalny wzór). Stara się zminimalizować liczbę porównań potrzebnych do uzyskania odpowiedzi:
Zauważ, że na przykład dla ciągu o długości 8 sprawdza tylko fragment o rozmiarze 4 i nie musi dalej testować, ponieważ wzorzec o długości 1 lub 2 spowodowałby powtórzenie wzorca o długości 4:
źródło
W odpowiedzi Davida Zhanga, jeśli mamy jakiś bufor okrągły, to nie zadziała:
principal_period('6210045662100456621004566210045662100456621')
ze względu na początek621
, w którym chciałbym, żeby wypluł:00456621
.Rozszerzając jego rozwiązanie, możemy użyć:
źródło
Oto kod w pythonie, który sprawdza powtarzalność podciągu w głównym ciągu podanym przez użytkownika .
Wejście :
Wyjście :
Wejście :
Wyjście :
źródło