Metóda prehľadávania s návratom (backtrack).

 

Gabriela Andrejková

Prírodovedecká fakulta, PF UPJŠ, Košice

e-mail: andrejk@kosice.upjs.sk

 

Abstrakt. Metóda s návratom je jednou z možnosti systématického prehľadávania priestoru, v ktorom sa riešenie môže nachádzať. Hlavnou myšlienkou tejto metódy je postupná konštrukcia riešenia určitého problému, tak aby, pokiaľ je to možné, boli ušetrené zbytočné kroky, ktoré nepovedú k riešeniu daného problému.

 

Úvod

Predpokladajme, že riešením problému, označme ho P, má byť vektor a[n] = (a[1], a[2], ..., a[n]), ktorý splňuje určitú vlastnosť, označme ju A. Vo všeobecnosti platí, že i-tá položka tohoto vektora bola vybratá z množiny povolených hodnôt V[i], t.j. a[i] Î V[i] pre i= 1, 2, ..., n. Vektor a[n] n, pre n=1, 2, ..., prvkov a je postupne predlžovaný, t.j. pre n=1 má jeden prvok, pre n=2 dva prvky, atď. Triviálna metóda riešenia by znamenala vyskúšať všetky možnosti (t.j. použiť hrubú silu ), ale  to by viedlo k prevereniu |V[1]|*|V[2]|*... *|V[n]| vektorov. Metóda prehľadávania s návratom umožňuje ušetriť skúmanie niektorých vektorov, pretože vektor riešenia je konštruovaný postupne a predlžovaný v súlade s vlastnosťou A. Predĺženie je vždy najprv skontrolované a je možné len vtedy, ak nebude porušená vlastnosť A.

Jedna z verzií algoritmu pre túto metódu môže byť zaznamenaná nasledovne:

  1. k:=1; a[0] := Ć ; { Vektor Ć je prázdny}
  2. IF V[k] = Ć THEN pokračuj bodom 8; { Vo V[k] nie sú ďalší kandidáti}
  3. Vyber a[k] z V[k]; V[k] := V[k] - {a[k]}; {Z V[k] vybrať ďalšieho kandidáta}
  4. a[k] := a[k-1] a[k]; { Vektor a[k-1] je predĺžený o 1 prvok}
  5. IF k >= n THEN Vypíš riešenie; Koniec
  6. k := k + 1; { Prechod k ďalšiemu predĺženiu}
  7. Pokračuj krokom 2;
  8. k := k - 1; { Odstránenie posledného predĺženia}
  9. IF k=0 THEN Vypíš "Riešenie neexistuje"; Koniec
  10. Pokračuj krokom 2;

Táto metóda je mnohokrát používaná v rekurzívnom zápise, čo je veľmi prehľadné a jednoduché. Rekurzívna verzia tejto metódy:

procedure Skus_dalsie_predlzenie;

BEGIN

Inicializácia výberu ďalšieho predĺženia;

REPEAT Vyber ďalšieho kandidáta z množiny možných kandidátov;

IF prijateľný THEN

BEGIN

Zaznamenaj predĺženie;

IF riešenie nie je úplné THEN

BEGIN

Skus_dalsie_predlzenie;

IF neúspešný THEN Vymaž predchádzajúce predĺženie

END

END

UNTIL (nie sú ďalší kandidáti) alebo (riešenie je nájdené)

END

 

Problém ôsmich dám

Problém ôsmich dám je jedným zo známych problémov, pri ktorých riešení je možné aplikovať metódu prehľadávania s návratom. Preskúmal ho C. F. Gauss v roku 1850, ale jeho riešenie nedoviedol do úspešného konca. Nepodarilo sa mu nájsť analytické vyjadrenie riešenia a preskúmať všetky možnosti bolo vtedy veľmi náročné na čas a pracné.

Tento problém možno sformulovať takto:

Osem dám je treba rozmiestniť na šachovnici takým spôsobom, aby sa navzájom neohrozovali (v zmysle šachových pravidiel). Príklad takého rozloženia je uvedený na Obr. 1.

 

D

D

D

D

D

D

D

D

Obr 1. Uloženie dám tak, že sa navzájom neohrozujú

 

Riešenie:

Prípravné kroky k vytvoreniu programu, ktorý nájde riešenie.

Sformulujeme rekurzívnu procedúru v hrubých rysoch nasledovne:

procedure Skus_dalsiu_damu (i: integer; var q: boolean);

BEGIN

Inicializácia výberu pozícií pre i-tú dámu;

REPEAT

Urobiť výber ďalšej pozície; q:=false;

IF Umiestniteľná THEN

BEGIN

Ulož dámu;

IF i<8 THEN

BEGIN

Skus_dalsiu_damu(i+1, q);

IF Neúspešný THEN Odstráň dámu

END

END

UNTIL (Nie sú ďalší kandidáti) alebo (Úspešný)

END; {Skus_dalsiu_damu}

Teraz vyjadríme jednotlivé nepresné formulácie v tejto procedúre a vytvoríme program, ktorý nájde jedno riešenie.

Prv než urobíme upresnenie uvedených formulácií, určíme dátové typy, s ktorými budeme pracovať, t.j. dátové typy, ktoré budú reprezentovať uloženie dámy a obsadenie riadkov a uhlopriečok.

Pretože vieme, že v každom stĺpci má byť uložená práve jedna dáma, nie je nutné používať na ukladanie dvojrozmerné pole, postačí jednorozmerné pole, v ktorom i-tý prvok bude vyjadrovať číslo riadku v i-tom stĺpci, kde je postavená i-tá dáma. Teda deklarujeme

x: array [1..8] of integer;

Obsadenie j-tého riadku budeme zaznamenávať v poli a, kde j-tý prvok nadobudne hodnotu true, ak je riadok voľný a inak nadobudne hodnotu false. Teda deklarujeme

a: array [1..8] of boolean;

Ak zavedieme číslovanie riadkov zhora nadol 1.. 8 a číslovanie stĺpcov zľava napravo 1..8 na Obr. 1, môžeme jednotlivé uhlopriečky označiť číslami i+j , t.j. uhlopriečky 2..16 smerujúce nadol doprava a uhlopriečky smerujúce sprava doľava nadol i -j , tieto uhlopriečky majú čísla –7..7. Teda deklarujeme

b: array [2..16] of boolean;

c: array [-7..7] of boolean;

Ak bude uhlopriečka voľná prvok poľa, ktorý ju reprezentuje nadobudne hodnotu true, inak nadobudne hodnotu false.

Použitím týchto definícií je možné vyjadriť nepresné formulácie presnejšie takto:

  1. Inicializácia výberu pozícií pre i-tú dámu: Pre každú dámu je v stĺpci 8 možností na jej uloženie (8 riadkov). Na prechod cez tieto možností použijeme premennú j a cyklus repeat. Pred vstupom do cyklu nastavíme j:= 0.
  2. Urobiť výber ďalšej pozície: Ďalšia pozícia je určovaná zvýšením hodnoty j , t.j. príkazom j:= j+1.
  3. Umiestniteľná: Ak má byť i-tá dáma umiestniteľná v j-tom riadku, musí byť tento riadok voľný a podobne voľné musia byť voľné obe diagonály, teda výraz a[i] and b[i+j] and c[i-j] musí nadobudnúť hodnotu true.
  4. Ulož dámu:Uloženie dámy sa uskutoční obsadením riadku a obsadením oboch diagonál, t.j. x[i]:= j; a[j]:= false; b[i+j]:= false; c[i-j]:= false;
  5. Neúspešný:Neúspech je možné vyjadriť pomocou premennej q, ktorá sleduje, či bolo nájdené riešenie, t.j. pomocou not q.
  6. Odstráň dámu:Odstránenie dámy znamená uvoľnenie riadku a oboch diagonál, t.j.:
  7. a[j]:= true; b[i+j]:= true; c[i-j]:=true;

  8. Nie sú ďalší kandidáti: Ak premenná j dosiahne hodnotu 8, t.j. poslednú pozíciu riadku, nie sú ďalší kandidáti.

Program v jazyku Turbo Pascal môže byť zapísaný nasledovne:

program Osem_dam;

{Najde jedno riesenie problemu 8 dam}var i: integer; {poradove cislo damy}

q: boolean; {q=true, ak je riesenie najdene}

a: array [1..8] of boolean; {vektor urcujuci obsadenie riadkov}

b: array [2..16] of boolean; {vektor urcujuci obsadenie diagonal}

c: array [-7..7] of boolean; {vektor urcujuci obsadenie diagonal}

x: array [1..8] of integer; {vektor urcujuci ulozenie dam}

procedure Skus_dalsiu_damu( i: integer; var q: boolean);

{Rekurzivna metoda prehladavania s navratom pre postupne ukladanie dam}

var j: integer; {pozicia i-tej damy v riadku}

begin

j:= 0;

repeat

j:= j+1; q:= false;

if a[j] and b[i+j] and c[i-j] then

begin {je mozne ulozenie v riadku j}

x[i]:= j; {ulozenie i-tej damy v j-tom riadku}

a[j]:= false;

b[i+j]:= false; c[i-j]:= false;

if i<8 then

begin {pocet doposial ulozenych dam je

mensi ako 8}

Skus_dalsiu_damu(i+1, q);

if not q then

begin {posledna dama nemoze byt ulozena}

a[j]:= true; {v j_tom riadku}

b[i+j]:= true; c[i-j]:= true;

end

end else q:= true

end

until q or (j=8);

end; {Skus_dalsiu_damu}

begin {hlavny program}

for i:= 1 to 8 do a[i]:= true;

for i:= 2 to 16 do b[i]:= true;

for i:=-7 to 7 do c[i]:= true;

Skus_dalsiu_damu(1,q);

if q then

begin

write(' Riesenie problemu 8 dam: ');

for i:= 1 to 8 do write(x[i]:3); writeln

end else writeln('Riesenie neexistuje.');

end.

Program nájde jediné riešenie: 1, 5, 8, 6, 3, 7, 2, 4.

 

Problém jazdca na šachovnici

Tento problém je tiež typickým problémom, pri ktorého riešení je možné použiť metódu prehľadávania s návratom. Problém je možné sformulovať nasledovne:

Jazdec sa po šachovnici typu n x n pohybuje v zmysle pravidiel šachu. Je potrebné nájsť jeho cestu po šachovnici takú, že prejde celú šachovnicu a každé políčko navštívi práve jedenkrát.

V tomto prípade algoritmus pracuje s dvojrozmerným poľom, do ktorého zapisuje poradové čísla vykonaných skokov. Ak ďalší skok nie je možný, vráti sa naspäť, číslo vykonaného skoku vymaže a pokračuje po inej ceste. Teda deklarujeme

h: array [index, index] of integer;

Z  pozícií v strede šachovnice má jazdec 8 možností skokov, na okraji a v rohoch menej, preto je potrebné kontrolovať, či indexy prvku poľa ďalšieho skoku sú na šachovnici. Uvedené možnosti skokov sú preskúmavané systematicky, rovnako pri každom skoku. Zmeny pozícií pri vykonaní ďalšieho skoku sú stanovené v poliach

a: array[1..8] of integer = (2, 1, -1, -2, -2, -1, 1, 2);

b: array[1..8] of integer = (1, 2, 2, 1, -1, -2, -2, -1);

To znamená, ak bol jazdec v pozícii [x, y] a vykoná k-tý skok, k=1, 2, .... 8, tak jeho nová pozícia bude [x+a[k], y+b[k].

Uvedieme výsledný program pre riešenie tohto problému, ktorý má nasledujúci tvar:program Pohyb_jazdca;{Pokrytie sachovnice skokmi jazdca tak, aby kazde policko bolo navstivene prave jedenkrat a bola prejdena cela sachovnica.}const n=5; nsq=n*n; {Velkost sachovnice je 5}

a: array[1..8] of integer = (2, 1, -1, -2, -2, -1, 1, 2);

b: array[1..8] of integer = (1, 2, 2, 1, -1, -2, -2, -1);

{ a, b -- pomocne polia vyjadrujuce zmenu suradnic jazdca z danej

pozicie}

type index=1..n;

var i, j: integer; {Riadiace premenne cyklov}

s: set of index; {Mnozina pozicii v jednom smere}

q: boolean; {Vyjadruje, ci riesenie najdene}

h: array [index, index] of integer; {Sachovnica}

procedure vyskusaj(i: integer; x, y: index; var q: boolean);

{Vykona i-ty skok z pozicie (x, y) a rekuzivne pokracuje dalej;

q nadobudne hodnotu true, ak je riesenie najdene}

var k, u, v: integer;

q1: boolean;

begin

k:=0; {Inicializacia pre moznosti skokov}

repeat

k:= k+1; {Prechod na skumanie dalsej moznosti}

q1:= false; { pre i-ty skok}

u:= x+ a[k]; v:= y+ b[k];

{Vypocet novej pozicie pri k-tej moznosti}

if (u in s) and (v in s) then

if h[u, v]=0 then

begin {Skok je vykonatelny}

h[u, v]:= i;

if i<nsq then {Sachovnica este nie je pokryta}

begin {Prechod na vykonanie dalsieho skoku}

vyskusaj(i+1, u, v, q1);

if not q1 then h[u, v]:= 0;

{Zrusenie posledneho skoku}

end else q1:=true {Riesenie je najdene}

end

until q1 or (k=8);

q:= q1;

end; {vyskusaj}

begin {hlavny program}

s:= [];

for i:= 1 to n do s:= s + [i];

for i:= 1 to n do

for j:=1 to n do h[i, j]:= 0;

h[1, 1]:= 1; {Jazdec zacina na policku (1,1)}

vyskusaj(2, 1, 1, q);

if q then

for i:= 1 to n do

begin for j:= 1 to n do write(h[i, j]:3);

writeln

end else writeln(' Riesenie neexistuje ')

end.

Riešenie, ktoré je nájdené pre n=5:

1

6

15

10

21

14

9

20

5

16

19

2

7

22

11

8

13

24

17

4

25

18

3

12

23

 

Literatúra:

  1. J. Jinoch a kol.: Programovací jazyk PASCAL, SNTL, Praha, 1985
  2. N. Wirth: Algorithms+Data Structures=Programs, Prentice-Hall, 1976,
  3. ex. slovenský preklad v roku 1987

  4. P. Satrapa: Začíname v PASCALU. Neokortex spol. s r. o., Praha, 1998.