Algorytm = Logika + Sterowanie

fd_labeling/2

W notatce poświęconej predykatowi fd_labeling/1 przedstawiono predykat generujący permutacje liczb od 1 do N.

Wykorzystamy ten predykat do znajdowania rozmieszczenia N niebijących się nawzajem hetmanów na szachownicy rozmiaru NxN (o ustawianiu hetmanów można poczytać tutaj).

W poniższym predykacie hetmany/2 umieszczono przed etykietowaniem zmiennych (podstawianiem wartości) dodatkowy warunek zapewniający, że żadne dwa hetmany, ustawione zgodnie z permutacją P, nie biją się po przekątnej:

hetmany(N, P) :-
     length(N, P),
     fd_domain(P, 1, N),
     dobra(P),
     fd_labeling(P).

dobra([]).
dobra([X | L]) :-
     dobra(L, X, 1),
     dobra(L).

dobra([], _, _).
dobra([Y | L], X, K) :-
     X+K #\= Y,
     X #\= Y+K,
     K1 is K+1,
     dobra(L, X, K1).


Gdy podstawianie wartości odbywa się przy użyciu predykatu fd_labeling/1, wtedy zmienne podstawiane są w takiej kolejności w jakiej występują na liście P, tzn. P1, P2, ..., Pn, oraz w kolejnych próbach podstawiane są pod nie wartości 1, 2, ..., N, tj. od najmniejszej do największej.

Na poniższym rysunku przedstawiono proces poszukiwania rozwiązania w przypadku N=6:


rys3


Jak widać, osiem ścieżek zakończyło się niepowodzeniem (sytuacje S1, S2, ..., S8), natomiast dopiero dziewiąta ścieżka doprowadziła do ustawienia wszystkich sześciu hetmanów (sytuacja S9).

Sytuacje S1-S9 przedstawiono na poniższym rysunku:

rys4

Liczba ścieżek zakończonych niepowodzeniem przed znalezieniem rozwiązania nazywa się liczbą nawrotów (ang. backtracks).

W wywołaniu predykatu fd_labeling/2 można jako drugi argument podać listę opcji:

fd_labeling(+ListaFDzmiennych, +ListaOpcji).

Jedną z opcji jest term backtracks(B), gdzie B jest zmienną pod którą zostanie podstawiona liczba nawrotów jaka potrzebna była do znalezienia rozwiązania.

Gdy zmienne etykietuje się po kolei a wartości podstawiane są od najmniejszej do największej, to do znalezienia ustawienia sześciu hetmanów potrzeba ośmiu nawrotów. Jednak do ustawienia 30 hetmanów potrzeba w ten sposób ponad 7 milionów nawrotów!

Aby przyspieszyć poszukiwanie rozwiązania, tj. zmniejszyć liczbę nawrotów, można zastosować heurystykę wyboru zmiennej oraz heurystykę wyboru wartości.

Heurystykę wyboru zmiennej określa się podając w drugim parametrze predykatu fd_labeling opcję variable_method(H), gdzie H jest nazwą jednej z dostępnych heurystyk:

  • standard - zmienne wybierane w kolejności występowania na liście (domyślna kolejność);
  • first_fail (albo ff) - w pierwszej kolejności wybierana jest zmienna o najmniejszej liczbie wartości w dziedzinie;
  • most_constrained - podobnie jak w przypadku first_fail ale jeśli dwie zmienne mają ten sam rozmiar dziedziny, to wybierana jest zmienna występująca w większej licznie ograniczeń;
  • smallest - w pierwszej kolejności wybierana jest zmienna o najmniejszej wartości w swojej dziedzinie, przy czym jeśli dwie zmienne mają tę samą najmniejszą wartość, to wybierana jest zmienna występująca w większej liczbie ograniczeń;
  • largest - podobnie jak w przypadku smallest z tą różnicą, że zmienne wybierane są w malejącej kolejności największych wartości w dziedzinach;
  • max_regret - wybierana jest zmienna o największej różnicy między najmniejszą a kolejną wartością w dziedzinie, przy czym jeśli dwie zmienne mają taką samą największą tę różnicę, to wybierana jest ta zmienna, która występuje w większej liczbie ograniczeń;
  • random - zmienne wybierane są w losowej kolejności.

Na liczbę nawrotów ma również wpływ kolejność wyboru wartości podstawianych pod zmienną. Zadaje się nią w drugim parametrze predykatu fd_labeling podając opcję value_method(H), gdzie H jest jedną z heurystyk:

  • min - wartości wybierane są od najmniejszej do największej;
  • max - wartości wybierane są od największej do najmniejszej;
  • middle - wartości wybierane są od środkowej w dziedzinie ku jej brzegom;
  • random - wartości wybierane są w losowej kolejności.

W poniższej tabeli dokonano porównania liczby nawrotów w przypadku zastosowania różnych heurystyk i dla różnego rozmiaru problemu (liczby ustawianych hetmanów):

__N_______brak___middle_________ff__ff+middle
_10_________24________2__________9__________0
_20_____37,320_______38_________26__________2
_30__7,472,978______169________127_________35
_40_____________406,195_________22_________50
_50__________________________8,891__________0
_60____________________________224__________8
_70__________________________8,941________117
_80_________________________68,064__________0
_90______________________1,935,628_________48
100_________________________24,345__________1

Powyższe wyniki zebrano na wykresie:

rys5

Jak widać, przy tym samym rozmiarze problemu, można zmniejszyć liczbę nawrotów nawet setki tysięcy razy przez odpowiedni dobór heurystyk sterujących pracą predykatu fd_labeling/2.

Opierając się na powyższych wynikach eksperymentów, można zaproponować następującą wersję predykatu hetmany/2:

hetmany(N, P) :-
     length(P, N),
     fd_domain(P, 1, N),
     fd_all_different(P),
     dobra(X),
     fd_labeling(P, [
         variable_method(ff),
         value_method(middle)]).


Jeśli żadna z dostępnych heurystyk nie zmniejszy liczby nawrotów w stopniu, który umożliwia rozwiązanie jakiegoś problemu w akceptowalnym czasie, to zawsze można próbować zastąpić predykat fd_labeling/2 własnym predykatem, który dokonuje etykietowania w odpowiedniejszy sposób, zależny od własności rozwiązywanego problemu. Wymaga to jednak dokładnego zbadania problemu i opracowania takiego wyspecjalizowanego sposobu etykietowania.