Perl - 116 byte 87 byte (vedi aggiornamento di seguito)
#!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 lo shebang come un byte, sono state aggiunte nuove linee per il buonsenso orizzontale.
Qualcosa di una combinazione di codice-golf invio di codice più veloce .
La complessità media (peggiore?) Del caso sembra essere O (log n) O (n 0,07 ) . Nulla ho trovato corre più lento di 0.001s, e ho controllato l'intero spettro dal 900000000 - 999999999 . Se trovi qualcosa che richiede molto più tempo di quello, ~ 0.1s o più, per favore fatemelo sapere.
Esempio di utilizzo
$ 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
Gli ultimi due di questi sembrano essere scenari peggiori per altre osservazioni. In entrambi i casi, la soluzione mostrata è letteralmente la prima cosa da verificare. Perché 123456789
è il secondo.
Se si desidera testare un intervallo di valori, è possibile utilizzare il seguente 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);
Ideale se reindirizzato a un file. L'intervallo 1..1000000
richiede circa 14 secondi sul mio computer (71000 valori al secondo) e l'intervallo 999000000..1000000000
richiede circa 20 secondi (50000 valori al secondo), coerentemente con la complessità media di O (log n) .
Aggiornare
Modifica : si scopre che questo algoritmo è molto simile a quello che è stato usato dai calcolatori mentali per almeno un secolo .
Sin dalla pubblicazione originale, ho controllato tutti i valori nell'intervallo da 1 a 1000000000 . Il comportamento del "caso peggiore" è stato dimostrato dal valore 699731569 , che ha testato un totale di 190 combinazioni prima di arrivare a una soluzione. Se consideri 190 come una piccola costante - e certamente lo faccio - il comportamento peggiore nell'intervallo richiesto può essere considerato O (1) . Cioè, veloce come cercare la soluzione da un tavolo gigante e, in media, molto probabilmente più veloce.
Un'altra cosa però. Dopo 190 iterazioni, nulla di più grande di 144400 non è nemmeno riuscito a superare il primo passaggio. La logica per la prima traversata non ha valore: non viene nemmeno utilizzata. Il codice sopra può essere abbreviato un po ':
#!perl -p
$.*=2,$_/=4until$_&3;
@a=map{$=-=$%*($%=$=**.5-$_);$%*$.}$j++,(0)x3while$=&&=$_;
$_="@a"
Che esegue solo il primo passaggio della ricerca. Dobbiamo confermare che non ci sono valori inferiori a 144400 che richiedessero il secondo passaggio, tuttavia:
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");
}
In breve, per l'intervallo 1..1000000000 , esiste una soluzione temporale pressoché costante e la stai osservando.
Aggiornamento aggiornato
@Dennis e io abbiamo apportato numerosi miglioramenti a questo algoritmo. Puoi seguire i progressi nei commenti qui sotto e la discussione successiva, se questo ti interessa. Il numero medio di iterazioni per l'intervallo richiesto è sceso da poco più di 4 fino a 1.229 e il tempo necessario per testare tutti i valori per 1..1000000000 è stato migliorato da 18m 54s a 2m 41s. Il caso peggiore in precedenza richiedeva 190 iterazioni; il caso peggiore ora, 854382778 , richiede solo 21 .
Il codice Python finale è il seguente:
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
Questo utilizza due tabelle di correzione pre-calcolate, una da 10kb e l'altra da 253kb. Il codice sopra include le funzioni del generatore per queste tabelle, sebbene queste dovrebbero probabilmente essere calcolate al momento della compilazione.
Una versione con tabelle di correzione di dimensioni più modeste può essere trovata qui: http://codepad.org/1ebJC2OV Questa versione richiede una media di 1.620 iterazioni al termine, con un caso peggiore di 38 , e l'intera gamma funziona in circa 3m 21s. Si compensa un po 'di tempo, usando bit and
per bit b correzione , piuttosto che modulo.
miglioramenti
I valori pari hanno maggiori probabilità di produrre una soluzione rispetto ai valori dispari.
L'articolo di calcolo mentale collegato in precedenza nota che se, dopo aver rimosso tutti i fattori di quattro, il valore da scomporre è pari, questo valore può essere diviso per due e la soluzione ricostruita:
Sebbene ciò possa avere senso per il calcolo mentale (i valori più piccoli tendono ad essere più facili da calcolare), non ha molto senso algoritmicamente. Se si prende 256 casuali 4 -tuples, ed esaminare la somma dei quadrati modulo 8 , si noterà che i valori 1 , 3 , 5 e 7 sono ciascuno raggiunti in media 32 volte. Tuttavia, i valori 2 e 6 vengono raggiunti ciascuno 48 volte. Moltiplicare i valori dispari per 2 troverà una soluzione, in media, in iterazioni del 33% in meno. La ricostruzione è la seguente:
Cura deve essere presa che un e b hanno la stessa parità, nonché c e d , ma se una soluzione è stata trovata affatto, un ordinamento corretto è garantito per esistere.
I percorsi impossibili non devono essere controllati.
Dopo aver selezionato il secondo valore, b , potrebbe già essere impossibile che esista una soluzione, dati i possibili residui quadratici per un dato modulo. Invece di verificare comunque, o passare alla successiva iterazione, il valore di b può essere "corretto" diminuendolo del minimo importo che potrebbe portare a una soluzione. Le due tabelle di correzione memorizzano questi valori, uno per b e l'altro per c . L'uso di un modulo più elevato (più precisamente, l'utilizzo di un modulo con relativamente meno residui quadratici) comporterà un miglioramento migliore. Il valore a non necessita di alcuna correzione; modificando n per essere pari, tutti i valori di a sono validi.