Backtracking e Cut

Backtracking

backtracking/es1.pl

  1. Sottomettendo all'interprete Prolog la dopmanda
    ?- persona(X).
    e forzando il backtracking si ottiene:X = paolo
    
    X = paolo
    Yes
    
    X = giulio
    Yes
    
    X = luca
    Yes
    
    No
    
    
  2. Le soluzioni vengono generate in questo ordine, perchè è con cui i fatti persona sono stati immessi nel database. (L'ordine con cui i fatti erano scritti nel file, nella "text area" nel nostro caso, che è stato consultato.

backtracking/es2.pl

In risposta al goal:
?- trace,uomo(francesca).
si ottiene:
   
Call:  ( 15) uomo(francesca) ?    
Call:  ( 16) persona(francesca) ?    
Call:  ( 17) maschio(francesca) ?    
Fail:  ( 17) maschio(francesca) ?    
Redo:  ( 16) persona(francesca) ?    
Call:  ( 17) femmina(francesca) ?    
Exit:  ( 17) femmina(francesca) ?    
Exit:  ( 16) persona(francesca) ?    
Exit:  ( 15) uomo(francesca) ? 
Tenedo presente che fatti e regole erano stati così numerati:
maschio(paolo).                  /* fatto 1 */
femmina(francesca).              /* fatto 2 */
uomo(X) :-                       /* regola 1 */
        persona(X).
persona(Y) :-                    /* regola 2 */
        maschio(Y).
persona(Z) :-                    /* regola 3 */
        femmina(Z).
Si vede che per rispondere all'interrogazione uomo(francesca) si utilizza la regola 1. Il nuovo goal diventa persona(francesca). Si cerca una soluzione usando la regola 2 che genera il goal maschio(francesca). Ma questo goal fallisce innescando il meccanismo di backtracking. A questo punto si torna a considerare il goal persona(francesca) ma si cerca di risolverlo con la regola 3 per persona. questa regola genera il goal femmina(francesca) che ha successo per via del fatto 2. Il goal iniziale risulta così soddisfatto.

Cut

cut/es1.pl

la definizione:
 fact(0,1).
 fact(N,X):- M is N-1,fact(M,Y),X is Y*N.
Non funziona correttamente. Se si sottomette al Prolog la domanda
?- fact(0,X)
si ottiene come prima risposta X=1 in quanto viene utilizzato il fatto. Tuttavia se si forza il backtrackig, invece del fatto si utilizza la regola viene così generato un nuovo goal
?- fact(-1,X)
e, dopo una ulteriore utilizzazione della stessa regola,
?- fact(-2,X)
Si cade quindi in un loop che non ha termine.
Anche se si chiede di calcolare il fattoriale di un intero positivo, si ottiene una prima risposta tramite il fatto ?-fact(0,X) che mette fine alle chiamate ricorsive, ma una richiesta di backtracking manda in loop l'interprete. Possiamo schematizzare i goal generati dalle chiamate ricorsive per la domanda
?- fact(2,X)
in questo modo:
                 -------------
                 | fact(2,X) |
                 ------------- 
                       |
                       V 
                 -------------
                 | fact(1,X) |
                 ------------- 
                       |
                       V 
                 -------------
                 | fact(0,X) |
                 ------------- 
                   |       \
                   V        \
               Termina       \
               dando la       \
               prima           |
              soluzione        V
                         --------------
                         | fact(-1,X) |
                         - ------------- 
                               |
                               V 
                         --------------
                         | fact(-2,X) |
                         --------------
                               | 
                               V
                            .......

cut/es2.pl

cut/es3.pl

dividi(N1, N2, Risultato) :-
  intero(Risultato),
  Prod1 is Risultato * N2,
  Prod2 is (Risultato + 1) * N2,
  Prod1 =< N1, Prod2 > N1,
  !.

intero(0).
intero(N) :- intero(N1), N is N1 + 1.
Rimuovendo il cut alla fine della regola dividi, se si forza il backtracking dopo aver trovato la prima (e unica) soluzione, intero genera il numero successivo che però non soddisfa la condizione Prod1 =< N1. Il fallimento di questa condizione fa generare a intero un nuovo numero, e il processo non termina.

cut/es4.pl

Il predicato membro puo essere scritto sia in versione non deterministica che in versione deterministica.
/* appartenenza ad una lista - non deterministico */
membro(X,[X|_]).
membro(X,[_|Y]) :- membro(X,Y).

/* appartenenza ad una lista - deterministico*/
membro1(X,[X|_]) :- !.
membro1(X,[_|C]) :- membro1(X,C).
Posssiamo confrontare goal in cui ambedue gli argomenti sono istanziati.
?- membro(y,[a,y,b,y]).
Yes
Yes
No

?- membro1(y,[a,y,b,y]).
Yes
No
In questo caso si può notare che la prina versione non deterministica trova due soluzioni, in quanto y compare 2 volte nella liste che compare come secondo argomento. Nel caso della versione deterministica il cut fa si che si veda solo la prima soluzione.
Se usiamo membro col primo argomento variabile quello che succede nei due casi è questo:
?- membro(X,[a,b,c,d]). 

X = a
Yes
X = b
Yes
X = c
Yes
X = d
Yes
No

?- membro1(X,[a,b,c,d]).
X = a
Yes
No
Se invece usiamo membro con il secondo argomento variabile si ha:
 

?- membro(a,L).   con "solution=once"
L = [a|_G2525]
Yes

?- membro(a,L).  va in loop se si usa "solution=all"

?- membro1(a,L). con "solution=all"
L = [a|_G2092]
Yes
No
Nel primo caso membro va in loop in quanto si usa "solutions=all" infatti ci sono infinite soluzioni. a può essere il primo, il secondo il terzo ..... elemento della lista L. Con il cut ci si ferma alla prima soluzione.

cut/es5.pl

Queste sono le deue versioni del predicato che concatena due liste:

/* concatena due liste */
conc([],L,L).
conc([X|L1],L2,[X|L3]) :- conc(L1,L2,L3).

/* concatena due liste - usa il cut*/
conc1([],L,L):- !.
conc1([X|L1],L2,[X|L3]) :- conc1(L1,L2,L3).

Puoi confrontare queste due versioni sfruttando questi goal (usare "solutions=all"):
?- conc([a,b,c],[e,f],L).
?- conc1([a,b,c],[e,f],L).
in questo caso le risposte coincidono. L'unica soluzione è L=[a,b,c,d,e,f]. Anche non istanziando il secondo argomento
?- conc([a,b,c],X,L).
?- conc1([a,b,c],X,L).
si ottengono risposte coincidenti: X=_G3757,L=[a,b,c|_G3757].
Le cose vanno diversamente se il primo argomento di conc non e' istanziato. In questo caso il goal
?- conc(X,[a,b,c],L).  /* attenzione questo va in loop */
manda in loop l'interprete, mentre il goal
?- conc1(X,[a,b,c],L).
Produce come risposta:
X = []
L = [a, b, c]
Yes

No
Istanziando solo l'ultimo argomento, conc produce tutte le possibili suddivisioni della lista che compare come terzo argomento.
 
?- conc(X,Y,[a,b,c]).
conc(X,Y,[a,b,c]).

X = []
Y = [a, b, c]
Yes

X = [a]
Y = [b, c]
Yes

X = [a, b]
Y = [c]
Yes

X = [a, b, c]
Y = []
Yes

No
Al contrario, conc1, a causa del cut nella proma clausola, da solamente la prima risposta.
?- conc1(X,Y,[a,b,c]).

X = []
Y = [a, b, c]
Yes

No

Operazioni sugli insiemi

cut/insiemi.pl

  1. intersezione([],_Y,[]).
    
    questa prima clausola dice semplicenente che l'intesezione di un insieme vuoto con un secondo insieme deve dare quest'ultimo come risultato.
    intersezione([X|R],Y,[X|Z]) :-
      appartiene(X,Y),
      !,
      intersezione(R,Y,Z).
    
    In questa clausola si dice che se il primo insieme è dato da un primo elemento X seguito da altri elementi R e X appartiene all'insieme Y, X deve essere aggiunto all'intersezione Z fra R e Y.
    intersezione([_X|R],Y,Z) :- 
      intersezione(R,Y,Z).
    
    Per effetto del cut si arriva a questa clausola solo se nella clausola precedente è fallito appartiene(X,Y). In questo caso l'elemento X non appartiene all' insieme Y e non deve essere inserito nel risultato. Un ragionamento analogo vale per il predicato unione.
    unione([],X,X).
    unione([X|C],Y,Z) :- 
      appartiene(X,Y),
      !,
      unione(C,Y,Z).
    unione([X|C],Y,[X|Z]) :-
      unione(C,Y,Z).
    
    In questo caso se X appartiene all'insieme Y non deve essere incluso nell'unione. Il cut congela questa scelta. In caso contrario il cut non viene eseguito e si passa alla terza clausola che inserisce X nell' unione.
  2. Se non inseriamo il intersezione senza il cut nella clausola per l'intersezione (senza altre modifiche) quello che l'effetton è che un elemento del primo insieme che appartenga anche al secondo può essere o non essere incluso nel risultato. Infatti se appartiene(X,Y) ha successo, l'elemento viene incluso nel risultato, ma se interviene un backtracking, si può utilizzare anche la terza clausola che non inserice X nel risultato.
    % file: cut/insiemi1.pl       SBAGLIATO !
    
    % versione sbagliata - e' stato sopresso il "cut
    % nei predicati intersezione e unione senza altre modifiche
      
    appartiene(X,[X|_]) :- !.
    appartiene(X,[_|Y]) :- appartiene(X,Y).
    
    incluso([],_).
    incluso([X|C],Y) :-
      appartiene(X,Y),
      incluso(C,Y). 
    
    intersezione([],_Y,[]).
    intersezione([X|R],Y,[X|Z]) :-
      appartiene(X,Y),
      intersezione(R,Y,Z).
    intersezione([_X|R],Y,Z) :- 
      intersezione(R,Y,Z).
    
    unione([],X,X).
    unione([X|C],Y,Z) :- 
      appartiene(X,Y),
      unione(C,Y,Z).
    unione([X|C],Y,[X|Z]) :-
      unione(C,Y,Z).
    
    /* provate, forzando il backtraking con "solutions=all",
    i goal seguenti:
    ?- intersezione([4],[4,1],L).
    ?- unione([4],[4,1],L).
    */
    
    Se si prova a passare all' interprete il goal
    ?- intersezione([4],[4,1],L).
    usando "options=all" per forzare il backtracking, quello che si ottiene è:
    
    L = [4]
    Yes
    
    L = []
    Yes
    
    No
    

    Nel caso dell'unione se non si inserisce il cut un elemento X del primo insieme se appartiene al secondo non verrà inserito nel risultato (seconda clausola). Ma senza il cut anche la terza clausola può essere utilizzata in queste condizioni, e l'elemento X comparirà più di una volta.

    Ad esempio in risposta alla domanda

    ?- unione([4],[4,1],L).
    usando "options=all" per forzare il backtracking, quello che si ottiene è:
    
    L = [4, 1]
    Yes
    
    L = [4, 4, 1]
    Yes
    
    No
    
  3. Se non vogliamo usare il cut, possiamo rendere la seconda e la terza clausola di intersezione e di unione mutualmente esclusive con l'uso del predicato predefinito \+ (not).
      
    % file: cut/insiemi2.pl
    
    % versione di intersezione e unione che usano "not"
    % al posto del "cut"
      
    appartiene(X,[X|_]) :- !.
    appartiene(X,[_|Y]) :- appartiene(X,Y).
    
    incluso([],_).
    incluso([X|C],Y) :-
      appartiene(X,Y),
      incluso(C,Y). 
    
    intersezione([],_Y,[]).
    intersezione([X|R],Y,[X|Z]) :-
      appartiene(X,Y),
      intersezione(R,Y,Z).
    intersezione([X|R],Y,Z) :- 
      \+ appartiene(X,Y),
      intersezione(R,Y,Z).
    
    unione([],X,X).
    unione([X|C],Y,Z) :- 
      appartiene(X,Y),
      unione(C,Y,Z).
    unione([X|C],Y,[X|Z]) :-
      \+ appartiene(X,Y),
      unione(C,Y,Z).
    
    /* provate, forzando il backtraking con "solutions=all",
    i goal seguenti:
    ?- intersezione([4],[4,1],L).
    ?- unione([4],[4,1],L).
    */
    
    

    Utilizzando il not si ha una esecuzione meno efficiente in quanto, in alcuni casi, il goal appartiene(X,Y) viene invocato due volte.

    Ricerca di percorsi con il backtracking

    backtracking/perc1.pl

    Se passiamo al Prolog il goal:
    ?- trace,percorso(a,b).
    
    otteniamo
    Call:  ( 14) percorso(a, b) ?    
    Call:  ( 15) connesso(a, b) ?    
    Call:  ( 16) strada(a, b) ?    
    Exit:  ( 16) strada(a, b) ?    
    Exit:  ( 15) connesso(a, b) ?    
    Exit:  ( 14) percorso(a, b) ? 
    
    Infatti a e b sono direttamente connessi.
    In questo secondo caso
    ?- trace,percorso(a,e).
    
    otteniamo:
    Call:  ( 28) percorso(a, e) ?    
    Call:  ( 29) connesso(a, e) ?    
    Call:  ( 30) strada(a, e) ?    
    Fail:  ( 30) strada(a, e) ?    
    Redo:  ( 29) connesso(a, e) ?    
    Call:  ( 30) strada(e, a) ?    
    Fail:  ( 30) strada(e, a) ?    
    Redo:  ( 29) connesso(a, e) ?    
    Fail:  ( 29) connesso(a, e) ?    
    Redo:  ( 28) percorso(a, e) ?    
    Call:  ( 29) connesso(a, _L302) ?    
    Call:  ( 30) strada(a, _L302) ?    
    Exit:  ( 30) strada(a, d) ?    
    Exit:  ( 29) connesso(a, d) ?    
    Call:  ( 29) percorso(d, e) ?    
    Call:  ( 30) connesso(d, e) ?    
    Call:  ( 31) strada(d, e) ?    
    Fail:  ( 31) strada(d, e) ?    
    Redo:  ( 30) connesso(d, e) ?    
    Call:  ( 31) strada(e, d) ?    
    Exit:  ( 31) strada(e, d) ?    
    Exit:  ( 30) connesso(d, e) ?    
    Exit:  ( 29) percorso(d, e) ?    
    Exit:  ( 28) percorso(a, e) ? 
    
    In questo caso la prima regola per percorso fallisce perchè non c'è una connessione diretta fra a ed e. Con il backtracking si passa alla seconda regola, e si cerca una città direttamente connessa con a. La regola connesso, rimanda al fatto strada. I fatti vengono scanditi nell'ordine in cui sono stati immessi nel database del Prolog. Quindi il primo fatto utlizzato sara strada(a,d)>. Da d c'è poi una connessione diretta con e. Il percorso risultante è quindi a-d-e.

    backtracking/perc2.pl

    Ogni nuova città che viene raggiunta, viene inserita intesta alla lista delle città visitate come si puo' vedere osservando la seconda regola di percorso. Il punto iniziale e' inserito per primo, quello finale per ultimo. Questo spiega perchè vediamo il percorso in ordine inverso
    Se, utilizzando "solution=all", chiediamo le risposte al goal:
    percorso(a,c,[a]).
    otteniamo i seguenti percorsi:
    [c, b, d, a]
    Yes
    [c, e, d, a]
    Yes
    [c, b, a]
    Yes
    [c, e, d, b, a]
    Yes
    No
    
    Poichè a e a non sono direttamente connessi si usa per prima cosa la seconda regola per percorso
    percorso(X,Y,T) :-
      connesso(X,Y), 
      write([Y|T]),nl.
    percorso(X,Y,T) :- 
      connesso(X,Z),
      Z \= Y,
      \+ membro(Z,T),
      percorso(Z,Y,[Z|T]).
    
    Si deve quindi trovare uno Z direttamente connesso ad a. La regola connesso rimanda ai fatti strada che nel nostro esempio compaiono in questo ordine:
    strada(a,d).
    strada(a,b).
    strada(b,c).
    strada(c,e).
    strada(d,b).
    strada(d,e).
    
    Il primo fatto che viene utilizzato è: strada(a,d). Si va inizialmente in d e da qui si prosegue. È quindi l'ordine dei fatti strada che determina l'ordine con cui vengono trovati i percorsi. Scambiando i primi due fatti si otterrebbe per primo un percorso che passa inizialmente per b.

    backtracking/perc3.pl

    Per cercare i percorsi che connettono a e c passando per e si può usare il seguente goal:
    ?- percorso(a,c,[a],L), member(e,L).
    
    e si ottiene:
    L = [c, e, d, a]
    Yes
    L = [c, e, d, b, a]
    Yes
    No