Solución 1: Greedy iterativo

Técnicas utilizadas. Greedy (voraz). Construir la pirámide perfecta nivel por nivel, sumando cajas hasta quedarnos sin ellas. Si sobran, agregarlas secuencialmente al piso, expandiendo la base paso a paso de la forma más barata posible.

Idea de la solución. Dividimos el problema en dos fases.

Fase 1: construir la pirámide perfecta más grande

Armar niveles completos es lo más eficiente, así que la primera decisión voraz es levantar la pirámide perfecta más alta posible sin pasarnos de .

  1. Empezamos con altura .
  2. Aumentamos de a 1.
  3. En cada paso calculamos cuántas cajas extra necesita la base () y cuántas suma el total ().
  4. Repetimos mientras el total no supere .
  5. Apenas nos pasaríamos, frenamos. Nos quedamos con la pirámide perfecta anterior y sabemos cuántas cajas hay en el piso y cuántas “sobran” en la mano para la Fase 2.

Fase 2: acomodar el sobrante

Queda un remanente que no alcanza para un nivel completo. Lo agregamos expandiendo la base lo mínimo indispensable:

  1. Si agregás 1 caja al piso pegada a la pirámide, arriba podés poner 0 (acomodado: 1).
  2. Si agregás 1 más (van 2), podés apoyar 1 encima (acomodado: 3).
  3. Si agregás 1 más (van 3), podés apoyar 2 encima (acomodado: 6).

Cada caja nueva de piso aumenta la “capacidad” siguiendo la secuencia de números triangulares.

Código.

def minimumBoxes(n: int) -> int:
    cajas_piso = 0      # nuestra base (b_k)
    cajas_totales = 0   # total en la pirámide (t_k)
    nivel = 0           # altura (k)
 
    # FASE 1: construir la pirámide perfecta máxima.
    # Chequeamos si al agregar un nivel más no nos pasamos de n.
    while cajas_totales + cajas_piso + nivel + 1 <= n:
        nivel += 1
        cajas_piso += nivel            # aumentamos la base
        cajas_totales += cajas_piso    # sumamos la nueva base al total
 
    # Si la pirámide quedó justa, terminamos.
    if cajas_totales == n:
        return cajas_piso
 
    # FASE 2: acomodar el sobrante.
    cajas_sobrantes = n - cajas_totales
    cajas_extra_piso = 0
    capacidad_extra = 0
    while capacidad_extra < cajas_sobrantes:
        cajas_extra_piso += 1
        capacidad_extra += cajas_extra_piso
 
    return cajas_piso + cajas_extra_piso

Traza de ejemplo. Instancia (la misma de la descripción). La Fase 1 recorre:

IteraciónCondición while (futuro )nivelcajas_pisocajas_totales
Inicial-000
1 (V)111
2 (V)234
3 (V)3610
4 (F)3610

Como cajas_totales == 10 == n, no hay Fase 2. Respuesta: 6.

Para ver la Fase 2 en acción, con : la Fase 1 corta igual en nivel 3 (piso 6, total 10) y sobran 3. La Fase 2 agrega piso hasta que la capacidad () llegue a 3: con 2 cajas de piso la capacidad es . Respuesta: .

Complejidad.

  • Temporal: . En la Fase 1 el total crece , así que el número de niveles es proporcional a . En la Fase 2, el sobrante está acotado por la diferencia entre dos tetraédricos consecutivos (un número triangular), y recorrerlo también queda en . Sumando ambos bucles secuenciales: . Incluso para son apenas unos miles de iteraciones.
  • Espacial: . Solo un puñado de contadores primitivos, sin estructuras dinámicas ni recursión.

Cuándo usar esta técnica.

  • Favorable cuando querés una solución simple, legible y sin riesgo numérico. Trabaja con aritmética entera exacta, así que nunca falla por redondeo, y a es muy rápida igual (para , ~1000 vueltas).
  • Limitaciones: no es : recorre los niveles uno por uno. Si fuera descomunal o la función se llamara millones de veces por segundo, la versión en forma cerrada es más conveniente.
  • Comparación con la Solución 2 (forma cerrada). Las dos usan la misma estrategia voraz (pirámide máxima + relleno del sobrante); cambia solo la implementación. Esta itera y suma nivel a nivel (exacta y robusta, ); la otra invierte las fórmulas con raíces (, pero con punto flotante que hay que controlar). Regla práctica: iterativo para claridad y robustez; forma cerrada para rendimiento máximo cuando la fórmula existe y está validada.

Solución 2: Greedy en forma cerrada O(1)

Técnicas utilizadas. Greedy (voraz). Misma estrategia que la Solución 1 (pirámide completa más grande que entre, y después rellenar con lo que sobra), pero cada paso no se resuelve iterando sino despejando: se obtiene el valor exacto con una sola cuenta. Por eso todo el algoritmo corre en .

Idea de la solución.

La forma óptima: una pirámide en la esquina

Para gastar la menor cantidad de cajas en el piso, conviene apilar todo en una esquina formando una escalera piramidal. Necesitamos, para una pirámide completa, cuántas cajas usa en total y cuántas apoya en el piso.

Fórmulas “hacia adelante”

Cada piso es una capa triangular. El piso tiene:

Piso triangular

Ese es el -ésimo número triangular, . Una pirámide completa es la suma de todos sus pisos:

Piramide Tetraedrica

Desarrollando la suma:

Ese es el -ésimo número tetraédrico, . Ambas fórmulas van de tamaño a cajas: le doy y me dicen cuántas cajas hay.

Paso 1: despejar la fórmula de la pirámide (la cúbica)

Tenemos cajas y queremos el más grande cuya pirámide entre sin pasarse. En vez de probar , planteamos la igualdad al revés:

Expandiendo el producto , queda una ecuación cúbica:

Su raíz real positiva me da la “altura ideal” de la pirámide. Como casi nunca es un tetraédrico exacto, esa raíz sale con decimales, y me quedo con el entero de abajo:

Paso 1 Representacion Cubica

Igual que deshace elevar al cuadrado, la raíz de esta cúbica deshace la fórmula de la pirámide: de cajas vuelve al tamaño.

Con definido, calculamos:

Si , la pirámide quedó justa y la respuesta es directamente .

Paso 2: ubicar las sobrantes (la cuadrática)

Si sobran cajas, las apoyamos en una diagonal nueva al lado de la pirámide. La clave es que las cajas de esa diagonal se apilan: la -ésima caja de piso aguanta una pila de altura .

Fase2 Diagonal

Entonces con cajas de piso alojo

Tengo las sobrantes y quiero el menor con . Lo despejo como igualdad, paso a paso:

Esto es una ecuación cuadrática con . Se resuelve con la fórmula resolvente:

El discriminante se simplifica:

Paso2 Parabola

Me quedo con la raíz del . Como , entonces , y por lo tanto:

La negativa no tiene sentido (no existe “cantidad negativa de cajas”). Siempre sale una de cada signo porque el producto de las raíces es . Por último redondeo para arriba, porque necesito que alcance:

Ejemplo del redondeo: si , da → con 2 no alcanza, arranco la 3ª → .

Resultado final: las de piso que ya tenía de la pirámide más las nuevas para las sobrantes:

Código.

import math
 
def tetra(r):
    return r * (r + 1) * (r + 2) // 6
 
def resolver_cubica(a, b, c, d):
    # raíz real (positiva) de a·x³ + b·x² + c·x + d = 0  - deshace la fórmula de la pirámide
    B, C, D = b/a, c/a, d/a
    p = (3*C - B**2) / 3
    q = (2*B**3 - 9*B*C + 27*D) / 27
    disc = (q/2)**2 + (p/3)**3
    if disc > 0:
        raiz = math.sqrt(disc)
        u = math.copysign(abs(-q/2 + raiz)**(1/3), -q/2 + raiz)
        v = math.copysign(abs(-q/2 - raiz)**(1/3), -q/2 - raiz)
        return (u + v) - B/3
    r_val = math.sqrt(-(p/3)**3)
    phi   = math.acos(-q / (2*r_val))
    f     = 2 * math.sqrt(-p/3)
    return max(f*math.cos((phi + 2*math.pi*k)/3) - B/3 for k in range(3))
 
def minimumBoxes(n):
    if n <= 3:
        return n
 
    # Paso 1: mayor r con Tet(r) <= n, despejando la cúbica en O(1)
    r = int(math.floor(resolver_cubica(1, 3, 2, -6*n)))
    if r < 0:
        r = 0
    while tetra(r + 1) <= n:   # corrección por error de punto flotante (1-2 pasos, sigue O(1))
        r += 1
    while tetra(r) > n:
        r -= 1
 
    c_suelo = r * (r + 1) // 2
    c_sobrantes = n - tetra(r)
    if c_sobrantes == 0:
        return c_suelo
 
    # Paso 2: menor p con p(p+1)/2 >= sobrantes, despejando la cuadrática
    p = math.ceil((-1 + math.sqrt(1 + 8*c_sobrantes)) / 2)
    return c_suelo + p

Traza de ejemplo. Instancia (la misma de la descripción):

PasoCálculoResultado
¿?sigue
Doy vuelta la cúbicaraíz de exacto →
Chequeo
Piso de la pirámide
Sobrantes
Resultado devuelvo

Coincide con la Solución 1. Para ejercitar el Paso 2 (la cuadrática), con : , , , y (con daría ; con , ). Respuesta: .

Complejidad.

  • Temporal: . No hay bucles que dependan de ni recursión. Todo es aritmética más , raíz cúbica y , de costo constante. Los dos while de corrección hacen a lo sumo 1–2 iteraciones (el error de punto flotante desvía la raíz en menos de ), así que no cambian la cota.
  • Espacial: . Apenas un puñado de variables escalares.

Cuándo usar esta técnica.

  • Favorable cuando la estructura óptima del greedy tiene una fórmula cerrada conocida (acá, y ): conviene despejar en vez de iterar. Ideal si puede ser gigante o si la función se llama muchísimas veces.
  • Limitaciones: depende de punto flotante (raíces); para muy grande hay que reforzar con la corrección entera, si no un redondeo puede desviar en . Además no generaliza a variantes sin forma cerrada, y es menos evidente de leer (la corrección conceptual vive en la geometría, no en el código).
  • Comparación con la Solución 1 (iterativa). Misma estrategia voraz; esta gana en velocidad bruta ( vs ) a cambio de fragilidad numérica y menor claridad. La iterativa es trivialmente correcta y sin riesgo de redondeo, pero recorre los niveles. En la práctica, para la diferencia de tiempo es imperceptible, así que la elección real es legibilidad y robustez (iterativa) vs elegancia matemática y (forma cerrada).

Referencias.