Wydrukuj następny najmniejszy z 2 ^ i * 5 ^ j gdzie i, j> = 0

10

Zadano mi to pytanie podczas technicznego przeglądu telefonu i nie zrobiłem tego dobrze. Pytanie zostało zawarte dosłownie poniżej.

Generuj {2^i * 5^j | i,j >= 0}posortowaną kolekcję. Ciągłe drukowanie następnej najmniejszej wartości.

Przykład: { 1, 2, 4, 5, 8, 10...}

„Następny najmniejszy” sprawia, że ​​myślę, że wiąże się to z niewielką stertą, ale tak naprawdę nie wiedziałem, dokąd się udać i ankieter nie udzielił żadnej pomocy.

Czy ktoś ma porady, jak rozwiązać taki problem?

Justin Skiles
źródło
Myślę, że wywiad chce cię prosić o zrobienie tego w stałej pamięci. Korzystanie z pamięci O (n) czyni to dość banalnym. Lub przynajmniej używając pamięci O (logn), ponieważ rozmiar kodowania dla wejścia n byłby logn. O (n) dla rozwiązania pamięci to rozwiązanie pamięci wykładniczej.
InformedA

Odpowiedzi:

14

Przeredagujmy problem: wypisz każdą liczbę od 1 do nieskończoności, aby liczba nie zawierała żadnych czynników oprócz 2 i 5.

Poniżej znajduje się prosty fragment kodu w języku C #:

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

Podejście Kiliana / QuestionC jest znacznie bardziej wydajne. Fragment kodu C # przy takim podejściu:

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 zapobiega duplikowaniu wstawek.

Zasadniczo działa, upewniając się, że następny numer w sekwencji jest w itms.

Dowód, że to podejście jest poprawne:
opisany algorytm zapewnia, że ​​po dowolnej liczbie danych wyjściowych w formularzu 2^i*5^j, zestaw zawiera teraz 2^(i+1)*5^ji 2^i*5^(j+1). Załóżmy, że kolejnym numerem w sekwencji jest 2^p*5^q. Musi istnieć poprzednio wyprowadzony numer formularza 2^(p-1)*5^(q)lub 2^p*5^(q-1)(lub oba, jeśli ani p, ani q nie są równe 0). Jeśli nie, to 2^p*5^qnie jest kolejny numer, ponieważ 2^(p-1)*5^(q)i 2^p*5^(q-1)to zarówno mniejsze.

Drugi fragment wykorzystuje O(n)pamięć (gdzie n jest liczbą wyprowadzonych liczb), ponieważ O(i+j) = O(n)(ponieważ oba i i są mniejsze niż n), i znajdzie n liczb w O(n log n)czasie. Pierwszy fragment znajduje liczby w czasie wykładniczym.

Brian
źródło
1
Cześć, widzicie, dlaczego byłem zdezorientowany podczas wywiadu, mam nadzieję. W rzeczywistości podanym przykładem są dane wyjściowe z zestawu opisanego w pytaniu. 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,
Czy te się powtarzają .Remove()i .Add()będą wywoływać złe zachowanie ze śmietnika, czy to wszystko rozwiąże?
Snowbody
1
@Snowbody: Pytanie op jest pytaniem algorytmicznym, więc jest trochę nieistotne. Ignorując to, twoją pierwszą troską powinno być zajmowanie się bardzo dużymi liczbami całkowitymi, ponieważ staje się to problemem znacznie wcześniej niż narzut na śmieci.
Brian
8

To pytanie jest dość powszechne, dlatego warto znać odpowiedź. Oto odpowiedni wpis w moim osobistym arkuszu łóżeczka:

  • do generowania liczb postaci 3 5 b 7 c , aby rozpocząć się 1, treści wszystkich trzech możliwych programów (3, 5, 7) na konstrukcji pomocniczej, a następnie dodać najmniejszą liczbę od niego liście.

Innymi słowy, potrzebujesz dwuetapowego podejścia z dodatkowym posortowanym buforem, aby rozwiązać to skutecznie. (Dobry dłuższy opis znajduje się w Wywiad Cracking the Coding przeprowadzonym przez Gayle McDowell.

Kilian Foth
źródło
3

Oto odpowiedź, która działa ze stałą pamięcią, kosztem procesora. To nie jest dobra odpowiedź w kontekście pierwotnego pytania (tj. Odpowiedzi podczas wywiadu). Ale jeśli wywiad trwa 24 godziny, to nie jest tak źle. ;)

Chodzi o to, że jeśli mam n, która jest poprawną odpowiedzią, to następną w sekwencji będzie n razy potęga dwóch, podzielona przez potęgę 5. Albo n razy potęga 5, podzielona przez potęga dwóch. Pod warunkiem, że dzieli się równomiernie. (... lub dzielnikiem może być 1;), w którym to przypadku pomnożymy przez 2 lub 5)

Na przykład, aby przejść z 625 do 640, należy pomnożyć przez 5 ** 4/2 ** 7. Lub, bardziej ogólnie, pomnożyć przez pewną wartość 2 ** m * 5 ** ndla pewnego m, n, gdzie jeden jest dodatni, a drugi ujemny lub zero, a mnożnik dzieli liczbę równomiernie.

Teraz trudną częścią jest znalezienie mnożnika. Wiemy jednak: a) dzielnik musi dzielić liczbę równomiernie, b) mnożnik musi być większy niż jeden (liczby ciągle rosną), c) jeśli wybierzemy najniższy mnożnik większy niż 1 (tj. 1 <f <wszystkie pozostałe f ), to z pewnością będzie nasz następny krok. Kolejny krok będzie najniższym krokiem.

Paskudną częścią jest znalezienie wartości m, n. Możliwości są tylko log (n), ponieważ jest tylko tyle 2 lub 5 do zrezygnowania, ale musiałem dodać współczynnik -1 do +1 jako niechlujny sposób radzenia sobie z zaokrągleniem. Musimy więc tylko iterować przez O (log (n)) na każdym kroku. Więc ogólnie jest to O (n log (n)).

Dobrą wiadomością jest to, że ponieważ wymaga ona wartości i znajduje następną wartość, możesz zacząć w dowolnym miejscu w sekwencji. Jeśli więc chcesz mieć następny po 1 miliardie, możesz go po prostu znaleźć, wykonując iteracje 2/5 lub 5/2 i wybierając najmniejszy mnożnik większy niż 1.

(pyton)

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

Zweryfikowałem pierwsze 10 000 liczb, które to generuje, względem pierwszych 10 000 wygenerowanych przez posortowane rozwiązanie listy, i działa to przynajmniej tak daleko.

BTW, następny po bilionie wydaje się być 1 024 000 000 000.

...

Hm Mogę uzyskać wydajność O (n) - O (1) na wartość (!) - i zużycie pamięci O (log n) traktując best()jako tabelę odnośników, którą stopniowo rozszerzam. Obecnie oszczędza pamięć, iterując za każdym razem, ale wykonuje wiele zbędnych obliczeń. Trzymając te wartości pośrednie - i listę minimalnych wartości - mogę uniknąć zduplikowanej pracy i bardzo ją przyspieszyć. Jednak lista wartości pośrednich będzie rosła wraz z n, stąd pamięć O (log n).

Obrabować
źródło
Świetna odpowiedź. Mam podobny pomysł, że nie kodowałem. W tym pomyśle trzymam moduł śledzący dla 2 i 5. Spowoduje to śledzenie maksimum ni mktóre zostały wykorzystane w liczbach w sekwencji do tej pory. W każdej iteracji, nczy mmoże lub nie może iść w górę. Tworzymy nowy numer, a 2^(max_n+1)*5^(max_m+1)następnie zmniejszamy go w sposób wyczerpujący rekurencyjny przy każdym wywołaniu, zmniejszając wykładnik o 1, dopóki nie otrzymamy minimum większego niż obecny numer. Aktualizujemy max_n, max_mile potrzeba. To stałe mem. Może być O(log^2(n))zapamiętany, jeśli pamięć podręczna DP jest używana w połączeniu redukcyjnym
Poinformowano
Ciekawy. Optymalizacja polega na tym, że nie trzeba uwzględniać wszystkich par m & n, ponieważ wiemy, że poprawne m, n da najbliższy mnożnik do 1. Więc muszę tylko oszacować m = -i do max_i, a ja mogę po prostu obliczyć n, wrzucając śmieci do zaokrąglenia (byłem niechlujny i po prostu iterowałem od -1 do 1, ale to wymaga więcej myślenia;)).
Rob
Jednak myślę trochę tak jak ty ... sekwencja będzie deterministyczna ... to naprawdę przypomina duży trójkąt Pascala i + 1 w jednym kierunku i j + 1 w drugim. Zatem sekwencja powinna być matematycznie deterministyczna. Dla każdego węzła w trójkącie zawsze będzie matematycznie określony następny węzeł.
Rob
1
Może być formuła na następną, być może nie będziemy musieli wyszukiwać. Nie wiem na pewno.
InformedA
Kiedy o tym myślę, forma algebraiczna następnego może nie istnieć (nie wszystkie problemy deterministyczne mają formę algebraiczną dla rozwiązań), a ponadto, gdy jest więcej liczb pierwszych niż tylko 2 i 5, formuła może być dość trudna do znalezienia, jeśli jedna naprawdę chce wypracować tę formułę. Jeśli ktoś zna tę formułę, prawdopodobnie przeczytałbym o niej trochę, brzmi to interesująco.
InformedA
2

Brian miał absolutną rację - moja inna odpowiedź była zbyt skomplikowana. Oto prostszy i szybszy sposób na zrobienie tego.

Wyobraź sobie kwadrant I płaszczyzny euklidesowej, ograniczony do liczb całkowitych. Nazwij jedną oś osią i, a drugą oś osią j.

Oczywiście punkty w pobliżu źródła będą wybierane przed punktami daleko od źródła. Należy również pamiętać, że obszar aktywny odsunie się od osi i, zanim odejdzie od osi j.

Kiedy punkt zostanie wykorzystany, nigdy więcej go nie użyje. I punktu można użyć tylko wtedy, gdy punkt znajdujący się bezpośrednio pod nim lub po jego lewej stronie został już użyty.

Zestawiając je razem, możesz wyobrazić sobie „granicę” lub „krawędź wiodącą”, która zaczyna się wokół początku i rozkłada się w górę iw prawo, rozciągając się wzdłuż osi i dalej niż na osi j.

W rzeczywistości możemy dowiedzieć się czegoś więcej: na granicy / krawędzi będzie co najwyżej jeden punkt dla dowolnej wartości i. (Musisz zwiększyć i więcej niż 2 razy, aby równać się przyrostowi j.) Tak więc możemy przedstawić granicę jako listę zawierającą jeden element dla każdej współrzędnej i, zmieniając się tylko ze współrzędną j i wartością funkcji.

Za każdym przejściem wybieramy minimalny element na krawędzi natarcia, a następnie przesuwamy go raz w kierunku j. Jeśli zdarza się, że podnosimy ostatni element, dodajemy nowy ostatni jeszcze element o zwiększonej wartości i i wartości j równej 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;
            }
        }
    }
}

Spacja: O (n) w liczbie wydrukowanych do tej pory elementów.

Wstawki Speed: O (1), ale nie są wykonywane za każdym razem. (Czasami dłuższy, gdy List<>musi rosnąć, ale nadal amortyzuje się O (1)). Zlewem czasowym jest poszukiwanie minimum, O (n) w liczbie wydrukowanych do tej pory elementów.

Snowbody
źródło
1
Jakiego algorytmu używa ten? Dlaczego to działa? Kluczową częścią zadawanego pytania jest Does anyone have advice on how to solve such a problem?próba zrozumienia zasadniczego problemu. Zrzut kodu nie odpowiada dobrze na to pytanie.
Dobra uwaga, wyjaśniłem swoje myślenie.
Snowbody
+1 Chociaż jest to mniej więcej odpowiednik mojego drugiego fragmentu, twoje użycie niezmiennych krawędzi sprawia, że ​​staje się wyraźniejsze, jak rośnie liczba krawędzi.
Brian
Jest to zdecydowanie wolniejsze niż poprawiony fragment Briana, ale zachowanie pamięci powinno być o wiele lepsze, ponieważ nie usuwa i nie dodaje elementów. (Chyba że CLR lub SortedSet <> ma jakąś metodę ponownego wykorzystywania elementów, o których nie wiem)
Snowbody
1

Rozwiązanie oparte na zestawie było prawdopodobnie tym, czego szukał twój ankieter, ma jednak niefortunną konsekwencję posiadania O(n)pamięci i O(n lg n)całkowitego czasu na sekwencjonowanie nelementów.

Trochę matematyki pomaga nam znaleźć rozwiązanie dotyczące O(1)przestrzeni i O(n sqrt(n))czasu. Zauważ to 2^i * 5^j = 2^(i + j lg 5). Znalezienie pierwszych nelementów {i,j > 0 | 2^(i + j lg 5)}sprowadza się do znalezienia pierwszych nelementów {i,j > 0 | i + j lg 5}, ponieważ funkcja (x -> 2^x)jest ściśle monotonicznie rośnie, więc jedynym sposobem dla niektórych a,bto 2^a < 2^bjest, jeśli a < b.

Teraz potrzebujemy algorytmu, aby znaleźć sekwencję i + j lg 5, gdzie i,jsą liczby naturalne. Innymi słowy, biorąc pod uwagę naszą bieżącą wartość tego i, j, co minimalizuje następny ruch (tj. Daje nam następną liczbę w sekwencji), następuje pewien wzrost jednej z wartości (powiedzmy j += 1) wraz ze spadkiem drugiej ( i -= 2). Jedyną rzeczą, która nas ogranicza, jest to i,j > 0.

Są tylko dwa przypadki do rozważenia - iwzrosty lub jwzrosty. Jeden z nich musi wzrosnąć, ponieważ nasza sekwencja rośnie, i oba nie rosną, ponieważ w przeciwnym razie pomijamy termin, w którym mamy tylko jeden i,jwzrost. W ten sposób jeden wzrasta, a drugi pozostaje taki sam lub maleje. W języku C ++ 11 cały algorytm i jego porównanie z zestawem rozwiązań są dostępne tutaj .

Osiąga to stałą pamięć, ponieważ w metodzie jest tylko stała liczba obiektów, oprócz tablicy wyjściowej (patrz link). Metoda osiąga czas logarytmiczny przy każdej iteracji, ponieważ dla dowolnej danej (i,j), przechodzi do najlepszej pary (a, b), która (i + a, j + b)jest najmniejszym wzrostem wartości i + j lg 5. To przejście to 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)

Każda iteracja próbuje inastępnie zaktualizować ji idzie w parze z mniejszą aktualizacją obu.

Ponieważ ii jjesteśmy najwyżej O(sqrt(n)), mamy całkowity O(n sqrt(n))czas. ii jrosną z szybkością kwadratu nod tego czasu dla dowolnych maksymalnych wartości imaxi jmaxistnieją O(i j)unikalne pary, z których można utworzyć naszą sekwencję, jeśli nasza sekwencja jest nwyrażeniami, ii jrosną w obrębie pewnego stałego współczynnika względem siebie (ponieważ wykładnik składa się z liniowego połączenie dwóch), wiemy o tym ii jO(sqrt(n)).

Nie trzeba się zbytnio przejmować błędem zmiennoprzecinkowym - ponieważ warunki rosną wykładniczo, musielibyśmy poradzić sobie z przepełnieniem, zanim błąd flopa nas dopadnie, o kilka wielkości. Dodam do tego więcej dyskusji, jeśli będę miał czas.

VF1
źródło
świetna odpowiedź, myślę, że istnieje również wzorzec zwiększania sekwencji dla dowolnej liczby liczb pierwszych
InformedA
@randomA Dzięki. Po kilku dalszych przemyśleniach doszedłem do wniosku, że na obecnym etapie mój algorytm nie jest tak szybki, jak myślałem. Jeśli istnieje szybszy sposób oceny „Próby zwiększenia i / j”, myślę, że to jest klucz do uzyskania czasu logarytmicznego.
VF1,
Myślałem o tym: wiemy, że aby zwiększyć liczbę, musimy zwiększyć liczbę jednej liczby pierwszej. Na przykład, jednym ze sposobów na zwiększenie jest zmielenie przez 8 i podzielenie przez 5. Otrzymujemy zestaw wszystkich sposobów na zwiększenie i zmniejszenie liczby. Będzie to zawierało tylko podstawowe sposoby, takie jak mul 8 dział 5, a nie mul 16 dział 5. Istnieje również inny zestaw podstawowych sposobów zmniejszania. Posortuj te dwa zestawy według ich współczynnika wzrostu lub zmniejszenia. Biorąc pod uwagę liczbę, następną można znaleźć, znajdując odpowiedni sposób zwiększenia z najmniejszym czynnikiem z zestawu wzrostów.
InformedA
.. zastosowanie oznacza, że ​​jest wystarczająca liczba pierwsza, aby wykonać mul i div. Następnie znajdujemy drogę do nowej liczby, więc zaczynając od tej, która zmniejsza się najbardziej. Używaj nowych sposobów zmniejszania, a my przestajemy, gdy nowy numer jest mniejszy niż pierwotnie podany numer. Ponieważ zbiór liczb pierwszych jest stały, oznacza to stały rozmiar dwóch zbiorów. To również wymaga trochę dowodu, ale dla mnie wygląda na stały czas, stałą pamięć przy każdej liczbie. Tak więc stała pamięć i liniowy czas drukowania n liczb.
InformedA
@randomA skąd masz podział? Nie masz nic przeciwko udzieleniu pełnej odpowiedzi - nie do końca rozumiem twoje komentarze.
VF1,