Saya ditanya pertanyaan ini selama pemutaran telepon teknis baru-baru ini dan tidak melakukannya dengan baik. Pertanyaannya termasuk kata demi kata di bawah ini.
Hasilkan
{2^i * 5^j | i,j >= 0}
koleksi yang diurutkan. Terus mencetak nilai terkecil berikutnya.Contoh:
{ 1, 2, 4, 5, 8, 10...}
"Terkecil berikutnya" membuat saya berpikir ada tumpukan kecil yang terlibat, tetapi saya tidak benar-benar tahu ke mana harus pergi dari sana dan tidak ada bantuan yang diberikan oleh pewawancara.
Adakah yang punya saran tentang cara mengatasi masalah seperti itu?
algorithms
Justin Skiles
sumber
sumber
Jawaban:
Mari kita ulangi masalahnya: Keluarkan setiap angka dari 1 hingga tak terbatas sehingga angka tersebut tidak memiliki faktor kecuali 2 dan 5.
Di bawah ini adalah cuplikan C # sederhana:
Pendekatan Kilian / QuestionC jauh lebih berkinerja. Cuplikan C # dengan pendekatan ini:
SortedSet
mencegah penyisipan duplikat.Pada dasarnya, ini bekerja dengan memastikan bahwa nomor berikutnya dalam urutan masuk
itms
.Bukti bahwa pendekatan ini valid:
Algoritma yang dijelaskan memastikan bahwa setelah untuk setiap angka keluaran dalam bentuk
2^i*5^j
, set sekarang berisi2^(i+1)*5^j
dan2^i*5^(j+1)
. Misalkan angka selanjutnya dalam urutan tersebut adalah2^p*5^q
. Harus ada nomor keluaran sebelumnya dari formulir2^(p-1)*5^(q)
atau2^p*5^(q-1)
(atau keduanya, jika p atau q sama dengan 0). Jika tidak, maka2^p*5^q
itu bukan angka berikutnya, karena2^(p-1)*5^(q)
dan2^p*5^(q-1)
keduanya lebih kecil.Cuplikan kedua menggunakan
O(n)
memori (di mana n adalah jumlah angka yang telah dikeluarkan), karenaO(i+j) = O(n)
(karena i dan j keduanya kurang dari n), dan akan menemukan n angka dalamO(n log n)
waktu. Cuplikan pertama menemukan angka dalam waktu eksponensial.sumber
1 = 2^0*5^0, 2 = 2^1*5^0, 4 = 2^2*5^0, 5 = 2^0*5^1, 8 = 2^3*5^0, 10 = 2^1*5^1
..Remove()
dan.Add()
akan memicu perilaku buruk dari pengumpul sampah, atau akankah hal itu dipecahkan?Ini adalah pertanyaan wawancara yang cukup umum yang berguna untuk mengetahui jawabannya. Inilah entri yang relevan di lembar buaian pribadi saya:
Dengan kata lain, Anda memerlukan pendekatan dua langkah dengan buffer tambahan yang diurutkan untuk menyelesaikannya secara efisien. (Deskripsi yang lebih panjang dalam Cracking the Coding Wawancara oleh Gayle McDowell.
sumber
Inilah jawaban yang berjalan dengan memori konstan, dengan mengorbankan CPU. Ini bukan jawaban yang baik dalam konteks pertanyaan awal (yaitu jawaban saat wawancara). Tetapi jika wawancaranya panjang 24 jam, maka itu tidak terlalu buruk. ;)
Idenya adalah bahwa jika saya memiliki n yang merupakan jawaban yang valid, maka selanjutnya dalam urutan akan n kali beberapa kekuatan dua, dibagi dengan beberapa kekuatan 5. Atau yang lain n kali kekuatan 5, dibagi dengan kekuatan dua. Asalkan terbagi rata. (... atau pembagi bisa 1;) dalam hal ini Anda hanya mengalikan 2 atau 5)
Misalnya, untuk beralih dari 625 ke 640, kalikan dengan 5 ** 4/2 ** 7. Atau, lebih umum, kalikan dengan beberapa nilai
2 ** m * 5 ** n
untuk beberapa m, n di mana satu positif dan satu negatif atau nol, dan pengali membagi angka secara merata.Sekarang, bagian yang sulit adalah menemukan pengganda. Tetapi kita tahu a) pembagi harus membagi bilangan secara merata, b) pengali harus lebih besar dari satu (angka terus meningkat), dan c) jika kita memilih pengali terendah lebih besar dari 1 (yaitu 1 <f <semua f ), maka itu dijamin menjadi langkah kita selanjutnya. Langkah setelah itu akan menjadi langkah terendah.
Bagian yang jahat adalah menemukan nilai m, n. Hanya ada log (n) kemungkinan, karena hanya ada begitu banyak 2 atau 5 untuk menyerah, tetapi saya harus menambahkan faktor -1 hingga +1 sebagai cara ceroboh untuk menangani pembulatan. Jadi kita hanya perlu beralih melalui O (log (n)) setiap langkah. Jadi itu adalah O (n log (n)) secara keseluruhan.
Berita baiknya adalah, karena mengambil nilai dan menemukan nilai berikutnya, Anda dapat mulai dari mana saja dalam urutan. Jadi, jika Anda ingin yang berikutnya setelah 1 miliar, itu hanya dapat menemukannya dengan mengulangi 2/5 atau 5/2 dan memilih pengganda terkecil yang lebih besar dari 1.
(python)
Saya memvalidasi 10.000 angka pertama yang dihasilkan ini dibandingkan 10.000 pertama yang dihasilkan oleh solusi daftar yang diurutkan, dan berfungsi setidaknya sejauh itu.
BTW yang berikutnya setelah satu triliun tampaknya menjadi 1.024.000.000.000.
...
Hm Saya bisa mendapatkan kinerja O (n) - O (1) per nilai (!) - dan O (log n) penggunaan memori dengan memperlakukan
best()
sebagai tabel pencarian yang diperluas secara bertahap. Saat ini menghemat memori dengan mengulangi setiap kali, tapi itu melakukan banyak perhitungan yang berlebihan. Dengan memegang nilai-nilai menengah - dan daftar nilai min - saya dapat menghindari pekerjaan duplikat & mempercepatnya banyak. Namun, daftar nilai-nilai perantara akan tumbuh dengan n, maka memori O (log n).sumber
n
danm
yang telah digunakan melalui angka-angka dalam urutan sejauh ini. Pada setiap iterasi,n
ataum
mungkin atau mungkin tidak naik. Kami membuat nomor baru2^(max_n+1)*5^(max_m+1)
saat mengurangi angka ini dengan cara rekursif lengkap setiap panggilan mengurangi eksponen dengan 1 sampai kami mendapatkan minimum yang lebih besar dari nomor saat ini. Kami memperbaruimax_n
,max_m
sesuai kebutuhan. Mem konstan ini. DapatO(log^2(n))
mem jika cache DP digunakan dalam panggilan penguranganBrian benar sekali - jawaban saya yang lain terlalu rumit. Inilah cara yang lebih sederhana dan lebih cepat untuk melakukannya.
Bayangkan Kuadran I dari pesawat Euclidean, terbatas pada bilangan bulat. Sebut satu sumbu sumbu-i dan sumbu lainnya sumbu-j.
Jelas, poin yang dekat dengan titik asal akan diambil sebelum titik yang jauh dari titik asal. Perhatikan juga bahwa area aktif akan menjauh dari sumbu i sebelum bergerak menjauh dari sumbu j.
Setelah titik digunakan, itu tidak akan pernah digunakan lagi. Dan sebuah titik hanya dapat digunakan jika titik tepat di bawahnya atau di sebelah kiri telah digunakan.
Menyatukan ini, Anda dapat membayangkan "perbatasan" atau "ujung depan" yang dimulai di sekitar titik asal dan menyebar ke kanan dan ke kanan, menyebar di sepanjang sumbu-i lebih jauh daripada di sumbu-j.
Bahkan, kita bisa mencari tahu sesuatu yang lebih: akan ada paling banyak satu titik di perbatasan / tepi untuk setiap nilai-i yang diberikan. (Anda harus menambah i lebih dari 2 kali untuk sama dengan kenaikan j.) Jadi, kita dapat mewakili perbatasan sebagai daftar yang berisi satu elemen untuk setiap koordinat i, hanya bervariasi dengan koordinat j dan nilai fungsi.
Setiap pass, kita memilih elemen minimum di edge terdepan, dan kemudian memindahkannya ke arah j sekali. Jika kita meningkatkan elemen terakhir, kita menambahkan elemen terakhir yang lebih baru dengan nilai-i yang bertambah dan nilai-j 0.
Spasi: O (n) dalam jumlah elemen yang dicetak sejauh ini.
Kecepatan: O (1) menyisipkan, tetapi itu tidak dilakukan setiap waktu. (Kadang-kadang lebih lama ketika
List<>
harus tumbuh, tetapi masih O (1) diamortisasi). Tenggelam waktu besar adalah pencarian minimum, O (n) dalam jumlah elemen yang dicetak sejauh ini.sumber
Does anyone have advice on how to solve such a problem?
dalam upaya untuk mendapatkan pemahaman tentang masalah yang mendasarinya. Sebuah dump kode tidak menjawab pertanyaan itu dengan baik.Solusi berbasis set mungkin adalah apa yang dicari pewawancara Anda, namun, memiliki konsekuensi yang tidak menguntungkan karena memiliki
O(n)
ingatan danO(n lg n)
waktu total untuk mengurutkann
elemen.Sedikit matematika membantu kita menemukan solusi
O(1)
ruang danO(n sqrt(n))
waktu. Perhatikan itu2^i * 5^j = 2^(i + j lg 5)
. Menemukan pertaman
unsur{i,j > 0 | 2^(i + j lg 5)}
mengurangi untuk menemukan pertaman
unsur{i,j > 0 | i + j lg 5}
karena fungsi(x -> 2^x)
secara ketat monoton meningkat, sehingga satu-satunya cara untuk beberapaa,b
yang2^a < 2^b
adalah jikaa < b
.Sekarang, kita hanya perlu algoritma untuk menemukan urutan
i + j lg 5
, di manai,j
bilangan asli. Dengan kata lain, mengingat nilai kami saat inii, j
, yang meminimalkan langkah berikutnya (yaitu, memberi kami angka berikutnya dalam urutan), adalah beberapa peningkatan dalam salah satu nilai (katakanlahj += 1
) bersama dengan penurunan yang lain (i -= 2
). Satu-satunya hal yang membatasi kita adalah itui,j > 0
.Hanya ada dua kasus untuk dipertimbangkan -
i
meningkat atauj
meningkat. Salah satunya harus meningkat karena urutan kita meningkat, dan keduanya tidak meningkat karena kalau tidak kita melewatkan istilah di mana kita hanya memiliki satui,j
peningkatan. Jadi satu meningkat, dan yang lainnya tetap sama atau menurun. Dinyatakan dalam C ++ 11, keseluruhan algoritma dan perbandingannya dengan solusi yang ditetapkan tersedia di sini .Ini mencapai memori konstan karena hanya ada jumlah konstan objek yang dialokasikan dalam metode, selain dari array keluaran (lihat tautan). Metode ini mencapai waktu logaritmik setiap iterasi karena untuk setiap yang diberikan
(i,j)
, itu melintasi untuk pasangan terbaik(a, b)
sehingga(i + a, j + b)
merupakan peningkatan terkecil dalam nilaii + j lg 5
. Traversal ini adalahO(i + j)
:Setiap iterasi mencoba memperbarui
i
, kemudianj
, dan berjalan dengan pembaruan yang lebih kecil dari keduanya.Karena
i
danj
paling banyakO(sqrt(n))
, kami memilikiO(n sqrt(n))
waktu total .i
danj
tumbuh pada tingkat kuadratn
sejak untuk setiap nilai maksimumimax
danjmax
adaO(i j)
pasangan unik dari mana untuk membuat urutan kita jika urutan kita adalahn
istilah, dani
danj
tumbuh dalam beberapa faktor konstan satu sama lain (karena eksponen terdiri dari linier kombinasi untuk keduanya), kita tahu itui
danj
sekarangO(sqrt(n))
.Tidak ada terlalu banyak yang perlu dikhawatirkan tentang kesalahan floating point - karena persyaratan tumbuh secara eksponensial, kita harus berurusan dengan overflow sebelum kesalahan gagal menyusul kami, dengan beberapa besaran. Saya akan menambahkan lebih banyak diskusi untuk ini jika saya punya waktu.
sumber