Me hicieron esta pregunta durante una evaluación técnica telefónica recientemente y no me fue bien. La pregunta se incluye textualmente a continuación.
Generar
{2^i * 5^j | i,j >= 0}
colección ordenada. Imprima continuamente el siguiente valor más pequeño.Ejemplo:
{ 1, 2, 4, 5, 8, 10...}
El "siguiente más pequeño" me hace pensar que hay un montón mínimo involucrado, pero realmente no sabía a dónde ir desde allí y el entrevistador no me brindó asistencia.
¿Alguien tiene consejos sobre cómo resolver tal problema?
algorithms
Justin Skiles
fuente
fuente
Respuestas:
Reformulemos el problema: envíe cada número del 1 al infinito de modo que el número no tenga factores, excepto 2 y 5.
A continuación se muestra un fragmento de C # simple:
El enfoque de Kilian / QuestionC es mucho más eficaz. Fragmento de C # con este enfoque:
SortedSet
evita inserciones duplicadas.Básicamente, funciona asegurándose de que esté el siguiente número en la secuencia
itms
.Prueba de que este enfoque es válido:
el algoritmo descrito asegura que después de cualquier salida numérica en el formulario
2^i*5^j
, el conjunto ahora contiene2^(i+1)*5^j
y2^i*5^(j+1)
. Supongamos que el siguiente número en la secuencia es2^p*5^q
. Debe existir un número de salida anterior de la forma2^(p-1)*5^(q)
o2^p*5^(q-1)
(o ambos, si ni p ni q son iguales a 0). Si no, entonces2^p*5^q
no es el siguiente número, ya que2^(p-1)*5^(q)
y2^p*5^(q-1)
son a la vez más pequeño.El segundo fragmento utiliza
O(n)
memoria (donde n es el número de números que se han emitido), dado queO(i+j) = O(n)
(porque i y j son menos que n), y encontrará n números aO(n log n)
tiempo. El primer fragmento encuentra números en tiempo exponencial.fuente
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()
y.Add()
van a desencadenar un mal comportamiento del recolector de basura, o resolverá las cosas?Esta es una pregunta de entrevista bastante común que es útil saber la respuesta. Aquí está la entrada relevante en mi hoja de cuna personal:
En otras palabras, necesita un enfoque de dos pasos con un búfer ordenado adicional para resolver esto de manera eficiente. (Una buena descripción más larga se encuentra en Cracking the Coding Interview por Gayle McDowell.
fuente
Aquí hay una respuesta que se ejecuta con memoria constante, a expensas de la CPU. Esta no es una buena respuesta en el contexto de la pregunta original (es decir, la respuesta durante una entrevista). Pero si la entrevista dura 24 horas, entonces no está tan mal. ;)
La idea es que si tengo n, que es una respuesta válida, entonces la siguiente en la secuencia será n veces alguna potencia de dos, dividida por alguna potencia de 5. O bien n veces una potencia de 5, dividida por un poder de dos. Siempre que se divida de manera uniforme. (... o el divisor puede ser 1;) en cuyo caso solo está multiplicando por 2 o 5)
Por ejemplo, para pasar de 625 a 640, multiplique por 5 ** 4/2 ** 7. O, más generalmente, multiplique por algún valor de
2 ** m * 5 ** n
para algunos m, n donde uno es positivo y uno es negativo o cero, y el multiplicador divide el número de manera uniforme.Ahora, la parte difícil es encontrar el multiplicador. Pero sabemos que a) el divisor debe dividir el número de manera uniforme, b) el multiplicador debe ser mayor que uno (los números siguen aumentando) yc) si elegimos el multiplicador más bajo mayor que 1 (es decir, 1 <f <todos los demás f) ), entonces ese es nuestro próximo paso. El siguiente paso será el más bajo.
La parte desagradable es encontrar el valor de m, n. Solo hay posibilidades de log (n), porque solo hay que renunciar a 2 o 5, pero tuve que agregar un factor de -1 a +1 como una forma descuidada de lidiar con el redondeo. Entonces solo tenemos que iterar a través de O (log (n)) en cada paso. Entonces es O (n log (n)) en general.
La buena noticia es que, dado que toma un valor y encuentra el siguiente valor, puede comenzar en cualquier parte de la secuencia. Entonces, si desea el siguiente después de mil millones, puede encontrarlo iterando a través de los 2/5 o 5/2 y seleccionando el multiplicador más pequeño mayor que 1.
(pitón)
Validé los primeros 10,000 números que esto genera contra los primeros 10,000 generados por la solución de lista ordenada, y funciona al menos hasta ahora.
Por cierto, el siguiente después de un billón parece ser 1,024,000,000,000.
...
Hm. Puedo obtener el rendimiento de O (n) - O (1) por valor (!) - y el uso de memoria O (log n) al tratar
best()
como una tabla de búsqueda que extiendo de forma incremental. En este momento ahorra memoria al iterar cada vez, pero está haciendo muchos cálculos redundantes. Al mantener esos valores intermedios, y una lista de valores mínimos, puedo evitar el trabajo duplicado y acelerarlo mucho. Sin embargo, la lista de valores intermedios crecerá con n, de ahí la memoria O (log n).fuente
n
ym
ha sido utilizado a través de los números en la secuencia hasta ahora. En cada iteración,n
om
podría o no subir. Creamos un nuevo número a medida que2^(max_n+1)*5^(max_m+1)
reducimos este número de manera exhaustiva y recursiva cada llamada reduciendo el exponente en 1 hasta que obtengamos el mínimo que sea mayor que el número actual. Actualizamosmax_n
,max_m
según sea necesario. Esto es constante mem. Puede serO(log^2(n))
mem si se usa caché DP en llamada de reducciónBrian tenía toda la razón: mi otra respuesta fue demasiado complicada. Aquí hay una manera más simple y rápida de hacerlo.
Imagine el cuadrante I del plano euclidiano, restringido a los enteros. Llame a un eje el eje i y al otro eje el eje j.
Obviamente, los puntos cercanos al origen se seleccionarán antes que los puntos alejados del origen. También tenga en cuenta que el área activa se alejará del eje i antes de alejarse del eje j.
Una vez que se ha usado un punto, nunca más se volverá a usar. Y un punto solo se puede usar si el punto directamente debajo de él o a la izquierda del mismo ya se ha usado.
Al unirlos, puede imaginar una "frontera" o "borde de ataque" que comienza alrededor del origen y se extiende hacia arriba y hacia la derecha, extendiéndose a lo largo del eje i más allá del eje j.
De hecho, podemos descubrir algo más: habrá como máximo un punto en la frontera / borde para cualquier valor i dado. (Debe incrementar i más de 2 veces para igualar un incremento de j.) Entonces, podemos representar la frontera como una lista que contiene un elemento para cada coordenada i, que solo varía con la coordenada j y el valor de la función.
Cada pasada, elegimos el elemento mínimo en el borde de ataque y luego lo movemos en la dirección j una vez. Si estamos aumentando el último elemento, agregamos un nuevo último elemento más con un valor i incrementado y un valor j de 0.
Espacio: O (n) en número de elementos impresos hasta ahora.
Velocidad: O (1) inserta, pero eso no se hace todo el tiempo. (Ocasionalmente más tiempo cuando
List<>
tiene que crecer, pero aún O (1) amortizado). El gran sumidero de tiempo es la búsqueda del mínimo, O (n) en el número de elementos impresos hasta el momento.fuente
Does anyone have advice on how to solve such a problem?
un intento de comprender el problema subyacente. Un volcado de código no responde bien a esa pregunta.Probablemente, la solución basada en conjuntos era lo que buscaba su entrevistador, sin embargo, tiene la desafortunada consecuencia de tener
O(n)
memoria yO(n lg n)
tiempo total para la secuencia den
elementos.Un poco de matemática nos ayuda a encontrar una solución de
O(1)
espacio yO(n sqrt(n))
tiempo. Tenga en cuenta que2^i * 5^j = 2^(i + j lg 5)
. La búsqueda de los primerosn
elementos de la{i,j > 0 | 2^(i + j lg 5)}
reduce a la búsqueda de los primerosn
elementos de{i,j > 0 | i + j lg 5}
porque la función(x -> 2^x)
es estrictamente monótona en aumento, por lo que la única manera de que algunaa,b
de que2^a < 2^b
es sia < b
.Ahora, solo necesitamos un algoritmo para encontrar la secuencia de
i + j lg 5
dóndei,j
están los números naturales. En otras palabras, dado nuestro valor actual dei, j
, lo que minimiza el siguiente movimiento (es decir, nos da el siguiente número en la secuencia), es un aumento en uno de los valores (digamosj += 1
) junto con una disminución en el otro (i -= 2
). Lo único que nos limita es esoi,j > 0
.Solo hay dos casos a considerar:
i
aumentos oj
aumentos. Uno de ellos debe aumentar ya que nuestra secuencia está aumentando, y ambos no aumentan porque de lo contrario estamos omitiendo el término en el que solo tenemos uno dei,j
aumento. Así, uno aumenta y el otro permanece igual o disminuye. Expresado en C ++ 11, el algoritmo completo y su comparación con la solución establecida están disponibles aquí .Esto logra una memoria constante ya que solo hay una cantidad constante de objetos asignados en el método, aparte de la matriz de salida (ver enlace). El método logra el tiempo logarítmico en cada iteración, ya que, para cualquier caso
(i,j)
, atraviesa el mejor par de(a, b)
manera que(i + a, j + b)
sea el menor incremento en el valor dei + j lg 5
. Este recorrido esO(i + j)
:Cada iteración intenta actualizarse
i
, entoncesj
, y va con la actualización más pequeña de las dos.Desde
i
yj
como muchoO(sqrt(n))
, tenemosO(n sqrt(n))
tiempo total .i
yj
crecer a la velocidad del cuadrado den
desde para cualquier valor máximoimax
yjmax
existenO(i j)
pares únicos a partir de los cuales hacer nuestra secuencia si nuestra secuencia esn
términos,i
yj
crecer dentro de un factor constante entre sí (porque el exponente está compuesto por un lineal combinación de los dos), lo sabemosi
yj
somosO(sqrt(n))
.No hay mucho de qué preocuparse con respecto al error de coma flotante, ya que los términos crecen exponencialmente, tendríamos que lidiar con el desbordamiento antes de que el error de flop nos alcance, en varias magnitudes. Agregaré más discusión a esto si tengo tiempo.
fuente