Sonraki en küçük 2 ^ i * 5 ^ j arasından yazdırın, burada i, j> = 0


10

Son zamanlarda bir teknik telefon taraması sırasında bu soruyu sordum ve iyi yapmadım. Soru, aşağıda kelimesi kelimesine verilmiştir.

Sıralı {2^i * 5^j | i,j >= 0}koleksiyon oluşturun . Bir sonraki en küçük değeri sürekli olarak yazdırın.

Misal: { 1, 2, 4, 5, 8, 10...}

"Bir sonraki en küçük", bana bir yığın yığını olduğunu düşündürüyor, ama oradan nereye gideceğimi gerçekten bilmiyordum ve görüşmeci tarafından hiçbir yardım verilmedi.

Böyle bir sorunun nasıl çözüleceği konusunda tavsiyesi olan var mı?


Bence röportaj sizden bunu sürekli hafızada yapmanızı istiyor. O (n) bellek kullanmak bunu oldukça önemsiz kılar. Veya en azından O (logn) hafızasını kullanarak, giriş n için kodlama boyutu logn olacağı için. Bellek çözümü için O (n) üstel bir bellek çözümüdür.
InformedA

Yanıtlar:


14

Sorunu yeniden gözden geçirelim: Sayının 2 ve 5 dışında hiçbir faktörü olmayacak şekilde her sayıyı 1'den sonsuza çıkarın.

Aşağıda basit bir C # snippet'i verilmiştir:

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);
}

Kilian / QuestionC'nin yaklaşımı çok daha başarılı. Bu yaklaşımla C # snippet'i:

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 yinelenen eklemeleri önler.

Temel olarak, dizideki bir sonraki sayının girilmesini sağlayarak çalışır itms.

Bu yaklaşımın geçerli olduğunun kanıtı:
Açıklanan algoritma, formdaki herhangi bir sayı çıktısından sonra 2^i*5^j, kümenin şimdi 2^(i+1)*5^jve içermesini sağlar 2^i*5^(j+1). Sıradaki bir sonraki sayının olduğunu varsayalım 2^p*5^q. Daha önce çıkış biçiminin sayıda bulunması gerekir 2^(p-1)*5^(q)ya da 2^p*5^(q-1)(ne p de q 0 eşit olup olmadığını ya da her ikisi birden). Değilse 2^p*5^q, bir sonraki sayı değildir, çünkü 2^(p-1)*5^(q)ve 2^p*5^(q-1)her ikisi de daha küçüktür.

İkinci snippet, O(n)hafızayı kullanır (burada n, çıkan sayıların sayısıdır), çünkü O(i+j) = O(n)(i ve j'nin ikisi de n'den küçük olduğu için) ve O(n log n)zaman içinde n sayısı bulacaktır . İlk snippet, sayıları üstel zamanda bulur.


1
Merhaba, röportaj sırasında neden kafamın karıştığını umuyorum. Aslında, sağlanan örnek, soruda açıklanan setten çıktılardır. 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

Çöp toplayıcıdan tekrarlananlar .Remove()ve .Add()kötü davranışları tetikleyecekler mi yoksa işleri çözecekler mi?
Snowbody

1
@Bilim: Operasyonun sorusu bir algoritma sorusudur, bu yüzden biraz alakasızdır. Bunu göz ardı ederek, ilk endişeniz çok büyük tamsayılarla uğraşmak olmalıdır, çünkü bu çöp toplayıcı yükünden çok daha erken bir sorun haline gelir.
Brian

8

Bu, cevabı bilmenin yararlı olduğu yeterince yaygın bir görüşme sorusudur. Kişisel beşik sayfamdaki ilgili giriş:

  • form 3'ün bir 5 b 7 c sayılarını sırayla oluşturmak için , 1 ile başlayın, olası üç ardılı da (3, 5, 7) yardımcı bir yapıya doldurun , ardından en küçük sayıyı listenize ekleyin.

Başka bir deyişle, bunu verimli bir şekilde çözmek için ek bir sıralama tamponu ile iki aşamalı bir yaklaşıma ihtiyacınız vardır. (Daha iyi bir açıklama Gayle McDowell tarafından Cracking the Coding Röportajında .


3

İşte CPU'nun pahasına sabit bellekle çalışan bir cevap. Bu, asıl soru bağlamında iyi bir cevap değildir (yani bir röportaj sırasında cevap). Ancak röportaj 24 saat sürüyorsa, o kadar da kötü değil. ;)

Fikir şu ki, eğer geçerli bir cevap olan n varsa, o zaman sıradaki bir sonraki iki kuvvetin n katı, 5'in gücüne bölünür. Ya da n kere 5'in gücüne bölünür. ikisinin gücü. Eşit şekilde bölünmesi şartıyla. (... veya bölen 1 olabilir;) bu durumda sadece 2 veya 5 ile çarpıyorsunuz)

Örneğin, 625'ten 640'a gitmek için 5 ** 4/2 ** 7 ile çarpın. Veya daha genel olarak, 2 ** m * 5 ** nbir kişinin pozitif ve birinin negatif veya sıfır olduğu bazı m, n için bir değerle çarpın . çarpan sayıyı eşit olarak böler.

Şimdi, zor kısmı çarpanı bulmak. Ama biliyoruz ki a) bölen sayıyı eşit olarak bölmelidir, b) çarpan birden büyük olmalıdır (sayılar artmaya devam eder) ve c) 1'den büyük en düşük çarpanı seçersek (yani 1 <f <diğer tüm f'ler) ), bu durumda bir sonraki adımımız garanti edilir. Bundan sonraki adım en düşük adım olacaktır.

Kötü kısmı m, n değerini bulmaktır. Sadece log (n) olasılıkları vardır, çünkü vazgeçecek çok fazla 2 ya da 5 vardır, ama yuvarlak kapanışla başa çıkmak için özensiz bir yol olarak -1'den + 1'e bir faktör eklemek zorunda kaldım. Bu yüzden her adımı sadece O (log (n)) üzerinden yinelemeliyiz. Yani genel olarak O (n log (n)).

İyi haber şu ki, bir değer alıyor ve bir sonraki değeri buluyor, dizinin herhangi bir yerine başlayabilirsiniz. Yani 1 milyardan sonra bir sonrakini istiyorsanız, 2/5 veya 5 / 2'leri tekrarlayarak ve 1'den büyük en küçük çarpanı seçerek bulabilirsiniz.

(Piton)

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

Sıralanan liste çözümü tarafından üretilen ilk 10.000'e göre ürettiği ilk 10.000 sayısını doğruladım ve en azından o kadar çalışıyor.

BTW bir trilyondan sonra bir sonraki 1.024.000.000.000 gibi görünüyor.

...

Hm. best()Adım adım genişlettiğim bir arama tablosu olarak davranarak O (n) performansını - değer (!) Başına O (1) - ve O (günlük n) bellek kullanımını elde edebilirim . Şu anda her seferinde yineleyerek bellek tasarrufu sağlıyor, ancak çok fazla hesaplama yapıyor. Bu ara değerleri - ve min değerleri listesini - tutarak, yinelenen çalışmayı önleyebilir ve çok hızlandırabilirim. Bununla birlikte, ara değerlerin listesi n ile birlikte büyüyecektir, dolayısıyla O (log n) belleği.


Mükemmel cevap. Kodlamadığım için benzer bir fikrim var. Bu fikir, ben bu maksimum izler 2 ve 5 için bir izci tutmak nve mo ana kadar sırayla numaralar aracılığıyla dışarı kullanılmıştır. Her yinelemede nya mda yükselebilir veya yükselmeyebilir. Yeni bir sayı oluşturduktan 2^(max_n+1)*5^(max_m+1)sonra, bu sayıyı her bir çağrıyı üssü 1'e düşürerek mevcut sayıdan daha büyük olanı elde edene kadar kapsamlı bir özyinelemeli şekilde azaltırız. Biz güncellemek max_n, max_mgerektiği gibi. Bu sürekli mem. Olabilir O(log^2(n))DP önbellek azaltma çağrısında kullanılması durumunda mem
InformedA

İlginç. Buradaki optimizasyon, m & n'nin tüm çiftlerini dikkate almasına gerek olmadığıdır, çünkü doğru m'yi biliyoruz, n, 1'e en yakın çarpanı üretecektir. Bu yüzden sadece m = -i'yi max_i'ye değerlendirmem gerekiyor ve ben sadece n'yi hesaplayabilir, yuvarlama için bazı çöpleri atarım (özensiz ve sadece -1 ila 1 arasında yinelenir, ancak daha fazla düşünce taşır;)).
Rob

Ancak, sanki senin gibi düşünüyorum ... dizi deterministik olacak ... gerçekten bir yönde büyük bir Pascal üçgeni i + 1, diğerinde j + 1 gibi. Yani dizi matematiksel olarak deterministik olmalıdır. Üçgendeki herhangi bir düğüm için her zaman matematiksel olarak belirlenmiş bir sonraki düğüm olacaktır.
Rob

1
Bir sonraki formül için bir formül olabilir, arama yapmamız gerekmeyebilir. Emin değilim.
InformedA

Bunu düşündüğümde, bir sonrakinin cebirsel formu olmayabilir (tüm deterministik problemlerin çözümler için cebirsel formu yoktur), ayrıca sadece 2 ve 5'ten daha fazla prim olduğunda, formülün, gerçekten bu formülü çözmek istiyor. Birisi formülü biliyorsa, muhtemelen biraz okurdum, bu ilginç görünüyor.
InformedA

2

Brian kesinlikle haklıydı - diğer cevabım çok karmaşıktı. İşte bunu yapmanın daha basit ve daha hızlı bir yolu.

Öklid düzleminin, tamsayılarla sınırlandırılmış Çeyrek I düşünün. Bir eksene i eksenini, diğer eksene j eksenini çağırın.

Açıkçası, başlangıç ​​noktasına yakın noktalar başlangıç ​​noktasından uzak noktalardan önce seçilecektir. Ayrıca, etkin alanın j ekseninden uzaklaşmadan önce i ekseninden uzaklaşacağını unutmayın.

Bir nokta kullanıldığında, bir daha asla kullanılmayacaktır. Ve bir nokta sadece doğrudan altındaki veya solundaki nokta zaten kullanılmışsa kullanılabilir.

Bunları bir araya getirerek, başlangıç ​​noktasında başlayan ve i ekseni boyunca j ekseninden daha uzağa yayılan bir "sınır" veya "ön kenar" hayal edebilirsiniz.

Aslında, daha fazla bir şey bulabiliriz: herhangi bir i-değeri için sınırda / kenarda en fazla bir nokta olacaktır. (J'nin bir artışına eşit olması için i'yi 2 kattan fazla artırmanız gerekir.) Bu nedenle, sınırı her i koordinatı için bir öğe içeren bir liste olarak temsil edebiliriz, sadece j koordinatı ve işlev değeri ile değişir.

Her geçişte, ön kenardaki minimum elemanı seçer ve ardından bir kez j yönünde hareket ettiririz. Son öğeyi yükseltirsek, artan bir i değeri ve 0 değeri j olan yeni bir son öğe ekleriz.

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;
            }
        }
    }
}

Boşluk: O ana kadar yazdırılan eleman sayısı olarak O (n).

Hız: O (1) ekler, ancak bunlar her seferinde yapılmaz. ( List<>Büyümek gerektiğinde daha uzun , ancak yine de O (1) itfa edilir). Büyük zaman alıcısı şimdiye kadar basılmış eleman sayısında minimum, O (n) arayışıdır.


1
Bu hangi algoritmayı kullanıyor? Neden çalışıyor? Sorulan sorunun temel kısmı Does anyone have advice on how to solve such a problem?, altta yatan problemi anlama çabasıdır. Kod dökümü bu soruya iyi cevap vermiyor.

İyi bir nokta, düşüncemi açıkladım.
Snowbody

+1 Bu, ikinci snippet'ime kabaca eşdeğer olsa da, değişmez kenarları kullanmanız kenar sayısının nasıl büyüdüğünü daha net hale getirir.
Brian

Bu, Brian'ın gözden geçirilmiş snippet'inden kesinlikle daha yavaş, ancak bellek kullanım davranışı, öğeleri sürekli olarak silmediği ve eklemediği için çok daha iyi olmalı. (CLR veya SortedSet <> hakkında bilmediğim öğeleri yeniden kullanma yöntemi yoksa)
Snowbody

1

Kümeye dayalı çözüm muhtemelen görüşmecinizin aradığı şeydi, ancak öğelerin sıralanması için O(n)hafızaya ve O(n lg n)toplam süreye sahip olmanın talihsiz bir sonucu var n.

Biraz matematik bir O(1)yer ve O(n sqrt(n))zaman çözümü bulmamıza yardımcı olur . Dikkat edin 2^i * 5^j = 2^(i + j lg 5). İlk Bulma nunsurlarını {i,j > 0 | 2^(i + j lg 5)}birinci bulma azaltır nunsurlarını {i,j > 0 | i + j lg 5}fonksiyonu nedeniyle (x -> 2^x)kesinlikle monoton artan, bu nedenle bazıları için tek yol ise olduğunu .a,b2^a < 2^ba < b

Şimdi, biz sadece sırasını bulmak için bir algoritma gerek i + j lg 5, i,jdoğal sayılardır. Diğer bir deyişle, şimdiki değerimiz göz önüne alındığında i, j, bir sonraki hareketi en aza indirgeyen şey (yani, dizideki bir sonraki sayıyı verir), değerlerden birinde (örneğin j += 1) diğerinde ( i -= 2) bir azalma ile birlikte bir miktar artıştır . Bizi sınırlayan tek şey şu i,j > 0.

Dikkate alınması gereken sadece iki durum vardır - iartışlar veya jartışlar. Sekansımız arttığı için bunlardan biri artmalı ve her ikisi de artmıyor çünkü aksi takdirde sadece bir tanesine sahip olduğumuz terimi atlıyoruz i,j. Böylece biri artar, diğeri aynı kalır veya azalır. C ++ 11'de ifade edilen algoritmanın tamamı ve ayarlanan çözümle karşılaştırması burada mevcuttur .

Çıkış dizisinin yanı sıra, yöntemde yalnızca sabit miktarda nesne tahsis edildiğinden, bu sabit bellek sağlar (bkz. Bağlantı). Herhangi bir için çünkü yöntem logaritmik seferinde her yinelemesini başarır (i,j), en iyi çifti için geçtiği (a, b)böyle (i + a, j + b)değerinde en küçük artıştır i + j lg 5. Bu geçiş 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)

Her yineleme idaha sonra güncelleme yapmaya çalışır jve ikisinin daha küçük güncellemesiyle devam eder.

Yana ive jen altındadır O(sqrt(n)), biz toplam sahip O(n sqrt(n))zamanı. ive jkaresinin oranda büyümesine nbir maksimum Fonksiyonlar, değerlerin elde için yana imaxve jmaxorada mevcut O(i j)eden sekans ise eden dizisi yapmak için eşsiz çiftleri nterimler ve ive jüstel bir doğrusal oluşur, çünkü birbirlerine (bazı sabit faktör içinde büyümek ikisinin kombinasyonu), bunu biliyoruz ive jvardır O(sqrt(n)).

Kayan nokta hatası konusunda endişelenecek çok şey yok - terimler katlanarak büyüdüğü için, flop hatası bizimle birkaç büyüklükte yakalanmadan önce taşma ile uğraşmak zorundayız. Zamanım olursa buna daha fazla tartışma ekleyeceğim.


büyük cevap, ben de herhangi bir sayı prim için sırasını artırmak bir desen olduğunu düşünüyorum
Bilgilendirilmiş

@randomA Teşekkürler. Biraz daha düşündükten sonra, şu anda olduğu gibi algoritmamın düşündüğüm kadar hızlı olmadığı sonucuna vardım. Eğer "i / j'yi artırmaya çalışın" değerini değerlendirmenin daha hızlı bir yolu varsa, bence logaritmik zamanın anahtarı budur.
VF1

Bunu düşünüyordum: sayıyı arttırmak için asal sayılardan birinin sayısını arttırmamız gerektiğini biliyoruz. Örneğin, artırmanın bir yolu 8 ile katlamak ve 5'e bölmektir. Böylece sayıyı arttırmak ve azaltmak için tüm yollar setini alırız. Bu sadece 8 div 5 mul gibi temel yolları içerecek, 16 mul 5 değil. Bu iki seti artış veya azalma faktörlerine göre sıralayın. Bir sayı verildiğinde, bir sonraki artış setinden en küçük faktör ile uygulanabilir bir artış yolu bularak bulunabilir ..
InformedA

.. uygulanabilir araçlar, mul ve div yapmak için yeterli primer vardır. Sonra yeni sayıya bir azalma yolu buluyoruz, bu yüzden en çok azalandan başlıyoruz. Azaltmak için yeni yollar kullanmaya devam edin ve yeni sayı verilen orijinal sayıdan daha küçük olduğunda dururuz. Asal set sabit olduğundan, iki set için sabit boyut anlamına gelir. Bunun da biraz kanıta ihtiyacı var, ama bana her sayıda sabit zaman, sabit hafıza gibi geliyor. Böylece n hafızasını yazdırmak için sabit bellek ve doğrusal zaman.
InformedA

@randomA Nereden bölüntün? Tam bir cevap vermeyi düşünür müsünüz - Yorumlarınızı tam olarak anlamıyorum.
VF1
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.