Mengapa kasus terburuk untuk fungsi ini O (n ^ 2)?

44

Saya mencoba untuk belajar sendiri bagaimana menghitung notasi BigO untuk fungsi arbitrer. Saya menemukan fungsi ini di buku teks. Buku ini menegaskan bahwa fungsinya adalah O (n 2 ). Ini memberikan penjelasan mengapa ini terjadi, tetapi saya berjuang untuk mengikuti. Saya bertanya-tanya apakah seseorang mungkin bisa menunjukkan kepada saya matematika di balik mengapa demikian. Pada dasarnya, saya mengerti bahwa itu adalah sesuatu yang kurang dari O (n 3 ), tetapi saya tidak dapat secara mandiri mendarat di O (n 2 )

Misalkan kita diberi tiga urutan angka, A, B, dan C. Kita akan mengasumsikan bahwa tidak ada urutan individual yang mengandung nilai duplikat, tetapi mungkin ada beberapa angka yang ada dalam dua atau tiga urutan. Masalah disjointness set tiga-arah adalah untuk menentukan apakah persimpangan dari tiga sekuens kosong, yaitu, bahwa tidak ada elemen x sehingga x ∈ A, x ∈ B, dan x ∈ C.

Kebetulan, ini bukan masalah pekerjaan rumah bagi saya - bahwa kapal telah berlayar bertahun-tahun yang lalu :), hanya saya yang mencoba menjadi lebih pintar.

def disjoint(A, B, C):
        """Return True if there is no element common to all three lists."""  
        for a in A:
            for b in B:
                if a == b: # only check C if we found match from A and B
                   for c in C:
                       if a == c # (and thus a == b == c)
                           return False # we found a common value
        return True # if we reach this, sets are disjoint

[Sunting] Menurut buku teks:

Dalam versi yang ditingkatkan, bukan hanya kita menghemat waktu jika kita beruntung. Kami mengklaim bahwa waktu berjalan kasus terburuk untuk disjoint adalah O (n 2 ).

Penjelasan buku ini, yang saya susah payah ikuti, adalah ini:

Untuk menjelaskan keseluruhan waktu berjalan, kami memeriksa waktu yang dihabiskan untuk mengeksekusi setiap baris kode. Manajemen for for over A membutuhkan O (n) waktu. Manajemen for for over B menyumbang total waktu O (n 2 ), karena loop tersebut dieksekusi pada n waktu yang berbeda. Tes a == b dievaluasi O (n 2 ) kali. Sisa waktu yang dihabiskan tergantung pada berapa banyak pasangan yang cocok (a, b). Seperti yang telah kita catat, paling banyak ada n pasangan demikian, dan demikian pula pengelolaan loop di atas C, dan perintah-perintah di dalam tubuh loop itu, gunakan paling banyak pada waktu O (n 2 ). Total waktu yang dihabiskan adalah O (n 2 ).

(Dan untuk memberikan kredit yang layak ...) Buku ini adalah: Struktur Data dan Algoritma dalam Python oleh Michael T. Goodrich et. semua, Penerbitan Wiley, hal. 135

[Sunting] Suatu pembenaran; Di bawah ini adalah kode sebelum optimasi:

def disjoint1(A, B, C):
    """Return True if there is no element common to all three lists."""
       for a in A:
           for b in B:
               for c in C:
                   if a == b == c:
                        return False # we found a common value
return True # if we reach this, sets are disjoint

Pada contoh di atas, Anda dapat dengan jelas melihat bahwa ini adalah O (n 3 ), karena setiap loop harus dijalankan sepenuhnya. Buku ini akan menyatakan bahwa dalam contoh sederhana (diberikan pertama), loop ketiga hanya kompleksitas O (n 2 ), sehingga persamaan kompleksitas berjalan sebagai k + O (n 2 ) + O (n 2 ) yang akhirnya menghasilkan O (n 2 ).

Meskipun saya tidak dapat membuktikan hal ini (dengan demikian pertanyaannya), pembaca dapat setuju bahwa kompleksitas dari algoritma yang disederhanakan setidaknya kurang dari aslinya.

[Sunting] Dan untuk membuktikan bahwa versi yang disederhanakan adalah kuadratik:

if __name__ == '__main__':
    for c in [100, 200, 300, 400, 500]:
        l1, l2, l3 = get_random(c), get_random(c), get_random(c)
        start = time.time()
        disjoint1(l1, l2, l3)
        print(time.time() - start)
        start = time.time()
        disjoint2(l1, l2, l3)
        print(time.time() - start)

Hasil:

0.02684807777404785
0.00019478797912597656
0.19134306907653809
0.0007600784301757812
0.6405444145202637
0.0018095970153808594
1.4873297214508057
0.003167390823364258
2.953308343887329
0.004908084869384766

Karena perbedaan kedua sama, fungsi yang disederhanakan memang kuadratik:

masukkan deskripsi gambar di sini

[Sunting] Dan bukti lebih lanjut:

Jika saya menganggap kasus terburuk (A = B! = C),

if __name__ == '__main__':
    for c in [10, 20, 30, 40, 50]:
        l1, l2, l3 = range(0, c), range(0,c), range(5*c, 6*c)
        its1 = disjoint1(l1, l2, l3)
        its2 = disjoint2(l1, l2, l3)
        print(f"iterations1 = {its1}")
        print(f"iterations2 = {its2}")
        disjoint2(l1, l2, l3)

hasil:

iterations1 = 1000
iterations2 = 100
iterations1 = 8000
iterations2 = 400
iterations1 = 27000
iterations2 = 900
iterations1 = 64000
iterations2 = 1600
iterations1 = 125000
iterations2 = 2500

Menggunakan uji perbedaan kedua, hasil kasus terburuk adalah kuadratik.

masukkan deskripsi gambar di sini

SteveJ
sumber
6
Entah buku itu salah atau transkripsi Anda.
candied_orange
6
Nggak. Salah salah terlepas dari seberapa baik dikutip. Entah menjelaskan mengapa kita tidak bisa hanya menganggap ini jika berjalan dengan cara terburuk ketika melakukan analisis O besar atau menerima hasil yang Anda dapatkan.
candied_orange
8
@candied_orange; Saya telah menambahkan beberapa pembenaran lebih lanjut untuk yang terbaik dari kemampuan saya - bukan setelan kuat saya. Saya akan meminta Anda lagi mengijinkan kemungkinan bahwa Anda memang salah. Anda telah membuat poin Anda, sepatutnya diambil.
SteveJ
8
Angka acak bukan kasus terburuk Anda. Itu tidak membuktikan apa-apa.
Telastyn
7
ahh baik. "Tidak ada urutan memiliki nilai duplikat" tidak mengubah kasus terburuk karena C hanya dapat memicu sekali per A. A. Maaf tentang frustrasi - itulah yang saya dapatkan karena berada di stackexchange terlambat pada hari Sabtu: D
Telastyn

Jawaban:

63

Buku itu memang benar, dan itu memberikan argumen yang bagus. Perhatikan bahwa timing bukan merupakan indikator kompleksitas algoritme yang dapat diandalkan. Pengaturan waktu mungkin hanya mempertimbangkan distribusi data khusus, atau kasus uji mungkin terlalu kecil: kompleksitas algoritme hanya menjelaskan bagaimana skala penggunaan sumber daya atau runtime di luar beberapa ukuran input yang sesuai.

Buku ini membuat argumen bahwa kompleksitas adalah O (n²) karena if a == bcabang dimasukkan paling banyak n kali. Ini tidak jelas karena loop masih ditulis sebagai bersarang. Lebih jelas jika kita mengekstraknya:

def disjoint(A, B, C):
  AB = (a
        for a in A
        for b in B
        if a == b)
  ABC = (a
         for a in AB
         for c in C
         if a == c)
  for a in ABC:
    return False
  return True

Varian ini menggunakan generator untuk mewakili hasil antara.

  • Dalam generator AB, kita akan memiliki paling banyak n elemen (karena jaminan bahwa daftar input tidak akan mengandung duplikat), dan menghasilkan generator membutuhkan kompleksitas O (n²).
  • Memproduksi generator ABCmelibatkan loop atas generator ABdengan panjang n dan Cpanjang n , sehingga kompleksitas algoritmiknya adalah O (n²) juga.
  • Operasi ini tidak bersarang tetapi terjadi secara independen, sehingga kompleksitas totalnya adalah O (n² + n²) = O (n²).

Karena pasangan daftar input dapat diperiksa secara berurutan, maka menentukan apakah jumlah daftar yang terpisah dapat dilakukan dalam waktu O (n²).

Analisis ini tidak tepat karena mengasumsikan bahwa semua daftar memiliki panjang yang sama. Kita dapat mengatakan lebih tepat bahwa ABmemiliki min paling panjang (| A |, | B |) dan memproduksinya memiliki kompleksitas O (| A | • | B |). Memproduksi ABCmemiliki kompleksitas O (min (| A |, | B |) • | C |). Kompleksitas total tergantung pada bagaimana daftar input disusun. Dengan | A | ≤ | B | ≤ | C | kita mendapatkan total kompleksitas kasus terburuk O (| A | • | C |).

Perhatikan bahwa kemenangan efisiensi dimungkinkan jika wadah input memungkinkan untuk tes keanggotaan cepat daripada harus mengulangi semua elemen. Ini bisa menjadi kasus ketika mereka diurutkan sehingga pencarian biner dapat dilakukan, atau ketika mereka hash set. Tanpa loop bersarang eksplisit, ini akan terlihat seperti:

for a in A:
  if a in B:  # might implicitly loop
    if a in C:  # might implicitly loop
      return False
return True

atau dalam versi berbasis generator:

AB = (a for a in A if a in B)
ABC = (a for a in AB if a in C)
for a in ABC:
  return False
return True
amon
sumber
4
Ini akan jauh lebih jelas jika kita menghapus nvariabel ajaib ini , dan berbicara tentang variabel aktual yang sedang dimainkan.
Alexander
15
@code_dredd Tidak bukan, tidak memiliki koneksi langsung ke kode. Ini adalah abstraksi yang membayangkan itu len(a) == len(b) == len(c), yang walaupun benar dalam konteks analisis kompleksitas waktu, cenderung membingungkan pembicaraan.
Alexander
10
Mungkin mengatakan bahwa kode OP memiliki kompleksitas kasus terburuk O (| A | • | B | + min (| A |, | B |) • | C |) sudah cukup untuk memicu pemahaman?
Pablo H
3
Satu hal lagi tentang tes waktu: seperti yang Anda ketahui, mereka tidak membantu Anda memahami apa yang sedang terjadi. Di sisi lain, mereka tampaknya memberi Anda kepercayaan tambahan dalam menghadapi berbagai klaim yang salah tetapi dengan tegas menyatakan bahwa buku itu jelas salah, jadi itu hal yang baik, dan dalam hal ini, pengujian Anda mengalahkan pengambaran tangan yang intuitif .. Untuk memahami, cara pengujian yang lebih efektif adalah dengan menjalankannya di debugger dengan breakpoints (atau menambahkan cetakan nilai-nilai variabel) pada entri setiap loop.
sdenham
4
"Perhatikan bahwa timing bukanlah indikator yang berguna untuk kompleksitas algoritmik." Saya pikir ini akan lebih akurat jika dikatakan "teliti" atau "andal" daripada "berguna".
Akumulasi
7

Perhatikan bahwa jika semua elemen berbeda di setiap daftar yang diasumsikan, Anda dapat mengulangi C hanya sekali untuk setiap elemen dalam A (jika ada elemen dalam B yang sama). Jadi inner loop adalah total O (n ^ 2)

RiaD
sumber
3

Kami akan menganggap bahwa tidak ada urutan individual yang berisi duplikat.

adalah informasi yang sangat penting.

Kalau tidak, kasus terburuk dari versi yang dioptimalkan masih akan menjadi O (n³), ketika A dan B sama dan mengandung satu elemen yang digandakan n kali:

i = 0
def disjoint(A, B, C):
    global i
    for a in A:
        for b in B:
            if a == b:
                for c in C:
                    i+=1
                    print(i)
                    if a == c:
                        return False 
    return True 

print(disjoint([1] * 10, [1] * 10, [2] * 10))

yang keluaran:

...
...
...
993
994
995
996
997
998
999
1000
True

Jadi pada dasarnya, penulis berasumsi bahwa kasus terburuk O (n³) seharusnya tidak terjadi (mengapa?), Dan "membuktikan" bahwa kasus terburuk sekarang adalah O (n²).

Optimalisasi yang sebenarnya adalah dengan menggunakan set atau dikt untuk menguji inklusi dalam O (1). Dalam hal ini, disjointakan menjadi O (n) untuk setiap input.

Eric Duminil
sumber
Komentar terakhir Anda cukup menarik, tidak memikirkan itu. Apakah Anda menyarankan itu karena Anda dapat melakukan tiga operasi O (n) secara seri?
SteveJ
2
Kecuali jika Anda mendapatkan hash sempurna dengan setidaknya satu ember per elemen input, Anda tidak dapat menguji inklusi dalam O (1). Set diurutkan biasanya memiliki pencarian O (log n). Kecuali jika Anda berbicara tentang biaya rata-rata, tapi bukan itu pertanyaannya. Namun, memiliki set biner seimbang semakin sulit O (n log n) adalah sepele.
Jan Dorniak
@JanDorniak: Komentar luar biasa, terima kasih. Sekarang agak canggung: Saya mengabaikan kasus terburuk key in dict, seperti yang penulis lakukan. : - / Dalam pembelaan saya, saya pikir ini jauh lebih sulit untuk menemukan dict dengan nkunci dan ntabrakan hash daripada hanya membuat daftar dengan nnilai duplikat. Dan dengan satu set atau dict, benar-benar tidak ada nilai duplikat juga. Jadi yang terburuk-terburuk memang O (n²). Saya akan memperbarui jawaban saya.
Eric Duminil
2
@JanDorniak Saya pikir set dan dikt adalah tabel hash dengan python yang bertentangan dengan pohon merah-hitam di C ++. Jadi kasus terburuk absolut lebih buruk, hingga 0 (n) untuk pencarian, tetapi kasus rata-rata adalah O (1). Berbeda dengan O (log n) untuk C ++ wiki.python.org/moin/TimeComplexity . Mengingat bahwa ini adalah pertanyaan python, dan bahwa domain masalah mengarah ke kemungkinan tinggi kinerja kasus rata-rata, saya tidak berpikir klaim O (1) adalah yang buruk.
Baldrickk
3
Saya rasa saya melihat masalah di sini: ketika penulis mengatakan "kami akan berasumsi bahwa tidak ada urutan individu yang berisi nilai duplikat", itu bukan langkah dalam menjawab pertanyaan; melainkan, suatu prasyarat di mana pertanyaan itu akan diatasi. Untuk tujuan pedagogis, ini mengubah masalah yang tidak menarik menjadi masalah yang menantang intuisi orang tentang big-O - dan tampaknya telah berhasil pada saat itu, dilihat dari jumlah orang yang sangat bersikeras bahwa O (n²) pasti salah. .. Juga, ketika sedang diperdebatkan di sini, menghitung jumlah langkah dalam satu contoh bukanlah penjelasan.
sdenham
3

Untuk memasukkan hal-hal ke dalam istilah yang digunakan buku Anda:

Saya pikir Anda tidak memiliki masalah memahami bahwa cek untuk a == bkasus terburuk O (n 2 ).

Sekarang dalam kasus terburuk untuk loop ketiga, setiap ain Amemiliki kecocokan B, sehingga loop ketiga akan dipanggil setiap waktu. Dalam kasus di mana atidak ada C, itu akan dijalankan melalui seluruh Cset.

Dengan kata lain, ini adalah 1 kali untuk setiap adan 1 kali untuk setiap c, atau n * n. O (n 2 )

Jadi ada O (n 2 ) + O (n 2 ) yang ditunjukkan buku Anda.

Mars
sumber
0

Trik dari metode yang dioptimalkan adalah memotong sudut. Hanya jika a dan b cocok, c akan diberikan tampilan yang layak. Sekarang Anda dapat memperkirakan bahwa dalam kasus terburuk Anda masih harus mengevaluasi masing-masing c. Ini tidak benar.

Anda mungkin berpikir kasus terburuknya adalah bahwa setiap cek untuk == b menghasilkan run over C karena setiap cek untuk == b mengembalikan kecocokan. Tetapi ini tidak mungkin karena kondisi untuk ini saling bertentangan. Agar ini berfungsi, Anda membutuhkan A dan B yang berisi nilai yang sama. Mereka mungkin dipesan secara berbeda tetapi setiap nilai dalam A harus memiliki nilai yang cocok dalam B.

Sekarang inilah kickernya. Tidak ada cara untuk mengatur nilai-nilai ini sehingga untuk setiap a Anda harus mengevaluasi semua b sebelum Anda menemukan pasangan Anda.

A: 1 2 3 4 5
B: 1 2 3 4 5

Ini akan dilakukan secara instan karena pencocokan 1 adalah elemen pertama dalam kedua seri. Bagaimana dengan

A: 1 2 3 4 5
B: 5 4 3 2 1

Itu akan bekerja untuk run pertama di atas A: hanya elemen terakhir di B yang akan menghasilkan hit. Tetapi iterasi berikutnya atas A harus sudah lebih cepat karena tempat terakhir di B sudah ditempati oleh 1. Dan memang ini hanya akan mengambil empat iterasi kali ini. Dan ini menjadi sedikit lebih baik dengan setiap iterasi berikutnya.

Sekarang saya bukan ahli matematika jadi saya tidak bisa membuktikan ini akan berakhir di O (n2) tapi saya bisa merasakannya di bakiak saya.

Martin Maat
sumber
1
Urutan elemen tidak berperan di sini. Persyaratan penting adalah tidak ada duplikat; argumennya kemudian adalah bahwa loop dapat diubah menjadi dua O(n^2)loop terpisah ; yang memberi keseluruhan O(n^2)(konstanta diabaikan).
AnoE
@AnoE Memang, urutan elemen tidak masalah. Itulah yang saya tunjukkan.
Martin Maat
Saya melihat apa yang Anda coba lakukan, dan apa yang Anda tulis tidak salah, tetapi dari perspektif OP, jawaban Anda terutama menunjukkan mengapa alur pemikiran tertentu tidak relevan; tidak menjelaskan bagaimana sampai pada solusi yang sebenarnya. OP tampaknya tidak memberikan indikasi bahwa dia benar-benar berpikir ini terkait dengan pesanan. Jadi tidak jelas bagi saya bagaimana jawaban ini akan membantu OP.
AnoE
-1

Awalnya bingung, tetapi jawaban Amon sangat membantu. Saya ingin melihat apakah saya dapat melakukan versi yang sangat ringkas:

Untuk nilai tertentu adi A, fungsi membandingkan adengan setiap kemungkinan bdi B, dan itu tidak hanya sekali. Jadi untuk yang diberikan aitu melakukan a == btepat nwaktu.

Btidak mengandung duplikat (tidak ada daftar yang melakukan), jadi untuk yang diberikan aakan ada paling banyak satu pertandingan. (Itulah kuncinya). Di mana ada pertandingan, aakan dibandingkan dengan setiap kemungkinan c, yang berarti a == cdilakukan persis n kali. Di mana tidak ada kecocokan, a == ctidak terjadi sama sekali.

Jadi untuk yang diberikan a, ada nperbandingan, atau 2nperbandingan. Ini terjadi untuk setiap orang a, jadi kasus terbaik yang mungkin (n²) dan yang terburuk adalah (2n²).

TLDR: setiap nilai adibandingkan terhadap setiap nilai bdan terhadap setiap nilai c, tetapi tidak terhadap setiap kombinasi dari bdan c. Kedua masalah ini saling berkaitan, tetapi tidak berlipat ganda.

Douglas Winship
sumber
-3

Pikirkan seperti ini, beberapa angka mungkin dalam dua atau tiga urutan tetapi kasus rata-rata ini adalah bahwa untuk setiap elemen dalam himpunan A, pencarian lengkap dilakukan dalam b. Dijamin bahwa setiap elemen dalam himpunan A akan diulangi tetapi tersirat bahwa kurang dari setengah elemen di himpunan b akan diulangi.

Saat elemen dalam set b diulangi, iterasi terjadi jika ada kecocokan. ini berarti bahwa kasus rata-rata untuk fungsi disjoint ini adalah O (n2) tetapi kasus terburuk absolut untuk itu bisa menjadi O (n3). Jika buku itu tidak merinci, itu mungkin akan memberi Anda kasus rata-rata sebagai jawaban.

Eddie Ekpo
sumber
4
Buku ini cukup jelas bahwa O (n2) adalah kasus terburuk, bukan kasus rata-rata.
SteveJ
Deskripsi fungsi dalam notasi O besar biasanya hanya memberikan batas atas pada tingkat pertumbuhan fungsi. Terkait dengan notasi O besar adalah beberapa notasi terkait, menggunakan simbol o, Ω, ω, dan Θ, untuk menggambarkan jenis batas lainnya pada tingkat pertumbuhan asimptotik. Wikipedia - Big O
candied_orange
5
"Jika buku itu tidak merinci, itu mungkin akan memberi Anda kasus rata-rata sebagai jawaban." - Uhm, tidak. Tanpa kualifikasi eksplisit, kita biasanya berbicara tentang kompleksitas langkah terburuk dalam model RAM. Ketika berbicara tentang operasi pada struktur data, dan jelas dari konteksnya, maka kita mungkin berbicara tentang kompleksitas langkah terburuk yang diamortisasi dalam model RAM. Tanpa kualifikasi eksplisit , kita umumnya tidak akan berbicara tentang kasus terbaik, kasus rata-rata, kasus yang diharapkan, kompleksitas waktu, atau model lain selain RAM.
Jörg W Mittag