Stampa il più piccolo successivo da 2 ^ i * 5 ^ j dove i, j> = 0


10

Mi è stata posta questa domanda durante uno screening tecnico del telefono di recente e non ho fatto bene. La domanda è inclusa alla lettera di seguito.

Genera {2^i * 5^j | i,j >= 0}raccolta ordinata. Stampa continuamente il valore più piccolo successivo.

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

"Next smallest" mi fa pensare che sia coinvolto un min-heap, ma non sapevo davvero dove andare da lì e non è stata fornita assistenza dall'intervistatore.

Qualcuno ha consigli su come risolvere un simile problema?


Penso che l'intervista voglia chiederti di farlo a memoria costante. L'uso della memoria O (n) lo rende abbastanza banale. O almeno usando la memoria O (logn) perché la dimensione della codifica per l'ingresso n sarebbe logn. Una O (n) per la soluzione di memoria è una soluzione di memoria esponenziale.
Informato il

Risposte:


14

Riformuliamo il problema: emetti ogni numero da 1 a infinito in modo tale che il numero non abbia fattori tranne 2 e 5.

Di seguito è riportato un semplice frammento di 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);
}

L' approccio di Kilian / QuestionC è molto più performante. Snippet C # con questo approccio:

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 impedisce inserimenti duplicati.

Fondamentalmente, funziona assicurandosi che il prossimo numero nella sequenza sia in itms.

Prova che questo approccio è valido:
l'algoritmo descritto assicura che dopo ogni output numerico nel modulo 2^i*5^j, il set ora contenga 2^(i+1)*5^je 2^i*5^(j+1). Supponiamo che il prossimo numero nella sequenza sia 2^p*5^q. Deve esistere un numero di output precedente del modulo 2^(p-1)*5^(q)o 2^p*5^(q-1)(o entrambi, se né p né q sono uguali a 0). Se no, allora 2^p*5^qnon è il numero successivo, dal momento che 2^(p-1)*5^(q)e 2^p*5^(q-1)sono entrambi più piccoli.

Il secondo frammento utilizza la O(n)memoria (dove n è il numero di numeri che sono stati emessi), poiché O(i+j) = O(n)(poiché i e j sono entrambi inferiori a n) e troverà n numeri nel O(n log n)tempo. Il primo frammento trova i numeri in tempo esponenziale.


1
Ciao, puoi vedere perché spero sia stato confuso durante l'intervista. In effetti, l'esempio fornito è l'output dell'insieme descritto nella domanda. 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,

Quelli vengono ripetuti .Remove()e .Add()scateneranno comportamenti scorretti da parte del garbage collector o riusciranno a capire le cose?
Snowbody,

1
@Snowbody: La domanda dell'op è una domanda di algoritmi, quindi è in qualche modo irrilevante. Ignorandolo, la tua prima preoccupazione dovrebbe avere a che fare con numeri interi molto grandi, poiché questo diventa un problema molto prima del sovraccarico del Garbage Collector.
Brian,

8

Questa è una domanda di intervista abbastanza comune che è utile conoscere la risposta. Ecco la voce pertinente nel mio foglio personale del presepe:

  • per generare i numeri del modulo 3 a 5 b 7 c in ordine , iniziare con 1, inserire tutti e tre i successori possibili (3, 5, 7) in una struttura ausiliaria, quindi aggiungere il numero più piccolo da esso al proprio elenco.

In altre parole, è necessario un approccio in due passaggi con un buffer ordinato aggiuntivo per risolverlo in modo efficiente. (Una buona descrizione più lunga è in Cracking the Coding Interview di Gayle McDowell.


3

Ecco una risposta che funziona con memoria costante, a spese della CPU. Questa non è una buona risposta nel contesto della domanda originale (ovvero la risposta durante un'intervista). Ma se l'intervista dura 24 ore, non è poi così male. ;)

L'idea è che se ho n che è una risposta valida, allora il prossimo nella sequenza sarà n volte un potere di due, diviso per un potere di 5. Oppure n volte un potere di 5, diviso per un potenza di due. A condizione che divida uniformemente. (... o il divisore può essere 1;) nel qual caso stai solo moltiplicando per 2 o 5)

Ad esempio, per andare da 625 a 640, moltiplicare per 5 ** 4/2 ** 7. O, più in generale, moltiplicare per un valore di 2 ** m * 5 ** nper alcuni m, n dove uno è positivo e uno è negativo o zero, e il il moltiplicatore divide il numero in modo uniforme.

Ora, la parte difficile è trovare il moltiplicatore. Ma sappiamo a) il divisore deve dividere il numero in modo uniforme, b) il moltiplicatore deve essere maggiore di uno (i numeri continuano ad aumentare) ec) se scegliamo il moltiplicatore più basso maggiore di 1 (cioè 1 <f <tutti gli altri f ), questo è garantito per essere il nostro prossimo passo. Il passo successivo sarà il passo più basso.

La parte cattiva sta trovando il valore di m, n. Ci sono solo possibilità di log (n), perché ci sono solo tanti 2 o 5 da rinunciare, ma ho dovuto aggiungere un fattore da -1 a +1 come un modo sciatto per gestire il roundoff. Quindi dobbiamo solo scorrere attraverso O (log (n)) ogni passaggio. Quindi è O (n log (n)) in generale.

La buona notizia è che, poiché prende un valore e trova il valore successivo, puoi iniziare ovunque nella sequenza. Quindi, se vuoi il prossimo dopo 1 miliardo, puoi semplicemente trovarlo scorrendo i 2/5 o i 5/2 e selezionando il moltiplicatore più piccolo maggiore di 1.

(pitone)

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

Ho convalidato i primi 10.000 numeri generati da questo rispetto ai primi 10.000 generati dalla soluzione dell'elenco ordinato e funziona almeno fino a quel punto.

A proposito il prossimo dopo un trilione sembra essere 1.024.000.000.000.

...

Hm. Posso ottenere O (n) prestazioni - O (1) per valore (!) - e O (log n) utilizzo della memoria trattando best()come una tabella di ricerca che estendo in modo incrementale. In questo momento risparmia memoria ripetendo ogni volta, ma sta facendo molti calcoli ridondanti. Mantenendo quei valori intermedi - e un elenco di valori minimi - posso evitare il lavoro duplicato e accelerarlo molto. Tuttavia, l'elenco dei valori intermedi crescerà con n, quindi la memoria O (log n).


Bella risposta. Ho un'idea simile che non ho codificato. In questa idea, tengo un tracker per 2 e 5. Questo seguirà il massimo ne mche finora sono stati utilizzati attraverso i numeri della sequenza. Ad ogni iterazione, no mpotrebbe o non potrebbe aumentare. Creiamo un nuovo numero per 2^(max_n+1)*5^(max_m+1)ridurre questo numero in modo ricorsivo esaustivo ogni chiamata riducendo l'esponente di 1 fino a quando non otteniamo il minimo che è maggiore del numero corrente. Aggiorniamo max_n, max_mse necessario. Questo è un mem costante. Può essere O(log^2(n))mem se viene utilizzata la cache DP nella chiamata di riduzione
Informata il

Interessante. L'ottimizzazione qui è che non è necessario considerare tutte le coppie di m & n, perché sappiamo che m, n corretto produrrà il moltiplicatore più vicino a 1. Quindi ho solo bisogno di valutare m = -i in max_i e I posso semplicemente calcolare n, gettando un po 'di immondizia per il roundoff (ero sciatto e ho ripetuto da -1 a 1, ma porta più pensiero;)).
Rob,

Comunque, sto pensando come te ... la sequenza sarà deterministica ... è davvero come un grande triangolo di Pascal i + 1 in una direzione e j + 1 nell'altra. Quindi la sequenza dovrebbe essere matematicamente deterministica. Per ogni nodo nel triangolo, ci sarà sempre un nodo successivo determinato matematicamente.
Rob,

1
Potrebbe esserci una formula per il prossimo, potrebbe non essere necessario effettuare la ricerca. Non lo so per certo.
Informato il

Quando ci penso, la forma algebrica della prossima potrebbe non esistere (non tutti i problemi deterministici hanno una forma algebrica per le soluzioni) inoltre quando ci sono più numeri primi di soli 2 e 5, la formula potrebbe essere abbastanza difficile da trovare se uno vuole davvero elaborare questa formula. Se qualcuno conosce la formula, probabilmente ne leggerei un po ', sembra interessante.
Informato il

2

Brian aveva assolutamente ragione: la mia altra risposta era troppo complicata. Ecco un modo più semplice e veloce per farlo.

Immagina il quadrante I del piano euclideo, limitato agli interi. Chiamare un asse l'asse i e l'altro asse l'asse j.

Ovviamente, i punti vicini all'origine verranno raccolti prima dei punti lontani dall'origine. Si noti inoltre che l'area attiva si allontanerà dall'asse i prima che si allontani dall'asse j.

Una volta che un punto è stato usato, non verrà mai più usato. E un punto può essere usato solo se il punto direttamente sotto di esso o alla sua sinistra è già stato usato.

Mettendoli insieme, puoi immaginare una "frontiera" o "bordo anteriore" che inizia attorno all'origine e si diffonde su e a destra, diffondendosi lungo l'asse i più lontano rispetto all'asse j.

In effetti, possiamo capire qualcosa di più: ci sarà al massimo un punto sulla frontiera / bordo per ogni dato valore i. (Devi incrementare i più di 2 volte per uguagliare un incremento di j.) Quindi, possiamo rappresentare la frontiera come un elenco contenente un elemento per ciascuna coordinata i, variando solo con la coordinata j e il valore della funzione.

Ad ogni passaggio, selezioniamo l'elemento minimo sul bordo anteriore, quindi lo spostiamo nella direzione j una volta. Se ci capita di aumentare l'ultimo elemento, aggiungiamo un nuovo ultimo elemento in più con un valore i incrementato e un valore j di 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;
            }
        }
    }
}

Spazio: O (n) in numero di elementi stampati finora.

Velocità: O (1) inserisce, ma quelli non vengono eseguiti ogni volta. (Occasionalmente più a lungo quando List<>deve crescere, ma comunque O (1) ammortizzato). Il big time sink è la ricerca del minimo, O (n) nel numero di elementi stampati finora.


1
Quale algoritmo utilizza questo? Perché funziona La parte fondamentale della domanda che si pone è Does anyone have advice on how to solve such a problem?nel tentativo di comprendere il problema di fondo. Un dump del codice non risponde bene a questa domanda.

Buon punto, ho spiegato il mio pensiero.
Snowbody,

+1 Mentre questo è approssimativamente equivalente al mio secondo frammento, l'uso di bordi immutabili rende più chiaro come cresce il conteggio dei bordi.
Brian,

Questo è decisamente più lento dello snippet rivisto di Brian, ma il suo comportamento nell'uso della memoria dovrebbe essere molto migliore in quanto non elimina e aggiunge costantemente elementi. (A meno che il CLR o SortedSet <> non disponga di un metodo per riutilizzare elementi che non conosco)
Snowbody,

1

La soluzione basata sul set era probabilmente ciò che il tuo intervistatore stava cercando, tuttavia ha la sfortunata conseguenza di avere O(n)memoria e O(n lg n)tempo totale per gli nelementi di sequenziamento .

Un po 'di matematica ci aiuta a trovare una soluzione O(1)spaziale e O(n sqrt(n))temporale. Si noti che 2^i * 5^j = 2^(i + j lg 5). Trovare i primi nelementi di {i,j > 0 | 2^(i + j lg 5)}riduce a trovare i primi nelementi del {i,j > 0 | i + j lg 5}perché la funzione (x -> 2^x)è strettamente monotonicamente crescente, quindi l'unico modo per un po ' a,bche 2^a < 2^bè se a < b.

Ora, abbiamo solo bisogno di un algoritmo per trovare la sequenza di i + j lg 5, dove i,jsono i numeri naturali. In altre parole, dato il nostro valore attuale di i, j, ciò che minimizza la prossima mossa (cioè, ci dà il numero successivo nella sequenza), è un certo aumento di uno dei valori (diciamo j += 1) insieme a una diminuzione nell'altro ( i -= 2). L'unica cosa che ci sta limitando è quella i,j > 0.

Ci sono solo due casi da considerare: iaumenti o jaumenti. Uno di questi deve aumentare poiché la nostra sequenza è in aumento, ed entrambi non aumentano perché altrimenti saltiamo il termine in cui ne abbiamo solo uno i,j. Quindi uno aumenta e l'altro rimane uguale o diminuisce. Espresso in C ++ 11, l'intero algoritmo e il suo confronto con la soluzione impostata sono disponibili qui .

Ciò consente di ottenere una memoria costante poiché nel metodo sono allocati solo una quantità costante di oggetti, a parte l'array di output (vedere collegamento). Il metodo raggiunge il tempo logaritmico ogni iterazione poiché per ogni dato (i,j), attraversa per la coppia migliore in (a, b)modo che (i + a, j + b)sia il più piccolo aumento del valore di i + j lg 5. Questo attraversamento è 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)

Ogni iterazione tenta di aggiornare i, quindi j, e va con l'aggiornamento più piccolo dei due.

Poiché ie jsiamo al massimo O(sqrt(n)), abbiamo O(n sqrt(n))tempo totale . ie jcresce al ritmo del quadrato di da nallora per qualsiasi valore massimo imaxe jmaxesistono O(i j)coppie uniche da cui creare la nostra sequenza se la nostra sequenza è ntermini, ie jcrescono entro un fattore costante l'uno dall'altro (perché l'esponente è composto da un lineare combinazione dei due), lo sappiamo ie lo jsiamo O(sqrt(n)).

Non c'è molto di cui preoccuparsi riguardo all'errore in virgola mobile - poiché i termini crescono in modo esponenziale, dovremmo fare i conti con l'overflow prima che l'errore del flop ci raggiunga, di diverse dimensioni. Aggiungerò ulteriori discussioni a questo se avrò tempo.


ottima risposta, penso che ci sia anche un modello per aumentare la sequenza di numeri primi
Informato

@RandomA Grazie. Dopo qualche ulteriore riflessione, sono giunto alla conclusione che allo stato attuale il mio algoritmo non è veloce come pensavo. Se esiste un modo più veloce di valutare "Tentativo di aumentare i / j", penso che sia la chiave per ottenere il tempo logaritmico.
VF1

Ci stavo pensando: sappiamo che per aumentare il numero, dobbiamo aumentare il numero di uno dei numeri primi. Ad esempio, un modo per aumentare è mul con 8 e dividere per 5. Quindi otteniamo l'insieme di tutti i modi per aumentare e diminuire il numero. Questo conterrà solo modi base come mul 8 div 5 e non mul 16 div 5. C'è anche un'altra serie di modi base per diminuire. Ordina questi due set in base al loro fattore di aumento o diminuzione. Dato un numero, il prossimo può essere trovato trovando un modo di aumento applicabile con il più piccolo fattore dall'aumento impostato.
Informato il

.. applicabile significa che ci sono abbastanza numeri primi per eseguire mul e div. Quindi troviamo un modo decrescente per il nuovo numero, quindi a partire da quello che diminuisce di più. Continua a usare nuovi modi per diminuire e ci fermiamo quando il nuovo numero è più piccolo del numero dato originale. Poiché l'insieme di numeri primi è costante, ciò significa dimensioni costanti per due insiemi. Ciò richiede anche un po 'di prova, ma a me sembra un tempo costante, una memoria costante per ogni numero. Memoria costante e tempo lineare per la stampa di n numeri.
Informato il

@randomA da dove hai ottenuto la divisione? Ti dispiace fare una risposta completa? Non capisco bene i tuoi commenti.
VF1
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.