Перейти до основного вмісту

2-SAT

SAT (задача про виконуваність булевих формул, Boolean satisfiability problem) — це задача про присвоєння булевих значень змінним так, щоб задана булева формула була істинною. Зазвичай булева формула задається у КНФ (кон'юнктивній нормальній формі) — кон'юнкції кількох диз'юнктів, де кожен диз'юнкт є диз'юнкцією літералів (змінних або їхніх заперечень). 2-SAT (2-виконуваність) — це обмеження задачі SAT, у якому кожен диз'юнкт має рівно два літерали. Ось приклад такої задачі 2-SAT. Знайти присвоєння a,b,ca, b, c таке, щоб наведена формула була істинною:

(a¬b)(¬ab)(¬a¬b)(a¬c)(a \lor \lnot b) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b) \land (a \lor \lnot c)

SAT є NP-повною задачею, і для неї невідомо жодного ефективного розв'язку. Однак 2-SAT можна розв'язати ефективно за O(n+m)O(n + m), де nn — кількість змінних, а mm — кількість диз'юнктів.

Коли підходить цей алгоритм?
  • Задачу можна звести до набору булевих обмежень, де кожне обмеження стосується рівно двох літералів (диз'юнкт виду aba \lor b або імплікація «з xx випливає yy»)?
  • Потрібно лише перевірити сумісність і за потреби побудувати одне допустиме присвоєння (а не порахувати всі чи знайти оптимальне)?
  • Алгоритм спирається на пошук компонент сильної зв'язності графа імплікацій — ви знайомі з цією технікою?

Алгоритм:

Спершу нам потрібно перетворити задачу до іншого вигляду — так званої імплікативної нормальної форми. Зауважимо, що вираз aba \lor b еквівалентний ¬ab¬ba\lnot a \Rightarrow b \land \lnot b \Rightarrow a (якщо одна з двох змінних хибна, то інша має бути істинною).

Тепер побудуємо орієнтований граф цих імплікацій: для кожної змінної xx буде дві вершини vxv_x і v¬xv_{\lnot x}. Ребра відповідатимуть імплікаціям.

Розгляньмо приклад у формі 2-КНФ:

(a¬b)(¬ab)(¬a¬b)(a¬c)(a \lor \lnot b) \land (\lnot a \lor b) \land (\lnot a \lor \lnot b) \land (a \lor \lnot c)

Орієнтований граф міститиме такі вершини та ребра:

¬a¬baba¬b¬a¬cba¬b¬ab¬aca\begin{array}{cccc} \lnot a \Rightarrow \lnot b & a \Rightarrow b & a \Rightarrow \lnot b & \lnot a \Rightarrow \lnot c\\ b \Rightarrow a & \lnot b \Rightarrow \lnot a & b \Rightarrow \lnot a & c \Rightarrow a \end{array}

Граф імплікацій можна побачити на наступному зображенні:

Implication Graph of 2-SAT example

Варто звернути увагу на властивість графа імплікацій: якщо існує ребро aba \Rightarrow b, то також існує ребро ¬b¬a\lnot b \Rightarrow \lnot a.

Також зауважимо, що якщо xx досяжна з ¬x\lnot x, а ¬x\lnot x досяжна з xx, то задача не має розв'язку. Яке б значення ми не обрали для змінної xx, воно завжди призведе до суперечності: якщо xx буде присвоєно true\text{true}, то імплікація каже нам, що ¬x\lnot x також має бути true\text{true}, і навпаки. Виявляється, що ця умова не лише необхідна, а й достатня. Ми доведемо це у кількох абзацах нижче. Спершу пригадаємо: якщо одна вершина досяжна з другої, а друга досяжна з першої, то ці дві вершини належать до однієї компоненти сильної зв'язності. Тому критерій існування розв'язку можна сформулювати так:

Щоб ця задача 2-SAT мала розв'язок, необхідно й достатньо, щоб для будь-якої змінної xx вершини xx та ¬x\lnot x належали до різних компонент сильної зв'язності графа імплікацій.

Цей критерій можна перевірити за O(n+m)O(n + m), знайшовши всі компоненти сильної зв'язності.

На наступному зображенні показано всі компоненти сильної зв'язності для прикладу. Як легко перевірити, жодна з чотирьох компонент не містить водночас вершини xx та її заперечення ¬x\lnot x, тому приклад має розв'язок. У наступних абзацах ми навчимося обчислювати коректне присвоєння, а наразі для демонстрації наведемо розв'язок a=falsea = \text{false}, b=falseb = \text{false}, c=falsec = \text{false}.

Strongly Connected Components of the 2-SAT example

Тепер побудуємо алгоритм для знаходження розв'язку задачі 2-SAT за припущення, що розв'язок існує.

Зауважимо, що, попри існування розв'язку, може статися так, що ¬x\lnot x досяжна з xx у графі імплікацій, або (але не одночасно) що xx досяжна з ¬x\lnot x. У цьому випадку вибір true\text{true} чи false\text{false} для xx призведе до суперечності, тоді як вибір іншого значення — ні. Навчимося обирати значення так, щоб не породжувати суперечності.

Відсортуймо компоненти сильної зв'язності в топологічному порядку (тобто comp[v]comp[u]\text{comp}[v] \le \text{comp}[u], якщо існує шлях з vv до uu), і нехай comp[v]\text{comp}[v] позначає індекс компоненти сильної зв'язності, до якої належить вершина vv. Тоді, якщо comp[x]<comp[¬x]\text{comp}[x] < \text{comp}[\lnot x], ми присвоюємо xx значення false\text{false}, інакше — true\text{true}.

Доведемо, що з таким присвоєнням змінних ми не приходимо до суперечності. Припустимо, що xx присвоєно true\text{true}. Інший випадок можна довести аналогічно.

Спершу доведемо, що вершина xx не може досягти вершини ¬x\lnot x. Оскільки ми присвоїли true\text{true}, має виконуватися, що індекс компоненти сильної зв'язності xx більший за індекс компоненти ¬x\lnot x. Це означає, що ¬x\lnot x розташована ліворуч від компоненти, яка містить xx, а пізніша вершина не може досягти першої.

По-друге, доведемо, що не існує змінної yy такої, щоб вершини yy та ¬y\lnot y обидві були досяжними з xx у графі імплікацій. Це спричинило б суперечність, бо x=truex = \text{true} означає, що y=truey = \text{true} та ¬y=true\lnot y = \text{true}. Доведемо це від супротивного. Припустимо, що yy та ¬y\lnot y обидві досяжні з xx; тоді за властивістю графа імплікацій ¬x\lnot x досяжна як з yy, так і з ¬y\lnot y. За транзитивністю звідси випливає, що ¬x\lnot x досяжна з xx, що суперечить припущенню.

Отже, ми побудували алгоритм, який знаходить необхідні значення змінних за припущення, що для будь-якої змінної xx вершини xx та ¬x\lnot x належать до різних компонент сильної зв'язності. Вище ми показали коректність цього алгоритму. Як наслідок, ми водночас довели наведений вище критерій існування розв'язку.

Реалізація:

Тепер ми можемо реалізувати весь алгоритм. Спершу будуємо граф імплікацій і знаходимо всі компоненти сильної зв'язності. Це можна зробити за допомогою алгоритму Косараю за O(n+m)O(n + m). Під час другого обходу графа алгоритм Косараю відвідує компоненти сильної зв'язності в топологічному порядку, тому легко обчислити comp[v]\text{comp}[v] для кожної вершини vv.

Після цього ми можемо обрати присвоєння xx, порівнюючи comp[x]\text{comp}[x] та comp[¬x]\text{comp}[\lnot x]. Якщо comp[x]=comp[¬x]\text{comp}[x] = \text{comp}[\lnot x], ми повертаємо false\text{false}, щоб позначити, що не існує коректного присвоєння, яке задовольняє задачу 2-SAT.

Нижче наведено реалізацію розв'язку задачі 2-SAT для вже побудованого графа імплікацій adjadj та транспонованого графа adjadj^{\intercal} (у якому напрямок кожного ребра обернено). У графі вершини з індексами 2k2k та 2k+12k+1 — це дві вершини, що відповідають змінній kk, причому 2k+12k+1 відповідає запереченій змінній.

struct TwoSatSolver {
int n_vars;
int n_vertices;
vector<vector<int>> adj, adj_t;
vector<bool> used;
vector<int> order, comp;
vector<bool> assignment;

TwoSatSolver(int _n_vars) : n_vars(_n_vars), n_vertices(2 * n_vars), adj(n_vertices), adj_t(n_vertices), used(n_vertices), order(), comp(n_vertices, -1), assignment(n_vars) {
order.reserve(n_vertices);
}
void dfs1(int v) {
used[v] = true;
for (int u : adj[v]) {
if (!used[u])
dfs1(u);
}
order.push_back(v);
}

void dfs2(int v, int cl) {
comp[v] = cl;
for (int u : adj_t[v]) {
if (comp[u] == -1)
dfs2(u, cl);
}
}

bool solve_2SAT() {
order.clear();
used.assign(n_vertices, false);
for (int i = 0; i < n_vertices; ++i) {
if (!used[i])
dfs1(i);
}

comp.assign(n_vertices, -1);
for (int i = 0, j = 0; i < n_vertices; ++i) {
int v = order[n_vertices - i - 1];
if (comp[v] == -1)
dfs2(v, j++);
}

assignment.assign(n_vars, false);
for (int i = 0; i < n_vertices; i += 2) {
if (comp[i] == comp[i + 1])
return false;
assignment[i / 2] = comp[i] > comp[i + 1];
}
return true;
}

void add_disjunction(int a, bool na, int b, bool nb) {
// na та nb позначають, чи треба заперечувати a та b
a = 2 * a ^ na;
b = 2 * b ^ nb;
int neg_a = a ^ 1;
int neg_b = b ^ 1;
adj[neg_a].push_back(b);
adj[neg_b].push_back(a);
adj_t[b].push_back(neg_a);
adj_t[a].push_back(neg_b);
}

static void example_usage() {
TwoSatSolver solver(3); // a, b, c
solver.add_disjunction(0, false, 1, true); // a v not b
solver.add_disjunction(0, true, 1, true); // not a v not b
solver.add_disjunction(1, false, 2, false); // b v c
solver.add_disjunction(0, false, 0, false); // a v a
assert(solver.solve_2SAT() == true);
auto expected = vector<bool>{{true, false, true}};
assert(solver.assignment == expected);
}
};

Задачі для практики

Відеоматеріали