惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
The Hacker News
The Hacker News
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
aimingoo的专栏
aimingoo的专栏
S
Secure Thoughts
Last Week in AI
Last Week in AI
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
Engineering at Meta
Engineering at Meta
The Cloudflare Blog
G
Google Developers Blog
K
Kaspersky official blog
罗磊的独立博客
Scott Helme
Scott Helme
AWS News Blog
AWS News Blog
L
Lohrmann on Cybersecurity
T
The Blog of Author Tim Ferriss
Microsoft Azure Blog
Microsoft Azure Blog
T
Troy Hunt's Blog
博客园_首页
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tenable Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
The Exploit Database - CXSecurity.com
L
LangChain Blog
Blog — PlanetScale
Blog — PlanetScale
D
Docker
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
C
Cybersecurity and Infrastructure Security Agency CISA
Simon Willison's Weblog
Simon Willison's Weblog
SecWiki News
SecWiki News
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
S
SegmentFault 最新的问题
U
Unit 42
W
WeLiveSecurity
Security Latest
Security Latest
The Last Watchdog
The Last Watchdog
T
Threatpost
P
Privacy & Cybersecurity Law Blog
博客园 - Franky
Latest news
Latest news
WordPress大学
WordPress大学

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Когда всё решает «да» или «нет»: SAT-солверы и оптимизация в PySAT
Алексей Ложкинс · 2026-06-18 · via Все публикации подряд на Хабре

Средний

12 мин

8

В предыдущих работах я много писал про линейное и целочисленное программирование (PuLP, OR-Tools, pyomo), про задачи о назначениях, коммивояжёра, раскрой и генерацию столбцов. Сегодня сделаю шаг в соседнюю, но удивительно мощную область: удовлетворение булевой формулы (SAT). Это тот самый фундамент, на котором стоит вся теория NP-полноты, и одновременно крайне практичный инструмент, который иногда «уделывает» классические MIP-солверы на комбинаторных задачах.

Разберём, что такое SAT, какие алгоритмы заставляют современные солверы переваривать миллионы переменных, познакомимся с библиотекой PySAT и доступными через неё солверами, а в финале решим прикладную оптимизационную задачу с помощью MaxSAT.

Материал будет полезен специалистам по математической оптимизации и ML-инженерам, которым нужен ещё один инструмент в портфеле. А управленцы и владельцы процессов, возможно, узнают в примерах свою боль: ручное составление расписаний, конфигурирование заказов, распределение ограниченного ресурса - всё это аккуратно ложится на SAT и считается за секунды.

1. Задача SAT: о чём вообще речь

Задача о выполнимости булевой формулы (Boolean satisfiability problem, SAT) ставится предельно просто:

Дана формула логики высказываний. Существует ли набор значений переменных (True/False), при котором формула становится истинной?

Если такой набор есть, тогда формула выполнима (SAT), и солвер обязан вернуть конкретные значения переменных. Если набора нет ни одного, то формула невыполнима (UNSAT).

Историческое значение задачи трудно переоценить: именно SAT в теореме Кука-Левина (1971) стал первой задачей, для которой доказали NP-полноту. Грубо говоря, любую задачу из класса NP можно за полиномиальное время свести к SAT. Это и хорошая новость (универсальность), и плохая (в худшем случае экспонента).

Конъюнктивная нормальная форма (КНФ)

Почти все солверы работают с формулой в конъюнктивной нормальной форме (КНФ) это конъюнкция (логическое «И») клауз, где каждая клауза это дизъюнкция (логическое «ИЛИ») литералов, а литерал это переменная или её отрицание.

\underbrace{(x_1 \lor \lnot x_2)}_{\text{клауза}} \;\land\; (\lnot x_1 \lor x_3) \;\land\; (\lnot x_2 \lor \lnot x_3)

Структура формулы в КНФ: конъюнкция клауз, каждая клауза это дизъюнкция литералов

Структура формулы в КНФ: конъюнкция клауз, каждая клауза это дизъюнкция литералов

В программистской записи переменные нумеруют целыми числами, а отрицание - это минус. Та же формула:

[[1, -2], [-1, 3], [-2, -3]]

Клауза выполнена, если истинен хотя бы один её литерал. Формула выполнена, если выполнены все клаузы. Любую булеву формулу можно привести к КНФ (например, преобразованием Цейтина), поэтому ограничение на КНФ не сужает класс решаемых задач.

Именно отсюда растёт практическая ценность SAT для нас, «оптимизаторов»: множество дискретных ограничений естественно выражается клаузами, а булевы переменные - это те же 0/1-переменные, что и в целочисленном программировании.

И это ровно язык бизнес-решений «да/нет»: поставить сотрудника на смену или нет, совместима ли опция в комплектации, разрешён ли этот временной слот. Любое такое правило - это клауза, а вопрос «складывается ли план при всех правилах сразу» - это и есть SAT.

2. Основные алгоритмы

2.1. DPLL: каркас, на котором всё держится

Алгоритм Дэвиса-Патнема-Логемана-Лавленда (DPLL, 1962) - это поиск с возвратом (backtracking), усиленный двумя приёмами, которые радикально срезают дерево перебора:

  • Распространение единичных клауз (unit propagation). Если в клаузе остался единственный неопределённый литерал, а остальные уже ложны, то этот литерал обязан быть истинным. Присваиваем и каскадно проверяем, не стали ли единичными другие клаузы.

  • Чистые литералы (pure literal elimination). Если переменная встречается во всей формуле только в одной «полярности» (всегда без отрицания или всегда с отрицанием), её можно сразу зафиксировать так, чтобы удовлетворить все содержащие её клаузы.

Если приёмы исчерпаны, а решение не найдено, то алгоритм ветвится: выбирает переменную, пробует одно значение, рекурсивно спускается, при конфликте откатывается и пробует другое.

Блок-схема алгоритма DPLL

Блок-схема алгоритма DPLL

Псевдокод:

function DPLL(formula):
    formula = unit_propagate(formula)
    formula = pure_literal_assign(formula)
    if formula пуста:            
      return SAT
    if есть пустая клауза:       
      return UNSAT   # конфликт
    x = выбрать переменную
    return DPLL(formula ∧ x) or DPLL(formula ∧ ¬x)

2.2. CDCL: реально работает на промышленных задачах

Современные солверы представляют собой не «голый» DPLL, а CDCL (Conflict-Driven Clause Learning). Главная идея: каждый конфликт это не просто повод откатиться, а информация, из которой можно «выучить» новую клаузу и больше никогда не наступать на те же грабли.

Ключевые механизмы:

  • Анализ конфликта и обучение клауз (clause learning). При конфликте солвер по графу импликаций находит его причину и добавляет в формулу новую «выученную» клаузу, запрещающую повторение этой комбинации присваиваний.

  • Нехронологический откат (backjumping). Вместо отката на один шаг солвер прыгает сразу к тому уровню принятия решений, который действительно вызвал конфликт.

  • Two-watched literals. Хитрая структура данных: в каждой клаузе «следят» только за двумя литералами, поэтому unit propagation обходится дёшево даже на миллионах клауз.

  • VSIDS (Variable State Independent Decaying Sum) - эвристика выбора переменной для ветвления: приоритет получают переменные, часто участвующие в свежих конфликтах.

  • Рестарты. Периодический сброс дерева поиска (с сохранением выученных клауз) помогает выбраться из «невезучих» областей.

Блок-схема алгоритма CDCL: цикл «распространение - конфликт - обучение клаузы - откат»

Блок-схема алгоритма CDCL: цикл «распространение - конфликт - обучение клаузы - откат»

Именно связка «clause learning + watched literals + VSIDS + рестарты» превратила SAT из теоретической страшилки в инструмент, который на практике решает задачи с сотнями тысяч переменных за секунды.

2.3. Локальный поиск

Отдельная ветвь - стохастический локальный поиск (GSAT, WalkSAT): стартуем со случайного выбора значения переменной и итеративно переворачиваем значения переменных так, чтобы уменьшать число невыполненных клауз. Такие методы неполны (не доказывают UNSAT), но иногда очень быстро находят решение для выполнимых формул. На практике для бизнес-задач почти всегда используется полный CDCL. Предсказуемость важнее.

3. От разрешимости к оптимизации: MaxSAT

«Классический» SAT отвечает только «да/нет». Но нас, как специалистов по оптимизации, интересует лучшее решение, а не любое допустимое. Здесь на сцену выходит MaxSAT.

  • MaxSAT: найти значения переменных, максимизирующее число выполненных клауз.

  • Weighted Partial MaxSAT - самый практичный вариант. Клаузы делятся на два типа:

    • жёсткие (hard) - обязаны быть выполнены (это наши ограничения модели);

    • мягкие (soft) - каждая имеет вес; солвер старается выполнить как можно более «дорогой» их набор.

Солвер минимизирует суммарный вес нарушенных мягких клауз, эту величину принято называть cost. По сути это уже полноценная целевая функция, и аналогия с MIP становится прямой: жёсткие клаузы ≈ ограничения, веса мягких клауз ≈ коэффициенты целевой функции.

Формат хранения такой модели: WCNF (взвешенная КНФ).

Ограничения мощности (cardinality) и псевдобулевы ограничения

Отдельная боль при кодировании в SAT это ограничения вида «выбрать не более k из набора» или «ровно один». Наивное «не более одного» через попарные запреты даёт O(n^2) клауз. Существуют экономные кодировки:

  • sequential counter / totalizer для cardinality-ограничений (\sum x_i \le k) имеет линейный или O(n \log n) размер;

  • псевдобулевы ограничения (pseudo-Boolean, \sum w_i x_i \le K) используется для взвешенных сумм.

В PySAT всё это уже реализовано (модули pysat.card и pysat.pb), и кодировать вручную не придётся.

4. PySAT и солверы

PySAT это Python-обвязка вокруг целого зоопарка состязательных SAT-солверов плюс набор утилит для кодирования. Под капотом доступны, в частности: Glucose, MiniSat, CaDiCaL, Lingeling, MapleSAT, MergeSat, MiniCard и другие. Сменить решатель можно через замену аргумента.

Основные модули, которые нам понадобятся:

Модуль

Назначение

pysat.solvers

Интерфейсы к солверам (Glucose3, Cadical153, Minisat22, обёртка Solver)

pysat.formula

Контейнеры CNF, WCNF и менеджер переменных IDPool

pysat.card

Кодировки cardinality-ограничений (CardEnc, EncType)

pysat.pb

Псевдобулевы ограничения

pysat.examples.rc2

RC2 MaxSAT-солвер

Возможности MaxSAT расширяют спектор применения SAT на задачи оптимизации. Здесь уже две категории клауз: жесткие (hard) и мягкие (soft). Такое расслоение по типам клауз позволяет некоторые из них невыполнять (мягкие) с определенным весом, а задача сводится к минимизации взвешенных нарушений. Об этом чуть подробнее в разделе 6.

«Hello, SAT»

Решим формулу из раздела 1:

# Установка
!pip install python-sat[pblib,aiger]

from pysat.solvers import Solver

# (x1 ∨ x2) ∧ (¬x1 ∨ x3) ∧ (¬x2 ∨ ¬x3)
clauses = [[1, 2], [-1, 3], [-2, -3]]

with Solver(name='glucose3', bootstrap_with=clauses) as s:
    if s.solve():
        print('SAT:', s.get_model())   # напр. [1, -2, 3]  ->  x1=True, x2=False, x3=True
    else:
        print('UNSAT')

Менеджер переменных IDPool

В реальных задачах переменные удобнее адресовать не «магическими числами», а осмысленными ключами. IDPool отображает любой Python-объект в уникальный целочисленный id и обратно:

from pysat.formula import IDPool

vpool = IDPool()
v = vpool.id(('x', 2, 3))   # вернёт целое; повторный вызов вернёт то же число
print(vpool.obj(v))         # -> ('x', 2, 3)

Cardinality-ограничения «из коробки»

Кодировка «ровно один из списка литералов» масштабируемым способом. Для больших наборов вместо EncType.pairwise (даёт O(n^2) клауз) берут EncType.seqcounter или EncType.totalizer:

from pysat.card import CardEnc, EncType
from pysat.formula import CNF, IDPool

vpool = IDPool()
lits = [vpool.id(('x', c)) for c in range(5)]

cnf = CNF()
cnf.append(lits)                                          # хотя бы один
am1 = CardEnc.atmost(lits=lits, bound=1,
                     vpool=vpool, encoding=EncType.pairwise)
cnf.extend(am1.clauses)                                   # не более одного

5. Практика I: раскраска графа как задача SAT (разрешимость)

Начнём с decision-задачи, чтобы прочувствовать кодирование. Классика - раскраска графа (задачу упоминал в Типовых моделях). За этой абстракцией стоят вполне денежные сюжеты: распределение радиочастот, составление расписаний и экзаменов (вершины представляют предметы или смены, рёбра - «нельзя в один слот»), рассадка несовместимых процессов по серверам, регистровое распределение в компиляторах. По сути это вопрос «влезают ли все сущности в ограниченное число слотов без конфликтов».

Можно ли раскрасить вершины графа в k цветов так, чтобы концы любого ребра имели разные цвета?

Кодирование переменных (one-hot). Введём булевы переменные x_{v,c} - «вершина v покрашена в цвет c».

Ограничения:

  1. У каждой вершины ровно один цвет. Моделируем через 2 ограничения: «хотя бы один» (клауза-дизъюнкция) и «не более одного» (попарные запреты).

  2. Для каждого ребра (u,v) и каждого цвета c концы не могут делить цвет: (\lnot x_{u,c} \lor \lnot x_{v,c}).

Код реализации в PySat
from pysat.solvers import Glucose3
from pysat.formula import IDPool

def color_graph(edges, n_vertices, k_colors):
    vpool = IDPool()
    x = lambda v, c: vpool.id(('x', v, c))   # вершине v назначен цвет c

    solver = Glucose3()
    for v in range(n_vertices):
        solver.add_clause([x(v, c) for c in range(k_colors)])        # >= 1 цвет
        for c1 in range(k_colors):                                   # <= 1 цвет
            for c2 in range(c1 + 1, k_colors):
                solver.add_clause([-x(v, c1), -x(v, c2)])
    for (u, v) in edges:
        for c in range(k_colors):
            solver.add_clause([-x(u, c), -x(v, c)])                  # разные цвета

    if not solver.solve():
        solver.delete()
        return None

    model = set(solver.get_model())
    coloring = {v: c for v in range(n_vertices)
                     for c in range(k_colors) if x(v, c) in model}
    solver.delete()
    return coloring


# Цикл из 5 вершин C5 (нечётный цикл)
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]

print(color_graph(edges, 5, 2))   # -> None  (UNSAT)
print(color_graph(edges, 5, 3))   # -> {0: 0, 1: 1, 2: 0, 3: 1, 4: 2}  (SAT)

Результат поучителен: нечётный цикл нельзя раскрасить в 2 цвета (солвер честно возвращает UNSAT), а в 3 цвета можно. Так, перебирая k от меньшего к большему и ловя первый SAT, мы по сути находим хроматическое число, а это уже мостик к оптимизации через серию вызовов SAT.

6. Практика II: распределение каналов как Weighted Partial MaxSAT (оптимизация)

Теперь полноценная оптимизация. Возьмём прикладной сюжет, близкий к сетевым задачам из моих прошлых статей про размещение хабов.

Есть набор передатчиков. Близкие передатчики интерферируют, если работают на одном канале, и каждой такой паре сопоставлен вес (величина помех). Доступно всего k каналов, но их может не хватить, чтобы развести всех. Нужно назначить каждому передатчику канал так, чтобы минимизировать суммарный вес интерференции.

Это, по сути, взвешенная раскраска при недостатке цветов. Прямой раскраской (decision) задачу не решить, так как допустимого решения может не быть вовсе. Зато она идеально ложится на Weighted Partial MaxSAT:

  • Жёсткие клаузы: каждый передатчик получает ровно один канал (структурное ограничение, нарушать нельзя).

  • Мягкие клаузы: для каждой интерферирующей пары (u,v) и каждого канала c добавляем (\lnot x_{u,c} \lor \lnot x_{v,c}) с весом, равным весу пары. Если пара всё же оказалась на общем канале, ровно одна такая клауза нарушится и даст в cost штраф этой пары.

Отдельно стоит представить RC2 (название от Relaxable Cardinality Constraints) - точный MaxSAT-солвер из PySAT и победитель основного трека MaxSAT Evaluation 2018. Он не перебирает решения наугад, а работает «снизу вверх»: многократно зовёт обычный SAT-солвер и получает на невыполнимом запросе конфликтующее ядро unsat core (набор мягких клауз), которые нельзя удовлетворить разом. Далее «отпускает» ровно столько из них, сколько неизбежно, поднимая нижнюю оценку cost. Как только запрос становится выполнимым, накопленный cost уже доказанно минимален, то есть первое же допустимое решение оказывается оптимальным.

Минимизируя cost, RC2 находит именно глобальный минимум суммарной интерференции.

Код реализации в PySat
from pysat.formula import WCNF, IDPool
from pysat.examples.rc2 import RC2

def assign_channels(n_tx, k_ch, interference):
    vpool = IDPool()
    x = lambda t, c: vpool.id(('x', t, c))   # передатчику t назначен канал c

    wcnf = WCNF()
    # HARD: ровно один канал на передатчик
    for t in range(n_tx):
        wcnf.append([x(t, c) for c in range(k_ch)])              # >= 1
        for c1 in range(k_ch):
            for c2 in range(c1 + 1, k_ch):
                wcnf.append([-x(t, c1), -x(t, c2)])              # <= 1
    # SOFT: интерферирующая пара не должна делить канал; штраф = вес пары
    for (u, v, w) in interference:
        for c in range(k_ch):
            wcnf.append([-x(u, c), -x(v, c)], weight=w)

    with RC2(wcnf) as rc2:
        model = set(rc2.compute())
        cost = rc2.cost

    assignment = {t: c for t in range(n_tx)
                       for c in range(k_ch) if x(t, c) in model}
    return assignment, "cost: " + str(cost)


# 6 передатчиков; две 'тесные' группы 0-1-2 и 3-4-5 связаны слабым ребром (1,4)
interference = [
    (0, 1, 4), (1, 2, 3), (2, 0, 2),     # треугольник 0-1-2
    (2, 3, 5),                            # перемычка
    (3, 4, 4), (4, 5, 3), (5, 3, 2)     # треугольник 3-4-5
]

print(assign_channels(6, 2, interference))   # -> ({...}, 4)   при 2 каналах
print(assign_channels(6, 3, interference))   # -> ({...}, 0)   при 3 каналах

Что получаем. При k = 2 минимальная суммарная интерференция равна 4: два нечётных треугольника физически невозможно полностью «развести» двумя каналами, и оптимум жертвует двумя самыми дешёвыми конфликтами (веса 2 + 2). При k = 3 каналов уже хватает и cost = 0, интерференции нет вовсе.

Это типичный управленческий вывод оптимизационной модели: расчёт показывает, что добавление одного третьего канала полностью снимает помехи, а значит можно осмысленно сравнить стоимость лицензии на третий канал с ценой остаточной интерференции при двух.

7. Чем SAT и MaxSAT полезны бизнесу

За академической формулировкой скрывается очень «приземлённый» класс задач. Где это регулярно окупается:

  • Расписания и графики смен. Жёсткие правила (трудовой кодекс, квалификации, недопустимые сочетания обязанностей) моделируются с помощью hard-клауз; пожелания сотрудников и приоритеты через soft-клаузы с весами. Тот же каркас, что в примере с каналами.

  • Конфигурирование продуктов и комплектаций. «Можно ли вообще собрать такой заказ из совместимых опций?» — чистый SAT. «А если нельзя, какая ближайшая допустимая комплектация?» — MaxSAT.

  • Распределение ограниченного ресурса без конфликтов: частоты, слоты, причалы, дорожки, лицензии, переговорные, в общем, как в задаче из раздела 6.

  • Назначение исполнителей на заявки и маршруты с правилами совместимости и приоритетами.

А вот ради чего всё затевалось! Ниже привел четыре эффекта, которые видно не в логах солвера, а в бюджете и в нервах того, кто отвечает за план:

  • Жёсткие правила соблюдаются гарантированно, а не «на глаз». Ручное планирование на масштабе ошибается; модель либо выдаёт корректный план, либо честно сообщает, что его не существует.

  • Измеримый оптимум вместо просто «приемлемого» решения. На больших объёмах разница между «оптимально» и «нормально» может быть значимо положительной.

  • Быстрые what-if расчёты. Как в примере с третьим каналом: модель за секунды отвечает на «а что, если добавить смену/станок/канал?» и переводит вопрос на язык затрат.

  • Объяснение невозможности. Если требования противоречивы, солвер находит минимальное конфликтующее ядро (unsat core), т.е. какие именно правила несовместимы. Спор «почему расписание не складывается» превращается в конкретный список ограничений, которые надо ослабить.

Честная оговорка: SAT — не «серебряная пуля». Польза появляется там, где решения дискретны, правил много и они переплетены, а ручное планирование уже не справляется. Если задача про непрерывные объёмы и потоки — смотрите в сторону линейного и целочисленного программирования (об этом рассказываю в цикле публикаций проекта Make Optimization Simple).

8. Заметки на полях: SAT/MaxSAT или MIP?

Несколько практических наблюдений из опыта, чтобы понимать, когда тянуться за SAT-солвером, а когда оставаться в привычном MIP.

  • Размер кодировки решает. Попарная кодировка «ровно один» - это O(n^2) клауз; на сотнях значений она раздувает модель. Для масштаба берите seqcounter/totalizer из pysat.card. Хорошая модель в SAT - ровно та же дисциплина, о которой я писал в статье про эффективные мат. модели: лишние переменные и клаузы напрямую бьют по времени поиска.

  • SAT силён на «чисто комбинаторных» ограничениях - выполнимость, конфигурирование, верификация, головоломки, расписания с жёсткой логикой. Здесь CDCL с обучением клауз нередко обгоняет ветвление MIP.

  • MIP силён там, где много линейной арифметики над непрерывными величинами и плотная числовая целевая функция. Чистый SAT не умеет дробные переменные; для них либо дискретизация, либо гибрид (SMT, лениво добавляемые ограничения).

  • MaxSAT - это полноценная дискретная оптимизация. Если ваша задача целиком 0/1, а «арифметика» сводится к взвешенным суммам, связка WCNF + RC2 - серьёзный конкурент целочисленному солверу, и часто более быстрый.

  • Менять решатель - дёшево. В PySAT можно прогнать одну и ту же КНФ через несколько движков (glucose3, cadical153, minisat22) и выбрать лучший под ваш профиль задач, мини-benchmark пишется в десяток строк.

Заключение

SAT - это та самая «база», которую полезно держать в портфеле моделей рядом с линейным и целочисленным программированием. Сама задача проста до аскетизма (булевы переменные и клаузы), но за счёт CDCL-солверов и надстройки MaxSAT превращается в практичный инструмент дискретной оптимизации. А PySAT снимает почти весь порог входа: кодирование через IDPool и CardEnc, готовый MaxSAT-солвер RC2 и десяток сменных движков под капотом.

Мы прошли путь от определения и КНФ через DPLL и CDCL к рабочему коду: сначала проверили выполнимость (раскраска графа), затем решили настоящую оптимизационную задачу (распределение каналов) с жёсткими и мягкими взвешенными ограничениями.

Если материал зайдёт - в следующих частях «Make optimization simple» могу разобрать кодировку cardinality-ограничений «под капотом», сравнение SAT и CP-SAT из OR-Tools на одной и той же задаче расписаний или гибрид SMT для смешанных моделей.

А если в вашем процессе узнаётся одна из задач выше - напишите в комментариях, разберём постановку. По опыту, такая модель нередко окупается уже на первом пересчёте графика или загрузки ресурса.

Что почитать дальше

  • Biere, Heule, van Maaren, Walsh «Handbook of Satisfiability» (фундаментальный справочник).

  • Документация PySAT: pysathq.github.io

  • Marques-Silva, Lynce, Malik - обзорная глава про CDCL.

  • Описание состязательного MaxSAT-солвера RC2 (MaxSAT Evaluation).

Спасибо за внимание, буду рад вопросам и замечаниям в комментариях.