Trovare tutti i cicli


9

Ho un insieme finito , una funzione , e un ordine totale su . Voglio trovare il numero di cicli distinti in .f : S S < S SSf:SS<SS

Per un dato elemento posso usare l'algoritmo di Floyd (o Brent, ecc.) Per trovare la lunghezza del ciclo a cui ripetute applicazioni di invia ; con un po 'più di sforzo riesco a identificare questo ciclo (ad es. con il suo elemento -minimal). Un metodo errato per risolvere il problema sarebbe quello di ripetere questo ogni elemento, ordinare gli elementi minimi risultanti scartando i duplicati e restituire il conteggio. Ma ciò comporta potenzialmente molti passaggi sugli stessi elementi e requisiti di spazio elevati.f s <sSfs<

Quali metodi offrono migliori prestazioni in termini di tempo e spazio? Non sono nemmeno sicuro di quale sia il modo migliore per misurare lo spazio necessario: se è la funzione di identità, qualsiasi metodo che memorizza tutti i cicli utilizzerà lo spazio .Ω ( n )fΩ(n)


4
Uno dei modi naturali per misurare lo spazio è considerare S come l'insieme di stringhe n-bit e f come un oracolo. Quindi l'algoritmo ingenuo che hai descritto richiede spazio esponenziale. Si potrebbe cercare un algoritmo che utilizza solo lo spazio polinomiale, ma questo non sembra probabile che sia possibile per me.
Tsuyoshi Ito,

Questo è ciò che intendevo per "Non so quale sia il modo migliore per misurare lo spazio". Forse dovrei scegliere come target O (poli (n) + y) dove y è l'output, in modo che lo spazio utilizzato sia polinomiale fintanto che y è sufficientemente piccolo.
Charles,

La vostra funzione f ha alcuna proprietà utilizzabili? O se non l'algoritmo è polinomiale o esponenziale il tuo modo preferito per esprimere la dimensione di ingresso sarà un po 'discutibile se la risposta pratica è che l'algoritmo avrà tempo e nello spazio su ordine della cardinalità di S .
Niel de Beaudrap,

@Niel de Beaudrap: non sono sicuro di quali proprietà siano utili. Mi aspetto che il numero di cicli distinti sia piccolo, tuttavia, probabilmente ; ecco perché ho suggerito una funzione di e invece di solo . Sono disposto a utilizzare lo spazio esponenziale nel numero di bit di output, se necessario. ynnO(n3)ynn
Charles,

Risposte:


7

Se tutto ciò che vuoi fare è contare il numero di cicli, puoi farlo con 2 | S | bit (più cambio) di spazio. Sembra improbabile che tu possa fare molto meglio a meno che S o f non abbiano alcune proprietà particolarmente convenienti.

Inizia con un array A che memorizza numeri interi {0,1,2} - uno per elemento di S - inizializzato su zero; indicheremo questi come (unexplored), (partially explored)e (fully explored). Inizializza un contatore di cicli a zero. Per ogni elemento s  ∈  S in ordine, procedi come segue:

  1. Se A [ s ] =  (fully explored), vai al passaggio 6.
  2. Impostare A [ s ] ←  (partially explored)e impostare un iteratore j  ←  f (s) .
  3. Mentre A [ j ] =  (unexplored), imposta A [ j ] ←  (partially explored)e imposta j  ←  f (j) .
  4. Se A [ j ] =  (partially explored), abbiamo chiuso un nuovo ciclo; incrementa c di 1. (Se vuoi tenere un registro di un rappresentante di questo ciclo, il valore corrente di j farà una scelta arbitraria; ovviamente, questo non sarà necessariamente l'elemento minimo nel ciclo sotto il tuo preferito order <.) Altrimenti, abbiamo A [ j ] =  (fully explored), il che significa che abbiamo scoperto un'orbita pre-esplorata che termina in un ciclo già contato; non incrementare c .
  5. Per indicare che l'orbita che inizia da s è stata esplorata completamente, impostare j  ←  s .
    Mentre A [ j ] =  (partially explored), imposta A [ j ] ←  (fully explored)e imposta j  ←  f (j) .
  6. Procedere con l'elemento successivo s  ∈  S .

Pertanto, ogni ciclo tra le orbite indotte da f verrà conteggiato una volta; e tutti gli elementi che registri come rappresentanti saranno elementi di cicli distinti. I requisiti di memoria sono 2 | S | per l'array A, O (log | S |) per il conteggio dei cicli e altre probabilità e fine.

Ogni elemento s  ∈  S verrà visitato almeno due volte: una volta quando il valore di A [ s ] viene modificato da (unexplored)a (partially explored), e una volta per la modifica a (fully explored). Il numero totale di volte che qualsiasi nodo viene rivisitato dopo essere stato (fully explored)è limitato dal numero di tentativi di trovare nuovi cicli che non riescono a farlo, che è al massimo | S | - derivante dalla iterazione ciclo principale attraverso tutti gli elementi di S . Quindi, possiamo aspettarci che questo processo coinvolga al massimo 3 | S | attraversamenti dei nodi, contando tutte le volte in cui i nodi vengono visitati o rivisitati.

Se si desidera tenere traccia degli elementi rappresentativi dei cicli e si desidera che siano gli elementi minimi, è possibile limitare il numero di visite ai nodi a 4 | S |, se aggiungi un ulteriore "giro attorno al ciclo" al punto 4 per trovare un rappresentante più piccolo di quello in cui chiudi il ciclo. (Se le orbite sotto f consistessero solo di cicli, questo lavoro extra potrebbe essere evitato, ma questo non è vero per le f arbitrarie .)


Eccellente, questo migliora l' algoritmo spaziale che avevo in mente. In realtà non ho bisogno di rappresentanti; Ho introdotto nel caso fosse utile per qualche algoritmo. <O(|S|log|S|)<
Charles,

Mi chiedo se c'è un modo per usare molto meno spazio nel caso in cui ci siano pochi cicli totali senza usare più dello spazio polinomiale. Ah, non importa; questo farà per i miei bisogni.
Charles,

1
Mi sembra che questo dovrebbe essere in #L (usando il power matrix). Questo può essere # L-difficile?
Kaveh,

@Charles: vedi la mia risposta più recente che ti darà miglioramenti se sai che #cycles ∈ o ( | S | ). Utilizza più del polilogo | S | spazio, ma se sei disposto a scambiare spazio e tempo, potrebbe essere meglio per te.
Niel de Beaudrap,

@Niel de Beaudrap: grazie! +1 per entrambi. Questo algoritmo sembra migliore fintanto che i dati si adattano in memoria; una volta che uscirà vedrò di usare l'altro. (È possibile che l'altro sarebbe meglio se potessi inserire tutto nella cache, ma potrebbe essere troppo fastidioso.)
Charles

5

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 samplesche 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:

  1. Se s è samplesattivo, vai al passaggio 5.
  2. Inizializza un elenco vuoto cursample, un iteratore j  ← f ( s ) e un contatore t  ← 1.
  3. Mentre j non è in samples:
    - Se t  ∈  G , inserire j in entrambi cursamplee samples.
    - Incremento t e set j  ←  f (j) .
  4. 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 cursamplenell'elemento appropriato complistper 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.
  5. 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.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.