Perl - 116 bytes 87 bytes (ver actualización a continuación)
#!perl -p
$.<<=1,$_>>=2until$_&3;
{$n=$_;@a=map{$n-=$a*($a-=$_%($b=1|($a=0|sqrt$n)>>1));$_/=$b;$a*$.}($j++)x4;$n&&redo}
$_="@a"
Contando el shebang como un byte, se agregaron nuevas líneas para la cordura horizontal.
Algo de una combinación de código de golf más rápido de envío de código .
La complejidad del caso promedio (¿peor?) Parece ser O (log n) O (n 0.07 ) . Nada de lo que he encontrado funciona más lento que 0.001s, y he verificado todo el rango de 900000000 a 999999999 . Si encuentra algo que lleva mucho más tiempo que eso, ~ 0.1s o más, hágamelo saber.
Uso de muestra
$ echo 123456789 | timeit perl four-squares.pl
11110 157 6 2
Elapsed Time: 0:00:00.000
$ echo 1879048192 | timeit perl four-squares.pl
32768 16384 16384 16384
Elapsed Time: 0:00:00.000
$ echo 999950883 | timeit perl four-squares.pl
31621 251 15 4
Elapsed Time: 0:00:00.000
Los dos últimos de estos parecen ser los escenarios del peor de los casos para otras presentaciones. En ambos casos, la solución que se muestra es, literalmente, lo primero que se verifica. por123456789
, es el segundo.
Si desea probar un rango de valores, puede usar el siguiente script:
use Time::HiRes qw(time);
$t0 = time();
# enter a range, or comma separated list here
for (1..1000000) {
$t1 = time();
$initial = $_;
$j = 0; $i = 1;
$i<<=1,$_>>=2until$_&3;
{$n=$_;@a=map{$n-=$a*($a-=$_%($b=1|($a=0|sqrt$n)>>1));$_/=$b;$a*$i}($j++)x4;$n&&redo}
printf("%d: @a, %f\n", $initial, time()-$t1)
}
printf('total time: %f', time()-$t0);
Mejor cuando se canaliza a un archivo. El rango 1..1000000
toma alrededor de 14 segundos en mi computadora (71000 valores por segundo), y el rango 999000000..1000000000
toma alrededor de 20 segundos (50000 valores por segundo), de acuerdo con la complejidad promedio de O (log n) .
Actualizar
Editar : Resulta que este algoritmo es muy similar a uno que ha sido utilizado por calculadoras mentales durante al menos un siglo .
Desde la publicación original, he verificado todos los valores en el rango de 1..1000000000 . El comportamiento del "peor de los casos" fue exhibido por el valor 699731569 , que probó un total de 190 combinaciones antes de llegar a una solución. Si considera que 190 es una constante pequeña, y ciertamente lo hago, el peor de los casos en el rango requerido puede considerarse O (1) . Es decir, tan rápido como buscar la solución desde una mesa gigante, y en promedio, posiblemente más rápido.
Sin embargo, otra cosa. Después de 190 iteraciones, cualquier cosa mayor que 144400 ni siquiera ha superado el primer paso. La lógica del primer recorrido transversal no tiene valor, ni siquiera se usa. El código anterior se puede acortar bastante:
#!perl -p
$.*=2,$_/=4until$_&3;
@a=map{$=-=$%*($%=$=**.5-$_);$%*$.}$j++,(0)x3while$=&&=$_;
$_="@a"
Que solo realiza el primer paso de la búsqueda. Sin embargo, debemos confirmar que no hay valores por debajo de 144400 que necesiten la segunda pasada:
for (1..144400) {
$initial = $_;
# reset defaults
$.=1;$j=undef;$==60;
$.*=2,$_/=4until$_&3;
@a=map{$=-=$%*($%=$=**.5-$_);$%*$.}$j++,(0)x3while$=&&=$_;
# make sure the answer is correct
$t=0; $t+=$_*$_ for @a;
$t == $initial or die("answer for $initial invalid: @a");
}
En resumen, para el rango 1..1000000000 , existe una solución de tiempo casi constante, y la está viendo.
Actualización actualizada
@Dennis y yo hemos realizado varias mejoras en este algoritmo. Puede seguir el progreso en los comentarios a continuación y la discusión posterior, si eso le interesa. El número promedio de iteraciones para el rango requerido se ha reducido de poco más de 4 a 1.229 , y el tiempo necesario para probar todos los valores para 1..1000000000 se ha mejorado de 18m 54s, a 2m 41s. El peor de los casos requería anteriormente 190 iteraciones; El peor de los casos ahora, 854382778 , solo necesita 21 .
El código final de Python es el siguiente:
from math import sqrt
# the following two tables can, and should be pre-computed
qqr_144 = set([ 0, 1, 2, 4, 5, 8, 9, 10, 13,
16, 17, 18, 20, 25, 26, 29, 32, 34,
36, 37, 40, 41, 45, 49, 50, 52, 53,
56, 58, 61, 64, 65, 68, 72, 73, 74,
77, 80, 81, 82, 85, 88, 89, 90, 97,
98, 100, 101, 104, 106, 109, 112, 113, 116,
117, 121, 122, 125, 128, 130, 133, 136, 137])
# 10kb, should fit entirely in L1 cache
Db = []
for r in range(72):
S = bytearray(144)
for n in range(144):
c = r
while True:
v = n - c * c
if v%144 in qqr_144: break
if r - c >= 12: c = r; break
c -= 1
S[n] = r - c
Db.append(S)
qr_720 = set([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121,
144, 145, 160, 169, 180, 196, 225, 241, 244, 256, 265, 289,
304, 324, 340, 361, 369, 385, 400, 409, 436, 441, 481, 484,
496, 505, 529, 544, 576, 580, 585, 601, 625, 640, 649, 676])
# 253kb, just barely fits in L2 of most modern processors
Dc = []
for r in range(360):
S = bytearray(720)
for n in range(720):
c = r
while True:
v = n - c * c
if v%720 in qr_720: break
if r - c >= 48: c = r; break
c -= 1
S[n] = r - c
Dc.append(S)
def four_squares(n):
k = 1
while not n&3:
n >>= 2; k <<= 1
odd = n&1
n <<= odd
a = int(sqrt(n))
n -= a * a
while True:
b = int(sqrt(n))
b -= Db[b%72][n%144]
v = n - b * b
c = int(sqrt(v))
c -= Dc[c%360][v%720]
if c >= 0:
v -= c * c
d = int(sqrt(v))
if v == d * d: break
n += (a<<1) - 1
a -= 1
if odd:
if (a^b)&1:
if (a^c)&1:
b, c, d = d, b, c
else:
b, c = c, b
a, b, c, d = (a+b)>>1, (a-b)>>1, (c+d)>>1, (c-d)>>1
a *= k; b *= k; c *= k; d *= k
return a, b, c, d
Utiliza dos tablas de corrección calculadas previamente, una de 10 kb de tamaño y la otra de 253 kb. El código anterior incluye las funciones generadoras para estas tablas, aunque probablemente deberían calcularse en tiempo de compilación.
Puede encontrar una versión con tablas de corrección de tamaño más modesto aquí: http://codepad.org/1ebJC2OV Esta versión requiere un promedio de 1.620 iteraciones por término, con un peor caso de 38 , y el rango completo se ejecuta en aproximadamente 3 m 21 s. Se compensa un poco de tiempo, utilizando bitwise and
para la corrección b , en lugar del módulo.
Mejoras
Los valores pares tienen más probabilidades de producir una solución que los valores impares.
El artículo de cálculo mental vinculado a notas anteriores señala que si, después de eliminar todos los factores de cuatro, el valor a descomponer es par, este valor se puede dividir por dos, y la solución reconstruida:
Aunque esto podría tener sentido para el cálculo mental (los valores más pequeños tienden a ser más fáciles de calcular), no tiene mucho sentido algorítmicamente. Si toma 256 tuplas aleatorias de 4 y examina la suma de los cuadrados del módulo 8 , encontrará que los valores 1 , 3 , 5 y 7 se alcanzan en promedio 32 veces. Sin embargo, los valores 2 y 6 se alcanzan cada uno 48 veces. Multiplicando valores impares por 2 encontrará una solución, en promedio, en 33% menos iteraciones. La reconstrucción es la siguiente:
El cuidado necesita ser tomado que una y B tienen la misma paridad, así como c y d , pero si se encontró una solución en absoluto, una ordenación adecuada está garantizado para existir.
No es necesario verificar las rutas imposibles.
Después de seleccionar el segundo valor, b , puede que ya sea imposible que exista una solución, dados los posibles residuos cuadráticos para cualquier módulo dado. En lugar de verificar de todos modos, o pasar a la siguiente iteración, el valor de b puede 'corregirse' disminuyéndolo en la cantidad más pequeña que pueda conducir a una solución. Las dos tablas de corrección almacenan estos valores, uno para b y el otro para c . Usar un módulo más alto (más exactamente, usar un módulo con relativamente menos residuos cuadráticos) dará como resultado una mejor mejora. El valor a no necesita ninguna corrección; modificando n para que sea par, todos los valores deuna son válidos.