Quante coppie di staffe sono sufficienti per completare Brainfuck Turing?


12

Brainfuck è un linguaggio di programmazione completo di Turing che utilizza solo 8 simboli (6 se si ignora l'I / O).

I due più importanti che lo spingono alla completezza di Turing sono [e ], essenzialmente, l'etichetta e il goto di Brainfuck.

Normalmente, i programmi in Brainfuck utilizzano più set di [], ma mi chiedevo esattamente quante coppie di queste parentesi debbano essere utilizzate per completare Brainfuck Turing?

Più semplicemente, qual è la quantità minima di parentesi di cui avresti bisogno per simulare una Turing Machine n-state (Fornisci il numero di parentesi per 1, 2 e tre macchine Turing state)?

Appunti:

Stiamo assumendo un nastro infinito e senza limiti computazionali.

È una macchina di Turing a 2 simboli


1
"quante coppie di queste parentesi devono essere utilizzate?" Puoi chiarire "devono essere usati". Ad esempio, se chiedessi a BrainF di contare fino a ? 21000000
John L.

@ Apass.Jack il numero minimo di parentesi
MilkyWay90

1
Oh, intendevi il numero minimo di parentesi per simulare una macchina Turing in funzione di n ? Ad ogni modo, puoi fare un esempio non banale il più semplice possibile? nn
John L.

1
@ Apass.Jack Okay, sto inventando un programma BF difettoso che funziona per una Turing Machine a uno stato
MilkyWay90

@ Apass.Jack Nevermind, è troppo difficile per me. Fondamentalmente crea un interprete BF per il mio linguaggio di programmazione, Turing Machine But Way Worse , quando usa solo due simboli possibili (0 e 1) e rimuove l'I / O e interrompe completamente l'aspetto
MilkyWay90

Risposte:


9

Questo è un ulteriore sviluppo della risposta di @ ais523 , riducendola a solo due serie di parentesi e usando anche un posizionamento cellulare più compatto basato sulla teoria del righello Golomb. ais523 ha creato un compilatore per questa costruzione , così come questa sessione TIO che mostra un programma BF risultante di esempio in esecuzione con traccia di debug dei contatori TWM.

Come l'originale, questo inizia con un programma in The Waterfall Model , con alcune restrizioni che non perdono la generalità:

  1. Tutti i contatori hanno lo stesso valore di auto-reset R ; vale a dire, la mappa di trigger TWM f ha la proprietà che f(x,x)=R per tutti x .
  2. C'è un solo contatore di arresto h .
  3. Il numero c di contatori è (p1)/2 per alcuni numeri primi p .

Righello Golomb

Combiniamo la costruzione Erdős – Turán con la funzione di permutazione di un array Welch – Costas per ottenere un righello Golomb con le proprietà necessarie.

(Sono sicuro che questa costruzione combinata non può essere una nuova idea ma abbiamo appena trovato e messo insieme questi due pezzi di Wikipedia.)

Sia r una radice primitiva di p=2c+1 . Definire la funzione

g(k)=4ck((rk1)mod(2c+1)),k=0,,2c1.

  1. g è un sovrano Golomb dell'ordine . Cioè, la differenza è unica per ogni coppia di numeri distinti .2 c g ( i ) - g ( j ) i , j { 0 , , 2 c - 1 }2cg(i)g(j)i,j{0,,2c1}
  2. g(k)mod(2c) assume ogni valore esattamente una volta.0,,2c1

Struttura a nastro

Per ogni contatore TWM , assegniamo due posizioni di cella nastro BF, una cella di fallback e una cella di valore :x{0,,c1}u ( x ) v ( x ) u(x) v(x)

u(x)=g(k1)<v(x)=g(k2) with u(x)v(x)x(modc)

Con la seconda proprietà di ci sono esattamente due distinti valori tra cui scegliere.gk1,k2

Il contenuto di una cella di fallback verrà mantenuto per lo più a , tranne quando il suo contatore è stato appena visitato, quando sarà a , il doppio del valore di auto-reset del contatore. Una cella di valore verrà mantenuta al doppio del valore del contatore TWM corrispondente.02R

Tutte le altre celle che possono essere raggiunte dall'esecuzione del programma BF (un numero finito) saranno mantenute a valori dispari, in modo che testino sempre come diverso da zero. Dopo l'inizializzazione, questo è automatico perché tutte le regolazioni delle celle sono pari.

Se lo si desidera, tutte le posizioni delle celle possono essere spostate a destra di una costante per evitare di spostarsi a sinistra della posizione iniziale del nastro BF.

Struttura del programma BF

Sia la distanza tra il valore del contatore di arresto e le celle di fallback, e sia un numero abbastanza grande che per tutti i contatori . Quindi la struttura di base del programma BF èH=v(h)u(h)NcN+1v((x+1)modc)u(x)x

inizializzazione regolazioni[ >×(H+cN+1) [ <×c × H] <×H ]

Inizializzazione

La fase di inizializzazione imposta tutte le celle raggiungibili dal programma sui loro valori iniziali, in uno stato come se l'ultimo contatore fosse stato appena visitato e la sola cella attiva fosse la sua cella di fallback :u(c1)

  1. Le celle valore vengono inizializzate al doppio del contenuto iniziale del contatore TWM corrispondente, tranne per il fatto che il contatore è pre-decrementato.0
  2. Le celle di fallback sono impostate su , tranne la cella , che è impostata su .0u(c1)2R
  3. Tutte le altre celle raggiungibili dal programma (un numero finito) sono impostati a .1

Quindi il puntatore del nastro viene spostato in posizione (una cella sempre diversa da zero) prima di raggiungere il primo programma .u(c1)H[

Inizio del ciclo esterno

All'inizio di un'iterazione del ciclo esterno, il puntatore del nastro sarà su oppure per un contatore .u(x)Hv(x)Hx

Sia il prossimo contatore da visitare.y=((x+1)modc)

Il movimento posiziona il puntatore del nastro su una posizione che è e non a sinistra di .>×(H+cN+1)y(modc)v(y)

Il ciclo interno ora cerca a sinistra nei passi di una cella zero. Se il contatore è zero, allora si fermerà nella cella del valore (zero) ; altrimenti troverà la cella di fallback .[ <×c c y v ( y ) u ( y )]cyv(y)u(y)

Qualunque cella venga trovata diventa la nuova cella attiva .

regolazioni

La fase di regolazione regola varie celle sul nastro in base alla loro posizione rispetto alla cella attiva. Questa sezione contiene solo +-><comandi e quindi queste regolazioni avvengono incondizionatamente. Tuttavia, poiché tutte le celle correlate al contatore sono in un modello di righello Golomb, qualsiasi regolazione non appropriata per la cella attiva corrente mancherà a tutte le celle importanti e regolerà invece alcune celle irrilevanti (mantenendole dispari).

È quindi necessario includere nel programma un codice separato per ogni possibile coppia richiesta di cella attiva e regolata, fatta eccezione per l'autoregolazione di una cella attiva, che, poiché la regolazione si basa esclusivamente sulla posizione relativa, deve essere condivisa tra tutte.

Le regolazioni richieste sono:

  1. Regola la cella di fallback del contatore precedente di .u(x)2R
  2. Regola la cella di fallback del contatore corrente di , tranne se la cella attiva corrente è e quindi dovremmo fermarci.u(y)2Rv(h)
  3. Regola la cella del valore del contatore successivo di (decrementando il contatore).v((y+1)modc)2
  4. Quando la cella attiva è una cella di valore (quindi il contatore ha raggiunto lo zero), regolare tutte le celle di valore di dalla mappa di trigger TWM. stesso viene regolato da .v(y)yv(z)2f(y,z)v(y)2R

Il primo e secondo suddette regolazioni sono rese necessarie dal fatto che tutte le cellule attive devono adeguarsi stessi per lo stesso valore, che è per le cellule valore, e quindi anche per le cellule di ripiego. Ciò richiede la preparazione e la pulizia delle celle di fallback per garantire che tornino a in entrambi i rami valore e fallback.2R0

Fine dell'anello esterno

Il movimento rappresenta che al termine della fase di regolazione, il puntatore del nastro viene spostato in posizioni a sinistra della cella attiva.<×HH

Per tutte le celle attive diverse dalla cella del valore del contatore di arresto , questa è una cella irrilevante, quindi dispari e diversa da zero, e il ciclo esterno continua per un'altra iterazione.v(h)

Per , il puntatore viene invece posizionato sulla corrispondente cella di fallback , per la quale abbiamo fatto un'eccezione sopra per mantenerlo zero, e quindi il programma esce attraverso il finale e si ferma.v(h)u(h)]


12

Non sono sicuro al 100% che sia impossibile farlo con due serie di parentesi. Tuttavia, se le celle del nastro BF consentono valori illimitati, sono sufficienti tre serie di parentesi. (Per semplicità, supporrò anche che possiamo spostare la testina del nastro a sinistra oltre il suo punto di partenza, anche se poiché questa costruzione utilizza solo una regione finita del nastro, potremmo sollevare tale limitazione aggiungendo un numero sufficiente di >comandi all'inizio del programma.) La costruzione seguente richiede l'assunzione della congettura di Artinessere in grado di compilare programmi arbitrariamente grandi; tuttavia, anche se la congettura di Artin è falsa, sarà comunque possibile mostrare la completezza di Turing indirettamente tramite la traduzione di un interprete per un linguaggio completo di Turing in BF utilizzando la costruzione seguente, e l'esecuzione di programmi arbitrari tramite l'input di tale interprete.

Il linguaggio completo di Turing che stiamo compilando in BF illimitato è The Waterfall Model , che è uno dei modelli computazionali conosciuti più semplici. Per le persone che non lo conoscono già, è costituito da un numero di contatori (e valori iniziali per essi) e una funzione da coppie di contatori a numeri interi; l'esecuzione del programma consiste nel sottrarre ripetutamente 1 da ogni contatore, quindi se ogni contatore è 0, aggiungendo a ciascun contatore (il programma è scritto in modo tale che ciò non accada mai a più contatori contemporaneamente). C'è una prova di completezza di Turing per questa lingua dietro il mio link. Senza perdita di generalità, assumeremo che tutti i contatori abbiano lo stesso valore di auto-reset (esfxf(x,y)yf(x,x) è lo stesso per tutte le ); questo è un presupposto sicuro perché per ogni specifico , l'aggiunta della stessa costante ad ogni non cambierà il comportamento del programma.xxf(x,y)

Sia il numero di contatori; senza perdita di generalità (assumendo la congettura di Artin), supponiamo che abbia una radice primitiva 2. Sia essere , dove è il potere più basso di 2 maggiore di . Senza perdita di generalità, sarà inferiore a ( è limitato polinomialmente, cresce esponenzialmente, quindi qualsiasi sufficientemente grande funzionerà).ppqp(1+s+s2)sp2q2p2q2pp

La disposizione del nastro è la seguente: numeriamo ogni contatore con un numero intero (e senza perdita di generalità, assumiamo che ci sia un singolo contatore di stop e lo numeriamo ). Il valore della maggior parte dei contatori è memorizzato nella cella a nastro , ad eccezione del contatore 0, che è memorizzato nella cella a nastro . Per ogni cella a nastro dispari dalla cella -1 fino a inclusi0i<p22i2q2p+1+2p+1, quella cella a nastro contiene sempre 1, a meno che non sia immediatamente alla sinistra di un contatore, nel qual caso contiene sempre 0. Le celle a nastro con numero pari che non vengono utilizzate come contatori hanno valori irrilevanti (che potrebbero essere o non essere 0 ); e anche le celle a nastro dispari al di fuori dell'intervallo indicato hanno valori irrilevanti. Si noti che l'impostazione del nastro in uno stato iniziale appropriato richiede l'inizializzazione solo finita di molti elementi nastro a valori costanti, il che significa che possiamo farlo con una sequenza di <>+-istruzioni (in realtà, >+sono necessari solo ), quindi senza parentesi. Al termine di questa inizializzazione, spostiamo il puntatore del nastro sulla cella -1.

La forma generale del nostro programma sarà simile a questa:

inizializzazione regolazione[>>>[ >×(2p1) [ <×(2p) ]>-] <<<]

L'inizializzazione mette il nastro nella forma prevista e il puntatore sulla cella -1. Questa non è la cella a sinistra di un contatore (0 non è una potenza di 2), quindi ha valore 1 e entriamo nel loop. L'invariante del ciclo per questo ciclo più esterno è che il puntatore del nastro è (all'inizio e alla fine di ogni iterazione del ciclo) tre celle a sinistra di un contatore; si può vedere che il loop uscirà quindi solo se ci sono tre celle a sinistra del contatore 2 (ogni altro contatore ha 1 tre celle alla sua sinistra, poiché avere uno 0 implica che le posizioni del nastro di due contatori erano separate da 2 celle; le uniche due potenze di 2 che differiscono di 2 sono e e la rappresentazione binaria di cambia da stringhe di stringhe di2122q01s o viceversa almeno quattro volte e quindi non può essere 1 lontano da una potenza di 2).

Il secondo ciclo si ripete ripetutamente sui contatori, diminuendoli. L'invariante del ciclo è che il puntatore del nastro punta sempre a un contatore; quindi il loop uscirà quando un contatore diventa 0. Il decremento è giusto -; il modo in cui passiamo da un contatore all'altro è più complesso. L'idea di base è che lo spostamento di spazi a destra da ci posizionerà su una cella dispari , che è alla destra di qualsiasi contatore ( è l'ultimo contatore, è positivo perché è positivo); modulo , questo valore è congruente a (dal piccolo teorema di Fermat) . Il ciclo più interno si sposta ripetutamente a sinistra2p12x2p+2x12p2x1x2p2x+12p spazi, anche non modificando l'indice della cella a nastro modulo , e alla fine deve trovare la cella congruente a modulo che abbia il valore (che sarà la cella a sinistra di qualche contatore); a causa del nostro requisito di radice primitiva c'è esattamente una di queste celle ( è congruente a modulo e è congruente a per qualsiasi altro , dove è il logaritmo discreto su base 2 modulo ). Inoltre, si può vedere che la posizione del puntatore a nastro modulo2p2x+12p2q112p2log2,p(r)+112r1rlog2,pp2paumenta di ogni volta attorno al ciclo centrale. Pertanto, il puntatore a nastro deve scorrere tra tutti i contatori (in ordine dei loro valori modulo ). Così, ogni iterazioni, si diminuiscono ogni contatore (come richiesto). Se il ciclo si interrompe parzialmente attraverso un'iterazione, riprenderemo la diminuzione quando entreremo nuovamente nel ciclo (poiché il resto del ciclo più esterno non modifica la posizione del puntatore del nastro).2p2pp

Quando un contatore colpisce 0, il circuito intermedio si interrompe, portandoci al codice di "regolazione". Questo è fondamentalmente solo una codifica di ; per ogni coppia , esso aggiunge all'elemento nastro che è la stessa distanza sinistra / destra del puntatore del nastro come contatore 's posizione del nastro viene sinistra / destra del contatore ' s posizione del nastro (e quindi rimuove il puntatore del nastro nel punto in cui è stato avviato). Ogni volta che , questa distanza risulta unica:f(x,y)f(x,y)yxxy

  • La differenza tra due potenze di 2 è un numero binario costituito da una stringa di 1 o più s seguita da una stringa di 0 o più s (con i valori di posizione dell'inizio del numero e l'inizio della stringa , a seconda della maggiore e minore rispettivamente di ed ); quindi tutte queste differenze sono distinte. * Per quanto riguarda la differenza di una potenza di 2 e , deve contenere almeno due transizioni tra stringhe di s e s (100xyq10q contiene almeno quattro di tali transizioni, la sottrazione può solo rimuovere 2), quindi è distinta da tutte le differenze di due potenze di due, e queste differenze sono ovviamente distinte anche l'una dall'altra.

Per , troviamo ovviamente che la distanza spostata è 0. Ma poiché tutte sono uguali, possiamo semplicemente usarlo come regolazione per la cella corrente. E si può vedere che il codice di regolazione implementa così l'effetto "quando un contatore colpisce 0" per ogni contatore; tutte le celle che rappresentano effettivamente i contatori verranno regolate della quantità corretta e tutte le altre regolazioni influiranno sulle celle non numerate pari (la differenza tra due numeri pari è pari), che non ha alcun effetto sul comportamento del programma.x=yf(x,y)

Quindi, ora abbiamo una compilation funzionante di qualsiasi programma in The Waterfall Model su BF (incluso il comportamento di arresto, ma non incluso l'I / O, che non è necessario per la completezza di Turing) usando solo tre coppie di parentesi, e quindi tre coppie delle parentesi sono sufficienti per completezza di Turing.


Bel lavoro! Vedo che hai lavorato su questo in TNB!
MilkyWay90

Penso che tu abbia bisogno di essere almeno p + 2. Quando s = p + 1, q è 1 in meno di una potenza di 2.
Ørjan Johansen

Credo di aver trovato un molto più semplice (come nel richiedere nessun primo teoria dei numeri) posizionamento del contatore: 2p*2^i+2i.
Ørjan Johansen,

@ ØrjanJohansen: giusto, penso di aver menzionato quella costruzione in #esoterico (qualche tempo dopo aver scritto questo post)? Tutto ciò di cui hai effettivamente bisogno è un righello Golomb per il quale ogni elemento è distinto dal numero di elementi e ci sono vari modi per costruirli (anche se trovare quelli ottimali è difficile; il più lungo che ho trovato (tramite la forza bruta) è [0, 1, 3, 7, 20, 32, 42, 53, 58]per p = 9).
ais523,

Oh, così hai fatto (poco prima che dicessi che il mio cervello si rifiutava di essere in modalità matematica, quindi c'è la mia scusa: P). Immagino di aver scoperto che k = 0 era sufficiente, quindi. Penso che la Erdős – Turan_construction di Wikipedia ne dia una polinomialmente crescente (e presumibilmente O () - ottimale?) Se usi solo la prima metà degli elementi (l'altra metà si ripete (mod p)).
Ørjan Johansen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.