Садржај

SAT решавачи

Постоји веома ефикасан софтвер који може да испита задовољивост исказне формуле (који је, наравно, заснован на коришћењу другачијих метода). Такви програми се називају SAT решавачи (енг. SAT solvers). Постоје многи програми ове врсте, а најпознатији од њих је вероватно решавач MiniSat (http://minisat.se/, http://logicrunch.it.uu.se:4096/~wv/minisat/). Основни алгоритам на ком су засновани SAT решавачи је DPLL алгоритам (Дејвис-Патнам-Логеман-Ловеланд).

Иако они испитују само задовољивост формуле, могу се лако употребити да провере и таутологичност. Наиме, решавач треба да провери задовољивост негације формуле, па ако утврди да је негација незадовољива, полазна формула је таутологија. Савремени SAT решавачи могу да провере неке формуле са стотинама хиљада променљивих.

Да би се SAT решавачи могли користити, формула мора бити припремљена на одговарајући начин. Потребно је превести формулу у тзв. конјунктивну нормалну форму (КНФ), тј. представити је у облику конјункције клаузула (каже се и клауза), где је свака клаузула дисјункција литерала, а литерал је исказно слово или негација исказног слова. На пример, наредна формула је у КНФ.

\[(p \vee \neg q) \wedge (\neg p \vee q \vee r) \wedge \neg r\]

Она се састоји од 3 клаузуле: \(p \vee \neg q\) (она има два литерала), \(\neg p \vee q \vee r\) (она има три литерала) и \(\neg r\) (она има један литерал).

Превођење формуле у КНФ се може извршити коришћењем следећих логичких еквиваленција:

\[\begin{split}\begin{eqnarray*} A \Leftrightarrow B &\equiv& (A \Rightarrow B) \wedge (B \Rightarrow A)\\ A \Rightarrow B &\equiv& \neg A \vee B\\ \neg (A \wedge B) &\equiv& \neg A \vee \neg B\\ \neg (A \vee B) &\equiv& \neg A \wedge \neg B\\ \neg (\neg A) &\equiv& A\\ A \vee (B \wedge C) &\equiv& (A \vee B) \wedge (A \vee C)\\ (A \wedge B) \vee C &\equiv& (A \vee C) \wedge (B \vee C) \end{eqnarray*}\end{split}\]

Прве две омогућавају да се из формуле уклоне сва појављивања везника \(\Leftrightarrow\) и \(\Rightarrow\), наредне три да се све негације спусте до нивоа исказних слова, а последње две да се применом дистрибутивности добије жељени однос конјункција и дисјункција. Када је потребно, може се примењивати и асоцијативност конјункције и дисјункције.

За вежбу вам остављамо да докажете да су све претходне везе заиста логичке еквиваленције.

КНФ облик негације формуле из претходног примера је:

\[(\neg p \vee q \vee r) \wedge \neg r \wedge \neg q \wedge p\]

Формуле \(\neg (A \Rightarrow B)\) и \(A \wedge \neg B\) еквивалентне (што се лако може утврдити провером таутологичности формуле \(\neg (A \Rightarrow B) \Leftrightarrow A \wedge \neg B\)). Зато је негација формуле облика \(I_1 \wedge I_2 \Rightarrow I\) еквивалентна формули \(I_1 \wedge I_2 \wedge \neg I\), па је негација полазне формуле еквивалентна формули:

\[(p \Rightarrow q \vee r) \wedge \neg r \wedge \neg (\neg q \Rightarrow \neg p)\]

На основу истог правила претходна формула се може свести на:

\[(p \Rightarrow q \vee r) \wedge \neg r \wedge (\neg q \wedge \neg \neg p)\]

Сада можемо употребити чињеницу да су \(A \Rightarrow B\) и \(\neg A \vee B\) еквивалентне (што се може лако утврдити испитивањем таутологичности формуле \((A \Rightarrow B) \Leftrightarrow (\neg A \vee B)\)) и добити:

\[(\neg p \vee (q \vee r)) \wedge \neg r \wedge (\neg q \wedge \neg \neg p)\]

На крају, применом чињенице да су \(\neg \neg A\) и \(A\) еквивалентне и применом закона асоцијативности за конјункцију и дисјункцију (који нам омогућавају да неке заграде изоставимо) добијамо КНФ облик:

\[(\neg p \vee q \vee r) \wedge \neg r \wedge \neg q \wedge p\]

Применом дистрибутивности може се добити КНФ формула која је много већа од полазне (чак експоненцијално већа). Постоје и ефикасније методе свођења формуле на КНФ. Најпознатија од њих је Цајтинова трансформација (енг. Tseitin transform) која уводи нова исказна слова којим се обележавају потформуле, пре превођења у КНФ. На пример, размотримо следећу формулу:

\[(p \vee (q \wedge r)) \wedge (\neg p \vee \neg r)\]

Њену потформулу \(q \wedge r\) можемо обележити новим словом \(s_1\), чиме добијамо формулу:

\[(p \vee s_1) \wedge (\neg p \vee \neg r) \wedge (s_1 \Leftrightarrow q \wedge r)\]

Њену потформулу \(p \vee s_1\) можемо обележити новим словом \(s_2\), чиме добијамо формулу:

\[s_2 \wedge (\neg p \vee \neg r) \wedge (s_1 \Leftrightarrow q \wedge r) \wedge (s_2 \Leftrightarrow p \vee s_1)\]

Њену потформулу \(\neg p \vee \neg r\) можемо обележити новим словом \(s_3\), чиме добијамо формулу:

\[s_2 \wedge s_3 \wedge (s_1 \Leftrightarrow q \wedge r) \wedge (s_2 \Leftrightarrow p \vee s_1) \wedge (s_3 \Leftrightarrow \neg p \vee \neg r)\]

На крају, њену потформулу \(s_2 \wedge s_3\) можемо обележити новим словом \(s_4\), чиме добијамо формулу:

\[s_4 \wedge (s_1 \Leftrightarrow q \wedge r) \wedge (s_2 \Leftrightarrow p \vee s_1) \wedge (s_3 \Leftrightarrow \neg p \vee \neg r) \wedge (s_4 \Leftrightarrow s_2 \wedge s_3)\]

Сада се свака од логичких еквиваленција лако може засебно превести у КНФ, чиме се добија коначан КНФ облик полазне формуле.

\[\begin{split}\begin{array}{l} s_4 \\ (\neg s_1 \vee q) \wedge (\neg s_1 \vee r) \wedge (\neg q \vee \neg r \vee s_1)\ \wedge \\ (\neg s_2 \vee p \vee s_1) \wedge (\neg p \vee s_2) \wedge (\neg s_1 \vee s_2)\ \wedge\\ (\neg s_3 \vee \neg p \vee \neg r) \wedge (p \vee s_3)\wedge (r \vee s_3)\ \wedge \\ (\neg s_4 \vee s_2) \wedge (\neg s_4 \vee s_3)\wedge (\neg s_2 \vee \neg s_3 \vee s_4) \end{array}\end{split}\]

Тиме се полазна формула увећава само за константни фактор. Додуше, добијена формула неће више бити логички еквивалентна полазној, већ само еквизадовољива, што значи да је КНФ облик задовољив ако и само ако је задовољива полазна формула. Приметимо да нам је то сасвим довољно за примену SAT решавача, јер утврђивањем незадовољивости КНФ облика аутоматски знамо да је и полазна формула незадовољива, док се утврђивањем задовољивости КНФ облика добија валуација која је уједно и валуација у којој је полазна формула тачна (довољно је просто занемарити вредности новоуведних променљивих у тој валуацији).

Формуле у КНФ облику се обично описују у формату DIMACS (то је стандардни формат улаза SAT решавача). Променљиве се обележавају бројевима 1, 2, 3 итд. Негативни бројеви означавају негације променљивих. Свака клаузула се записује као низ бројева завршен нулом. На пример, DIMACS запис формуле \((p \vee \neg q) \wedge (\neg p \vee q \vee r) \wedge \neg r\) је:

p cnf 3 3
1 -2 0
-1 2 3 0
-3 0

Прва линија је заглавље и говори о томе да формула има 3 променљиве и 3 клаузуле. Променљиве \(p\), \(q\) и \(r\) су означене редом бројевима 1, 2 и 3.

Ако овај текст снимимо у датотеку formula.cnf и покренемо SAT решавач (на пример, MiniSAT) или је унесемо у веб-интерфејс SAT решавача MiniSAT (http://logicrunch.it.uu.se:4096/~wv/minisat/), добићемо резултат

SAT
-1 -2 -3 0

Ово значи да је формула задовољива и једна валуација у којој је та формула тачна је \(p \mapsto 0, q \mapsto 0, r \mapsto 0\).

Судоку

Прикажимо сада како можемо искористити SAT решавач да решимо логичку загонетку Судоку. Ова загонетка захтева да се поље димензије \(9 \times 9\) попуни бројевима од 1 до 9 тако да су у свакој врсти, у свакој колони и у сваком троуглу димензије \(3 \times 3\) бројеви различити. Потребно је да кодирамо овај проблем коришћењем исказне логике. Означимо поља табеле са \(A_{ij}\) за \(1 \leq i, j \leq 9\). На сваком пољу може бити уписан било који број од 1 до 9. Основни искази ће бити означени са \(p_{ijv}\) за \(1 \leq i, j, v \leq 9\) и означаваће да је на пољу \(A_{ij}\) уписана вредност \(v\). Сваки од ових исказа може бити или тачан или нетачан. Потребно је да одредимо и везе између исказа које ће осигурати да ће њихове истинитосне вредности одређивати исправно решење загонетке Судоку.

За почетак, на сваком пољу треба да пише тачно једна вредност, што значи да за сваки пар \(i\) и \(j\) од 1 до 9 тачно једна од променљивих \(p_{ij1}, \ldots, p_{ij9}\) треба да има вредност тачно. Ово можемо кодирати следећим формулама:

\[\begin{split}\begin{eqnarray*} p_{ij1} \vee p_{ij2} \vee \ldots \vee p_{ij9}\\ p_{ij1} \Rightarrow \neg p_{ij2} \wedge \neg p_{ij3} \ldots \neg p_{ij9}\\ p_{ij2} \Rightarrow \neg p_{ij1} \wedge \neg p_{ij3} \ldots \neg p_{ij9}\\ \ldots\\ p_{ij9} \Rightarrow \neg p_{ij1} \wedge \neg p_{ij2} \ldots \neg p_{ij8} \end{eqnarray*}\end{split}\]

Свака од ових импликација се може разбити на 8 мањих импликација, а затим се трансформацијом \(p \Rightarrow q\) у \(\neg p \vee q\) може добити следећи низ клаузула (генеришу се засебно клаузуле за свако \(i, j\) од 1 до 9):

\[\begin{split}\begin{eqnarray*} p_{ij1} \vee p_{ij2} \vee \ldots \vee p_{ij9}\\ \neg p_{ij1} \vee \neg p_{ij2} \\ \neg p_{ij1} \vee \neg p_{ij3} \\ \ldots \\ \neg p_{ij1} \vee \neg p_{ij9} \\ \neg p_{ij2} \vee \neg p_{ij3} \\ \neg p_{ij2} \vee \neg p_{ij4} \\ \ldots \\ \neg p_{ij2} \vee \neg p_{ij9} \\ \ldots \\ \ldots \\ \neg p_{ij8} \vee \neg p_{ij9} \\ \end{eqnarray*}\end{split}\]

На сличан начин треба да кодирамо услове да ће за сваку врсту \(i\) и сваку вредност \(v\) од 1 до 9 тачно једна променљива \(p_{i1v}, p_{i2v}, \ldots, p_{i9v}\) бити тачна, а затим услове да ће за сваку колону \(j\) и сваку вредност \(v\) од 1 до 9 тачно једна променљива \(p_{1jv}, p_{2jv}, \ldots, p_{9jv}\) бити тачна. На крају је потребно додати и услове за сваки од квадрата димензије \(3\times 3\). Сваки од 9 квадрата се може задати паром индекса \((k, l)\) између 0 и 2 (нпр. \((0, 0)\) означава горњи леви квадрат, а \((2, 2)\) доњи десни). За сваки квадрат \((k, l)\) и сваку вредност \(v\) између 1 и 9, тачно једна од променљивих

\[\begin{split}p_{(3k+1)(3l+1)v},\ p_{(3k+1)(3l+2)v},\ p_{(3k+1)(3l+3)v}\\ p_{(3k+2)(3l+1)v},\ p_{(3k+2)(3l+2)v},\ p_{(3k+2)(3l+3)v}\\ p_{(3k+3)(3l+1)v},\ p_{(3k+3)(3l+2)v},\ p_{(3k+3)(3l+3)v}\end{split}\]

треба да буде тачна.

У загонетнки Судоку обично су задате вредности које су уписане у нека поља (и то често тако да се гарантује да се остала поља могу попунити на јединствен начин, тј. да постоји само једно тачно решење загонетке). Задате вредности једноставно кодирамо тако што решавачу дамо једночлане клаузуле које садрже само променљиве за које унапред знамо да су тачне. Наредни програм у језику C# кодира загонетку Судоку у формату DIMACS, који је стандардни улазни формат SAT решавача. Имплементација је прилично праволинијска. Потребно је пресликати променљиве \(p_{ijv}\) на бројеве од \(1\) до \(9^3 = 729\). Најједноставнији начин да се то уради је да се индекси \(ijv\) тумаче као цифре (увећане за 1) у основи 9, тј. да се свака променљива \(p_{ijv}\) преслика у број \((i-1) + 9\cdot (j-1) + 81\cdot (v-1) + 1\). Тада је и декодирање једноставно (и своди се на одређивање вредности цифара у основи 9):

\[\begin{split}i = (p - 1)\ \mathrm{mod}\ 9 + 1\\ j = ((p - 1)\ \mathrm{div}\ 9)\ \mathrm{mod}\ 9 + 1\\ v = ((p - 1)\ \mathrm{div}\ 81) + 1\end{split}\]

Програм генерише 81 услов јединствености броја на пољу (за 81 поље), 81 услов јединствености броја у врсти (за 9 врста и по 9 бројева), 81 услов јединствености броја у колони (за 9 колона и по 9 бројева) и 81 услов јединствености броја у малом квадрату (за 9 малих квадрата и по 9 бројева). Сваки услов јединствености има једну позитивну клаузулу са 9 литерала и \({9 \choose 2} = 36\) негативних клаузула са по 2 литерала. Укупан број општих клаузула је зато \(4 \cdot 81 \cdot 37\), а њима се додају у једночлане клаузуле за свако унапред попуњено поље.

Коначна верзија програма дата је у наставку.

// ispisuje DIMACS zaglavlje
static void Zaglavlje(int BrojPromenljivih, int BrojKlauzula)
{
   Console.WriteLine("p cnf {0} {1}", BrojPromenljivih, BrojKlauzula);
}

// ispisuje klauzulu kao red u formatu DIMACS
static void Klauzula(int[] promenljive)
{
   foreach(int p in promenljive)
      Console.Write(p + " ");
   Console.WriteLine(0);
}

// kodira se uslov da je tacno jedna od promenljivih iz datog niza tacna
static void TacnoJedna(int[] promenljive)
{
    // klauzula koja dovodi do toga da bar jedna promenljiva iz niza
    // mora biti tacna
    Klauzula(promenljive);
    // klauzule koje za svaki par promenljivih zabranju da su obe
    // promenljive istovremeno tacne
    int[] kl = new int[2];
    for (int i = 0; i < promenljive.Length; i++)
        for (int j = i + 1; j < promenljive.Length; j++) {
            kl[0] = -promenljive[i];
            kl[1] = -promenljive[j];
            Klauzula(kl);
        }
}

// trojke (i, j, v) se kodiraju brojevima između 1 i 729
static int P(int i, int j, int v)
{
    return (i-1) + 9*(j-1) + 81*(v-1) + 1;
}

static void Main()
{
    // ucitavamo vrednosti na zadatim poljima
    List<int> zadate = new List<int>();
    string linija;
    while ((linija = Console.ReadLine()) != null)
    {
        string[] delovi = linija.Split();
        int i = int.Parse(delovi[0]);
        int j = int.Parse(delovi[1]);
        int v = int.Parse(delovi[2]);
        zadate.Add(P(i, j, v));
    }

    // stampamo DIMACS zaglavlje
    Zaglavlje(729, 4*81*37 + zadate.Count);

    // jedinstvenost vrednosti na svakom polju
    int[] promenljive = new int[9];
    for (int i = 1; i <= 9; i++)
       for (int j = 1; j <= 9; j++)
       {
           for (int v = 1; v <= 9; v++)
              promenljive[v-1] = P(i, j, v);
           TacnoJedna(promenljive);
       }

    // jedinstvenost vrednosti u svakoj vrsti
    for (int i = 1; i <= 9; i++)
       for (int v = 1; v <= 9; v++)
       {
           for (int j = 1; j <= 9; j++)
              promenljive[j-1] = P(i, j, v);
           TacnoJedna(promenljive);
       }

    // jedinstvenost vrednosti u svakoj koloni
    for (int j = 1; j <= 9; j++)
       for (int v = 1; v <= 9; v++)
       {
           for (int i = 1; i <= 9; i++)
              promenljive[i-1] = P(i, j, v);
           TacnoJedna(promenljive);
       }

    // jedinstvenost vrednosti u svakom kvadratu 3x3
    for (int k = 0; k < 3; k++)
       for (int l = 0; l < 3; l++)
          for (int v = 1; v <= 9; v++)
          {
              for (int a = 1; a <= 3; a++)
                 for (int b = 1; b <= 3; b++)
                     promenljive[3*(a-1)+(b-1)] = P(3*k+a, 3*l+b, v);
              TacnoJedna(promenljive);
          }

    // ispisujemo zadate promenljive
    foreach (int p in zadate)
    {
        int[] kl = {p};
        Klauzula(kl);
    }
}

Програм учитава унапред попуњене вредности. На пример, судоку загонетка:

. . . . . . . . 3
9 . . 3 . 2 . . .
6 8 . . . . . 5 .
1 . . . . 5 . . 9
5 . . 7 . . . 6 2
. . 4 . 1 . 5 3 8
3 4 . 8 . . 7 . .
. . 1 9 . . . . .
. 5 . . 7 3 . . .

се описује улазом:

1 9 3
2 1 9
2 4 3
2 6 2
3 1 6
3 2 8
3 8 5
4 1 1
4 6 5
4 9 9
5 1 5
5 4 7
5 8 6
5 9 2
6 3 4
6 5 1
6 7 5
6 8 3
6 9 8
7 1 3
7 2 4
7 4 8
7 7 7
8 3 1
8 4 9
9 2 5
9 5 7
9 6 3

Ако унесемо тај улаз када покренемо наш програм, добијамо исказну формулу sudoku1.cnf. Њеним решавањем помоћу SAT решавача добијамо задовољавајућу валуацију у којој су тачне следеће променљиве (све остале променљиве су нетачне):

  4  11  26  30  42  52  59  64  81
 90  91 103 114 125 128 138 151 158
169 175 183 191 203 216 224 231 235
244 259 267 279 281 293 301 314 318
329 342 344 352 367 373 384 390 404
408 420 432 436 442 458 461 473 484
492 503 505 518 531 534 547 553 560
575 579 590 601 607 613 630 632 645
650 662 673 683 687 699 703 720 724

Њиховим декодирањем добијамо следеће решење:

4 2 7 5 6 8 9 1 3
9 1 5 3 4 2 6 8 7
6 8 3 1 9 7 2 5 4
1 3 2 6 8 5 4 7 9
5 9 8 7 3 4 1 6 2
7 6 4 2 1 9 5 3 8
3 4 9 8 5 1 7 2 6
8 7 1 9 2 6 3 4 5
2 5 6 4 7 3 8 9 1

Вама препуштамо да сами напишете програм који ће приказати решење на основу решења добијеног од SAT решавача (програм треба да прочита решење, издвоји позитивне бројеве, декодира сваки од њих и да на основу тога формира и испише Судоку матрицу). Покушајте и да проширите формулу клаузулом која ће забранити добијање овог решења, да поново покренете SAT решавач и на тај начин да проверите да ли је ово решење јединствено.

Приметимо да је решавање Судоку загонетке свођењем на SAT веома декларативно. У програму смо само морали да опишемо (клаузулама) услове које решење мора да задовољава, а не и алгоритам како се до решења долази (SAT решавач је коришћењем веома ефикасних метода до тог решења дошао практично моментално).

Уместо да C# програм исписује излазну датотеку у формату DIMACS, која се онда шаље спољашњем SAT решавачу, могуће је SAT решавач покренути директно из C# програма преко API, које SAT решавачи обично нуде. Препуштамо ти да, на пример, инсталираш библиотеку Microsoft.Solver.Foundation (за то можеш користити NuGet), да проучиш како се она користи и да прилагодиш претходни програм тако да коришћењем ове библиотеке решава Судоку и приказује његово решење.

(Created using Swinx, RunestoneComponents and PetljaDoc)
© 2022 Petlja
A- A+