Cetak terkecil berikutnya dari 2 ^ i * 5 ^ j di mana i, j> = 0

10

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?

Justin Skiles
sumber
Saya pikir wawancara ingin meminta Anda untuk melakukannya dalam memori konstan. Menggunakan memori O (n) membuat ini sangat sepele. Atau setidaknya menggunakan memori O (logn) karena ukuran penyandian untuk input n adalah logn. Solusi O (n) untuk memori adalah solusi memori eksponensial.
InformedA

Jawaban:

14

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:

for (int i = 1;;++i)
{
    int num = i;
    while(num%2 == 0) num/=2;
    while(num%5 == 0) num/=5;
    if(num == 1) Console.WriteLine(i);
}

Pendekatan Kilian / QuestionC jauh lebih berkinerja. Cuplikan C # dengan pendekatan ini:

var itms = new SortedSet<int>();
itms.Add(1);
while(true)
{
    int cur = itms.Min;
    itms.Remove(itms.Min);
    itms.Add(cur*2);
    itms.Add(cur*5);
    Console.WriteLine(cur);
}

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 berisi 2^(i+1)*5^jdan 2^i*5^(j+1). Misalkan angka selanjutnya dalam urutan tersebut adalah 2^p*5^q. Harus ada nomor keluaran sebelumnya dari formulir 2^(p-1)*5^(q)atau 2^p*5^(q-1)(atau keduanya, jika p atau q sama dengan 0). Jika tidak, maka 2^p*5^qitu bukan angka berikutnya, karena 2^(p-1)*5^(q)dan 2^p*5^(q-1)keduanya lebih kecil.

Cuplikan kedua menggunakan O(n)memori (di mana n adalah jumlah angka yang telah dikeluarkan), karena O(i+j) = O(n)(karena i dan j keduanya kurang dari n), dan akan menemukan n angka dalam O(n log n)waktu. Cuplikan pertama menemukan angka dalam waktu eksponensial.

Brian
sumber
1
Hai, Anda dapat melihat mengapa saya bingung selama wawancara saya harap. Bahkan, contoh yang diberikan adalah keluaran dari himpunan yang dijelaskan dalam pertanyaan. 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.
Justin Skiles
Apakah itu berulang .Remove()dan .Add()akan memicu perilaku buruk dari pengumpul sampah, atau akankah hal itu dipecahkan?
Snowbody
1
@Snowbody: Pertanyaan op adalah pertanyaan algoritma, jadi agak tidak relevan. Mengabaikan itu, perhatian pertama Anda harus berurusan dengan bilangan bulat yang sangat besar, karena ini menjadi masalah jauh lebih cepat daripada overhead pengumpul sampah.
Brian
8

Ini adalah pertanyaan wawancara yang cukup umum yang berguna untuk mengetahui jawabannya. Inilah entri yang relevan di lembar buaian pribadi saya:

  • untuk menghasilkan angka-angka dari bentuk 3 a 5 b 7 c secara berurutan , mulailah dengan 1, masukkan ketiga kemungkinan penerus (3, 5, 7) ke dalam struktur tambahan, kemudian tambahkan angka terkecil dari itu ke daftar Anda.

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.

Kilian Foth
sumber
3

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 ** nuntuk 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)

MAX = 30
F = - math.log(2) / math.log(5)

def val(i, j):
    return 2 ** i * 5 ** j

def best(i, j):
    f = 100
    m = 0
    n = 0
    max_i = (int)(math.log(val(i, j)) / math.log(2) + 1) if i + j else 1
    #print((val(i, j), max_i, x))
    for mm in range(-i, max_i + 1):
        for rr in {-1, 0, 1}:
            nn = (int)(mm * F + rr)
            if nn < -j: continue
            ff = val(mm, nn)
            #print('  ' + str((ff, mm, nn, rr)))
            if ff > 1 and ff < f:
                f = ff
                m = mm
                n = nn
    return m, n

def detSeq():

    i = 0
    j = 0
    got = [val(i, j)]

    while len(got) < MAX:
        m, n = best(i, j)

        i += m
        j += n
        got.append(val(i, j))

        #print('* ' + str((val(i, j), m, n)))
        #print('- ' + str((v, i, j)))

    return got

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).

rampok
sumber
Jawaban yang bagus Saya punya ide serupa yang belum saya kodekan. Dalam ide ini, saya menyimpan pelacak untuk 2 dan 5. Ini akan melacak maksimum ndan myang telah digunakan melalui angka-angka dalam urutan sejauh ini. Pada setiap iterasi, natau mmungkin atau mungkin tidak naik. Kami membuat nomor baru 2^(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 memperbarui max_n, max_msesuai kebutuhan. Mem konstan ini. Dapat O(log^2(n))mem jika cache DP digunakan dalam panggilan pengurangan
InformedA
Menarik. Optimasi di sini adalah tidak perlu mempertimbangkan semua pasangan m & n, karena kita tahu m yang benar, n akan menghasilkan pengganda terdekat dengan 1. Jadi saya hanya perlu mengevaluasi m = -i ke max_i, dan saya hanya bisa menghitung n, membuang beberapa sampah untuk pembulatan (saya ceroboh dan hanya iterasi -1 ke 1, tetapi lebih banyak berpikir;)).
Rob
Namun, saya agak berpikir seperti Anda ... urutannya akan menjadi deterministik ... itu benar-benar seperti segitiga Pascal besar i +1 di satu arah dan j +1 di yang lain. Jadi urutannya harus deterministik secara matematis. Untuk setiap simpul dalam segitiga, akan selalu ada simpul berikutnya yang ditentukan secara matematis.
Rob
1
Mungkin ada formula untuk yang berikutnya, kita mungkin tidak perlu melakukan pencarian. Saya tidak tahu pasti.
InformedA
Ketika saya memikirkannya, bentuk aljabar yang berikutnya mungkin tidak ada (tidak semua masalah deterministik memiliki bentuk aljabar untuk solusi) selain itu ketika ada lebih banyak bilangan prima daripada hanya 2 dan 5, rumusnya mungkin cukup sulit ditemukan jika ada benar-benar ingin mengerjakan formula ini. Jika seseorang mengetahui rumusnya, saya mungkin akan membacanya sedikit, ini terdengar menarik.
DiinformasikanA
2

Brian 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.

using System;
using System.Collections.Generic;
using System.Text;

namespace TwosFives
{
    class LatticePoint : IComparable<LatticePoint>
    {
      public int i;
      public int j;
      public double value;
      public LatticePoint(int ii, int jj, double vvalue)
      {
          i = ii;
          j = jj;
          value = vvalue;
      }
      public int CompareTo(LatticePoint rhs)
      {
          return value.CompareTo(rhs.value);
      }
    }


    class Program
    {
        static void Main(string[] args)
        {
            LatticePoint startPoint = new LatticePoint(0, 0, 1);

            var leadingEdge = new List<LatticePoint> { startPoint } ;

            while (true)
            {
                LatticePoint min = leadingEdge.Min();
                Console.WriteLine(min.value);
                if (min.j + 1 == leadingEdge.Count)
                {
                    leadingEdge.Add(new LatticePoint(0, min.j + 1, min.value * 2));
                }
                min.i++;
                min.value *= 5;
            }
        }
    }
}

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.

Snowbody
sumber
1
Algoritma apa yang digunakan ini? Mengapa ini berhasil? Bagian kunci dari pertanyaan yang diajukan adalah 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.
Poin yang bagus, saya menjelaskan pemikiran saya.
Snowbody
+1 Meskipun ini kira-kira setara dengan cuplikan kedua saya, penggunaan tepi yang tidak dapat diubah membuat lebih jelas bagaimana jumlah tepi bertambah.
Brian
Ini jelas lebih lambat dari potongan Brian yang direvisi, tetapi perilaku penggunaan memorinya harus jauh lebih baik karena tidak selalu menghapus dan menambahkan elemen. (Kecuali jika CLR atau SortedSet <> memiliki beberapa metode untuk menggunakan kembali elemen yang tidak saya ketahui)
Snowbody
1

Solusi berbasis set mungkin adalah apa yang dicari pewawancara Anda, namun, memiliki konsekuensi yang tidak menguntungkan karena memiliki O(n)ingatan dan O(n lg n)waktu total untuk mengurutkan nelemen.

Sedikit matematika membantu kita menemukan solusi O(1)ruang dan O(n sqrt(n))waktu. Perhatikan itu 2^i * 5^j = 2^(i + j lg 5). Menemukan pertama nunsur {i,j > 0 | 2^(i + j lg 5)}mengurangi untuk menemukan pertama nunsur {i,j > 0 | i + j lg 5}karena fungsi (x -> 2^x)secara ketat monoton meningkat, sehingga satu-satunya cara untuk beberapa a,byang 2^a < 2^badalah jika a < b.

Sekarang, kita hanya perlu algoritma untuk menemukan urutan i + j lg 5, di mana i,jbilangan asli. Dengan kata lain, mengingat nilai kami saat ini i, j, yang meminimalkan langkah berikutnya (yaitu, memberi kami angka berikutnya dalam urutan), adalah beberapa peningkatan dalam salah satu nilai (katakanlah j += 1) bersama dengan penurunan yang lain ( i -= 2). Satu-satunya hal yang membatasi kita adalah itu i,j > 0.

Hanya ada dua kasus untuk dipertimbangkan - imeningkat atau jmeningkat. Salah satunya harus meningkat karena urutan kita meningkat, dan keduanya tidak meningkat karena kalau tidak kita melewatkan istilah di mana kita hanya memiliki satu i,jpeningkatan. 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 nilai i + j lg 5. Traversal ini adalah O(i + j):

Attempt to increase i:
++i
current difference in value CD = 1
while (j > 0)
  --j
  mark difference in value for
     current (i,j) as CD -= lg 5
  while (CD < 0) // Have to increase the sequence
    ++i          // This while will end in three loops at most.
    CD += 1
find minimum among each marked difference ((i,j) -> CD)

Attempt to increase j:
++j
current difference in value CD = lg 5
while (j > 0)
  --i
  mark difference in value for
     current (i,j) as CD -= 1
  while (CD < 0) // have to increase the sequence
    ++j          // This while will end in one loop at most.
    CD += lg 5
find minimum among each marked difference ((i,j) -> CD)

Setiap iterasi mencoba memperbarui i, kemudian j, dan berjalan dengan pembaruan yang lebih kecil dari keduanya.

Karena idan jpaling banyak O(sqrt(n)), kami memiliki O(n sqrt(n))waktu total . idan jtumbuh pada tingkat kuadrat nsejak untuk setiap nilai maksimum imaxdan jmaxada O(i j)pasangan unik dari mana untuk membuat urutan kita jika urutan kita adalah nistilah, dan idan jtumbuh dalam beberapa faktor konstan satu sama lain (karena eksponen terdiri dari linier kombinasi untuk keduanya), kita tahu itu idan jsekarang O(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.

VF1
sumber
jawaban yang bagus, saya pikir ada juga pola dalam meningkatkan urutan untuk bilangan prima nomor
InformedA
@randomA Terima kasih. Setelah beberapa pemikiran lebih lanjut, saya mencapai kesimpulan bahwa saat ini berdiri algoritma saya tidak secepat yang saya pikirkan. Jika ada cara yang lebih cepat untuk mengevaluasi "Mencoba meningkatkan i / j", saya pikir itulah kunci untuk mendapatkan waktu logaritmik.
VF1
Saya berpikir dalam hal itu: kita tahu bahwa untuk menambah jumlahnya, kita harus menambah jumlah salah satu bilangan prima. Misalnya, salah satu cara untuk meningkatkan adalah dengan mul dengan 8 dan bagi dengan 5. Jadi kita mendapatkan set semua cara untuk menambah dan mengurangi angka. Ini hanya akan berisi cara-cara dasar seperti mul 8 div 5 dan bukan mul 16 div 5. Ada juga serangkaian cara dasar untuk mengurangi. Urutkan kedua set ini dengan faktor kenaikan atau penurunannya. Diberi nomor, yang berikutnya dapat ditemukan dengan menemukan cara peningkatan yang berlaku dengan faktor terkecil dari peningkatan yang ditetapkan ..
InformedA
.. berlaku berarti ada cukup bilangan prima untuk melakukan mul dan div. Kemudian kami menemukan cara penurunan ke nomor baru, jadi mulai dengan yang menurun paling banyak. Tetap menggunakan cara baru untuk mengurangi dan kami berhenti ketika nomor baru lebih kecil dari nomor yang diberikan sebelumnya. Karena himpunan bilangan prima adalah konstan, ini berarti ukuran konstan untuk dua set. Ini perlu sedikit bukti juga, tetapi sepertinya waktu yang konstan, memori konstan di setiap nomor bagi saya. Jadi memori konstan dan waktu linier untuk mencetak nomor n.
InformedA
@randomA dari mana Anda mendapatkan divisi? Apakah Anda keberatan membuat jawaban lengkap - Saya tidak mengerti komentar Anda.
VF1