Berapa waktu berjalan dari algoritma rekursif ini?

8

Saya membuat program Haskell (ungolfed) berikut untuk tantangan kode golf untuk komputasi yang pertamannilai A229037 .

Ini adalah solusi yang saya usulkan untuk menghitung nnilai 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] ]

Perhatikan bahwa Haskell tidak secara otomatis menyimpan atau mem-memo nilai-nilai ini.

Halaman OEIS untuk urutan memberikan fakta itu a(n)(n+1)/2, sehingga [1..]bisa diganti oleh [1..(n+1)/2], karena algoritme tidak akan pernah mencapaix lebih besar dari n+12.

Mencoba menghitung panggilan fungsi, saya menurunkan batas atas berikut T(n), jumlah fungsi yang dipanggil oleh algoritma untuk 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))

Saya memasukkan rumus terakhir ke dalam Mathematica:

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

Dan dapatkan, setelah sedikit penyederhanaan: T(n) 2n n! (n+1)!

Rasio rata-rata antara ini dan waktu pelaksanaan program Haskell, untuk n[12,20] adalah 2.01039 dan standar deviasi rasio sekitar 6.01039. (Cukup aneh, plot log rasio tampaknya menjadi garis lurus).

Rasio dengan baris pertama, menentukan T(n), memiliki mean dan standar deviasi 4.8106 dan 1.8106, masing-masing, tetapi plotnya banyak melompat.

Bagaimana saya bisa mendapatkan keterikatan yang lebih baik pada kompleksitas waktu dari algoritma ini?

Berikut ini adalah algoritma dalam C yang valid (minus deklarasi maju), yang saya percaya kira-kira setara dengan kode 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;
}

Versi C membutuhkan waktu sekitar 5 menit untuk menghitung a(17) dan versi Haskell membutuhkan waktu yang hampir sama untuk a(19).

Kali pertama versi:

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]
Michael Klein
sumber
Saya telah mengubah tag dan judul untuk memperjelas bahwa ini adalah analisis algoritma, bukan pertanyaan teori kompleksitas. "Dengan asumsi bahwa multiplikasi dan penambahan dapat diabaikan" - bisakah Anda ? Benarkah ? Masih lebih baik untuk mengatakan apa yang Anda hitung, karena kemungkinan besar Anda tidak menghitung banyak hal. Lihat juga pertanyaan referensi kami .
Raphael
Sudahkah Anda mencoba memplot hasil Anda (dengan beberapa faktor konstan) terhadap waktu berjalan ukuran aktual? (Sering kali lebih informatif untuk merencanakan rasio dan menebak jika konvergen ke sesuatu masukO(1).) Yang mengatakan, saya merasa sulit untuk membantu di sini sejak ansatz untuk Ttergantung pada keterangan dari Haskell, yang tidak semua orang di sini berbicara. Secara khusus, bagaimana pemahaman set ini dievaluasi? Apakah ayang memoized? Anda mungkin mendapatkan jawaban yang lebih baik (atau ada, sungguh!) Jika Anda memasukkan versi pseudo-code yang memaparkan apa yang sebenarnya terjadi sebagaimana diperlukan untuk analisis yang ketat.
Raphael
Akhirnya, menggunakan metode pas untuk mendapatkan batas Landau mungkin sia-sia. Setiap fungsi seperti itu hanya dapat cocok dengan satu set fungsi tetap; Saya menduga bahwa Mathematica digunakan pada model eksponensial terburuk di sana, sehingga melakukan pekerjaan yang buruk menangkap pertumbuhan super-eksponensial.
Raphael
@Raphael Komentar Anda sangat membantu. Saya akan melihat lebih dalam ketika saya punya waktu. JugaO(n22)datang dari menyesuaikan logaritma nilai ke garis, yang lebih merupakan bidikan dalam kegelapan daripada yang lain.
Michael Klein

Jawaban:

2

Anda dapat menuliskan perulangan Anda sebagai

T(n)=(n+1)(T(n1)+2T(n2)+T(n3)+2T(n4)+).
Khususnya, T(n)(n+1)T(n1). Ini artinya urutanT(n) tumbuh sangat cepat, dan khususnya
T(n1)+2T(n2)+T(n1)[1+2n+1n(n1)+2n(n1)(n2)+]=(1+O(1/n))T(n1).
Karena itu
T(n)(n+O(1))T(n1).
Ini artinya
T(n)=O((n+O(1))!),
dan sebagainya
T(n)=O(nO(1)(n/e)n).
Ini meningkatkan Anda terikat oleh akar kuadrat.
Yuval Filmus
sumber