*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Deve essere eseguito con i -ln
flag della riga di comando (quindi +4 byte). Stampa 0
per numeri composti e 1
per numeri primi.
Provalo online!
Penso che questo sia il primo programma Stack Cats non banale.
Spiegazione
Una rapida introduzione su Stack Cats:
- Stack Cats opera su un nastro infinito di pile, con una testina che punta verso uno stack corrente. Ogni stack viene inizialmente riempito con una quantità infinita di zeri. Generalmente ignorerò questi zeri nella mia formulazione, quindi quando dico "il fondo dello stack" intendo il valore inferiore a zero e se dico "lo stack è vuoto" intendo che ci sono solo zeri.
- Prima dell'avvio del programma, a
-1
viene inserito nello stack iniziale, quindi viene inserito l'intero input. In questo caso, a causa del -n
flag, l'input viene letto come un numero intero decimale.
- Alla fine del programma, lo stack corrente viene utilizzato per l'output. Se c'è una
-1
in fondo, verrà ignorata. Ancora una volta, a causa della -n
bandiera, i valori dalla pila vengono semplicemente stampati come numeri interi decimali separati da avanzamento riga.
- Stack Cats è un linguaggio di programma reversibile: ogni parte di codice può essere annullata (senza che Stack Cats tenga traccia di una cronologia esplicita). Più specificamente, per invertire qualsiasi parte di codice, è sufficiente rispecchiarlo, ad esempio
<<(\-_)
diventa (_-/)>>
. Questo obiettivo di progettazione pone restrizioni piuttosto severe su quali tipi di operatori e costrutti di flusso di controllo esistono nel linguaggio e quali tipi di funzioni è possibile calcolare sullo stato della memoria globale.
Per finire, ogni programma Stack Cats deve essere auto-simmetrico. Potresti notare che questo non è il caso del codice sorgente sopra. Questo è lo scopo della -l
bandiera: rispecchia implicitamente il codice a sinistra, usando il primo carattere per il centro. Quindi il programma attuale è:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Programmare in modo efficace con l'intero codice è estremamente banale e non intuitivo e non ha ancora capito come un essere umano possa farlo. Abbiamo forzato brutalmente questo programma per compiti più semplici, ma non saremmo riusciti ad avvicinarci a quello a mano. Fortunatamente, abbiamo trovato un modello di base che ti consente di ignorare metà del programma. Anche se questo è sicuramente non ottimale, è attualmente l'unico modo noto per programmare in modo efficace in Stack Cats.
Quindi in questa risposta, il modello di detto modello è questo (c'è una certa variabilità nel modo in cui viene eseguito):
[<(...)*(...)>]
All'avvio del programma, il nastro dello stack si presenta così (per input 4
, diciamo):
4
... -1 ...
0
^
Le [
mosse top dello stack a sinistra (e la testa nastro lungo) - chiamiamo questa "spinta". E <
muove la testina da sola. Quindi dopo i primi due comandi, abbiamo questa situazione:
... 4 -1 ...
0 0 0
^
Ora (...)
c'è un loop che può essere usato abbastanza facilmente come condizionale: il loop viene inserito e lasciato solo quando la parte superiore dello stack corrente è positiva. Dato che attualmente è zero, saltiamo l'intera prima metà del programma. Ora il comando centrale è *
. Questo è semplicemente XOR 1
, cioè attiva o disattiva il bit meno significativo della parte superiore dello stack, e in questo caso trasforma il 0
in un 1
:
... 1 4 -1 ...
0 0 0
^
Ora incontriamo l'immagine speculare di (...)
. Questa volta la cima della pila è positivo e ci fa inserire il codice. Prima di esaminare cosa succede tra parentesi, lasciami spiegare come andremo a finire alla fine: vogliamo assicurarci che alla fine di questo blocco, abbiamo di nuovo la testina su un valore positivo (in modo che il ciclo termina dopo una singola iterazione e viene utilizzato semplicemente come un condizionale lineare), che la pila a destra mantiene l'uscita e che il diritto pila di che detiene un -1
. In tal caso, lasciamo il loop, ci >
spostiamo sul valore di output e lo ]
spingiamo su in -1
modo da avere uno stack pulito per l'output.
Questo è quanto. Ora tra parentesi possiamo fare tutto ciò che vogliamo controllare la primalità purché ci assicuriamo di sistemare le cose come descritto nel paragrafo precedente alla fine (cosa che può essere facilmente fatta con qualche spinta e movimento della testina). Per prima cosa ho provato a risolvere il problema con il teorema di Wilson, ma alla fine ho superato i 100 byte, perché il calcolo fattoriale quadrato è in realtà piuttosto costoso in Stack Cats (almeno non ho trovato una soluzione breve). Quindi sono andato invece con la divisione di prova e questo è risultato molto più semplice. Diamo un'occhiata al primo bit lineare:
>:^]
Hai già visto due di quei comandi. Inoltre, :
scambia i primi due valori dello stack corrente e ^
XORs il secondo valore nel valore superiore. Questo rende :^
un modello comune per duplicare un valore su uno stack vuoto (tiriamo uno zero sopra il valore e poi trasformiamo lo zero in 0 XOR x = x
). Quindi, dopo questo, la sezione del nostro nastro appare così:
4
... 1 4 -1 ...
0 0 0
^
L'algoritmo della divisione di prova che ho implementato non funziona per l'input 1
, quindi in questo caso dovremmo saltare il codice. Possiamo facilmente mappare 1
per 0
e tutto il resto a valori positivi con *
, quindi ecco come lo facciamo:
*(*...)
Cioè ci trasformiamo 1
in 0
, saltiamo una grande parte del codice se otteniamo effettivamente 0
, ma all'interno annulliamo immediatamente il valore in *
modo da recuperare il nostro valore di input. Dobbiamo solo assicurarci di nuovo che finiamo con un valore positivo alla fine delle parentesi in modo che non inizino il ciclo. All'interno del condizionale, spostiamo uno stack a destra con il >
e quindi avviamo il ciclo principale della divisione di prova:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
Le parentesi graffe (al contrario delle parentesi) definiscono un diverso tipo di loop: è un loop do-while, il che significa che corre sempre per almeno un'iterazione. L'altra differenza è la condizione di terminazione: quando si entra nel ciclo Stack Cat ricorda il valore massimo dello stack corrente ( 0
nel nostro caso). Il ciclo verrà quindi eseguito fino a quando questo stesso valore non verrà nuovamente visualizzato al termine di un'iterazione. Questo è conveniente per noi: in ogni iterazione semplicemente calcoliamo il resto del prossimo potenziale divisore e lo spostiamo su questo stack su cui stiamo iniziando il loop. Quando troviamo un divisore, il resto è 0
e il ciclo si interrompe. Proveremo i divisori a partire da n-1
e poi li diminuiremo fino a 1
. Ciò significa che a) sappiamo che questo terminerà quando raggiungiamo1
al più tardi eb) possiamo quindi determinare se il numero è primo o no controllando l'ultimo divisore che abbiamo provato (se è 1
, è un numero primo, altrimenti non lo è).
Andiamo a questo. C'è una breve sezione lineare all'inizio:
<-!<:^>[:
Sai cosa fanno ormai la maggior parte di quelle cose. I nuovi comandi sono -
e !
. Stack Cats non ha operatori di incremento o decremento. Tuttavia ha -
(negazione, cioè moltiplicare per -1
) e !
(NOT bit a bit, cioè moltiplicare per -1
e decrementare). Questi possono essere combinati in un incremento !-
o in decremento -!
. Quindi decrementiamo la copia n
sopra -1
, quindi ne facciamo un'altra copia n
a sinistra, quindi prendiamo il nuovo divisore di prova e lo mettiamo sotto n
. Quindi sulla prima iterazione, otteniamo questo:
4
3
... 1 4 -1 ...
0 0 0
^
Su ulteriori iterazioni, la 3
volontà verrà sostituita con il successivo divisore di prova e così via (mentre le due copie di n
avranno sempre lo stesso valore a questo punto).
((-<)<(<!-)>>-_)
Questo è il calcolo del modulo. Poiché i loop terminano su valori positivi, l'idea è di iniziare -n
e aggiungere ripetutamente il divisore di prova d
fino a quando non si ottiene un valore positivo. Una volta fatto, sottraiamo il risultato d
e questo ci dà il resto. Il punto difficile qui è che non possiamo semplicemente mettere uno -n
in cima allo stack e iniziare un ciclo che aggiunge d
: se la parte superiore dello stack è negativa, il ciclo non verrà inserito. Tali sono i limiti di un linguaggio di programmazione reversibile.
Quindi, per aggirare questo problema, iniziamo con n
in cima allo stack, ma lo neghiamo solo alla prima iterazione. Ancora una volta, sembra più semplice di quanto sembri ...
(-<)
Quando la parte superiore della pila è positiva (cioè solo sulla prima iterazione), la neghiamo con -
. Tuttavia, non possiamo semplicemente farlo (-)
perché non lasceremo il ciclo fino a quando non verrà -
applicato due volte. Quindi spostiamo una cella a sinistra <
perché sappiamo che c'è un valore positivo lì (il 1
). Bene, quindi ora abbiamo negato in modo affidabile n
sulla prima iterazione. Ma abbiamo un nuovo problema: ora la testina del nastro si trova in una posizione diversa rispetto alla prima iterazione rispetto a tutte le altre. Dobbiamo consolidarlo prima di procedere. Il prossimo <
sposta la testina a sinistra. La situazione alla prima iterazione:
-4
3
... 1 4 -1 ...
0 0 0 0
^
E sulla seconda iterazione (ricorda che ora abbiamo aggiunto d
una volta -n
):
-1
3
... 1 4 -1 ...
0 0 0
^
Il condizionale successivo unisce nuovamente questi percorsi:
(<!-)
Alla prima iterazione la testina del nastro punta a zero, quindi questo viene saltato del tutto. Su ulteriori iterazioni, la testina del nastro punta verso una, quindi eseguiamo questa operazione, ci spostiamo a sinistra e incrementiamo la cella lì. Poiché sappiamo che la cella inizia da zero, ora sarà sempre positiva in modo da poter lasciare il ciclo. Questo assicura che finiamo sempre due stack a sinistra dello stack principale e ora possiamo tornare indietro >>
. Quindi alla fine del ciclo modulo facciamo -_
. Lo sai già -
. _
è sottrarre ciò che ^
è XOR: se la parte superiore dello stack è a
e il valore sottostante è b
sostituito a
da b-a
. Da quando abbiamo negata a
però, -_
sostituisce a
con b+a
, aggiungendo cosìd
nel nostro totale corrente.
Dopo che il ciclo termina (abbiamo raggiunto un valore positivo), il nastro si presenta così:
2
3
... 1 1 4 -1 ...
0 0 0 0
^
Il valore più a sinistra potrebbe essere qualsiasi numero positivo. In effetti, è il numero di iterazioni meno uno. Ora c'è un altro breve bit lineare:
_<<]>:]<]]
Come ho detto prima, dobbiamo sottrarre il risultato d
per ottenere il resto ( 3-2 = 1 = 4 % 3
), quindi lo facciamo _
ancora una volta. Successivamente, dobbiamo ripulire lo stack che abbiamo incrementato a sinistra: quando proviamo il divisore successivo, deve essere nuovamente zero, perché la prima iterazione funzioni. Quindi ci spostiamo lì e spingiamo quel valore positivo sull'altro stack di supporto con <<]
e poi torniamo al nostro stack operativo con un altro >
. Ci fermiamo d
con :
e lo rimettiamo sul -1
con ]
e poi spostiamo il resto sul nostro stack condizionale con <]]
. Questa è la fine del ciclo della divisione di prova: questo continua fino a quando non otteniamo un resto zero, nel qual caso lo stack a sinistra contienen
il più grande divisore (diverso da n
).
Al termine del ciclo, c'è poco *<
prima di unire 1
nuovamente i percorsi con l'input . La *
trasforma semplicemente lo zero in una 1
, che avremo bisogno di un po ', e poi ci spostiamo il divisore con <
(in modo che siamo sulla stessa pila come per l'ingresso 1
).
A questo punto aiuta a confrontare tre diversi tipi di input. Innanzitutto, il caso speciale in n = 1
cui non abbiamo fatto nessuna delle cose della divisione di prova:
0
... 1 1 -1 ...
0 0 0
^
Quindi, nel nostro esempio precedente n = 4
, un numero composto:
2
1 2 1
... 1 4 -1 1 ...
0 0 0 0
^
E infine, n = 3
un numero primo:
3
1 1 1
... 1 3 -1 1 ...
0 0 0 0
^
Quindi, per i numeri primi, abbiamo uno 1
su questo stack e per i numeri compositi abbiamo 0
un numero positivo o maggiore di 2
. Trasformiamo questa situazione in 0
o 1
abbiamo bisogno con il seguente pezzo finale di codice:
]*(:)*=<*
]
spinge questo valore verso destra. Quindi *
viene utilizzato per semplificare notevolmente la situazione condizionale: alternando il bit meno significativo, trasformiamo 1
(primo) in 0
, 0
(composito) in valore positivo 1
e tutti gli altri valori positivi rimarranno positivi. Ora dobbiamo solo distinguere tra 0
positivo e positivo. Ecco dove ne usiamo un altro (:)
. Se la parte superiore dello stack è 0
(e l'input era un numero primo), questo viene semplicemente ignorato. Ma se la parte superiore dello stack è positiva (e l'input era un numero composto) questo lo scambia con il 1
, in modo che ora abbiamo 0
per composito e1
per i numeri primi - solo due valori distinti. Certo, sono l'opposto di ciò che vogliamo produrre, ma questo è facilmente risolvibile con un altro *
.
Ora non resta che ripristinare il modello di stack previsto dal nostro framework circostante: testina di stampa su un valore positivo, risultato in cima allo stack a destra e un singolo -1
nello stack a destra di quello . Questo è ciò che =<*
serve. =
scambia le cime delle due pile adiacenti, spostando in tal modo la -1
a destra del risultato, ad es. per inserire 4
nuovamente:
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
Quindi ci spostiamo a sinistra con <
e trasformiamo quello zero in uno con *
. E quello è quello.
Se vuoi approfondire il funzionamento del programma, puoi utilizzare le opzioni di debug. Aggiungi il -d
flag e inserisci "
dove vuoi vedere lo stato di memoria corrente, ad esempio in questo modo , oppure usa il -D
flag per ottenere una traccia completa dell'intero programma . In alternativa, è possibile utilizzare EsotericIDE di Timwi che include un interprete Stack Cats con un debugger passo-passo.