Jak sprawdzić, czy dwie listy są cyklicznie identyczne w Pythonie


145

Na przykład mam listy:

a[0] = [1, 1, 1, 0, 0]
a[1] = [1, 1, 0, 0, 1]
a[2] = [0, 1, 1, 1, 0]
# and so on

Wydają się być różne, ale jeśli przypuszcza się, że początek i koniec są połączone, to są kołowo identyczne.

Problem w tym, że każda lista, którą mam, ma długość 55 i zawiera tylko trzy jedynki i 52 zera. Bez warunku kołowego istnieje 26235 (55 do wyboru 3) list. Jeśli jednak istnieje warunek „cykliczny”, istnieje ogromna liczba cyklicznie identycznych list

Obecnie sprawdzam tożsamość cyklicznie, wykonując następujące czynności:

def is_dup(a, b):
    for i in range(len(a)):
        if a == list(numpy.roll(b, i)): # shift b circularly by i
            return True
    return False

Ta funkcja w najgorszym przypadku wymaga 55 operacji zmiany cyklicznej. Istnieje 26235 list do porównania. Krótko mówiąc, potrzebuję 55 * 26,235 * (26,235 - 1) / 2 = 18 926 847 225 obliczeń. To prawie 20 Giga!

Czy jest jakiś dobry sposób na zrobienie tego przy mniejszej liczbie obliczeń? Lub jakiekolwiek typy danych, które obsługują cykliczne ?


Tylko przeczucie: czuję, że drzewa z przyrostkami mogą tu pomóc. en.wikipedia.org/wiki/Suffix_tree . Aby go zbudować, zobacz en.wikipedia.org/wiki/Ukkonen%27s_algorithm
Rerito,

1
@Mehrdad Ale o wiele gorszy czas działania niż jakakolwiek odpowiedź, która konwertuje do postaci kanonicznej, znacznie gorszy czas działania niż konwersja na liczbę całkowitą i znacznie gorszy czas działania niż David Eisenstat.
Veedrac

2
Wszystkie odpowiedzi próbują rozwiązać ogólny problem, ale w tym konkretnym przypadku tylko 3 jedynki można przedstawić każdą listę z 3 liczbami będącymi liczbą zer między jedynkami. Listę z pytania można przedstawić jako [0,0,2], [0,2,0], [2,0,0]. Możesz po prostu skrócić listę w jednym przebiegu, a następnie sprawdzić listę skróconą. Jeśli są „kołowo identyczne”, to także oryginały.
abc667

1
Wydaje mi się, że przepełnienie stosu nie wymaga wtedy głosowania. Wystarczy uruchomić kod we wszystkich rozwiązaniach i przedstawić je w kolejności, w jakiej się kończą.
Dawood ibn Kareem

2
Ponieważ do tej pory nie wspomniano, „forma kanoniczna”, do której odwołują się @ abc667, Veedrac i Eisenstat, nosi nazwę Run Length Encoding en.wikipedia.org/wiki/Run-length_encoding
David Lovell

Odpowiedzi:


133

Po pierwsze, można to zrobić pod O(n)względem długości listy.Możesz zauważyć, że jeśli zduplikujesz swoją listę 2 razy ( [1, 2, 3]) będzie[1, 2, 3, 1, 2, 3] to na nowej liście na pewno się wszystkie możliwe listy cykliczne.

Wszystko, czego potrzebujesz, to sprawdzić, czy lista, której szukasz, znajduje się 2 razy w stosunku do listy początkowej. W Pythonie można to osiągnąć w następujący sposób (zakładając, że długości są takie same).

list1 = [1, 1, 1, 0, 0]
list2 = [1, 1, 0, 0, 1]
print ' '.join(map(str, list2)) in ' '.join(map(str, list1 * 2))

Kilka wyjaśnień na temat mojego onelinera: list * 2połączy listę ze sobą, map(str, [1, 2])zamieni wszystkie liczby na łańcuch i ' '.join()zamieni tablicę ['1', '2', '111']na łańcuch'1 2 111' .

Jak zauważyli niektórzy w komentarzach, oneliner może potencjalnie dać fałszywe alarmy, aby uwzględnić wszystkie możliwe przypadki skrajne:

def isCircular(arr1, arr2):
    if len(arr1) != len(arr2):
        return False

    str1 = ' '.join(map(str, arr1))
    str2 = ' '.join(map(str, arr2))
    if len(str1) != len(str2):
        return False

    return str1 in str2 + ' ' + str2

PS1 , mówiąc o złożoności czasowej, warto zauważyć, że O(n)zostanie to osiągnięte, jeśli w O(n)czasie uda się znaleźć podciąg . Nie zawsze tak jest i zależy od implementacji w Twoim języku ( chociaż potencjalnie można to zrobić liniowo na przykład czasie KMP).

PS2 dla ludzi, którzy boją się działania strun i uważają, że odpowiedź nie jest dobra. Ważna jest złożoność i szybkość. Ten algorytm potencjalnie działa w O(n)czasie i O(n)przestrzeni, co czyni go znacznie lepszym niż cokolwiek innego w O(n^2)domenie. Aby zobaczyć to na własne oczy, możesz uruchomić mały test porównawczy (tworzy losową listę, wysuwa pierwszy element i dołącza go na koniec, tworząc w ten sposób cykliczną listę. Możesz swobodnie wykonywać własne manipulacje)

from random import random
bigList = [int(1000 * random()) for i in xrange(10**6)]
bigList2 = bigList[:]
bigList2.append(bigList2.pop(0))

# then test how much time will it take to come up with an answer
from datetime import datetime
startTime = datetime.now()
print isCircular(bigList, bigList2)
print datetime.now() - startTime    # please fill free to use timeit, but it will give similar results

0,3 sekundy na moim komputerze. Niezbyt długo. Teraz spróbuj porównać to z O(n^2)rozwiązaniami. Porównując to, możesz podróżować z USA do Australii (najprawdopodobniej statkiem wycieczkowym)


3
Wystarczy dodać spacje wypełniające (1 przed i 1 po każdym łańcuchu). Nie ma potrzeby zbytniego komplikowania rzeczy za pomocą wyrażeń regularnych. (Oczywiście zakładam, że porównujemy listy o tej samej długości)
Rerito

2
@Rerito, chyba że którakolwiek z list zawiera ciągi, które same mogą mieć spacje początkowe lub końcowe. Nadal może powodować kolizje.
Adam Smith

12
Nie podoba mi się ta odpowiedź. Bzdury związane ze strunami sprawiły, że mi się to nie podobało, a odpowiedź Davida Eisenstata skłoniła mnie do przegłosowania. To porównanie można wykonać w czasie O (n) ze stringiem, ale można to również zrobić w czasie O (n) z liczbą całkowitą [potrzeba 10k jako samo usuniętego], co jest szybsze. Niemniej jednak odpowiedź Davida Eisenstata pokazuje, że jakiekolwiek porównania są bezcelowe, ponieważ odpowiedź ich nie potrzebuje.
Veedrac

7
@Veedrac żartujesz sobie ze mnie? Czy słyszałeś o złożoności obliczeniowej? Odpowiedź Davidsa zajmuje O (n ^ 2) czasu i O (n ^ 2) miejsca tylko po to, aby wygenerować wszystkie jego powtórzenia, które nawet przy małych nakładach 10 ^ 4 trwają około 22 sekund i kto wie, ile pamięci RAM. Nie wspominając o tym, że nie zaczęliśmy teraz niczego szukać (właśnie wygenerowaliśmy wszystkie cykliczne obroty). A mój nonsens ze stringami daje pełny wynik dla danych wejściowych, takich jak 10 ^ 6, w mniej niż 0,5 sekundy. Potrzebuje również O (n) miejsca do przechowywania. Więc proszę, poświęć trochę czasu na zrozumienie odpowiedzi, zanim przejdziesz do konkluzji.
Salvador Dali

1
@SalvadorDali Wydajesz się bardzo (miękko) skoncentrowany na czasie ;-)
e2-e4

38

Nie posiadam wystarczającej wiedzy w Pythonie, aby odpowiedzieć na to pytanie w żądanym języku, ale w C / C ++, biorąc pod uwagę parametry twojego pytania, zamieniłbym zera i jedynki na bity i wypchnąłbym je na najmniej znaczące bity uint64_t. Umożliwi to porównanie wszystkich 55 bitów za jednym zamachem - 1 zegar.

Niesamowicie szybko, a całość zmieści się w wbudowanych pamięciach podręcznych (209880 bajtów). Wsparcie sprzętowe dla przesunięcia wszystkich 55 elementów listy w prawo jednocześnie jest dostępne tylko w rejestrach CPU. To samo dotyczy jednoczesnego porównywania wszystkich 55 członków. Pozwala to na mapowanie problemu 1 do 1 na rozwiązanie programowe. (i przy użyciu 256-bitowych rejestrów SIMD / SSE, w razie potrzeby do 256 członków) W rezultacie kod jest od razu widoczny dla czytelnika.

Możesz być w stanie zaimplementować to w Pythonie, po prostu nie znam tego wystarczająco dobrze, aby wiedzieć, czy to możliwe lub jaka może być wydajność.

Po spaniu na nim kilka rzeczy stało się oczywistych i wszystko na lepsze.

1.) Tak łatwo jest obrócić listę połączoną cyklicznie za pomocą bitów, że bardzo sprytna sztuczka Dalego nie jest konieczna. Wewnątrz 64-bitowego rejestru standardowe przesunięcie bitów spowoduje bardzo prostą rotację i usiłuje uczynić to wszystko bardziej przyjaznym dla Pythona, używając arytmetyki zamiast operacji bitowych.

2.) Przesunięcie końcówki można łatwo wykonać za pomocą dzielenia przez 2.

3.) Sprawdzenie końca listy na 0 lub 1 można łatwo zrobić za pomocą modulo 2.

4.) „Przeniesienie” 0 na początek listy z ogona można wykonać dzieląc przez 2. Dzieje się tak, ponieważ gdyby zero zostało faktycznie przesunięte, 55-ty bit byłby fałszywy, co już jest, nie robiąc absolutnie nic.

5.) „Przeniesienie” 1 na początek listy z ogona można wykonać dzieląc przez 2 i dodając 18 014 398 509 481 984 - co jest wartością utworzoną przez oznaczenie 55. bitu jako prawda, a cała reszta jako fałszywa.

6.) Jeśli porównanie zakotwiczenia i złożonego uint64_t jest PRAWDA po dowolnym podanym obrocie, przerwij i zwróć PRAWDA.

Przekonwertowałbym całą tablicę list na tablicę uint64_ts na samym początku, aby uniknąć konieczności wielokrotnego wykonywania konwersji.

Po spędzeniu kilku godzin na optymalizacji kodu i nauce języka asemblera udało mi się skrócić o 20% czas pracy. Powinienem dodać, że kompilator O / S i MSVC również został wczoraj zaktualizowany w południe. Z jakiegoś powodu jakość kodu, który utworzył kompilator C, znacznie się poprawiła po aktualizacji (15.11.2014). Czas działania wynosi teraz ~ 70 zegarów, 17 nanosekund na skomponowanie i porównanie pierścienia kotwicy ze wszystkimi 55 zwojami pierścienia testowego, a NxN wszystkich pierścieni ze wszystkimi pozostałymi zajmuje 12,5 sekundy .

Ten kod jest tak ciasny, że wszystkie rejestry oprócz 4 siedzą wokół, nie robiąc nic w 99% przypadków. Język asemblera pasuje do kodu C prawie wiersz po wierszu. Bardzo łatwe do odczytania i zrozumienia. Świetny projekt montażowy, gdyby ktoś się tego uczył.

Sprzęt to Hazwell i7, 64-bitowy MSVC, pełna optymalizacja.

#include "stdafx.h"
#include "stdafx.h"
#include <string>
#include <memory>
#include <stdio.h>
#include <time.h>

const uint8_t  LIST_LENGTH = 55;    // uint_8 supports full witdth of SIMD and AVX2
// max left shifts is 32, so must use right shifts to create head_bit
const uint64_t head_bit = (0x8000000000000000 >> (64 - LIST_LENGTH)); 
const uint64_t CPU_FREQ = 3840000000;   // turbo-mode clock freq of my i7 chip

const uint64_t LOOP_KNT = 688275225; // 26235^2 // 1000000000;

// ----------------------------------------------------------------------------
__inline uint8_t is_circular_identical(const uint64_t anchor_ring, uint64_t test_ring)
{
    // By trial and error, try to synch 2 circular lists by holding one constant
    //   and turning the other 0 to LIST_LENGTH positions. Return compare count.

    // Return the number of tries which aligned the circularly identical rings, 
    //  where any non-zero value is treated as a bool TRUE. Return a zero/FALSE,
    //  if all tries failed to find a sequence match. 
    // If anchor_ring and test_ring are equal to start with, return one.

    for (uint8_t i = LIST_LENGTH; i;  i--)
    {
        // This function could be made bool, returning TRUE or FALSE, but
        // as a debugging tool, knowing the try_knt that got a match is nice.
        if (anchor_ring == test_ring) {  // test all 55 list members simultaneously
            return (LIST_LENGTH +1) - i;
        }

        if (test_ring % 2) {    //  ring's tail is 1 ?
            test_ring /= 2;     //  right-shift 1 bit
            // if the ring tail was 1, set head to 1 to simulate wrapping
            test_ring += head_bit;      
        }   else    {           // ring's tail must be 0
            test_ring /= 2;     // right-shift 1 bit
            // if the ring tail was 0, doing nothing leaves head a 0
        }
    }
    // if we got here, they can't be circularly identical
    return 0;
}
// ----------------------------------------------------------------------------
    int main(void)  {
        time_t start = clock();
        uint64_t anchor, test_ring, i,  milliseconds;
        uint8_t try_knt;

        anchor = 31525197391593472; // bits 55,54,53 set true, all others false
        // Anchor right-shifted LIST_LENGTH/2 represents the average search turns
        test_ring = anchor >> (1 + (LIST_LENGTH / 2)); //  117440512; 

        printf("\n\nRunning benchmarks for %llu loops.", LOOP_KNT);
        start = clock();
        for (i = LOOP_KNT; i; i--)  {
            try_knt = is_circular_identical(anchor, test_ring);
            // The shifting of test_ring below is a test fixture to prevent the 
            //  optimizer from optimizing the loop away and returning instantly
            if (i % 2) {
                test_ring /= 2;
            }   else  {
                test_ring *= 2;
            }
        }
        milliseconds = (uint64_t)(clock() - start);
        printf("\nET for is_circular_identical was %f milliseconds."
                "\n\tLast try_knt was %u for test_ring list %llu", 
                        (double)milliseconds, try_knt, test_ring);

        printf("\nConsuming %7.1f clocks per list.\n",
                (double)((milliseconds * (CPU_FREQ / 1000)) / (uint64_t)LOOP_KNT));

        getchar();
        return 0;
}

wprowadź opis obrazu tutaj


23
ludzie wciąż mówią o „rozwiązaniu Salvadora Dali”, a ja siedziałem tu zdezorientowany, zastanawiając się, czy malarz o tym samym nazwisku był także matematykiem, który w jakiś znaczący sposób przyczynił się do powstania klasycznych algorytmów. potem zdałem sobie sprawę, że to nazwa użytkownika osoby, która opublikowała najpopularniejszą odpowiedź. nie jestem mądrym człowiekiem.
Woodrow Barlow

Dla każdego, kto ma 10 tys. Powtórzeń, a implementacja jest dostępna tutaj przy użyciu Numpy i wektoryzacji. Lustro Gist dla tych <10k . Usunąłem moją odpowiedź, bo David Eisenstat za odpowiedź wskazuje, że nie trzeba robić porównań w ogóle , jak można po prostu wygenerować unikalne list od razu i chcę, aby zachęcić ludzi do korzystania z jego znacznie lepszą odpowiedź.
Veedrac

@RocketRoy Jak myślisz, dlaczego Python nie miałby operacji bitowych? Heck, używam operacji bitowych w kodzie, który połączyłem . Nadal uważam, że ta odpowiedź jest w większości niepotrzebna (odpowiedź Davida Eisenstata zajmuje 1 ms na całość), ale uznałem to stwierdzenie za dziwne. FWIW, podobny algorytm w Numpy do przeszukiwania 262M- "list" zajmuje na moim komputerze około 15s (zakładając, że nie znaleziono żadnego dopasowania), tylko rotacja listy odbywa się w pętli zewnętrznej, a nie wewnętrznej.
Veedrac

@Quincunx, dziękuję za twoją edycję, aby uzyskać poprawne kolorowanie składni dla C ++. Mile widziane!

@RocketRoy Nie ma problemu. Kiedy odpowiesz na wiele pytań dotyczących PPCG , nauczysz się kolorować składnię.
Justin

33

Czytając między wierszami, wygląda na to, że próbujesz wyliczyć jednego przedstawiciela każdej cyklicznej klasy równoważności ciągów z 3 jedynkami i 52 zerami. Przejdźmy od gęstej reprezentacji do rzadkiej (zestaw trzech liczb w range(55)). W tej reprezentacji, okrągły przesunięcie sBy kdana jest zrozumienie set((i + k) % 55 for i in s). Przedstawicielem minimum leksykograficznego w klasie jest zawsze pozycja 0. Biorąc pod uwagę zestaw formularzy {0, i, j}z 0 < i < j, pozostałymi kandydatami na minimum w klasie są {0, j - i, 55 - i}i {0, 55 - j, 55 + i - j}. Dlatego potrzebujemy, (i, j) <= min((j - i, 55 - i), (55 - j, 55 + i - j))aby oryginał był minimalny. Oto kod wyliczenia.

def makereps():
    reps = []
    for i in range(1, 55 - 1):
        for j in range(i + 1, 55):
            if (i, j) <= min((j - i, 55 - i), (55 - j, 55 + i - j)):
                reps.append('1' + '0' * (i - 1) + '1' + '0' * (j - i - 1) + '1' + '0' * (55 - j - 1))
    return reps

2
@SalvadorDali Źle zrozumiałeś odpowiedź (ja też to zrobiłem, dopóki on jej nie wskazał!). To bezpośrednio generuje „jednego przedstawiciela każdej cyklicznej klasy równoważności ciągów z 3 jedynkami i 52 zerami”. Jego kod nie generuje wszystkich cyklicznych obrotów. Pierwotny koszt¹ to T (55² · 26235²). Twój kod poprawia 55² do 55, więc jest tylko T (55 * 26235²). Odpowiedź Davida Eisenstat jest między 55² i 55³ dla całej sprawy . 55³ ≪ 55 · 26235². ¹ Nie mówimy tutaj o terminach „wielkie-O” jako o faktycznym koszcie w O (1) we wszystkich przypadkach.
Veedrac

1
@Veedrac Ale 99% czytelników, którzy przyjdą do tego pytania w przyszłości, nie będzie miało jego ograniczeń i wierzę, że moja odpowiedź będzie dla nich bardziej odpowiednia. Nie nadymając dalej rozmowy, wyjdę do OP, aby wyjaśnić, czego dokładnie chce.
Salvador Dali

5
Wydaje się, że @SalvadorDali OP padł ofiarą problemu XY . Na szczęście samo pytanie wyjaśnia, czego nie zawiera tytuł, a David był w stanie czytać między wierszami. Jeśli tak jest w rzeczywistości, należy zmienić tytuł i rozwiązać rzeczywisty problem, zamiast odpowiadać na tytuł i ignorować pytanie.
Aaron Dufour

1
@SalvadorDali, pod okładkami Twój kod w Pythonie wywołuje odpowiednik metody C strstr (), która wyszukuje ciąg w celu znalezienia podłańcucha. To z kolei wywołuje strcmp (), która uruchamia pętlę for () porównującą każdy znak w łańcuchu string1 z string2. Dlatego to, co wygląda jak O (n), to O (n * 55 * 55) przy założeniu, że poszukiwanie zakończy się niepowodzeniem. Języki wysokiego poziomu to miecz obosieczny. Ukrywają przed tobą szczegóły implementacji, ale potem też ukrywają przed tobą szczegóły implementacji. FWIW, Twój wgląd w konkatenację listy był genialny. Jeszcze szybszy jak uint8 i znacznie szybszy jak bity - które można łatwo obracać sprzętowo.

2
@AleksandrDubinsky Prostszy dla komputera, bardziej skomplikowany dla ludzi. Jest wystarczająco szybki, jak jest.
David Eisenstat

12

Powtórz pierwszą tablicę, a następnie użyj algorytmu Z (czas O (n)), aby znaleźć drugą tablicę w pierwszej.

(Uwaga: nie musisz fizycznie kopiować pierwszej tablicy. Możesz po prostu zawijać podczas dopasowywania).

Zaletą algorytmu Z jest to, że jest bardzo prosty w porównaniu z KMP, BM itp.
Jednakże, jeśli czujesz się ambitny, możesz dopasować łańcuchy w czasie liniowym i stałej przestrzeni - strstrna przykład robi to. Wdrożenie go byłoby jednak bardziej bolesne.


6

Kontynuując bardzo inteligentne rozwiązanie Salvadora Dali, najlepszym sposobem na jego obsługę jest upewnienie się, że wszystkie elementy są tej samej długości, a obie LISTY są tej samej długości.

def is_circular_equal(lst1, lst2):
    if len(lst1) != len(lst2):
        return False
    lst1, lst2 = map(str, lst1), map(str, lst2)
    len_longest_element = max(map(len, lst1))
    template = "{{:{}}}".format(len_longest_element)
    circ_lst = " ".join([template.format(el) for el in lst1]) * 2
    return " ".join([template.format(el) for el in lst2]) in circ_lst

Nie ma pojęcia, czy jest to szybsze czy wolniejsze niż zalecane przez AshwiniChaudhary rozwiązanie regex w odpowiedzi Salvadora Dali, które brzmi:

import re

def is_circular_equal(lst1, lst2):
    if len(lst2) != len(lst2):
        return False
    return bool(re.search(r"\b{}\b".format(' '.join(map(str, lst2))),
                          ' '.join(map(str, lst1)) * 2))

1
na wiki, ponieważ po prostu poprawiłem odpowiedź Salvadora Dali i sformatowałem zmiany Ashwini. Bardzo niewiele z tego należy do mnie.
Adam Smith

1
dziękuję za wkład. Myślę, że uwzględniłem wszystkie możliwe przypadki w moim edytowanym rozwiązaniu. Daj mi znać, jeśli czegoś brakuje.
Salvador Dali

@SalvadorDali ah, tak ... sprawdzam, czy struny mają taką samą długość. Przypuszczam, że byłoby to łatwiejsze niż przeglądanie listy w poszukiwaniu najdłuższego elementu, a następnie wywoływanie str.format nczasów w celu sformatowania powstałego ciągu. Przypuszczam ... :)
Adam Smith

3

Biorąc pod uwagę, że musisz wykonać tak wiele porównań, czy może warto byłoby wykonać wstępne przejrzenie list, aby przekształcić je w jakąś formę kanoniczną, którą można łatwo porównać?

Czy próbujesz uzyskać zestaw cyklicznie unikalnych list? Jeśli tak, możesz wrzucić je do zestawu po konwersji na krotki.

def normalise(lst):
    # Pick the 'maximum' out of all cyclic options
    return max([lst[i:]+lst[:i] for i in range(len(lst))])

a_normalised = map(normalise,a)
a_tuples = map(tuple,a_normalised)
a_unique = set(a_tuples)

Przepraszamy Davida Eisenstata za to, że nie zauważył jego podobnej odpowiedzi.


3

Możesz rzucić jedną listę w ten sposób:

list1, list2 = [0,1,1,1,0,0,1,0], [1,0,0,1,0,0,1,1]

str_list1="".join(map(str,list1))
str_list2="".join(map(str,list2))

def rotate(string_to_rotate, result=[]):
    result.append(string_to_rotate)
    for i in xrange(1,len(string_to_rotate)):
        result.append(result[-1][1:]+result[-1][0])
    return result

for x in rotate(str_list1):
    if cmp(x,str_list2)==0:
        print "lists are rotationally identical"
        break

3

Najpierw przekonwertuj wszystkie elementy listy (w kopii, jeśli to konieczne) do tej obróconej wersji, która jest leksykalnie najlepsza.

Następnie posortuj wynikową listę list (zachowując indeks w pierwotnej pozycji listy) i ujednolicaj posortowaną listę, zaznaczając w razie potrzeby wszystkie duplikaty z oryginalnej listy.


2

Opierając się na obserwacji @ SalvadorDali dotyczącej wyszukiwania dopasowań a w dowolnym wycinku o długości a w b + b, oto rozwiązanie wykorzystujące tylko operacje na listach.

def rollmatch(a,b):
    bb=b*2
    return any(not any(ax^bbx for ax,bbx in zip(a,bb[i:])) for i in range(len(a)))

l1 = [1,0,0,1]
l2 = [1,1,0,0]
l3 = [1,0,1,0]

rollmatch(l1,l2)  # True
rollmatch(l1,l3)  # False

2. podejście: [usunięto]


Pierwsza wersja to O (n²), a druga nie działa rollmatch([1, 0, 1, 1], [0, 1, 1, 1]).
Veedrac

Niezły chwyt, usunę to!
PaulMcG

1

Nie jest to pełna, samodzielna odpowiedź, ale w kwestii optymalizacji poprzez redukcję porównań również myślałem o znormalizowanych reprezentacjach.

Mianowicie, jeśli twój alfabet wejściowy to {0, 1}, możesz znacznie zmniejszyć liczbę dozwolonych permutacji. Obróć pierwszą listę do postaci (pseudo) znormalizowanej (biorąc pod uwagę rozkład w Twoim pytaniu, wybrałbym taką, w której jeden z bitów 1 znajduje się po lewej stronie, a jeden z bitów 0 po prawej stronie). Teraz, przed każdym porównaniem, kolejno obracaj drugą listę przez możliwe pozycje z tym samym wzorem wyrównania.

Na przykład, jeśli masz w sumie cztery 1 bity, mogą istnieć maksymalnie 4 permutacje z tym wyrównaniem, a jeśli masz klastry z sąsiednimi 1 bitami, każdy dodatkowy bit w takim klastrze zmniejsza liczbę pozycji.

List 1   1 1 1 0 1 0

List 2   1 0 1 1 1 0  1st permutation
         1 1 1 0 1 0  2nd permutation, final permutation, match, done

Uogólnia to na większe alfabety i różne wzorce wyrównania; głównym wyzwaniem jest znalezienie dobrej normalizacji za pomocą zaledwie kilku możliwych reprezentacji. Najlepiej byłoby, gdyby była to właściwa normalizacja z jedną unikalną reprezentacją, ale biorąc pod uwagę problem, nie sądzę, aby to było możliwe.


0

Kontynuując odpowiedź RocketRoy: Konwertuj wszystkie swoje listy z góry na liczby 64-bitowe bez znaku. Dla każdej listy obróć te 55 bitów, aby znaleźć najmniejszą wartość liczbową.

Masz teraz jedną 64-bitową wartość bez znaku dla każdej listy, którą możesz bezpośrednio porównać z wartościami z innych list. Funkcja is_circular_identical () nie jest już wymagana.

(Zasadniczo tworzysz wartość tożsamości dla swoich list, na którą nie ma wpływu rotacja elementów list). To zadziałałoby nawet, gdybyś na swoich listach miał dowolną liczbę takich osób.


0

To jest ten sam pomysł Salvadora Dali, ale nie wymaga konwersji ciągów. Za tym stoi ten sam pomysł odzyskiwania KMP, aby uniknąć niemożliwej inspekcji zmiany. Oni dzwonią tylko do KMPModified (lista1, lista2 + lista2).

    public class KmpModified
    {
        public int[] CalculatePhi(int[] pattern)
        {
            var phi = new int[pattern.Length + 1];
            phi[0] = -1;
            phi[1] = 0;

            int pos = 1, cnd = 0;
            while (pos < pattern.Length)
                if (pattern[pos] == pattern[cnd])
                {
                    cnd++;
                    phi[pos + 1] = cnd;
                    pos++;
                }
                else if (cnd > 0)
                    cnd = phi[cnd];
                else
                {
                    phi[pos + 1] = 0;
                    pos++;
                }

            return phi;
        }

        public IEnumerable<int> Search(int[] pattern, int[] list)
        {
            var phi = CalculatePhi(pattern);

            int m = 0, i = 0;
            while (m < list.Length)
                if (pattern[i] == list[m])
                {
                    i++;
                    if (i == pattern.Length)
                    {
                        yield return m - i + 1;
                        i = phi[i];
                    }
                    m++;
                }
                else if (i > 0)
                {
                    i = phi[i];
                }
                else
                {
                    i = 0;
                    m++;
                }
        }

        [Fact]
        public void BasicTest()
        {
            var pattern = new[] { 1, 1, 10 };
            var list = new[] {2, 4, 1, 1, 1, 10, 1, 5, 1, 1, 10, 9};
            var matches = Search(pattern, list).ToList();

            Assert.Equal(new[] {3, 8}, matches);
        }

        [Fact]
        public void SolveProblem()
        {
            var random = new Random();
            var list = new int[10];
            for (var k = 0; k < list.Length; k++)
                list[k]= random.Next();

            var rotation = new int[list.Length];
            for (var k = 1; k < list.Length; k++)
                rotation[k - 1] = list[k];
            rotation[rotation.Length - 1] = list[0];

            Assert.True(Search(list, rotation.Concat(rotation).ToArray()).Any());
        }
    }

Mam nadzieję, że to pomoże!


0

Upraszczanie problemu

  • Zadanie polega na spisie zamówionych pozycji
  • Domena wartości jest binarna (0,1)
  • Możemy zmniejszyć problem, mapując kolejne 1s na liczbę
  • i kolejne 0s do liczby ujemnej

Przykład

A = [ 1, 1, 1, 0, 0, 1, 1, 0 ]
B = [ 1, 1, 0, 1, 1, 1, 0, 0 ]
~
A = [ +3, -2, +2, -1 ]
B = [ +2, -1, +3, -2 ]
  • Ten proces wymaga, aby pierwsza i ostatnia pozycja były różne
  • Ogółem zmniejszy to liczbę porównań

Proces sprawdzania

  • Jeśli założymy, że są duplikatami, możemy założyć, czego szukamy
  • Zasadniczo pierwsza pozycja z pierwszej listy musi istnieć gdzieś na drugiej liście
  • Następnie następuje to, co znajduje się na pierwszej liście i w ten sam sposób
  • Poprzednie pozycje powinny być ostatnimi z pierwszej listy
  • Ponieważ jest okrągły, kolejność jest taka sama

Uchwyt

  • Pytanie brzmi, od czego zacząć, technicznie znane jako lookupilook-ahead
  • Po prostu sprawdzimy, gdzie istnieje pierwszy element pierwszej listy na drugiej liście
  • Prawdopodobieństwo częstego elementu jest mniejsze, biorąc pod uwagę, że zmapowaliśmy listy na histogramy

Pseudo kod

FUNCTION IS_DUPLICATE (LIST L1, LIST L2) : BOOLEAN

    LIST A = MAP_LIST(L1)
    LIST B = MAP_LIST(L2)

    LIST ALPHA = LOOKUP_INDEX(B, A[0])

    IF A.SIZE != B.SIZE
       OR COUNT_CHAR(A, 0) != COUNT_CHAR(B, ALPHA[0]) THEN

        RETURN FALSE

    END IF

    FOR EACH INDEX IN ALPHA

        IF ALPHA_NGRAM(A, B, INDEX, 1) THEN

            IF IS_DUPLICATE(A, B, INDEX) THEN

                RETURN TRUE

            END IF

        END IF

    END FOR

    RETURN FALSE

END FUNCTION

FUNCTION IS_DUPLICATE (LIST L1, LIST L2, INTEGER INDEX) : BOOLEAN

    INTEGER I = 0

    WHILE I < L1.SIZE DO

        IF L1[I] != L2[(INDEX+I)%L2.SIZE] THEN

            RETURN FALSE

        END IF

        I = I + 1

    END WHILE

    RETURN TRUE

END FUNCTION

Funkcje

  • MAP_LIST(LIST A):LIST ELEMENTY KONSEKWENCYJNE NA MAPIE LICZĄ SIĘ W NOWEJ LIŚCIE

  • LOOKUP_INDEX(LIST A, INTEGER E):LISTLISTA ZWROTNA WSKAŹNIKÓW, W KTÓRYCH ELEMENT EISTNIEJE NA LIŚCIEA

  • COUNT_CHAR(LIST A , INTEGER E):INTEGERLICZ ESIĘ ILE RAZY WYSTĘPUJE ELEMENT NA LIŚCIEA

  • ALPHA_NGRAM(LIST A,LIST B,INTEGER I,INTEGER N):BOOLEANSPRAWDŹ, CZY B[I]JEST RÓWNOWAŻNA A[0] N-GRAMW OBU KIERUNKACH


Wreszcie

Jeśli rozmiar listy będzie dość duży lub jeśli element, od którego zaczynamy sprawdzać cykl, jest często wysoki, możemy wykonać następujące czynności:

  • Na początek poszukaj najrzadziej występującego elementu na pierwszej liście

  • zwiększ parametr n-gramowy N, aby zmniejszyć prawdopodobieństwo przejścia przez kontrolę liniową


0

Wydajną, łatwą do obliczenia „formę kanoniczną” dla przedmiotowych list można uzyskać jako:

  • Policz liczbę zer między jedynkami (ignorując zawijanie), aby otrzymać trzy liczby.
  • Obróć trzy liczby, tak aby największa liczba była pierwsza.
  • Pierwsza liczba ( a) musi mieścić się w przedziale od 18do 52(włącznie). Ponownie zakoduj go jako między 0a34 .
  • Druga liczba ( b) musi znajdować się między 0a 26, ale nie ma to większego znaczenia.
  • Usuń trzecią liczbę, ponieważ jest tylko 52 - (a + b)i nie dodaje żadnych informacji

Postać kanoniczna to liczba całkowita z b * 35 + aprzedziału od 0do 936(włącznie), która jest dość zwarta ( 477w sumie są listy cyklicznie unikalne).


0

Napisałem proste rozwiązanie, które porównuje obie listy i po prostu zwiększa (i zawija) indeks porównywanej wartości dla każdej iteracji.

Nie znam dobrze Pythona, więc napisałem go w Javie, ale jest naprawdę prosty, więc powinno być łatwo zaadaptować go do dowolnego innego języka.

W ten sposób możesz również porównać listy innych typów.

public class Main {

    public static void main(String[] args){
        int[] a = {0,1,1,1,0};
        int[] b = {1,1,0,0,1};

        System.out.println(isCircularIdentical(a, b));
    }

    public static boolean isCircularIdentical(int[] a, int[]b){
        if(a.length != b.length){
            return false;
        }

        //The outer loop is for the increase of the index of the second list
        outer:
        for(int i = 0; i < a.length; i++){
            //Loop trough the list and compare each value to the according value of the second list
            for(int k = 0; k < a.length; k++){
                // I use modulo length to wrap around the index
                if(a[k] != b[(k + i) % a.length]){
                    //If the values do not match I continue and shift the index one further
                    continue outer;
                }
            }
            return true;
        }
        return false;
    }
}

0

Jak wspominali inni, po znalezieniu znormalizowanej rotacji listy możesz je porównać.

Oto działający kod, który to robi. Podstawową metodą jest znalezienie znormalizowanej rotacji dla każdej listy i porównanie:

  • Oblicz znormalizowany indeks rotacji na każdej liście.
  • Zapętlaj obie listy z ich przesunięciami, porównując każdy element i zwracając, jeśli nie pasują.

Zauważ, że ta metoda nie zależy od liczb, możesz przekazywać listy ciągów (dowolne wartości, które można porównać).

Zamiast przeszukiwać listę na liście, wiemy, że chcemy, aby lista zaczynała się od wartości minimalnej - dzięki czemu możemy zapętlić wartości minimalne, przeszukując, aż znajdziemy, która ma najniższe kolejne wartości, przechowując to do dalszych porównań dopóki nie będziemy mieli najlepszych.

Istnieje wiele możliwości wcześniejszego wyjścia podczas obliczania indeksu, szczegóły dotyczące niektórych optymalizacji.

  • Pomiń wyszukiwanie najlepszej wartości minimalnej, gdy jest tylko jedna.
  • Pomiń wyszukiwanie wartości minimalnych, gdy poprzednia jest również wartością minimalną (nigdy nie będzie lepszym dopasowaniem).
  • Pomiń wyszukiwanie, gdy wszystkie wartości są takie same.
  • Niepowodzenie wcześnie, gdy listy mają różne wartości minimalne.
  • Użyj zwykłego porównania, gdy przesunięcia są zgodne.
  • Dostosuj przesunięcia, aby uniknąć zawijania wartości indeksu na jednej z list podczas porównania.

Zauważ, że w Pythonie przeszukiwanie listy na liście może być szybsze, jednak byłem zainteresowany znalezieniem wydajnego algorytmu - który mógłby być używany również w innych językach. Unikanie tworzenia nowych list ma również pewne zalety.

def normalize_rotation_index(ls, v_min_other=None):
    """ Return the index or -1 (when the minimum is above `v_min_other`) """

    if len(ls) <= 1:
        return 0

    def compare_rotations(i_a, i_b):
        """ Return True when i_a is smaller.
            Note: unless there are large duplicate sections of identical values,
            this loop will exit early on.
        """
        for offset in range(1, len(ls)):
            v_a = ls[(i_a + offset) % len(ls)]
            v_b = ls[(i_b + offset) % len(ls)]
            if v_a < v_b:
                return True
            elif v_a > v_b:
                return False
        return False

    v_min = ls[0]
    i_best_first = 0
    i_best_last = 0
    i_best_total = 1
    for i in range(1, len(ls)):
        v = ls[i]
        if v_min > v:
            v_min = v
            i_best_first = i
            i_best_last = i
            i_best_total = 1
        elif v_min == v:
            i_best_last = i
            i_best_total += 1

    # all values match
    if i_best_total == len(ls):
        return 0

    # exit early if we're not matching another lists minimum
    if v_min_other is not None:
        if v_min != v_min_other:
            return -1
    # simple case, only one minimum
    if i_best_first == i_best_last:
        return i_best_first

    # otherwise find the minimum with the lowest values compared to all others.
    # start looking after the first we've found
    i_best = i_best_first
    for i in range(i_best_first + 1, i_best_last + 1):
        if (ls[i] == v_min) and (ls[i - 1] != v_min):
            if compare_rotations(i, i_best):
                i_best = i

    return i_best


def compare_circular_lists(ls_a, ls_b):
    # sanity checks
    if len(ls_a) != len(ls_b):
        return False
    if len(ls_a) <= 1:
        return (ls_a == ls_b)

    index_a = normalize_rotation_index(ls_a)
    index_b = normalize_rotation_index(ls_b, ls_a[index_a])

    if index_b == -1:
        return False

    if index_a == index_b:
        return (ls_a == ls_b)

    # cancel out 'index_a'
    index_b = (index_b - index_a)
    if index_b < 0:
        index_b += len(ls_a)
    index_a = 0  # ignore it

    # compare rotated lists
    for i in range(len(ls_a)):
        if ls_a[i] != ls_b[(index_b + i) % len(ls_b)]:
            return False
    return True


assert(compare_circular_lists([0, 9, -1, 2, -1], [-1, 2, -1, 0, 9]) == True)
assert(compare_circular_lists([2, 9, -1, 0, -1], [-1, 2, -1, 0, 9]) == False)
assert(compare_circular_lists(["Hello" "Circular", "World"], ["World", "Hello" "Circular"]) == True)
assert(compare_circular_lists(["Hello" "Circular", "World"], ["Circular", "Hello" "World"]) == False)

Zobacz: ten fragment kodu, aby uzyskać więcej testów / przykładów.


0

Możesz łatwo sprawdzić, czy lista A jest równa cyklicznemu przesunięciu listy B w oczekiwanym czasie O (N).

Użyłbym wielomianowej funkcji skrótu do obliczenia skrótu listy A i każdego cyklicznego przesunięcia listy B. Tam, gdzie przesunięcie listy B ma taki sam skrót jak lista A, porównałbym rzeczywiste elementy, aby sprawdzić, czy są równe .

Powodem, dla którego jest to szybkie, jest to, że dzięki wielomianowym funkcjom skrótu (które są niezwykle powszechne!), Możesz obliczyć skrót każdego cyklicznego przesunięcia z poprzedniego w stałym czasie, dzięki czemu możesz obliczyć skróty dla wszystkich cyklicznych przesunięć w O ( N) czas.

Działa to tak:

Powiedzmy, że B ma N elementów, to skrót B używający liczby pierwszej P to:

Hb=0;
for (i=0; i<N ; i++)
{
    Hb = Hb*P + B[i];
}

Jest to zoptymalizowany sposób oceny wielomianu w P i jest równoważny z:

Hb=0;
for (i=0; i<N ; i++)
{
    Hb += B[i] * P^(N-1-i);  //^ is exponentiation, not XOR
}

Zauważ, że każde B [i] jest mnożone przez P ^ (N-1-i). Jeśli przesuniemy B w lewo o 1, to każde B [i] zostanie pomnożone przez dodatkowe P, z wyjątkiem pierwszego. Ponieważ mnożenie rozkłada się na dodawanie, możemy pomnożyć wszystkie składniki naraz, mnożąc cały hash, a następnie ustalić współczynnik dla pierwszego elementu.

Skrót lewego przesunięcia B jest po prostu

Hb1 = Hb*P + B[0]*(1-(P^N))

Drugie przesunięcie w lewo:

Hb2 = Hb1*P + B[1]*(1-(P^N))

i tak dalej...

UWAGA: cała powyższa matematyka jest wykonywana modulo do pewnego rozmiaru słowa maszynowego i wystarczy tylko raz obliczyć P ^ N.


-1

Aby przykleić się do najbardziej pytonicznego sposobu, użyj zestawów!

from sets import Set
a = Set ([1, 1, 1, 0, 0])
b = Set ([0, 1, 1, 1, 0]) 
c = Set ([1, 0, 0, 1, 1])
a==b
True
a==b==c
True

oznaczałoby to również dopasowanie ciągów o tej samej liczbie
GeneralBecos,

GeneralBecos: Po prostu wybierz te struny i sprawdź kolejność w drugim kroku
Louis

Nie są w tej samej liniowej kolejności. Są w tym samym porządku „okrężnym”. To, co opisujesz jako krok 2, jest pierwotnym problemem.
GeneralBecos,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.