← Static tasks

E073. Задача 2-SAT

emaxx algorithm

#algorithm#emaxx#graph

Task

Источник: e-maxx.ru/algo, страница PDF 219.

Задача 2-SAT (2-satisfiability) - это задача распределения значений булевым переменным таким образом, чтобы они удовлетворяли всем наложенным ограничениям. Задачу 2-SAT можно представить в виде конъюнктивной нормальной формы, где в каждом выражении в скобках стоит ровно по две переменной; такая форма называется 2-CNF (2-conjunctive normal form). Например:

(a || c) && (a || !d) && (b || !d) && (b || !e) && (c || d)

Приложения

Алгоритм для решения 2-SAT может быть применим во всех задачах, где есть набор величин, каждая из которых может принимать 2 возможных значения, и есть связи между этими величинами:

● Расположение текстовых меток на карте или диаграмме.

Имеется в виду нахождение такого расположения меток, при котором никакие две не пересекаются. Стоит заметить, что в общем случае, когда каждая метка может занимать множество различных позиций, мы получаем задачу general satisfiability, которая является NP-полной. Однако, если ограничиться только двумя возможными позициями, то полученная задача будет задачей 2-SAT.

● Расположение рёбер при рисовании графа.

Аналогично предыдущему пункту, если ограничиться только двумя возможными способами провести ребро, то мы придём к 2-SAT.

● Составление расписания игр.

Имеется в виду такая система, когда каждая команда должна сыграть с каждой по одному разу, а требуется распределить игры по типу домашняя-выездная, с некоторыми наложенными ограничениями.

● и т.д.

Алгоритм

Сначала приведём задачу к другой форме - так называемой импликативной форме. Заметим, что выражение вида a || b эквивалентно !a => b или !b => a. Это можно воспринимать следующим образом: если есть выражение a || b, и нам необходимо добиться обращения его в true, то, если a=false, то необходимо b=true, и наоборот, если b=false, то необходимо a=true. Построим теперь так называемый граф импликаций: для каждой переменной в графе будет по две вершины, обозначим их через xi и !xi. Рёбра в графе будут соответствовать импликативным связям. Например, для 2-CNF формы:

(a || b) && (b || !c)

Граф импликаций будет содержать следующие рёбра (ориентированные):

!a => b

!b => a

!b => !c

c => b

Стоит обратить внимание на такое свойство графа импликаций, что если есть ребро a => b, то есть и ребро !b => !a. Теперь заметим, что если для какой-то переменной x выполняется, что из x достижимо !x, а из !x достижимо x, то задача решения не имеет. Действительно, какое бы значение для переменной x мы бы ни выбрали, мы всегда придём к противоречию - что должно быть выбрано и обратное ему значение. Оказывается, что это условие является не только достаточным, но и необходимым (доказательством этого факта будет описанный ниже алгоритм). Переформулируем данный критерий в терминах теории графов. Напомним, что если из одной вершины достижима другая, а из той вершины достижима первая, то эти две вершины находятся в одной сильно связной компоненте. Тогда мы можем сформулировать критерий существования решения следующим образом: Для того, чтобы данная задача 2-SAT имела решение, необходимо и достаточно, чтобы для любой переменной x вершины x и !x находились в разных компонентах сильной связности графа импликаций. Этот критерий можно проверить за время O (N + M) с помощью алгоритма поиска сильно связных компонент. Теперь построим собственно алгоритм нахождения решения задачи 2-SAT в предположении, что решение существует. Заметим, что, несмотря на то, что решение существует, для некоторых переменных может выполняться, что из x достижимо !x, или (но не одновременно), из !x достижимо x. В таком случае выбор одного из значений переменной x будет приводить к противоречию, в то время как выбор другого - не будет. Научимся выбирать из двух значений то, которое не приводит к возникновению противоречий. Сразу заметим, что, выбрав какое-либо значение, мы должны запустить из него обход в глубину/ширину и пометить все значения, которые следуют из него, т.е. достижимы в графе импликаций. Соответственно, для уже помеченных вершин никакого выбора между x и !x делать не нужно, для них значение уже выбрано и зафиксировано. Нижеописанное правило применяется только к непомеченным ещё вершинам. Утверждается следующее. Пусть comp[v] обозначает номер компоненты сильной связности, которой принадлежит вершина v, причём номера упорядочены в порядке топологической сортировки компонент сильной связности в графе компонентов (т.е. более ранним в порядке топологической сортировки соответствуют большие номера: если есть путь из v в w, то comp[v] <= comp[w]). Тогда, если comp[x] < comp[!x], то выбираем значение !x, иначе, т. е. если comp[x] > comp[!x], то выбираем x. Докажем, что при таком выборе значений мы не придём к противоречию. Пусть, для определённости, выбрана вершина x (случай, когда выбрана вершина !x, доказывается симметрично). Во-первых, докажем, что из x не достижимо !x. Действительно, так как номер компоненты сильной связности comp [x] больше номера компоненты comp[!x], то это означает, что компонента связности, содержащая x, расположена левее компоненты связности, содержащей !x, и из первой никак не может быть достижима последняя. Во-вторых, докажем, что никакая вершина y, достижимая из x, не является "плохой", т.е. неверно, что из y достижимо ! y. Докажем это от противного. Пусть из x достижимо y, а из y достижимо !y. Так как из x достижимо y, то, по свойству графа импликаций, из !y будет достижимо !x. Но, по предположению, из y достижимо !y. Тогда мы получаем, что из x достижимо !x, что противоречит условию, что и требовалось доказать. Итак, мы построили алгоритм, который находит искомые значения переменных в предположении, что для любой переменной x вершины x и !x находятся в разных компонентах сильной связности. Выше показали корректность этого алгоритма. Следовательно, мы одновременно доказали указанный выше критерий существования решения. Теперь мы можем собрать весь алгоритм воедино:

● Построим граф импликаций.

● Найдём в этом графе компоненты сильной связности за время O (N + M), пусть comp[v] - это номер компоненты

сильной связности, которой принадлежит вершина v.

● Проверим, что для каждой переменной x вершины x и !x лежат в разных компонентах, т.е. comp[x] ≠ comp[!x]. Если

это условие не выполняется, то вернуть "решение не существует".

● Если comp[x] > comp[!x], то переменной x выбираем значение true, иначе - false.

Реализация

Ниже приведена реализация решения задачи 2-SAT для уже построенного графа импликаций g и обратного ему графа gt (т.е. в котором направление каждого ребра изменено на противоположное). Программа выводит номера выбранных вершин, либо фразу "NO SOLUTION", если решения не существует.

int n;

vector < vector<int> > g, gt;

vector<bool> used;

vector<int> order, comp;

void dfs1 (int v) {

used[v] = true;

for (size_t i=0; i<g[v].size(); ++i) {
int to = g[v][i];
if (!used[to])

dfs1 (to);

}

order.push_back (v);

}

void dfs2 (int v, int cl) {

comp[v] = cl;

for (size_t i=0; i<gt[v].size(); ++i) {
int to = gt[v][i];
if (comp[to] == -1)

dfs2 (to, cl);

} }

int main() {

... чтение n, графа g, построение графа gt ...

used.assign (n, false);

for (int i=0; i<n; ++i)
if (!used[i])

dfs1 (i);

comp.assign (n, -1);

for (int i=0, j=0; i<n; ++i) {
int v = order[n-i-1];
if (comp[v] == -1)

dfs2 (v, j++);

}

for (int i=0; i<n; ++i)
if (comp[i] == comp[i^1]) {

puts ("NO SOLUTION");

return 0;

}

for (int i=0; i<n; ++i) {
int ans = comp[i] > comp[i^1] ? i : i^1;

printf ("%d ", ans);

} }

C# solution

auto-draft, review before submit
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    (a || c) && (a || !d) && (b || !d) && (b || !e) && (c || d)
    (a || b) && (b || !c)
    !a => b
    !b => a
    !b => !c
    c => b
    int n;
    vector < List<int> > g, gt;
    List<bool> used;
    List<int> order, comp;
    void dfs1 (int v) {
            used[v] = true;
            for (size_t i=0; i<g[v].size(); ++i) {
                    int to = g[v][i];
                    if (!used[to])
                            dfs1 (to);
            }
            order.push_back (v);
    }
    void dfs2 (int v, int cl) {
            comp[v] = cl;
            for (size_t i=0; i<gt[v].size(); ++i) {
                    int to = gt[v][i];
                    if (comp[to] == -1)
                            dfs2 (to, cl);
            }
    }
    int main() {
            ... чтение n, графа g, построение графа gt ...
            used.assign (n, false);
            for (int i=0; i<n; ++i)
                    if (!used[i])
                            dfs1 (i);
            comp.assign (n, -1);
            for (int i=0, j=0; i<n; ++i) {
                    int v = order[n-i-1];
                    if (comp[v] == -1)
                            dfs2 (v, j++);
            }
            for (int i=0; i<n; ++i)
                    if (comp[i] == comp[i^1]) {
                            puts ("NO SOLUTION");
                            return 0;
                    }
            for (int i=0; i<n; ++i) {
                    int ans = comp[i] > comp[i^1] ? i : i^1;
                    Console.Write ("%d ", ans);
            }
    }
}

C++ solution

matched/original
(a || c) && (a || !d) && (b || !d) && (b || !e) && (c || d)
(a || b) && (b || !c)
!a => b
!b => a
!b => !c
c => b
int n;
vector < vector<int> > g, gt;
vector<bool> used;
vector<int> order, comp;
void dfs1 (int v) {
        used[v] = true;
        for (size_t i=0; i<g[v].size(); ++i) {
                int to = g[v][i];
                if (!used[to])
                        dfs1 (to);
        }
        order.push_back (v);
}
void dfs2 (int v, int cl) {
        comp[v] = cl;
        for (size_t i=0; i<gt[v].size(); ++i) {
                int to = gt[v][i];
                if (comp[to] == -1)
                        dfs2 (to, cl);
        }
}
int main() {
        ... чтение n, графа g, построение графа gt ...
        used.assign (n, false);
        for (int i=0; i<n; ++i)
                if (!used[i])
                        dfs1 (i);
        comp.assign (n, -1);
        for (int i=0, j=0; i<n; ++i) {
                int v = order[n-i-1];
                if (comp[v] == -1)
                        dfs2 (v, j++);
        }
        for (int i=0; i<n; ++i)
                if (comp[i] == comp[i^1]) {
                        puts ("NO SOLUTION");
                        return 0;
                }
        for (int i=0; i<n; ++i) {
                int ans = comp[i] > comp[i^1] ? i : i^1;
                printf ("%d ", ans);
        }
}

Java solution

auto-draft, review before submit
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    (a || c) && (a || !d) && (b || !d) && (b || !e) && (c || d)
    (a || b) && (b || !c)
    !a => b
    !b => a
    !b => !c
    c => b
    int n;
    vector < ArrayList<Integer> > g, gt;
    ArrayList<Boolean> used;
    ArrayList<Integer> order, comp;
    void dfs1 (int v) {
            used[v] = true;
            for (size_t i=0; i<g[v].size(); ++i) {
                    int to = g[v][i];
                    if (!used[to])
                            dfs1 (to);
            }
            order.push_back (v);
    }
    void dfs2 (int v, int cl) {
            comp[v] = cl;
            for (size_t i=0; i<gt[v].size(); ++i) {
                    int to = gt[v][i];
                    if (comp[to] == -1)
                            dfs2 (to, cl);
            }
    }
    int main() {
            ... чтение n, графа g, построение графа gt ...
            used.assign (n, false);
            for (int i=0; i<n; ++i)
                    if (!used[i])
                            dfs1 (i);
            comp.assign (n, -1);
            for (int i=0, j=0; i<n; ++i) {
                    int v = order[n-i-1];
                    if (comp[v] == -1)
                            dfs2 (v, j++);
            }
            for (int i=0; i<n; ++i)
                    if (comp[i] == comp[i^1]) {
                            puts ("NO SOLUTION");
                            return 0;
                    }
            for (int i=0; i<n; ++i) {
                    int ans = comp[i] > comp[i^1] ? i : i^1;
                    System.out.print ("%d ", ans);
            }
    }
}

Explanation

Материал разбит как алгоритмическая задача: изучить постановку, понять асимптотику и реализовать алгоритм на выбранном языке.