Codice Gray (versione Marzo 2007) Data una formula booleana con n variabili, per costruire una tavola di verità per questa formula è necessario generare tutte le combinazioni di valori per le n variabili. Usando 1 per vero e 0 per falso, di solito si parte con n incrementare di 1 in binario; ad es. con n=2: 0 0 1 1
zeri e poi si continua ad
0 1 0 1
Per un certo tipo di tavole di verità (le "mappe di Karnaugh") serve elencare le combinazioni in modo tale che tra due consecutive ci sia esattamente un bit di differenza; questo modo di elencare le combinazioni si chiama Codice Gray. Nella tabella di sopra, la 1a e la 2a riga differiscono di 1 bit, ma la 2a e la 3a hanno 2 bit di differenza. E` facile rimediare, basta invertire le ultime due righe: 0 0 1 1
0 1 1 0
Notare che il codice Gray non e` unico; anche quello che segue ha la proprietà voluta 0 1 1 0
0 0 1 1
Nel seguito cerchiamo di ottenere il codice "del primo tipo"; quindi diciamo "il codice Gray" ed usiamo Gn per indicare il codice Gray per n variabili. Problema:
trovare un modo per generare il codice Gray per n variabili (n > 0).
più precisamente:
scrivere una procedura che, dato n, stampa il codice Gray per n.
Approccio induttivo alla soluzione: • proviamo a risolvere per n piccolo (questa è la base dell'induzione); • vediamo se, avendo costruito il codice Gray per 1, 2, ...., n-1 variabili, riusciamo a generare quello per n variabili usando i precedenti (questo è il passo induttivo). La base è facile, perchè per n=1 il codice è banale:
G1 = 0
(Inoltre abbiamo anche la soluzione per n=2.)
1
Per capire il passo induttivo, vediamo il caso n=3. Pensandoci un po' , si capisce che: scrivendo 0 seguito da G2 abbiamo scritto metà di G3
il resto si ha scrivendo 1 seguito da G2 in ordine rovesciato (cioè scrivendolo a partire dall'ultima riga):
G3 =
0 0 0 0 1 1 1 1
0 0 1 1 1 1 0 0
0 1 1 0 0 1 1 0
[ questo è G2 ]
[ questo è G2 rovesciato ]
E` immediato verificare che si può ragionare cosí anche per andare da G1 a G2
G2 =
0 0 1 1
0 1 1 0
[ questo è G1 ] [ questo è G1
rovesciato ]
A questo punto dovrebbe essere chiaro che la cosa vale in generale. Scrivendo GRn per il codice Gray per n variabili scritto in ordine inverso, abbiamo:
Gn+1 =
0
Gn
1
GRn
A questo punto, se si deve risolvere il problema "su un pezzo di carta", abbiamo finito: partendo da G1 riusciamo a generare tutti i Gn Volendo scrivere una procedura per risolvere il problema, dopo un po' si capisce che bisogna anche trovare una "formula" per GRn Studiando i casi n = 1, 2, 3, vediamo che: GR1 = 1 0
1 1 0 0
GR2 =
GR3 =
1 1 1 1 0 0 0 0
0 0 1 1 1 1 0 0
0 1 1 0 0 1 1 0 0 1 1 0
1 1 0 0
=
=
[ questo è G1 ]
0 1 1 0 1 1 1 1 0 0 0 0
[ questo è GR1 ]
0 0 1 1 1 1 0 0
0 1 1 0 0 1 1 0
[ questo è G2 ]
[ questo è GR2 ]
2
Quindi si capisce che la formula generale è: GRn+1 =
1
Gn
0
GRn
A questo punto, avendo un modo induttivo per generare sia Gn che GRn possiamo procedere per capire come scrivere la procedura per stampare il codice. Algoritmo - Prima idea: per generare G n definiamo una matrice n x 2n e poi la riempiamo seguendo lo schema di sopra; alla fine basterà stampare la matrice. Definiamo una procedura
gray (n: int)
che stampa Gn
Questa dichiara una matrice tab di dimensione n x 2n e poi usa due ulteriori procedure, gd per generare il codice Gray e gr per generare il codice rovesciato. Guardando i disegni di sopra, si capisce che per generare, ad esempio, G 7 servirà, ad esempio, generare G3 e GR3 piu' volte ed in posizioni diverse della matrice; quindi gd e gr avranno come parametri: la matrice tab e poi 4 indici che individuano la poosizione della matrice su cui lavorare: left_col (colonna sinistra), right_col (colonna destra), upper_row (riga superiore), lower_row (riga inferiore); ovviamente, se, come sopra, il codice da generare è G3 (oppure GR3 ), avremo che: right_col = left_col + 3 quindi non c'è bisogno del parametro corrispondente al numero di variabili. Per adeguarci al C, facciamo partire gli indici degli array da 0. procedura gray( n : integer var
IN ) { qui n > 0
k, j, num_righe : integer;
num_righe <--- 2n {
blocco con dichiarazioni: var
tab : array [ 0 .. n-1 ; 0 .. num_righe-1] of char ora generiamo la tabella completa da colonna 0 a colonna n-1 e da riga 0 a riga num_righe-1 :
gd(tab, 0, n-1, 0, num_righe-1); e poi stampiamo il risultato : per k = 0, 1, 2, ... num_righe-1 { per j = 0, 1, 2, ..., n-1
scrivi ( tab[k][j] )
vai a capo } } }
chiude il per su k
chiude blocco chiude procedura
3
procedura gd(
tab [ ] [ ] of char
IN - OUT
left_col, right_col, upper_row, lower_row : integer IN) { if (left_col <= right_col) { int k, med; med <---- (upper_row + lower_row) div 2; per k = upper_row, upper_row+1, .... , med: tab[k][left_col] <--- '0'; gd(tab, left_col+1, right_col, upper_row, med); per k = med+1, med+2,.... lower_row : tab[k][left_col] <---- '1'; gr(tab, left_col+1, right_col, med+1, lower_row); } } procedura gr(
tab [ ] [ ] of char
IN - OUT
left_col, right_col, upper_row, lower_row : integer IN) { e` uguale a gd, tranne che si scambiano '0' e '1' if (left_col <= right_col) { int k, med; med <---- (upper_row + lower_row) div 2; per k = upper_row, upper_row+1, .... , med: tab[k][left_col] <--- '1'; gd(tab, left_col+1, right_col, upper_row, med); per k = med+1, med+2,.... lower_row : tab[k][left_col] <---- '0'; gr(tab, left_col+1, right_col, med+1, lower_row); }
La versione C segue questo schema, però c'è un problema: nelle procedure non è possibile usare un parametro array con due dimensioni non specificate. Per risolvere il problema, in una prima versione, gray_1.c, si fissa un limite al numero di variabili; nella seconda versione, gray_1_bis.c, si usano i puntatori.
Algoritmo - seconda idea: per generare G n utilizziamo solo un vettore di lunghezza n; generiamo una riga della tabella alla volta e poi la stampiamo. Vediamo prima l'algoritmo e poi lo commentiamo.
4
procedura gray( n : integer ) { qui n > 0 var :
k : integer w : array [0 .. n] of char
per k = 0, 1, ..., n-1 : w[k] <--- ' '
come una stringa in C, usiamo terminatore : '\0' per togliere eventuali caratteri nulli presenti
w[n] <--- '\0' ; gd(0, w); } procedura gd( k : integer IN, w : array [ ] of char IN-OUT )
{
if (w[k] = '\0') then scrivi ( w ) else {
w[k] <--- '0' gd(k+1, w); w[k] <--- '1'; gr(k+1, w);
} } procedura gr( k : integer IN, w : array [ ] of char IN-OUT )
{
come gd, ma scambiando 0 ed 1 if (w[k] = '\0') then scrivi ( w ) else {
w[k] <--- '0' gd(k+1, w); w[k] <--- '1'; gr(k+1, w);
} }
Non è per nulla evidente che funzioni; in effetti è anche difficile spiegare come si arriva a questo algoritmo. L'idea che c'è sotto è: ricordiamo che w è un array con indici da 0 a n; indichiamo con wk la stringa w[0] w[1] ... w[k-1]
inoltre w[n] = terminatore; (nota: w0 = stringa nulla);
allora gd(k, w) stampa, nell'ordine previsto dal nostro codice Gray, tutte le stringhe della forma wk•u con u che appartiene a Gi con i = n-k. (qui: il • indica concatenazione tra stringhe e G0 = { e }, dove e = stringa vuota) Quindi, gd(0, w) stampa Gn In effetti, per capire veramente come funzionano le cose, bisogna anche precisare cosa produce gr(k, w), ma questo è facile: come per gd(k, w) , ma usando GRi . 5
Proviamo a dimostrare il tutto per induzione. Ipotesi:
w è un array con indici da 0 a n (n > 0); inoltre w[n] = terminatore;
Notazione: wk indica a stringa w[0] w[1] ... w[k-1]
(w0 = e = stringa nulla);
Tesi: [a] gd(k, w) stampa, nell'ordine previsto dal nostro codice Gray, tutte le stringhe della forma wk•u con u che appartiene a Gi con i = n-k (dove G0 = { e }). [b] gr(k, w) stampa, nell'ordine previsto dal nostro codice Gray, tutte le stringhe della forma wk•u con u che appartiene a GRi con i = n-k (dove GR0 = { e }). Dimostrazione per induzione (a passi) su i = n-k (0 <= i <= n) Base, i=0. La tesi è verificata, perchè per k=n sia gd(k,w) che gr(k,w) stampano la stringa contenuta in w, cioè wn. Passo:
supponiamo vera la tesi per i-1 (0 <= i-1 < n) e dimostriamo che vale anche per i.
Vediamo prima la tesi [a]. Poichè i > 0, abbiamo k < n; quindi nell'eseguire gd(k,w) si sceglie il ramo else. Poichè n-(k+1) = i-1, per ipotesi induttiva la tesi vale per le due chiamate gd(k+1,w) e gr(k+1,w); quindi: gd(k+1, w) stampa, nell'ordine previsto ... , tutte le stringhe della forma wk+1•u con u che appartiene a Gi-1 e, successivamente, gr(k+1, w) stampa, nell'ordine previsto ... , tutte le stringhe della forma wk+1•u con u che appartiene a GRi-1 A causa delle due assegnazioni
w[k] <--- '0'
e
w[k] <--- '1'
quanto sopra si può riscrivere come segue: gd(k+1, w) stampa, nell'ordine previsto ... , tutte le stringhe della forma wk•"0"•u con u che appartiene a Gi-1 e, successivamente, gr(k+1, w) stampa, nell'ordine previsto ... , tutte le stringhe della forma wk•"1"•u con u che appartiene a GRi-1 Ma allora gd(k,w) stampa proprio tutte le stringhe della forma wk•u con u che appartiene a Gi come si voleva. La tesi [b] si dimostra in modo del tutto analogo. Quindi la dimostrazione è fatta ed abbiamo finito !!! 6