Recentemente, foi-me feita essa pergunta durante uma triagem técnica por telefone e não me saí bem. A questão está incluída literalmente abaixo.
Gere
{2^i * 5^j | i,j >= 0}
coleção classificada. Imprima continuamente o próximo valor menor.Exemplo:
{ 1, 2, 4, 5, 8, 10...}
"Próximo menor" me faz pensar que está envolvido um min-heap, mas eu realmente não sabia para onde ir a partir daí e nenhuma assistência foi fornecida pelo entrevistador.
Alguém tem conselhos sobre como resolver esse problema?
algorithms
Justin Skiles
fonte
fonte
Respostas:
Vamos reformular o problema: imprima todos os números de 1 ao infinito, de modo que o número não tenha fatores, exceto 2 e 5.
Abaixo está um trecho simples de C #:
A abordagem de Kilian / QuestionC tem muito mais desempenho. Fragmento de C # com esta abordagem:
SortedSet
impede inserções duplicadas.Basicamente, ele funciona garantindo que o próximo número na sequência esteja
itms
.Prova de que essa abordagem é válida:
O algoritmo descrito garante que, após qualquer número de saída no formulário
2^i*5^j
, o conjunto agora contenha2^(i+1)*5^j
e2^i*5^(j+1)
. Suponha que o próximo número na sequência seja2^p*5^q
. Deve existir um número de saída anterior do formulário2^(p-1)*5^(q)
ou2^p*5^(q-1)
(ou ambos, se nem p nem q forem iguais a 0). Se não, então2^p*5^q
não é o próximo número, uma vez2^(p-1)*5^(q)
e2^p*5^(q-1)
são ambos menores.O segundo trecho usa
O(n)
memória (onde n é o número de números que foram produzidos), poisO(i+j) = O(n)
(porque iej são ambos menores que n) e encontrará n números com oO(n log n)
tempo. O primeiro trecho encontra números em tempo exponencial.fonte
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()
e.Add()
vão desencadear um mau comportamento do coletor de lixo, ou isso vai resolver as coisas?Essa é uma pergunta de entrevista bastante comum e útil para saber a resposta. Aqui está a entrada relevante na minha ficha pessoal de berço:
Em outras palavras, você precisa de uma abordagem em duas etapas com um buffer classificado adicional para resolver isso com eficiência. (Uma boa descrição mais longa está em Cracking the Coding Interview, de Gayle McDowell.
fonte
Aqui está uma resposta que roda com memória constante, às custas da CPU. Esta não é uma boa resposta no contexto da pergunta original (ou seja, resposta durante uma entrevista). Mas se a entrevista durar 24 horas, não será tão ruim. ;)
A idéia é que, se eu tiver n, que é uma resposta válida, o próximo na sequência será n vezes alguma potência de dois, dividido por alguma potência de 5. Ou então n vezes uma potência de 5, dividido por um poder de dois. Desde que divida uniformemente. (... ou o divisor pode ser 1;) nesse caso, você está apenas multiplicando por 2 ou 5)
Por exemplo, para ir de 625 a 640, multiplique por 5 ** 4/2 ** 7. Ou, geralmente, multiplique por algum valor de
2 ** m * 5 ** n
para alguns m, n onde um é positivo e outro é negativo ou zero, e o valor multiplicador divide o número uniformemente.Agora, a parte complicada é encontrar o multiplicador. Mas sabemos que a) o divisor deve dividir o número uniformemente, b) o multiplicador deve ser maior que um (os números continuam aumentando) ec) se escolhermos o multiplicador mais baixo maior que 1 (ie 1 <f <todos os outros f's ), esse é certamente o próximo passo. O passo seguinte será o passo mais baixo.
A parte desagradável é encontrar o valor de m, n. Existem apenas possibilidades de log (n), porque existem apenas 2 ou 5 para desistir, mas eu tive que adicionar um fator de -1 a +1 como uma maneira desleixada de lidar com o arredondamento. Portanto, precisamos iterar através de O (log (n)) a cada passo. Portanto, é O (n log (n)) em geral.
A boa notícia é que, como leva um valor e encontra o próximo valor, você pode começar em qualquer lugar da sequência. Portanto, se você deseja o próximo após 1 bilhão, ele pode ser encontrado através da iteração entre 2/5 ou 5/2 e escolhendo o menor multiplicador maior que 1.
(Pitão)
Eu validei os primeiros 10.000 números que isso gera com relação aos primeiros 10.000 gerados pela solução da lista classificada e funciona pelo menos até agora.
Aliás, o próximo, depois de um trilhão, parece ser 1.024.000.000.000.
...
Hum. Posso obter O (n) desempenho - O (1) por valor (!) - e O (log n) uso de memória tratando
best()
como uma tabela de pesquisa que eu estendo gradualmente. No momento, ele economiza memória repetindo cada vez, mas está fazendo muitos cálculos redundantes. Mantendo esses valores intermediários - e uma lista de valores mínimos - eu posso evitar o trabalho duplicado e acelerar muito. No entanto, a lista de valores intermediários aumentará com n, daí a memória O (log n).fonte
n
em
que foram usados nos números da sequência até agora. A cada iteração,n
oum
pode ou não subir. Criamos um novo número e, em2^(max_n+1)*5^(max_m+1)
seguida, reduzimos esse número de maneira recursiva exaustiva a cada chamada, reduzindo o expoente em 1 até obtermos o mínimo que é maior que o número atual. Nós atualizamosmax_n
,max_m
conforme necessário. Isso é mem constante. Pode serO(log^2(n))
mem se o cache do DP é usado na chamada reduçãoBrian estava absolutamente certo - minha outra resposta foi muito complicada. Aqui está uma maneira mais simples e rápida de fazer isso.
Imagine o quadrante I do plano euclidiano, restrito aos números inteiros. Chame um eixo como eixo i e o outro eixo como eixo j.
Obviamente, os pontos próximos à origem serão escolhidos antes dos pontos distantes da origem. Observe também que a área ativa se afastará do eixo i antes de se afastar do eixo j.
Depois que um ponto é usado, ele nunca mais será usado. E um ponto só pode ser usado se o ponto diretamente abaixo ou à esquerda dele já tiver sido usado.
Juntando tudo isso, você pode imaginar uma "fronteira" ou "borda de ataque" que começa em torno da origem e se espalha para cima e para a direita, espalhando-se ao longo do eixo i mais do que no eixo j.
De fato, podemos descobrir algo mais: haverá no máximo um ponto na fronteira / borda para qualquer valor i. (Você deve incrementar i mais de 2 vezes para igualar um incremento de j.) Portanto, podemos representar a fronteira como uma lista contendo um elemento para cada coordenada i, variando apenas com a coordenada j e o valor da função.
A cada passagem, escolhemos o elemento mínimo na borda principal e depois o movemos na direção j uma vez. Se por acaso aumentarmos o último elemento, adicionamos um novo último elemento a mais com um valor i incrementado e um valor j de 0.
Espaço: O (n) em número de elementos impressos até o momento.
Velocidade: O (1) é inserido, mas isso não é feito sempre. (Ocasionalmente mais tempo quando o valor
List<>
precisa crescer, mas ainda assim O (1) é amortizado). O grande momento gasto é a busca pelo mínimo, O (n) no número de elementos impressos até o momento.fonte
Does anyone have advice on how to solve such a problem?
uma tentativa de entender o problema subjacente. Um despejo de código não responde bem a essa pergunta.A solução baseada em conjuntos foi provavelmente o que o entrevistador estava procurando, no entanto, tem a conseqüência infeliz de ter
O(n)
memória eO(n lg n)
tempo total paran
elementos de seqüenciamento .Um pouco de matemática nos ajuda a encontrar uma solução de
O(1)
espaço eO(n sqrt(n))
tempo. Observe isso2^i * 5^j = 2^(i + j lg 5)
. Encontrar os primeirosn
elementos de{i,j > 0 | 2^(i + j lg 5)}
reduz a encontrar os primeirosn
elementos de{i,j > 0 | i + j lg 5}
porque a função(x -> 2^x)
está aumentando estritamente monotonicamente, portanto, a única maneira de algunsa,b
deles2^a < 2^b
é sea < b
.Agora, precisamos apenas de um algoritmo para encontrar a sequência de
i + j lg 5
, ondei,j
estão os números naturais. Em outras palavras, dado o nosso valor atual dei, j
, o que minimiza o próximo movimento (ou seja, nos dá o próximo número na sequência), é um aumento em um dos valores (digamosj += 1
), juntamente com uma diminuição no outro (i -= 2
). A única coisa que está nos limitando é issoi,j > 0
.Existem apenas dois casos a considerar -
i
aumentos ouj
aumentos. Um deles deve aumentar, pois nossa sequência está aumentando e os dois não aumentam, porque, caso contrário, estamos pulando o termo em que temos apenas umi,j
aumento. Assim, um aumenta e o outro permanece o mesmo ou diminui. Expressado em C ++ 11, todo o algoritmo e sua comparação com a solução definida estão disponíveis aqui .Isso obtém memória constante, pois há apenas uma quantidade constante de objetos alocados no método, além da matriz de saída (consulte o link). O método atinge o tempo logarítmico a cada iteração, pois, para qualquer dado
(i,j)
, ele percorre o melhor par, de(a, b)
modo que(i + a, j + b)
seja o menor aumento no valor dei + j lg 5
. Este percurso éO(i + j)
:Cada iteração tenta atualizar
i
, entãoj
, e acompanha a atualização menor dos dois.Desde
i
ej
no máximoO(sqrt(n))
, temosO(n sqrt(n))
tempo total .i
ej
crescem na taxa do quadrado den
desde para quaisquer valores máximosimax
ejmax
existemO(i j)
pares únicos a partir dos quais fazer nossa sequência se nossa sequência forn
termos,i
ej
crescer dentro de algum fator constante um do outro (porque o expoente é composto de um linear combinação dos dois), sabemos dissoi
ej
somosO(sqrt(n))
.Não há muito com que se preocupar com o erro de ponto flutuante - como os termos crescem exponencialmente, teríamos que lidar com o estouro antes que o erro do flop nos alcance, por várias magnitudes. Vou acrescentar mais discussão a isso, se tiver tempo.
fonte