Lezione 4 di 8 · 18 min di lettura
Griglie, array a due dimensioni
Array di array per scacchiere, mappe e tabelle. L'ordine delle dimensioni (al contrario del C), righe e colonne con due indici, cicli annidati e un campo minato da calcolare.
Un array di array
Una scacchiera, il tabellone del tris, una mappa di un videogioco, un foglio di calcolo: sono tutti griglie, righe e colonne. Per rappresentarle basta un’idea che conosci già: un array i cui elementi sono, a loro volta, array.
int[4][3] grid;Il trucco per leggere questa dichiarazione è partire da sinistra, come si costruisce il tipo: int[4] è una riga di 4 interi; int[4][3] sono 3 di quelle righe. Quindi una griglia di 3 righe per 4 colonne.
Per raggiungere una casella servono due indici: prima la riga, poi la colonna.
grid[1][2] = 5; // row 1, column 2
io::printn(grid);
io::printfn("rows: %d, columns: %d", grid.len, grid[0].len);[[0, 0, 0, 0], [0, 0, 5, 0], [0, 0, 0, 0]]
rows: 3, columns: 4grid[1] è la seconda riga, un int[4]; grid[1][2] è la terza casella di quella riga. E grid.len conta le righe, mentre grid[0].len conta le colonne (quanti elementi ha una riga).
Inizializzare una griglia
I valori si scrivono riga per riga, con un paio di graffe per ogni riga. Andare a capo aiuta a vedere la forma della griglia:
int[3][2] table = {
{ 1, 2, 3 },
{ 4, 5, 6 },
};
io::printn(table[1][0]);4int[3][2]: 2 righe da 3. Se inverti le dimensioni per abitudine dal C, il compilatore trova il numero di righe sbagliato:
int[3][2] bad = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
// error: Too many (3) elements in initializer, expected 2.(La virgola dopo l’ultima riga, in table, è facoltativa: C3 la accetta, ed è comoda quando aggiungi righe.)
Quiz
Con int[5][2] m;, quale di questi accessi è fuori dai limiti?
Scorrere righe e colonne
Una griglia si scorre con due cicli, uno dentro l’altro: quello esterno prende le righe, quello interno le caselle di ogni riga. Con foreach è quasi una traduzione della frase “per ogni riga, per ogni valore”:
foreach (row : table)
{
foreach (value : row) io::printf("%3d", value);
io::printn();
} 1 2 3
4 5 6Ricordi %3d dal modulo 1? Larghezza minima 3, per incolonnare i numeri. L’io::printn() dopo il ciclo interno va a capo alla fine di ogni riga.
Se ti servono le coordinate, usa la forma con l’indice, o un for classico. Nell’esercizio finale della lezione ti serviranno per guardare le caselle vicine, quindi lì il for con r e c sarà più comodo.
Dettagli nerd Come sta una griglia in memoria?
La memoria del computer non ha righe e colonne: è una fila unica di byte. Una griglia viene quindi “srotolata”: prima tutta la riga 0, poi subito dopo tutta la riga 1, e così via, senza spazi in mezzo. int[4][3] sono 12 int consecutivi, 48 byte (int[4][3]::size vale proprio 48).
Per trovare grid[r][c] il computer calcola inizio + (r × 4 + c) × 4 byte: una moltiplicazione e una somma, come per l’array semplice. Questa disposizione ha un nome, row-major (“per righe”), e una conseguenza pratica: scorrere una griglia riga per riga, come fanno i nostri cicli, legge la memoria in ordine, ed è il modo più veloce. Scorrerla colonna per colonna salta avanti e indietro, e su griglie molto grandi si nota.
Una griglia di caratteri
Una riga di caratteri si può inizializzare con una stringa tra virgolette: è il modo più leggibile per disegnare una mappa.
char[3][3] board = {
"X.O",
".X.",
"O.X",
};
board[0][1] = 'O';
foreach (row : board)
{
foreach (cell : row) io::printf("%c", cell);
io::printn();
}XOO
.X.
O.XOgni stringa deve avere esattamente la lunghezza della riga, qui 3 caratteri. E le caselle si modificano come quelle di qualsiasi array, con un carattere tra apici singoli: 'O'.
Passare una griglia a una funzione
Anche qui le slice fanno il loro lavoro. Una funzione che riceve int[3][] accetta un numero qualsiasi di righe da 3:
fn int total(int[3][] rows)
{
int sum = 0;
foreach (row : rows)
{
foreach (v : row) sum += v;
}
return sum;
}io::printn(total(&table)); // 1 + 2 + ... + 6
io::printn(total(table[1..])); // only the second row21
15Il numero di righe è libero, la lunghezza di ogni riga no: fa parte del tipo degli elementi.
Esercizio · sul tuo computer
Campo minato
Nel gioco del campo minato, ogni casella senza mina mostra quante mine ci sono nelle otto caselle che la circondano (sopra, sotto, ai lati e in diagonale). Scrivi mines.c3 che parte da questa mappa, dove * è una mina:
const int ROWS = 4;
const int COLS = 6;
char[COLS][ROWS] field = {
"*.....",
"..*...",
"....*.",
"*....*",
};e stampa la griglia risolta: le mine restano *, le caselle con mine vicine mostrano il numero, quelle senza mine vicine restano ..
Suggerimenti:
- due
for(rsulle righe,csulle colonne) per ogni casella, e altri due (dredcda -1 a 1) per le vicine; - attenzione ai bordi: una casella nell’angolo ha solo 3 vicine, e
field[-1][0]non esiste. Salta concontinuele coordinate fuori dalla griglia; - la casella stessa (
dredcentrambi 0) non è una mina, altrimenti non la staresti contando: non serve escluderla.
Mostra una soluzione (prima prova da solo!)
import std::io;
const int ROWS = 4;
const int COLS = 6;
fn void main()
{
char[COLS][ROWS] field = {
"*.....",
"..*...",
"....*.",
"*....*",
};
for (int r = 0; r < ROWS; r++)
{
for (int c = 0; c < COLS; c++)
{
if (field[r][c] == '*')
{
io::print("*");
continue;
}
int count = 0;
for (int dr = -1; dr <= 1; dr++)
{
for (int dc = -1; dc <= 1; dc++)
{
int nr = r + dr;
int nc = c + dc;
if (nr < 0 || nr >= ROWS || nc < 0 || nc >= COLS) continue;
if (field[nr][nc] == '*') count++;
}
}
if (count == 0)
{
io::print(".");
}
else
{
io::printf("%d", count);
}
}
io::printn();
}
}Quattro cicli annidati possono spaventare, ma leggili a coppie: i due esterni scelgono una casella, i due interni guardano il quadrato 3×3 attorno a lei. Il controllo dei bordi è il cuore dell’esercizio: senza, il programma si fermerebbe alla prima casella con Array index out of bounds. Le costanti ROWS e COLS servono sia per dichiarare la griglia sia nei cicli: cambia la mappa, e cambi due numeri.
Ricapitolando
int[4][3]sono 3 righe da 4: il tipo si legge da sinistra, al contrario della dichiarazione in C.- L’accesso è
grid[riga][colonna];grid.lenconta le righe,grid[0].lenle colonne. - Si inizializza riga per riga, con graffe annidate; le righe di
charanche con stringhe della lunghezza giusta. - Si scorre con due cicli annidati; per le vicine di una casella, attenzione ai bordi.
- In memoria una griglia è una fila unica, riga dopo riga. Una funzione può ricevere
int[4][]: righe libere, colonne fisse.
Nei char della mappa c’era un indizio: una stringa è una fila di byte. Nella prossima lezione guardiamo le stringhe da vicino.