Laboratorio di Algoritmi e Strutture Dati. Risoluzione di Problemi con gli algoritmi Ricorsivi



Documenti analoghi
csp & backtracking informatica e laboratorio di programmazione Alberto Ferrari Informatica e Laboratorio di Programmazione

INFORMATICA A. Titolo presentazione sottotitolo. Laboratorio n 6 Dott. Michele Zanella Ing. Gian Enrico Conti

Constraint Satisfaction Problems

Fondamenti di Programmazione

Scritto di Algoritmi e s.d. (1o anno) 5 Luglio 2005

Problemi CSP: Problemi di. soddisfacimento di vincoli. Colorazione di una mappa. Formulazione di problemi CSP. Formulazione. Tipi di problemi CSP

Intelligenza Artificiale. Lezione 6bis. Sommario. Problemi di soddisfacimento di vincoli: CSP. Vincoli CSP RN 3.8, 4.3, 4.5.

Algoritmi e giochi combinatori

Esercitazione 4. Comandi iterativi for, while, do-while

Introduzione alla programmazione Algoritmi e diagrammi di flusso. Sviluppo del software

Fondamenti di Informatica Laurea in Ingegneria Civile e Ingegneria per l ambiente e il territorio

Esercizi su Python. 14 maggio Scrivere una procedura che generi una stringa di 100 interi casuali tra 1 e 1000 e che:

UNIVERSITÀ DEGLI STUDI DI PAVIA FACOLTÀ DI INGEGNERIA. Matlab: esempi ed esercizi

Laboratorio di Programmazione Laurea in Ingegneria Civile e Ambientale

Laboratorio di Programmazione Laurea in Ingegneria Civile e Ambientale

Informatica II. Capitolo 16 Backtrack

Informatica Generale Homework di Recupero 2016

RICORSIONE, PUNTATORI E ARRAY. Quarto Laboratorio

Debug di un programma

Lo sviluppo di un semplice programma e la dimostrazione della sua correttezza

complessità computazionale

ThreeChess. Ovvero, come giocare a scacchi in tre

ThreeChess. Ovvero, come giocare a scacchi in tre.

Laboratorio di Programmazione Laurea in Ingegneria Civile e Ambientale

Il Concetto Intuitivo di Calcolatore. Esercizio. I Problemi e la loro Soluzione. (esempio)

Corso di Algoritmi e Strutture Dati Informatica per il Management Prova Scritta, 9/9/2015

Soluzioni della settima esercitazione di Algoritmi 1

n n 1 n = > Il calcolo del fattoriale La funzione fattoriale, molto usata nel calcolo combinatorio, è così definita

3 Ricerca per Giochi e CSP

Ricorsione. DD cap. 5 pp KP cap. 5 pp

Corso di Laurea in Matematica per l Informatica e la Comunicazione Scientifica

Gioco 10x10. Prova con una matita, che punteggio ottieni?

Il Concetto Intuitivo di Calcolatore. Esercizio. I Problemi e la loro Soluzione. (esempio)

Note per la Lezione 4 Ugo Vaccaro

Matricola Nome Cognome Aula Fila (dalla cattedra) Posto (dalla porta)

ERRATA CORRIGE. void SvuotaBuffer(void); void SvuotaBuffer(void) { if(getchar()!=10) {svuotabuffer();} }

Matrici Un po di esercizi sulle matrici Semplici Lettura e scrittura Calcolo della trasposta Media difficoltà Calcolo l del determinante t È Difficili

... b 2 X 2. . b N X N. a 11 a 12.. a 1N a 21 a 22.. a 2N A =. a N1... a NN

AUTOMA A STATI FINITI

Corso di Fondamenti di Programmazione canale E-O. Un esempio. Funzioni ricorsive. La ricorsione

REGOLE DI BASE. Quando orientiamo la scacchiera la casa posta nell'angolo a destra di ogni giocatore deve essere di colore chiaro.

Il paradigma divide et impera. Paolo Camurati Dip. Automatica e Informatica Politecnico di Torino

ESERCIZIO MIN-MAX Si consideri il seguente albero di gioco dove i punteggi sono tutti dal punto di vista del primo giocatore.

Matrici.h Definizione dei tipi #define MAXROWS 10 #define MAXCOLS 10 #define ELEMENT int #define ELEMENTFORMAT "%d" Tipo degli elementi della matrice

Operativamente, risolvere un problema con un approccio ricorsivo comporta

Corso di Fondamenti di Informatica. La ricorsione

Note per il corso di Geometria Corso di laurea in Ing. Edile/Architettura. 4 Sistemi lineari. Metodo di eliminazione di Gauss Jordan

Certificati dei problemi in NP

LA RICORSIONE LA RICORSIONE LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO LA RICORSIONE: ESEMPIO

Perché il linguaggio C?

Le Regole del Gioco Elementi del Gioco

Parte 2. Ricorsione. [M.C.Escher Drawing hands, 1948] - AA. 2012/13 2.1

Note sul Sudoku. Marco Liverani. Dicembre 2005

11.4 Chiusura transitiva

Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

7.1 Progettare un algoritmo per costruire ciclo euleriano di un grafo non orientato.

Problemi di soddisfacimento di vincoli. Colorazione di una mappa. Formulazione di problemi CSP. Altri problemi

REGOLE DI BASE DEL GIOCO DEGLI SCACCHI

Informatica A (per gestionali) A.A. 2004/2005. Esercizi C e diagrammi a blocchi. Preparazione prima prova intermedia

Progetto: Dama. 1 - Descrizione. 2 - Regole del gioco. Appello di febbraio 2003

Informatica 16/06/2016 durata complessiva: 2h

La strategia MiniMax e le sue varianti

Alberi di copertura. Mauro Passacantando. Dipartimento di Informatica Largo B. Pontecorvo 3, Pisa

Esempio: il fattoriale di un numero fact(n) = n! n!: Z! N n! vale 1 se n " 0 n! vale n*(n-1)! se n > 0. Codifica:

INFORMATICA. Strutture iterative

I giochi con avversario. I giochi con avversario. Introduzione. Giochi come problemi di ricerca. Il gioco del NIM.

Nell informatica esistono alcuni problemi particolarmente rilevanti, poiché essi:

Parte V: Rilassamento Lagrangiano

LA RICORSIONE. Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

giochi sulla persistenza

LA RICORSIONE! Una funzione matematica è definita ricorsivamente quando nella sua definizione compare un riferimento a se stessa

Elementi di Informatica e Programmazione

Operativamente, risolvere un problema con un approccio ricorsivo comporta

INFORMATICA. Scienza dei calcolatori elettronici (computer science) Scienza dell informazione (information science)

Alfa. Il GO tra gioco, matematica ed economia. Alla ricerca della strategia ottimale

Corso di Informatica di Base

A. Ferrari Informatica

Università degli Studi di Brescia INFORMATICA. Dipartimento di Ingegneria Meccanica e Industriale

Agent and Object Technology Lab Dipartimento di Ingegneria dell Informazione Università degli Studi di Parma. Fondamenti di Informatica.

Laboratorio di Algoritmi e Strutture Dati

Algoritmi greedy. Gli algoritmi che risolvono problemi di ottimizzazione devono in genere operare una sequenza di scelte per arrivare alla soluzione

Scaletta. Cenni di computabilità. Cosa fa un programma? Definizioni (1/2) Definizioni (2/2) Problemi e domande. Stefano Mizzaro 1

! Problemi, domande, risposte. ! Algoritmi che calcolano funzioni. ! Funzioni computabili e non. ! Problema = insieme di domande omogenee. !

Introduzione alla programmazione

Un tipico esempio è la definizione del fattoriale n! di un numero n, la cui definizione è la seguente:

INFORMATICA GENERALE Prof. Alberto Postiglione Dipartimento Scienze della Comunicazione Università degli Studi di Salerno

Lo scopo. Il primo esperimento. Soluzione informale. Le variabili

Assegnazione di una variabile

IEIM Esercitazione XI Ricorsione e Ripasso. Alessandro A. Nacci -

Esercitazione 11. Liste semplici

Informatica 1. Prova di recupero 21 Settembre 2001

Laboratorio di Calcolatori 1 Corso di Laurea in Fisica A.A. 2006/2007

INFORMATICA DI BASE Linguaggio C Prof. Andrea Borghesan

ESERCIZI SULLA TECNICA BACKTRACKING e BRANCH & BOUND

Sviluppo di programmi

Risoluzione di un problema

Possibile applicazione

Transcript:

Laboratorio di Algoritmi e Strutture Dati Risoluzione di Problemi con gli algoritmi Ricorsivi Proff. Francesco Cutugno e Luigi Lamberti 2009-2010

A l goritmi R i c o r s ivi Viene detto algoritmo ricorsivo un algoritmo espresso in termini di sé stesso, ovvero in cui l'esecuzione dell'algoritmo su un insieme di dati comporta la semplificazione o suddivisione dell'insieme di dati e l'applicazione dello stesso algoritmo agli insiemi di dati semplificati. Questo tipo di algoritmo risulta particolarmente utile per eseguire dei compiti ripetitivi su di un set di input variabili. L'algoritmo richiama sé stesso generando una sequenza di chiamate che ha termine al verificarsi di una condizione particolare che viene chiamata condizione di terminazione, in base a particolari valori di input. La tecnica ricorsiva permette di scrivere algoritmi eleganti e sintetici per molti tipi di problemi comuni, anche se non sempre le soluzioni ricorsive sono le più efficenti. Questo è dovuto al fatto che comunemente la ricorsione viene implementata utilizzando le funzioni, e che l'invocazione di una funzione ha un costo rilevante, e questo rende più efficenti gli algoritmi iterativi. Requisiti di un algoritmo ricorsivo Non tutti gli algoritmi possono essere espressi in forma ricorsiva. Esistono in generale tre requisiti fondamentali affinché un algoritmo sia effettivamente esprimibile in forma ricorsiva: 1. Deve essere possibile formulare l'algoritmo in funzione di sé stesso. La regola che descrive tale possibilità è solitamente chiamata passo. 2. È necessario che in qualche modo l'applicazione del processo termini, ovvero deve esistere una condizione di terminazione, detta anche base, ovvero almeno una istanza del processo che non richiede di essere scomposta in ulteriori istanze più semplici, in modo che non si verifichi mai un ciclo infinito. Inoltre, non deve esistere un valore di ingresso che renda impossibile la terminazione dell'esecuzione. 3. Convergenza insiemistica. Non tutti gli insiemi di definizione degli algoritmi, in funzione della relativa formulazione, permettono la ricorsione. Vantaggi e svantaggi La ricorsione ha un vantaggio fondamentale: permette di scrivere poche linee di codice per risolvere un problema anche molto complesso. Tuttavia essa ha anche un enorme svantaggio: le prestazioni. Infatti la ricorsione genera una quantità enorme di overhead, occupando lo stack per un numero di istanze pari alle chiamate della funzione che è necessario effettuare per risolvere il problema. Funzioni che occupano una grossa quantità di spazio in memoria, pur potendo essere implementate ricorsivamente, potrebbero dare problemi a tempo di esecuzione. Inoltre la ricorsione impegna comunque il processore in maniera maggiore per popolare e distruggere gli stack Pertanto, se le prestazioni sono obiettivo principale del programma e non si dispone di sufficiente memoria, si consiglia di non utilizzare la ricorsione. Procedure ricorsive Una procedura, quindi, si dice ricorsiva quando: all interno della propria definizione, compare la chiamata alla procedura stessa; oppure compare la chiamata ad almeno una procedura che, direttamente o indirettamente, chiama la procedura stessa. La continua suddivisione del problema, deve portare ad ottenere sottoproblemi così semplici da essere risolvibili: in modo banale (es: insiemi di un solo elemento), oppure, con metodi alternativi alla ricorsione.

S t r a t egie p e r la risoluzione d i p r oblemi Risolvere un problema, significa, in generale, trovare la sequenza di azioni che ci conduca da una situazione di partenza di un dato Sistema, ad una condizione finale, che rappresenta la soluzione attesa (in base al contesto in cui è stato posto il problema). In particolare, ci occuperemo, con un approccio sistemico semplificato, di problemi la cui evoluzione avvenga in modo discreto, ovvero per passi distinti. Definizione Sia assegnato un Problema rappresentato da : o un insieme (finito) di variabili discrete V = {X 1, X 2, X n, o ciascuna associata ad un dominio D 1, D 2, D n, o un insieme di vincoli (relazioni tra le variabili): R = {C 1, C 2, C m. Chiameremo Stato una combinazione di valori delle variabili che definiscono il problema {X i = v i i=1,n nel rispetto dei vincoli imposti. Di conseguenza, lo Spazio degli Stati sarà l insieme di tutte le possibili combinazioni dei suddetti valori (nel nostro studio è un insieme finito). L insieme degli stati non coincide con il semplice prodotto cartesiano dei domini, per la presenza dei vincoli. Lo Stato Iniziale è la condizione di partenza del nostro problema; il problema può essere riformulato, assumendo di volta in volta differenti stati iniziali. Un Azione (o mossa legale) è l assegnazione di un valore ad una variabile nel rispetto dei vincoli; la modifica di una variabile, porta il Sistema da uno stato ad un altro (transizione). Il numero delle azioni possibili (input del Sistema) è limitato superiormente dalla cardinalità del prodotto dei domini. La Soluzione (o Stato Obiettivo) è una particolare assegnazione completa delle variabili, soddisfacente tutti i vincoli, che corrisponda ad una particolare richiesta del contesto in cui il Problema si svolge; la soluzione può non essere unica. Un Processo è la sequenza di azioni che conduce il Sistema da uno stato ad un altro; il Processo risolutivo, sarà la sequenza di azioni per giungere dallo Stato iniziale allo Stato Finale atteso. Applicazioni Scheduling di processi, Topologia di Circuiti, Dimostrazione automatica di Teoremi, Giochi, Ottimizzazione di processi industriali, Ricerca dei percorsi minimi, ecc In alcuni problemi, conoscendo lo Stato finale, occorre procedere a ritroso per trovare un possibile stato iniziale da cui iniziare l esecuzione del processo.

R a p p r e s e n t a z i o n e d e l p r o b l e m a Ad ogni passo, possiamo assegnare un valore ad una variabile, fino al raggiungimento dello stato finale (obiettivo); i possibili processi possono essere rappresentati tramite un Diagramma degli stati (Grafo), ove: ogni Nodo (vertice) corrisponde ad uno stato; ogni Arco ad una mossa legale in quello stato. Ogni arco congiunge le due configurazioni precedente e successiva alla mossa. A 1 A 3 A 5 Stato 1 A 2 Stato 2 Stato 3 Stato 4 A 4 A 6 A 7 Ovviamente il diagramma degli stati diventa improponibile al crescere della complessità del problema! Il grafo può contenere cicli e, quindi, nella ricerca del cammino risolutivo, può essere complicato dimostrare che l'algoritmo di ricerca termina. La gestione del problema è rappresentabile anche tramite un albero (che chiameremo Albero di Ricerca) ove la radice è lo Stato Iniziale, mentre le eventuali soluzioni sono da ricercarsi nelle foglie. Si elimina il problema dei cicli, ma lo stesso nodo potrebbe essere generato più volte. A 1 Stato 1 A 2 Stato 2 Stato 3 A 3 A 4 A 5 A 6 A 7 Stato 4 Stato 5 Stato 6 Stato 7 Stato 5

U n e s e m p i o : s a l v a r e P e c o r a e C a v o l i Problema: Variabili: Vincoli: Azioni: un Agricoltore va al mercato con un Lupo, una Pecora e un enorme Cavolfiore; deve attraversare un fiume potendo utilizzare solo una barca tanto piccola da poter portare con sé un solo oggetto per volta. detto 0 il lato di partenza del Fiume e 1 il lato di arrivo, avremo quattro variabili, ciascuna con due possibili valori: A {0,1, L {0,1, P {0,1, C {0,1. Lupo, Pecora e Cavolo non possono attraversare il fiume da soli. il Lupo mangia la Pecora, se lasciati soli senza l Agricoltore. la Pecora mangia il Cavolo, se lasciati soli senza l Agricoltore. l Agricoltore può attraversare il fiume in un verso o nell altro da solo (A = 1-A), o con un passeggero per volta ( A=1-A L=1-L, A=1-A P=1-P, A=1-A C=1-C ), che sia disponibile dal suo stesso lato (non tutte le azioni sono sempre legali). Spazio Stati: Abbiamo 16 stati possibili con le combinazioni ALPC. 0000 è lo stato iniziale; 1111 è lo stato obiettivo (finale) 0110, 0011, 0111, 1001, 1100, 1000 sono stati da evitare. A AL AC 0 000 1 000 1 100 1 001 AP A AL AP 1 010 0 010 1 110 0 100 AC AC AL A AP 0 011 1 011 0 001 1 101 A 0 110 0 111 0 101 AP 1 111 Ogni nodo individua una posizione dei quattro personaggi A-LPC; Per chiarezza, nel diagramma sono state rappresentate solo una parte delle Azioni possibili, in modo da evidenziare i due possibili processi risolutivi e qualche processo errato. Le azioni sono in genere reversibili con lo stesso input al sistema (gli archi blu). Le Azioni (in rosso) che conducono ad una catastrofe per il sistema (Lupo-mangia-Pecora o Pecora-mangia- Cavolo), sono considerate azioni illegali ovvero impossibili. Un modello diverso potrebbe considerare tre valori per le variabili P e C: lato di partenza, lato di arrivo, mangiato (cosa che porterebbe a 36 il numero degli stati) e considerare gli aspetti mangiatori non come vincoli ma come situazioni indesiderabili.

S t r a t egia d i Ricerca con Backtracking Ad ogni passo si assegna un valore ad una variabile, avanzando nel problema fino allo stato finale. In caso di fallimento (vicolo cieco, violazione dei vincoli, ritorno ciclico ad un nodo già visitato) si torna indietro e si fa una scelta diversa. Ovviamente è più efficiente eseguire un controllo anticipato della violazione dei vincoli: è inutile andare avanti fino alla fine e poi controllare; si può fare backtracking non appena si scopre un vincolo violato. Trovare una soluzione significa conoscere il percorso (sequenza delle azioni) e lo stato finale. Se si ritorna allo stato iniziale senza aver trovato la soluzione, vuol dire che il problema è irrisolubile. Il metodo backtracking è una strategia di ricerca Completa, in quanto garantisce di trovare un percorso risolutivo, se questo esiste. Algoritmo ricorsivo 1 Supponiamo che il problema non preveda possibili percorsi ciclici. funzione Ricerca ( Stato_Attuale ) { Risultato = Fallimento E = Elenco Azioni possibili secondo i vincoli // ovvero mosse legali per ogni Azione A E, mentre Risultato = Fallimento { Esegui A se Nuovo_Stato = Stato_Obiettivo Risultato = Soluzione altrimenti Risultato = Ricerca ( Nuovo_Stato ) se Risultato = Fallimento Annulla esecuzione di A // backtracking restituisci Risultato Osserviamo che se E =, la funzione restituisce Fallimento. Se necessario, restituiamo insieme alla soluzione anche l azione eseguita, in modo da poter concatenare le varie mosse per descrivere il processo risolutivo.

E s e m p i o : i l S u d o k u Un Sudoku è una griglia quadrata di 81 celle: 9 righe orizzontali per 9 colonne verticali; inoltre, la griglia è divisa in 9 riquadri 3x3 (gruppi) di 9 celle ciascuno. Ciascuna riga orizzontale, colonna verticale e riquadro di 3x3 celle contiene una sola volta tutti i numeri da 1 a 9; pertanto in nessuna riga, colonna o riquadro può esserci un numero ripetuto. 0 1 2 3 4 5 6 7 8 0 1 1 1 4 2 3 9 3 7 4 8 2 5 5 6 4 7 6 8 7 8 9 Nella tavola da gioco, ogni cella vale 0 se è libera, da 1 a 9 se occupata. Chiameremo Gruppi i 9 Riquadri 3x3 interni. Detta (r,c) una cella della Tavola, le coordinate della cella in alto a sinistra del suo gruppo di appartenenza saranno: rg = (r / 3) * 3 = r - r % 3 cg = (c / 3) * 3 = c - c % 3 Il gruppo sarà composto dalle nove celle (i,j) con rg <= i <= rg+2 e cg <= j <= cg+2 Segue un esempio di programma in C; per semplicità l interfaccia è elementare e manca una procedura per assegnare alla tavola di gioco lo schema parziale di partenza (il tutto è affidato allo studioso lettore ). #include <stdio.h> void IniziaTavola (int Tav[][9], int *Nvuote ); int CercaSudoku (int Tav[][9], int Nvuote ); void StampaTavola (int Tav[][9] ); int VerificaLegalita (int Tav[][9], int i, int j, int N); main (void) { int Tav [9][9], // Tavola di gioco Nvuote; // Numero di caselle vuote nella tavola IniziaTavola (Tav, &Nvuote); //-----> Aggiungere una lettura da file o da tastiera // dello schema parziale da completare StampaTavola (Tav); printf("\nbatti un tasto per iniziare l'analisi...\n"); getch(); if (Nvuote<1!VerificaVincoli(Tav)) //se partiamo da uno schema incongruente printf("\nil problema e` mal posto!\n"); else if (! CercaSudoku(Tav,Nvuote) ) printf("\nnon esistono soluzioni!\n"); else { printf("\nsoluzione trovata.\n"); StampaTavola (Tav); getch();

/*============================================================================= Inizializza la Tavola di gioco del Sudoku, svuotando le celle. Possiamo prevedere una diversa configurazione iniziale, con schema parzialmente riempito. -----------------------------------------------------------------------------*/ void IniziaTavola ( int Tav[][9], // Tavola di gioco int *Nvuote // Numero di caselle vuote nella tavola ) { int i, j, k; *Nvuote = 81; for (i=0; i<9; i++) for (j=0; j<9; j++) Tav [i][j] = 0; /*============================================================================= Cerca di completare la tavola da gioco, con ALMENO con una cella vuota, provando a riempire una qualsiasi cella vuota, ove il valore sia possibile. Se non sorgono incongruenze e se esitono altre caselle vuote, chiama ricorsivamente se stessa; Se sono finite le caselle vuote, trovata una soluzione. Se non è stata trovata una soluzione, prova un altro valore nella cella. Al termine dei valori possibili, senza successo, restituisce 0: ( è inutile ricominciare con un'altra cella! ) OUTPUT: 1 soluzione trovata e copiata sulla Tavola da gioco 0 nessuna soluzione possibile -----------------------------------------------------------------------------*/ int CercaSudoku ( int Tav[][9], // Tavola da esaminare int Nvuote // Numero di caselle vuote nella tavola > 0 ) { int r, c, // riga e colonna della cella da occupare N, // valore da inserire tra 1 e 9 i, j, Trovato; Trovato = 0; r = -1; // 0 nessuna soluzione possibile; // 1 soluzione trovata e scritta su Tav for (i=0 ; i<9 && r == -1 ; i++) // cerca una cella vuota che ESISTE! for (j=0 ; j<9 && r == -1 ; j++) if (Tav[i][j] == 0) {r=i; c=j; // termina se trova una cella vuota for (N=1; N<=9 &&!Trovato; N++) if ( VerificaLegalita (Tav, r, c, N) ) // se è possibile inserire N { Tav[r][c] = N; // se era l'ultima casella vuota, la soluzione è stata trovata Trovato = (Nvuote == 1)? 1 : CercaSudoku (Tav, Nvuote-1); // se la strada non è praticabile, annulla la mossa (backtracking) if (! Trovato) Tav[r][c] = 0; return Trovato;

/*============================================================================= Valuta la congruenza di uno schema di gioco, all'atto dell'inserimento di un nuovo valore in una casella vuota: per ogni riga, ogni colonna e ogni riquadro ciascun valore da 1 a 9 deve essere presente al più una volta. OUTPUT: 0 non è possibile inserire il valore nella cella 1 il gioco può proseguire alla ricerca di soluzioni -----------------------------------------------------------------------------*/ int VerificaLegalita ( int Tav[][9], // Tavola di gioco int r, int c, // riga e colonna della cella da occupare int N // valore da inserire ) { int i, j, rg, cg, // angolo in alto a sinistra del gruppo di appartenenza Congrua; // 0 = tavola inconguente Congrua = 1; for (j=0; j<9; j++) if (Tav[r][j] == N) Congrua = 0; for (i=0; i<9; i++) if (Tav[i][c] == N) Congrua = 0; // esamina la riga della tavola // esamina la colonna della tavola rg = r - r % 3; // esamina il riquadro cg = c - c % 3; for (i=0; i<3; i++) for (j=0; j<3; j++) if (Tav[rg+i][cg+j] == N) Congrua = 0; return Congrua; /*============================================================================= Valuta la congruenza dell`intero schema di gioco OUTPUT: 0 La tabella contiene incongruenze 1 il gioco può proseguire alla ricerca di soluzioni -----------------------------------------------------------------------------*/ int VerificaVincoli ( int Tav[][9] ) { int r, c, N, Congrua = 1; // 0 = tavola inconguente for (r=0 ; r<9 ; r++) for (c=0 ; c<9 ; c++) if ( Tav[r][c] ) // esaminiamo ogni elemento gia` inserito { N = Tav[r][c]; Tav[r][c] = 0; Congrua &= VerificaLegalita (Tav, r, c, N); Tav[r][c] = N; return Congrua;

/*============================================================================= Visualizza la matrice del campo di gioco. -----------------------------------------------------------------------------*/ void StampaTavola ( int Tav[][9] // Tavola di gioco ) { int i, j; int Nvuote; // Numero di caselle vuote nella tavola for (Nvuote= i= 0; i<9; i++) { if (i%3) printf ("\n +---+---+---H---+---+---H---+---+---+\n "); else printf ("\n *===========H===========H===========*\n "); for (j=0; j<9; j++) { if (Tav[i][j]) { printf (" %d ",Tav [i][j]); else { printf (" "); Nvuote ++; printf ( (j==2 j==5)? "H" : " "); printf ("\n *===========H===========H===========* Vuote = %d\n", Nvuote);

P r o b l e m i d i o t t i m i z z a z i o n e Per problemi di ottimizzazione, occorre cercare la migliore tra più soluzioni possibili, secondo una certa metrica. Il fallimento varrà -. Algoritmo ricorsivo 2 funzione Ricerca ( Stato_Attuale ) { E = Elenco Azioni possibili secondo i vincoli (ovvero mosse legali) V = elenco dei valori corrispondenti a ciascuna azione per ogni Azione Ai E { Esegui Ai se Nuovo_Stato = Soluzione Vi = Valore della Soluzione altrimenti Vi = Ricerca ( Nuovo_Stato ) se E Risultato = max(valori) altrimenti Risultato = - (Fallimento) restituisci Risultato Anche in questo caso, si potrebbe aggiungere la restituzione della migliore azione eseguibile, in modo da poter descrivere il processo risolutivo ottimale. E u r i s t i c h e La ricerca brutale di una soluzione, può richiedere l esame di tutti i possibili cammini dalla radice del problema agli stati terminali. Per diminuire il tempo di ricerca è possibile: 1. evitare di generare stati che portano ad inconsistenza (vincoli non soddisfatti); 2. accontentarsi di una buona soluzione invece della miglior soluzione. 3. scegliere oculatamente l ordine di esecuzione delle varie azioni; in genere operando prima sulla variabile più vincolata (quella che ha meno valori possibili) si scoprono prima i fallimenti. Se l analisi di tutte le possibili ramificazioni richiede un tempo inaccettabile, sarà necessario fermare la nostra ricerca fino ad un certo livello di profondità ed eseguire una valutazione approssimativa della situazione ottenuta. In tal modo avremo un sistema che si comporta generalmente in modo efficace ma, talvolta, conduce a soluzioni errate.

G i o c h i c o n A v v e r s a r i o Ci occuperemo di un particolare settore di applicazione del Problem Solving: i Giochi a due giocatori a conoscenza completa. Caratteristiche: Il gioco è definito da una serie di regole che definiscono le mosse lecite. Le mosse sono fatte alternativamente da 2 giocatori A e B. Ogni giocatore ha informazioni complete sullo stato del gioco in ogni istante. Non c è intervento del caso (sistema deterministico). Uno Stato Terminale rappresenta una situazione di Vittoria, Sconfitta o Pareggio (Win-Lose- Draw) per un giocatore (una mossa terminale conclude il gioco). L obiettivo è vincere, ma ciò non è possibile per entrambi. Soddisfano le regole suddette, ad esempio, Othello, Go, Scacchi, Dama, Tic-Tac-Toe, Awele, Nim, Forza4; restano esclusi Backgammon, Monopoli, Dadi, Scopone, Burraco, ecc Diremo che un gioco è Equo, quando nessuno dei due giocatori può vincere se l avversario gioca al meglio ; ad esempio il Tic-Tac-Toe (o Filetto 3x3) finisce sempre alla pari se entrambi i giocatori giocano razionalmente. Viceversa, nella variante del filetto detta il Mulino, il primo giocatore ha la possibilità di vincere al più in nove mosse. Se un gioco a conoscenza completa non contempla il pareggio, esiste sicuramente una strategia vincente per chi inizia il gioco oppure per chi gioca per secondo. Per molti giochi, tipo Othello o Scacchi, non è possibile affermare se sia un gioco equo o meno, in quanto il numero di configurazioni possibili è talmente alto da non consentire una valutazione completa ad un computer attuale durante la vita dell universo (si stima che l albero di gioco dell Othello abbia circa 10 40 nodi). Procedura Neg-Max Per trovare la mossa migliore, in una data configurazione, possiamo procedere con il backtracking ricorsivo; ad ogni passo verrà scelta la mossa che è più conveniente per A se tocca ad A, per B se tocca a B. Quindi la funzione di valutazione dovrebbe scegliere la mossa: con il valore massimo, se tocca ad A con il valore minimo, se tocca a B. Più semplicemente è possibile adottare un unica funzione di valutazione con l accortezza di capovolgere il parere dell avversario, negando il risultato restituito dalla successiva istanza ricorsiva. In altre parole, stabilito che un valore positivo indichi la vittoria, uno negativo la sconfitta e Zero il pareggio: A esamina lo stato attuale del gioco e cataloga le possibili mosse. A esegue ad una ad una le mosse possibili. o Se la mossa è terminale, assegna il valore conclusivo (ad esempio +1, -1, 0). o Se la mossa non è terminale, consegna lo stato del gioco a B chiedendo il suo parere (ricorsione). o B risponderà secondo il suo punto di vista: ad esempio restituirà +1 se è certo di vincere. o A trasforma il parere di B, assumendo -1 per quella mossa (il pareggio resta invariato). A conclude l analisi indicando come miglior mossa quella che ha il valore più alto dal suo punto di vista.

E s e m p i o : T i c - T a c - T o e Il Filetto 3x3 o Tic-Tac-Toe o, più semplicemente, Tris, è un gioco antichissimo, con numerose varianti, da giocare su un campo 3 per 3, con pedine di due colori o con carta e penna, con i simboli X e O. Nella più variante semplice ciascuno dei due giocatori inserisce a turno una pedina del proprio colore sul tavolo di gioco, fino a formare un Tris o Filetto, ovvero una fila di tre pedine dello stesso colore, in orizzontale, in verticale o in diagonale: vince chi realizza per primo un tris; riempito il campo senza alcun tris, la partita è Pari. Nella terminologia della teoria dei giochi, il filetto è un gioco a due persone, "finito" (cioè con un risultato determinato), senza elementi dovuti al caso e giocato con "informazione perfetta", in quanto tutte le mosse sono note ad entrambi i giocatori. Il numero delle sequenze di mosse possibili sembra grandissimo, 362880 = 9! = 9x8x7x6x5x4x3x2x1, ma in realtà vi sono solo pochi schemi fondamentali e chiunque può diventare un giocatore imbattibile con venti minuti di analisi del gioco: si scopre ben presto che giocando "razionalmente", senza distrazioni, non ci sono mai vincitori e il gioco finisce sempre Pari. Per trovare questo risultato il supercomputer di Wargame, il film di John Badham, impegna tutte le sue risorse e alla fine, saggiamente, abbandona il progetto di guerra totale che aveva avviato. Evidentemente gli autori del film non avevano mai visto un computer perché anche un Commodore 64 scopre in pochi minuti la parità del gioco, analizzando tutte le sequenze possibili; su un PC attuale il tempo non è quantificabile! Vediamo un semplice programma per giocare a Tris contro il Computer; anche in questo caso, occorre completare il software con un interfaccia più gradevole e con la possibilità di scegliere chi inizia. #include <stdio.h> #define INFINITO 99999 void StampaHelpMosse (void); int VerificaTris (int *Tav); void StampaTavola (int *Tav); int ValutaMossa (int *Tav, int Nvuote, int Who, int *DoveMeglio); /*--------------------------- T R I S --------------------------------------- Programmma per il gioco del Tris tra due giocatori scelti tra: -) Umano ( tramite keyboard ) -) PC ( tramite sub di valutazione mossa ) +-----+-----+-----+ 7 8 9 +-----+-----+-----+ 4 5 6 +-----+-----+-----+ 1 2 3 Tavola[10] +-----+-----+-----+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 1 2 3 4 5 6 7 8 9 La tavola di gioco (una scacchiera 3x3) viene allocata in un vettore; ogni casella della Tavola vale 0 se vuota, 1 se occupata dal giocatore 1, 2 se occupata dal giocatore 2 (in binario 01 e 10 rispettivamente). La valutazione della mossa migliore in ogni fase del gioco viene affidata ad una funzione ricorsiva, che deve essere in grado di valutare la mossa perfetta. =========================================================================*/

main ( void ) { int Tavola[10], // scacchiera di gioco Nvuote, // numero delle caselle vuote (con valore 0) Who, // giocatore che deve muovere: 1 o 2 DoveGioco, i, Finito; // mossa da fare da parte di Who // 0 = gioco in corso // 1 = gioco finito (vinto 1) // 2 = gioco finito (vinto 2) // 3 = gioco finito in pareggio // 27= gioco finito per abbandono Nvuote = 9; for (i=9; i; Tavola[i--]=0 ); // pulizia scacchiera StampaHelpMosse(); StampaTavola (Tavola); // visualizza il campo di gioco //--------- scegliere chi deve muovere per primo ------------ Who = 1; // ad esempio muove per primo il PC assegnato a 1 Finito = 0; while (! Finito) { if (Who == 1) // mossa decisa dal PC tramite la funzione di valutazione { ValutaMossa (Tavola, Nvuote, Who, &DoveGioco); else // Who == 2: mossa del giocatore umano tramite keyboard { DoveGioco = 0; do { i = getch(); if (i==27) // se si vuole abbandonare il gioco Finito = 27; // accettate solo mosse nelle caselle vuote else if (i>=49 && i<=57 && Tavola[i-48]==0) DoveGioco = i-48; while (!DoveGioco &&!Finito); if (!Finito) // se non c'è stato un abbandono con ESC { Tavola[DoveGioco] = Who; // occupa la casella if ( VerificaTris(Tavola) ) { Finito = Who; // mossa vincente else if (Nvuote == 1) // se era l'unica casella vuota, pareggio { Finito = 3; else // passa la scacchiera all'avversario { Who = 3 - Who; Nvuote --; StampaTavola (Tavola); // Visualizza il campo di gioco while (!Finito); // aggiungere messaggi adeguati per indicare chi ha vinto printf ("Partita terminata"); getch();

/*=========================================================================== Valutazione della mossa migliore in un dato stato del gioco supponendo che ciascuno giochi 'al meglio': ogni istanza della funzione ricorsiva, valuta tutte le mosse possibili; si ferma nell'analisi quando trova una mossa vincente. Output : valore della scacchiera vista da Who che deve muovere: -1 sconfitta, 0 pareggio, +1 vittoria. --------------------------------------------------------------------------*/ int ValutaMossa ( int *Tav, // scacchiera di gioco int Nvuote, // numero delle caselle vuote (valore 0) int Who, // giocatore che deve muovere: 1 o 2 int *DoveMeglio // casella della mossa migliore ) { int i, // indice sulle caselle della scacchiera Dove, // miglior contromossa dell'avversario Vale, // valore della contromossa MaxVale; // valore della miglior mossa possibile for (MaxVale = -INFINITO, i=9; i && MaxVale<1; i--) if (Tav[i]==0) // se la casella è vuota { Tav[i] = Who; // occupa la casella if ( VerificaTris(Tav) ) // se si è creato un tris Vale = +1; // mossa vincente else if (Nvuote == 1) // se era l'unica casella vuota, pareggio Vale = 0; else // situazione ancora tutta da valutare // chiede il parere dell'avversario, e poi considera il suo // miglior punteggio cambiato di segno (strategia NegMax) Vale = -ValutaMossa (Tav, Nvuote-1, 3-Who, &Dove); if (Vale > MaxVale) { MaxVale = Vale; *DoveMeglio = i; Tav[i] = 0; // in ogni caso, libera la casella return MaxVale; /*=========================================================================== Verifica se esiste un tris qualsiasi sulla tavola di gioco Output : 1 l'ultima mossa effettuata ha prodotto un Tris 0 non esistono tris sulla tavola --------------------------------------------------------------------------*/ int VerificaTris ( int *Tav // scacchiera di gioco ) { int Fatto; Fatto = ( Tav[1] & Tav[2] & Tav[3] // 1ø rigo Tav[4] & Tav[5] & Tav[6] // 2ø rigo Tav[7] & Tav[8] & Tav[9] // 3ø rigo Tav[1] & Tav[4] & Tav[7] // 1ø colonna Tav[2] & Tav[5] & Tav[8] // 2ø colonna Tav[3] & Tav[6] & Tav[9] // 3ø colonna Tav[1] & Tav[5] & Tav[9] // diagonale / Tav[7] & Tav[5] & Tav[3] ); // diagonale \ return Fatto;

/*============================================================================= Visualizza la matrice del campo di gioco. -----------------------------------------------------------------------------*/ void StampaTavola ( int *Tav // scacchiera di gioco ) { char Ped[] = { ' ', 'X', 'O', ' '; // pedine visualizzare printf (" +-----+-----+-----+\n"); printf (" %c %c %c \n", Ped[Tav[7]], Ped[Tav[8]], Ped[Tav[9]]); printf (" +-----+-----+-----+\n"); printf (" %c %c %c \n", Ped[Tav[4]], Ped[Tav[5]], Ped[Tav[6]]); printf (" +-----+-----+-----+\n"); printf (" %c %c %c \n", Ped[Tav[1]], Ped[Tav[2]], Ped[Tav[3]]); printf (" +-----+-----+-----+\n\n\n"); /*============================================================================= Visualizza l'help sui tasti da premere per le mosse -----------------------------------------------------------------------------*/ void StampaHelpMosse( void ) { printf ("Indicare le mosse come segue: \n"); printf (" +-----+-----+-----+\n"); printf (" 7 8 9 \n"); printf (" +-----+-----+-----+\n"); printf (" 4 5 6 \n"); printf (" +-----+-----+-----+\n"); printf (" 1 2 3 \n"); printf (" +-----+-----+-----+\n\n\n"); Una possibile variante della funzione ValutaMossa cerca di vincere la partita prima possibile. In questo caso occorre assegnare un punteggio diverso alla vittoria, pari al numero delle caselle vuote rimanenti, e non dovremo interrompere l analisi alla prima vittoria trovata, ma esaminare tutte le possibili mosse. for (MaxVale = -INFINITO, i=9; i ; i--) if (Tav[i]==0) { Tav[i] = Who; if ( VerificaTris(Tav) ) Vale = Nvuote; // mossa vincente valutata in base alle caselle vuote

H e x agon19 Sia assegnata una scacchiera con 19 caselle esagonali (inizialmente vuote) così disposte: Due giocatori (Nero e Bianco) collocano alternativamente una pedina in una casella vuota; il primo che è costretto ad occupare due caselle adiacenti, ha perso la partita. 1. Costruire una procedura che valuti la mossa migliore da compiere in qualsiasi situazione di gioco (considerando che anche l avversario giocherà sempre al meglio ), utilizzando una ricerca con Backtracking ricorsivo. 2. Utilizzare la suddetta procedura per stabilire se si può sicuramente vincere giocando per primi o per secondi (un pareggio è, banalmente, impossibile). 3. Consentire ad un umano di giocare contro il computer, sia pure con un interfaccia spartana.

B i bliografia Web http://it.wikipedia.org/wiki/ricorsione http://www.cad.polito.it/~bernardi/corsi/apa_ivrea/apa2/w4/minmax.pdf http://www.di.unipi.it/~simi/ai/si2007/lucidi/games.pdf http://www.cs.unibo.it/~cianca/wwwpages/chesssite/tozzi.pdf