Qual è il tempo di esecuzione di questo algoritmo ricorsivo?


8

Ho realizzato il seguente (non golf) programma Haskell per la sfida del codice golf del calcolo del primonvalori di A229037 .

Questa è la mia soluzione proposta per calcolare il nvalore th:

a n | n<1        = 0 
    | n<3        = 1
    | otherwise  = head (goods n)

goods n = [x | x <- [1..], isGood x n]

isGood x n = and [ x - a(n-k) /= a(n-k) - a(n-k-k) || a(n-k-k) == 0 | k <- [1..n] ]

Si noti che Haskell non memorizza nella cache o memorizza automaticamente questi valori.

La pagina OEIS per la sequenza indica che un'(n)(n+1)/2, quindi [1..]potrebbe essere sostituito da [1..(n+1)/2], poiché l'algoritmo non raggiungerà mai unX più grande di n+12.

Tentando di contare le chiamate di funzione, ho derivato il seguente limite superiore T(n), il numero di chiamate di funzione che l'algoritmo accetta per un input n:

T(n)=x=1(n+1)/2k=1n2 T(nk)+2 T(n2k)x=1(n+1)/2k=1n T(nk)x=1(n+1)/2k=1n4 T(n1)x=1(n+1)/24 n T(n1)4 n T(n1) n+122 n (n+1) T(n1))

Ho inserito la formula finale in Mathematica:

RSolve[{T[n] == 2*T[n - 1]*n*(n + 1), T[1] == 1}, T[n], n]

E ottenuto, dopo una piccola semplificazione: T(n) 2n n! (n+1)!

Il rapporto medio tra questo e il tempo di esecuzione del programma Haskell, per n[12,20] è 2.01039 e la deviazione standard dei rapporti è intorno 6.01039. (Curiosamente, la trama del registro dei rapporti sembra essere una linea retta).

I rapporti con la prima riga, definendo T(n), hanno una deviazione media e standard di 4.8106 e 1.8106, rispettivamente, ma la sua trama salta molto.

Come posso ottenere un migliore legame con la complessità temporale di questo algoritmo?

Ecco l'algoritmo in C valido (meno dichiarazioni in avanti), che credo sia approssimativamente equivalente al codice Haskell:

int a(int n){
    if (n < 1) {
        return 0;
    } else if (n < 3) {
        return 1;
    } else {
        return lowestValid(n);
    }
}

int lowestValid(int n){
    int possible = 1; // Without checking, we know that this will not exceed (n+1)/2

    while (notGood(possible, n)) {
        possible++;
    }
    return possible;
}

int notGood(int possible, int n){
    int k = 1;

    while (k <= n) {
        if ( ((possible - a(n-k)) == (a(n-k) - a(n-2*k))) && (a(n-2*k) != 0) ) {
            return 1;
        } else {
            k++;
        }
    }
    return 0;
}

Il calcolo della versione C richiede circa 5 minuti a(17) e la versione di Haskell richiede quasi lo stesso a(19).

Le prime volte delle versioni:

Haskell: [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0e-2,3.0e-2,9.0e-2,0.34,1.42,11.77,71.68,184.37,1815.91]
C:       [2.0e-6, 1.0e-6, 1.0e-6, 2.0e-6, 1.0e-6, 6.0e-6, 0.00003,0.00027, 0.002209, 0.005127, 0.016665, 0.080549, 0.243611, 0.821537, 4.56265, 24.2044, 272.212]

Ho cambiato tag e titolo per chiarire che si tratta di un'analisi algoritmica, non di una teoria della complessità. "Supponendo che la moltiplicazione e l'aggiunta siano trascurabili", puoi ? Davvero ? È ancora meglio dire ciò che stai contando, perché è probabile che non stai contando la maggior parte delle cose. Vedi anche la nostra domanda di riferimento .
Raffaello

Hai provato a tracciare il tuo risultato (con un fattore costante) rispetto ai tempi di esecuzione effettivi della misura? (Spesso è più informativo tracciare il rapporto e indovinare se converge in qualcosaO(1).) Detto questo, trovo difficile aiutare qui poiché Ansatz per Tdipende dai particolari di Haskell, che non tutti parlano qui. In particolare, come viene valutata questa comprensione insieme? Viene amemorizzato? Potresti ottenere risposte migliori (o qualsiasi, davvero!) Se includi una versione di pseudo-codice che espone quanto di ciò che accade realmente necessario per un'analisi rigorosa.
Raffaello

Infine, utilizzare metodi di adattamento per derivare i limiti di Landau è probabilmente inutile. Qualsiasi funzione di questo tipo può adattarsi solo a un set fisso di funzioni; Immagino che Mathematica abbia usato i peggiori modelli esponenziali lì, quindi ha fatto un brutto lavoro catturando una crescita super-esponenziale.
Raffaello

@Raphael I tuoi commenti sono stati molto utili. Ci penserò più quando avrò del tempo. Anche ilO(n22)derivava dall'adattare i logaritmi dei valori a una linea, che era più uno scatto nel buio che altro.
Michael Klein,

Risposte:


2

Puoi scrivere la tua ricorrenza come

T(n)=(n+1)(T(n-1)+2T(n-2)+T(n-3)+2T(n-4)+).
In particolare, T(n)(n+1)T(n-1). Questo significa che la sequenzaT(n) cresce molto rapidamente, e in particolare
T(n-1)+2T(n-2)+T(n-1)[1+2n+1n(n-1)+2n(n-1)(n-2)+]=(1+O(1/n))T(n-1).
Perciò
T(n)(n+O(1))T(n-1).
Ciò significa che
T(n)=O((n+O(1))!),
e così
T(n)=O(nO(1)(n/e)n).
Questo migliora il tuo limite da una radice quadrata.
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.