Se hai pochissimi cicli, ecco un algoritmo che utilizzerà meno spazio, ma impiegherà molto più tempo per terminare.
[Modifica.] La mia precedente analisi di runtime ha mancato il costo cruciale per determinare se i nodi che visitiamo sono tra quelli precedentemente campionati; questa risposta è stata in qualche modo rivista per correggere questo.
Abbiamo ancora una volta scorrere tutti gli elementi di S . Mentre esploriamo le orbite degli elementi s ∈ S , campioniamo dai nodi che abbiamo visitato, per essere in grado di verificare se li incontriamo di nuovo. Manteniamo anche un elenco di campioni di "componenti" - unioni di orbite che terminano in un ciclo comune (e che sono quindi equivalenti ai cicli) - che sono stati precedentemente visitati.
Inizializzare un elenco vuoto di componenti, complist
. Ogni componente è rappresentato da una raccolta di campioni da quel componente; manteniamo anche un albero di ricerca samples
che memorizza tutti quegli elementi che sono stati selezionati come campioni per un componente o altro. Sia G una sequenza di numeri interi fino a n , per cui l'appartenenza è efficacemente determinabile calcolando un predicato booleano; per esempio, i poteri di 2 o perfetti p th poteri per qualche intero p . Per ogni s ∈ S , procedi come segue:
- Se s è
samples
attivo, vai al passaggio 5.
- Inizializza un elenco vuoto
cursample
, un iteratore j ← f ( s ) e un contatore t ← 1.
- Mentre j non è in
samples
:
- Se t ∈ G , inserire j in entrambi cursample
e samples
.
- Incremento t e set j ← f (j) .
- Controlla se j è dentro
cursample
. In caso contrario, abbiamo riscontrato un componente esplorato in precedenza: controlliamo a quale componente j appartiene e inseriamo tutti gli elementi cursample
nell'elemento appropriato complist
per aumentarlo. Altrimenti, abbiamo incontrato nuovamente un elemento dall'orbita attuale, il che significa che abbiamo attraversato un ciclo almeno una volta senza incontrare alcun rappresentante di cicli precedentemente scoperti: inseriamo cursample
, come raccolta di campioni da un componente appena trovato, in complist
.
- Procedere con l'elemento successivo s ∈ S .
Per n = | S |, sia X (n) una funzione monotona crescente che descriva il numero atteso di cicli ( ad es. X (n) = n 1/3 ), e sia Y (n) = y (n) log ( n ) ∈ Ω ( X (n) log ( n )) è una funzione crescente monotona che determina un target per l'utilizzo della memoria ( es. Y (n) = n 1/2 ). Richiediamo y (n) ∈ Ω ( X (n) ) perché ci vorrà almeno X (n) log ( n ) spazio per memorizzare un campione da ciascun componente.
Maggiore è il numero di elementi di un'orbita che campioniamo, maggiore è la probabilità che selezioniamo rapidamente un campione nel ciclo alla fine di un'orbita e quindi rileviamo rapidamente quel ciclo. Da un punto di vista asintotico, ha quindi senso ottenere tutti i campioni consentiti dai nostri limiti di memoria: possiamo anche impostare G per avere un elemento y (n) atteso inferiore a n .
- Se la lunghezza massima di un'orbita in S dovrebbe essere L , potremmo lasciare che G sia il numero intero multiplo di L / y (n) .
- Se non è prevista una lunghezza, possiamo semplicemente campionare una volta ogni n / a (n)elementi; questo è comunque un limite superiore agli intervalli tra i campioni.
Se, cercando un nuovo componente, iniziamo a attraversare elementi di S che abbiamo precedentemente visitato (o da un nuovo componente scoperto o da uno vecchio il cui ciclo terminale è già stato trovato), ci vorrà al massimo n / y ( n) iterazioni per incontrare un elemento precedentemente campionato; questo è quindi un limite superiore per il numero di volte, per ogni tentativo di trovare un nuovo componente, attraversiamo nodi ridondanti. Poiché effettuiamo n tali tentativi, visiteremo quindi in modo ridondante elementi di S al massimo n 2 / y (n) volte in totale.
Il lavoro richiesto per verificare l'adesione samples
è O ( y (n) log y (n) ), che ripetiamo ad ogni visita: il costo cumulativo di questo controllo è O ( n 2 log y (n) ). C'è anche il costo di aggiungere i campioni alle rispettive raccolte, che cumulativamente è O ( y (n) log y (n) ). Infine, ogni volta che incontriamo un componente precedentemente scoperto, dobbiamo dedicare fino a X (n) log * y (n) tempo per determinare quale componente abbiamo riscoperto; poiché ciò può accadere fino a n volte, il lavoro cumulativo coinvolto è limitato da n X (n) log y (n) .
Pertanto, il lavoro cumulativo svolto per verificare se i nodi che visitiamo sono tra i campioni dominano il tempo di esecuzione: questo costa O ( n 2 log y (n) ). Quindi dovremmo rendere y (n) il più piccolo possibile, vale a dire O ( X (n) ).
Pertanto, si può enumerare il numero di cicli (che è uguale al numero di componenti che terminano in quei cicli) nello spazio O ( X (n) log ( n )), prendendo O ( n 2 log X (n) ) tempo per farlo, dove X (n) è il numero atteso di cicli.