Olimpiadi di Informatica 2010 Giornate preparatorie Dipartimento di Informatica Università di Torino marzo 2010 6 – Algoritmi di backtracking: il problema delle regine. (versione 21/12/2015) 12/21/2015 E. Giovannetti -- OI09. 1 Il problema delle otto (o n) regine. Disporre 8 regine sulla scacchiera 8 8 in modo che nessuna regina ne attacchi un’altra secondo le regole del gioco degli scacchi; cioè in modo che nessuna si trovi nella stessa riga, o nella stessa colonna, o nelle stesse “diagonali” di un’altra. Il problema è generalizzabile in modo banale a n regine su una scacchiera n n : • per n 4 il problema non è risolubile, come si vede con ispezione diretta; • per ciascun n 4 il problema ammette più soluzioni. 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 2 Come rappresentare le soluzioni. • Poiché non possono esserci due regine nella stessa riga, qualunque disposizione che risolva il problema deve avere una e una sola regina per riga. • Allora possiamo disporre la prima regina nella prima riga, la seconda regina nella seconda riga, … la i-esima regina nella i-esima riga, ecc. • Una soluzione è quindi semplicemente una permutazione di degli n numeri naturali da 1 a n o da 0 a n-1, rappresentabile per mezzo di un array. • Esempio: per la scacchiera 5 5 l’array {0, 2, 4, 1, 3} è la disposizione delle regine nelle caselle: (0,0); (1,2); (2,4); (3,1); (4,3). • Naturamente i ruoli delle righe e delle colonne sono perfettamente intercambiabili. 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 3 L’algoritmo ricorsivo: descrizione molto informale. Per posizionare le regine dall’i-esima riga in avanti: • se i è la riga successiva all’ultima, termina con successo; • altrimenti cerca una colonna j tale che siano prive di regine • la colonna j stessa, • la diagonale ascendente passante per la casella (i,j), • la diagonale discendente passante per la casella (i,j); • posiziona la i-esima regina nella casella (i,j); • posiziona le regine dall’ i+1-esima riga in avanti; • se tale posizionamento ha successo, termina con successo; • altrimenti backtrack: rimuovi la i-esima regina e poi cerca un’altra colonna j che soddisfi alle condizioni di cui sopra, e così via finché non ci sono più colonne da esaminare: • in tal caso termina con fallimento. 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 4 L’algoritmo ricorsivo: descrizione più precisa n = dimensione della tastiera; array indiciati da 0 a n-1 boolean posizionaDallaRiga(int i) { if(i == n) return true; else { for(int j = 0; j < n; j++) { if(colonna j è libera da regine && diagonale ascendente per (i,j) è libera da regine && diagonale discendente per (i,j) è libera da regine) { posiziona l’i-esima regina nella colonna j; if(posizionaDallaRiga(i+1)) return true; else rimuovi l’i-esima regina; continua il ciclo } } fine ciclo for return false; se si sono provate senza successo tutte } le colonne j, si fallisce } 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 5 La chiamata iniziale Naturalmente la funzione ricorsiva deve essere inizialmente invocata con il valore del primo indice (1 in Pascal, 0 in C): boolean problemaRegine() { return posizionaDallaRiga(0); } La soluzione è rappresentata da una struttura-dati globale; se si definisce una procedura che visualizza la soluzione, il main avrà la forma … problemaRegine(); visualizzaSoluzione(); … 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 6 Strutture dati convenienti. Per poter effettuare in tempo costante i test sulla presenza di regine nella colonna e nelle due diagonali, conviene tenere tre array di booleani, indiciati rispettivamente dal numero di colonna e dal “numero di diagonale”, dove il k-esimo elemento di ognuno degli array ha il significato: • la k-esima colonna, o diagonale in su, o in giù, è senza regine. In una scacchiera n n vi sono: • n colonne; • 2n diagonali ascendenti; • 2n diagonali discendenti. Osserva: • le caselle (i, j) di una data diagonale ascendente sono quelle per cui i+j = costante dipendente dalla diagonale; • le caselle di una data diagonale discendente sono quelle per cui i-j = costante dipendente dalla diagonale; 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 7 Strutture dati convenienti: Pascal. Indiciamo, come naturale, righe e colonne da 1 a 8: const n = …; var q: array[1..n] of integer; // posizioni delle regine freeCol: array[1..n] of boolean; Allora: • la somma i+j varia da 1+1 a n+n, cioè da 2 a 2n; • la differenza i-j varia da 1-n a n-1 (ad es., per n = 8, varia da -7 a +7). Quindi, poiché in Pascal gli array si possono indiciare con un range qualunque: freeDiagRise: array[2..2*n] of boolean; freeDiagFall: array[-n+1..n-1] of boolean; 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 8 Strutture-dati convenienti: C++ Gli array sono indiciabili solo a partire da 0, quindi: int q[n]; bool freeCol[n]; Poiché gl’indici di riga e di colonna vanno da 0 a n-1: • la somma i+j varia da 0 a 2n-2, cioè ha 2n-1 possibili valori, • la somma i-j varia da –n+1 a n-1, cioè ha 2n-1 possibili valori. Quindi: bool freeDiagRise[2*n - 1]; bool freeDiagFall[2*n - 1]; Nel codice, per l’accesso all’array delle diagonali discendenti occorrerà “traslare” il range –n+1..n-1 nel range 0..2n-1, sommando n-1 al valore di i-j. 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 9 Strutture-dati convenienti: nomi corti. Per ragioni di spazio nelle slides successive (o eventualmente per velocità di codifica) usiamo nomi più corti per gli array. In C++: int q[n]; bool c[n]; // colonne libere bool a[2*n - 1]; // diagonali ascendenti libere bool b[2*n - 1]; // diagonali discendenti libere In Pascal: var q: array[1..n] of integer; c: array[1..n] of boolean; a: array[2..2*n] of boolean; b: array[-n+1..n-1] of boolean; 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 10 La funzione ricorsiva (C++) bool placeQueens(int i) { if(i == n) return true; for(int j = 0; j < n; j++) { if(c[j] && a[i+j] && b[i-j+n-1]) { q[i] = j; c[j] = a[i+j] = b[i-j+n-1] = false; if(placeQueens(i+1)) return true; else c[j] = a[i+j] = b[i-j+n-1] = true; } } return false; } 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 11 La funzione top-level void queens() { for(int i = 0; i < n; i++) c[i] = true; for(int i = 0; i < 2*n-1; i++) a[i] = true; for(int i = 0; i < 2*n-1; i++) b[i] = true; placeQueens(0); } 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 12 Il ciclo for in Pascal In Pascal l’indiciamento degli array è più immediato: ... for j:= 1 to n do begin if (c[j] and a[i+j] and b[i-j]) then begin q[i]:= j; c[j]:= false; a[i+j]:= false; b[i-j]:=false; if placeQueens(i+1) then begin result:= true; exit end else begin c[j]:= true; a[i+j]:= true; b[i-j]:= true end end end; ... 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 13 Il problema delle n regine: tutte le soluzioni. Con una lieve modifica dell’algoritmo si possono generare tutte le soluzioni, cioè tutte le disposizioni di n regine sulla scacchiera n n che soddisfano alle condizioni del problema. Per ottenere ciò, basta non fermarsi quando si trova una soluzione, bensì continuare il backtracking. Non è quindi più necessario che la procedura restituisca un booleano: ogni volta che si trova una soluzione si può semplicemente stamparla, oppure aggiungerla ad una struttura-dati (ad es. una matrice) che rappresenta l’insieme delle soluzioni. Se si è interessati anche o solo al numero delle soluzioni, si può introdurre un contatore che conti il numero di soluzioni, da incrementarsi ogni volta che se trova una. 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 14 Il problema delle n regine: tutte le soluzioni. void placeQueens(int i) { for(int j = 0; j < n; j++) { if(c[j] && a[i+j] && b[i-j+n-1]) { q[i] = j; c[j] = a[i+j] = b[i-j+n-1] = false; if(i == n-1) { writeSolution(); numSolutions++; } else placeQueens(i+1); c[j] = a[i+j] = b[i-j+n-1] = true; } } } 21/12/2015 14:31 E. Giovannetti - AlgELab-09-10 - Lez.46 15