Задоволення обмежень: бектрекінг, поширення обмежень і AC-3 на судоку та мапі чеських країв
Змінні, домени та обмеження, граф обмежень, бектрекінг, евристики MRV і LCV, пряма перевірка, AC-3 і min-conflicts — на розфарбовуванні країв Чехії, судоку та восьми ферзях.
Серія Основи ШІ Частина 23 з 52
- Мапа штучного інтелекту: що є що і як усе це пов’язано
- Коротка історія штучного інтелекту: від Тюрінга до мовних моделей
- Дані, модель, тренування, інференс: основні поняття на одному прикладі
- Як читати математичний запис: символи, індекси, суми та квантори
- Множини й відношення: конструктор, з якого складаються означення в ШІ
- Функції й відображення: чому кожна модель у ШІ — це функція
- Логіка й доведення: правила, за якими міркують у ШІ
- Графи й дерева: структура, на якій тримаються пошук і нейронні мережі
- Лінійна алгебра для машинного навчання: вектори, матриці й чому ШІ передусім множить
- Похідні, градієнт і правило ланцюжка: як модель дізнається, у якому напрямку вчитися
- Градієнтний спуск: як навчається модель
- Що таке ймовірність: від кидка кубика до аксіом Колмогорова
- Умовна ймовірність і теорема Баєса: як змінити оцінку, коли дізнаєтеся щось нове
- Випадкова величина і функція розподілу: одна функція, яка каже про розподіл усе
- Функція щільності та функція маси ймовірності: чому неперервна величина має в точці ймовірність нуль
- Математичне сподівання, дисперсія і коваріація: де лежить розподіл, наскільки він широкий і що з чим пов’язано
- Огляд розподілів ймовірностей: Бернуллі, біноміальний, Пуассона, рівномірний, нормальний і експоненційний
- Закон великих чисел і центральна гранична теорема: чому середні поводяться так передбачувано
- Статистика й оцінювання параметрів: максимальна правдоподібність, MAP і довірчі інтервали
- Теорія інформації: ентропія, перехресна ентропія і KL-дивергенція — звідки беруться функції втрат
- Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями
- Ігри та суперник: мінімакс, альфа-бета відсікання і Monte Carlo tree search
- Задоволення обмежень: бектрекінг, поширення обмежень і AC-3 на судоку та мапі чеських країв
- Логіка й системи, засновані на знаннях: пряме і зворотне виведення, резолюція та експертні системи
- Планування: STRIPS, світ кубиків, евристики з релаксації та планування в AI-агентах
- Локальний пошук та імітація відпалу: комівояжер через обласні центри Чехії
- Генетичні алгоритми: популяція, схрещування, мутація і добір на рюкзаку для походу
- Еволюційні стратегії і генетичне програмування: від правила однієї п’ятої до закону Кеплера
- Оптимізація мурашиною колонією: як феромоновий слід знаходить короткий шлях
- Оптимізація роєм частинок: інерція, власний досвід і досвід рою
- Нейроеволюція: еволюція ваг і форми нейронних мереж та алгоритм NEAT
- Нечіткі множини й функції належності: як записати «трохи тепло»
- Нечітка логіка та ймовірність: чому «трохи тепло» не означає «імовірно тепло»
- Нечітка логіка: t-норми, імплікації та правила ЯКЩО–ТО
- Нечітке керування: Мамдані та Суґено від вентилятора до маятника на візку
- Нечітка кластеризація: алгоритм fuzzy c-means і точки між кластерами
- Нейро-нечіткі системи (ANFIS): нечіткі правила, які навчаються з даних
- Марковські ланцюги: матриця переходів, стаціонарний розподіл і PageRank
- Приховані марковські моделі: алгоритм Вітербі й нечесне казино
- Байєсівські мережі: чому трава мокра і що з цього випливає для дощувача
- Фільтр Калмана: як за неточними вимірюваннями оцінити, де авто і як швидко воно їде
- Види машинного навчання: з учителем, без учителя, з частковим залученням учителя та інші
- Промпти, контекст і RAG: як дати моделі правильну інформацію
- Варіаційні автокодувальники (VAE): нові зображення з неперервного латентного простору
- Генеративні змагальні мережі (GAN): фальшувальник проти детектива
- Дифузійні моделі: як із чистого шуму постає зображення
- Марковські процеси ухвалення рішень: стани, дії, винагороди й рівняння Беллмана
- Q-навчання і DQN: як навчитися цінності дій — від таблиці до нейронної мережі
- Policy gradient і PPO: навчання безпосередньо політики
- Як працює ШІ-агент: мовна модель у циклі з інструментами
- Програмування з AI-агентами: завдання, тести й перевірка
- Оцінювання і безпека систем ШІ: як вимірювати і як стежити
Повна програма серії Готово: 52 з 83 тем
- Як записати розклад, судоку чи розфарбовування мапи як змінні, домени та обмеження.
- Як бектрекінг з евристиками та прямою перевіркою скорочує пошук від мільйонів кроків до десятків.
- Як AC-3 поширює викреслювання графом обмежень і чому min-conflicts впорається навіть з мільйоном ферзів.
Що варто знати заздалегідь: Пошук у просторі станів: BFS, DFS, UCS і A* з анімаціями, Графи й дерева: структура, на якій тримаються пошук і нейронні мережі
Розклад уроків, у якому жоден учитель не веде два класи водночас. Судоку. Розміщення компонентів на платі, зміни в лікарні, призначення літаків до виходів на посадку. Усі ці задачі мають спільне те, що ми шукаємо не шлях, а присвоєння: кожній змінній дати значення так, щоб усі правила виконувалися водночас. Таку задачу називають задачею задоволення обмежень, і на відміну від загального пошуку вона має одну велику перевагу, — правила видно, тож алгоритм може з них міркувати, а не лише пробувати.
Ця частина продовжує частини «Пошук у просторі станів» і «Графи й дерева».
1. Що таке задача задоволення обмежень
Означення — CSP
Задача задоволення обмежень (constraint satisfaction problem, CSP) — це трійка (X, D, C), де
- X = {X1, …, Xn} — множина змінних,
- D = {D1, …, Dn}, де Di — домен, тобто множина значень, яких може набувати Xi,
- C — множина обмежень. Кожне обмеження — це пара (область, відношення): область — це список змінних, відношення каже, які комбінації їхніх значень дозволені.
Обмеження з двома змінними в області — бінарне, наприклад X1 ≠ X2.
Змінні — це питання, на які ви шукаєте відповідь («якого кольору буде цей край?»). Домен — можливі відповіді («червоний, зелений, синій»). Обмеження — правила, яких відповіді мусять дотримуватися («сусідні краї не можуть мати однаковий колір»).
Означення — присвоєння і розв’язок
Присвоєння дає значення з домену деяким змінним. Воно узгоджене, коли не порушує жодного обмеження, і повне, коли значення має кожна змінна.
Розв’язок CSP — це повне узгоджене присвоєння.
Незавершене судоку, у якому поки що ніщо не конфліктує, — це узгоджене, але неповне присвоєння. Заповнене судоку, у якому ніщо не конфліктує, — це розв’язок.
Чому б не розглядати це як звичайний пошук? Можна й так — стан є незавершеним присвоєнням, дія — «дай наступній змінній значення». Але пошук дивився б на стан як на чорну скриньку. CSP у скриньку бачить: знає, які правила діють і яких змінних вони стосуються. На цьому тримаються всі прийоми в цій частині.
2. Граф обмежень: розфарбовування мапи країв
Класична задача: розфарбувати мапу так, щоб жодні дві сусідні держави не мали однакового кольору. Візьмімо чотирнадцять країв Чехії. Змінні — це краї, домен — набір кольорів, а для кожного спільного кордону є обмеження «різні кольори».
Означення — граф обмежень
Граф обмежень бінарного CSP має вершинами змінні, а ребро між Xi і Xj — тоді й лише тоді, коли їх пов’язує якесь обмеження.
Кожен край — це кружечок, а лінія між двома кружечками означає «ці двоє сусідять, вони не можуть мати однаковий колір». Від мапи лишається тільки те, що важливо: хто з ким сусідить.
З чотирма кольорами все йде гладко. З трьома не виходить зовсім — і граф покаже чому. Середньочеський край сусідить із Прагою та із сімома краями, які утворюють навколо нього замкнене коло. Коло з непарною кількістю вершин саме по собі потребує трьох кольорів (два мусили б регулярно чергуватися, а за непарної кількості це не виходить). Середньочеський край сусідить з усіма сімома, тож для нього мусить залишитися четвертий колір:
Те, що чотирьох кольорів досить для будь-якої мапи, — знаменита теорема про чотири кольори. Довели її Аппель і Гакен 1976 року — як першу велику математичну теорему за допомогою комп’ютера, який перевірив майже дві тисячі випадків.
3. Бектрекінг
Означення — бектрекінг
Бектрекінг — це пошук у глибину, який на кожному кроці присвоює значення одній наступній змінній. Значення, що порушує обмеження з уже присвоєними змінними, він навіть не пробує. Коли для змінної не залишається жодного значення, він повертається до попередньої змінної і пробує її наступне значення.
У найгіршому випадку він переглядає O(dn) присвоєнь, де d — розмір домену, а n — кількість змінних. Загальна задача CSP NP-складна, тож у найгіршому випадку краще ми не вміємо.
Ви заповнюєте клітинку за клітинкою. Щойно щось не сходиться, ви не продовжуєте — повертаєтеся на крок, змінюєте останнє рішення і пробуєте далі. Як DFS із попередньої частини, лише з однією дрібницею, яка робить величезну різницю: порядок змінних на результат не впливає, тож досить вирішувати про одну змінну на поверх дерева.
def backtracking(assignment, csp):
if len(assignment) == len(csp.variables):
return assignment # усе має значення: розв’язок
p = select_variable(assignment, csp)
for h in csp.domains[p]:
if consistent(p, h, assignment, csp):
assignment[p] = h
result = backtracking(assignment, csp)
if result:
return result
del assignment[p] # глухий кут: повернися
return None
На краях із чотирма кольорами бектрекінг робить 14 присвоєнь, одне на край, і жодного разу не повертається. З трьома кольорами він мусить довести, що розв’язку не існує, — а це означає перепробувати все: 93 присвоєння, перш ніж здатися.
4. Яку змінну і яке значення
Кістяк бектрекінгу залишає відкритими два питання: яку змінну взяти наступною і в якому порядку пробувати її значення. Від відповідей залежить більше, ніж могло б здатися.
Означення — MRV, степінь, LCV
MRV (minimum remaining values): обери змінну з найменшою кількістю значень, які ще не конфліктують із присвоєнням.
Евристика степеня: за рівності обери змінну, яка має найбільше обмежень із ще не присвоєними змінними.
LCV (least constraining value): значення змінної пробуй, починаючи з того, яке викреслить сусідам найменше можливостей.
Змінну беріть найважчу: клітинку судоку, куди вміщується вже лише одне число, заповніть одразу — а якщо туди не підходить нічого, ви дізнаєтеся про це раніше, ніж збудуєте над помилкою ще двадцять поверхів. Значення, навпаки, беріть найпоступливіше, яке залишить іншим найбільше простору. Змінну обираєте так, щоб швидко зазнати невдачі, значення — так, щоб досягти успіху.
Для трьох кольорів на краях MRV разом із прямою перевіркою з наступного розділу зменшує кількість присвоєнь з 93 до 45 — починає з краю, якому залишилося найменше кольорів, і знаходить суперечність удвічі швидше.
5. Пряма перевірка
Означення — пряма перевірка
Після присвоєння Xi = v пройди кожну неприсвоєну змінну Xj, пов’язану з Xi обмеженням, і вилучи з Dj значення, несумісні з v. Якщо якийсь домен залишиться порожнім, негайно скасуй присвоєння.
Ви вписуєте в судоку сімку й одразу викреслюєте її з позначок усіх клітинок у тому самому рядку, стовпці та квадраті. Якби котрійсь клітинці через це нічого не залишилося, ви знаєте, що сімка була помилкою, — ще до того, як візьметеся за наступну клітинку.
Різницю видно на судоку нижче. Простий бектрекінг по клітинках рядок за рядком потребує понад 14 мільйонів присвоєнь. З прямою перевіркою їх 11 400. А коли до неї додамо MRV, кількість впаде до 81 — 30 заданих чисел і 51 доповнене, без жодного повернення.
6. Дугова узгодженість і AC-3
Пряма перевірка дивиться лише на крок уперед: на сусідів змінної, якій ми щойно присвоїли значення. Можна ретельніше — викреслювати й між змінними, які ще не мають значення.
Означення — дугова узгодженість
Змінна Xi дугово узгоджена щодо Xj, коли для кожного значення x ∈ Di існує y ∈ Dj таке, що пара (x, y) задовольняє обмеження між ними. CSP дугово узгоджена, коли це виконується для кожного орієнтованого ребра (Xi, Xj) графа обмежень.
Кожне значення мусить мати в сусіда принаймні одного «партнера», з яким може співіснувати. Значення без партнера мертве — у жодному розв’язку воно не з’явиться — і ви можете спокійно його викреслити.
Алгоритм — AC-3
- Додай до черги всі орієнтовані ребра (Xi, Xj).
- Візьми ребро з черги і вилучи з Di кожне значення, яке не має партнера в Dj.
- Якщо Di зменшився, поверни до черги всі ребра (Xk, Xi) для сусідів Xk ≠ Xj — їхні значення могли щойно втратити партнера. Якщо Di порожній, CSP не має розв’язку.
- Повторюй, доки черга не спорожніє.
Для c бінарних обмежень і доменів розміром щонайбільше d AC-3 триває O(c · d3).
Викреслювання поширюється, як хвиля: коли в клітинки меншає можливостей, мусять знову подивитися й її сусіди, бо в них міг зникнути партнер. Хвиля зупиняється, коли вже нічого викреслювати. Це називають поширенням обмежень.
from collections import deque
def ac3(csp):
queue = deque((xi, xj) for xi in csp.variables for xj in csp.neighbors[xi])
while queue:
xi, xj = queue.popleft()
if revise(csp, xi, xj):
if not csp.domains[xi]:
return False # порожній домен: розв’язку не існує
for xk in csp.neighbors[xi]:
if xk != xj:
queue.append((xk, xi))
return True
def revise(csp, xi, xj):
before = len(csp.domains[xi])
csp.domains[xi] = [x for x in csp.domains[xi]
if any(csp.allowed(xi, x, xj, y) for y in csp.domains[xj])]
return len(csp.domains[xi]) < before
7. Судоку: поширення обмежень на практиці
Судоку — це CSP як із підручника: 81 змінна, домени {1, …, 9} і обмеження «різні значення» для кожної пари клітинок в одному рядку, стовпці чи квадраті 3 × 3. Тож кожна клітинка має 20 сусідів у графі обмежень.
Запустімо на умові з 30 числами саме лише AC-3, без жодного присвоєння навмання. Він викреслить 408 значень — і кожній із 51 порожньої клітинки залишиться рівно одне. Судоку розв’язане чистим міркуванням:
З легкими судоку так і буває; у складніших поширення десь застрягне і настає черга пошуку — бектрекінгу, який після кожного присвоєння знову запускає поширення. Це поєднання (підтримування дугової узгодженості, MAC) — основа більшості справжніх розв’язувачів. Для порівняння на тій самій умові:
| Алгоритм | Присвоєння | Повернення |
|---|---|---|
| Бектрекінг рядок за рядком | 14 740 819 | 14 740 738 |
| + пряма перевірка | 11 400 | 11 319 |
| + пряма перевірка + MRV | 81 | 0 |
| AC-3 без пошуку | 0 | 0 |
Від чотирнадцяти мільйонів до нуля. Груба сила і розумне міркування розв’язують ту саму задачу — лише одна з них читає правила.
8. Локальний пошук: min-conflicts
Зовсім інша стратегія: не будувати розв’язок шматок за шматком, а почати з повного присвоєння, яке порушує купу обмежень, і поступово його виправляти. Це локальний пошук із частини про пошук, лише цільова функція — кількість порушених обмежень.
Алгоритм — min-conflicts
- Почни з випадкового повного присвоєння.
- Доки якесь обмеження не виконується: випадково обери змінну в конфлікті й дай їй значення, з яким вона порушує найменше обмежень (за рівності випадково).
Ви розставляєте все одразу, як випаде, а тоді виправляєте: берете одну річ, яка з чимось сперечається, і переставляєте її туди, де вона сперечається найменше. Ви не пам’ятаєте жодного дерева чи історії — лише поточний стан і те, що в ньому муляє.
Класичне випробування — задача n ферзів: розставити n шахових ферзів на шахівниці n × n так, щоб жодні два не загрожували один одному. Змінна — це рядок ферзя в кожному стовпці. Для восьми ферзів наш запуск почався із шести пар, що загрожують одна одній, і закінчився після 8 кроків:
Зверніть увагу на середні кроки: кількість конфліктів чотири кроки тримається на одиниці — алгоритм переставляє ферзів «убік» по плато, доки не знайде хід, який усуне суперечність. Дивує, як добре це масштабується: мільйон ферзів min-conflicts розв’язує в середньому приблизно за п’ятдесят кроків. Тим самим способом планували спостереження космічного телескопа «Габбл».
9. Обмеження на практиці
- Розклади та планування. Шкільні розклади, зміни, розподіл аудиторій і виробничі плани задають як змінні, домени та обмеження. Розв’язувачі на кшталт Google OR-Tools поєднують поширення обмежень, пошук і локальне вдосконалення.
- SAT-розв’язувачі розв’язують CSP, у яких кожна змінна — істина/хиба. Вони перевіряють проєкти чипів, шукають помилки в програмах, а менеджери пакетів (наприклад, у Linux) складають із їхньою допомогою версії бібліотек, які працюють разом.
- Компілятори розподіляють змінні програми по регістрах процесора розфарбовуванням графа: дві змінні, що живуть одночасно, не можуть ділити регістр — точнісінько як сусідні краї колір.
- Конфігуратори в інтернет-магазинах і на виробництві стежать, щоб вибрані компоненти пасували одне до одного.
- ШІ-агенти та мовні моделі у CSP часто не блищать: для судоку чи розкладу надійніше підходить розв’язувач. Тому добрий агент не обмірковує задачу «в голові» — він переводить її у змінні та обмеження і викликає для неї інструмент.
10. Підсумок
- CSP — це трійка (X, D, C): змінні, домени та обмеження. Розв’язок — повне присвоєння, яке не порушує жодного обмеження.
- Бектрекінг присвоює змінні одну за одною і повертається з глухих кутів; у найгіршому випадку він експоненційний.
- Евристики MRV і LCV обирають найважчу змінну та найпоступливіше значення; пряма перевірка виявляє глухий кут на крок раніше.
- AC-3 поширює викреслювання графом обмежень, доки кожне значення не матиме партнера; легке судоку він розв’язує взагалі без пошуку.
- Min-conflicts виправляє повне присвоєння і впорається навіть з величезними задачами, як-от мільйон ферзів.
Хочете спробувати самі? Попросіть ШІ-агента написати розв’язувач судоку з бектрекінгом, а тоді додавайте до нього пряму перевірку, MRV і AC-3 — і рахуйте, наскільки менше стане присвоєнь.
Маєте іншу думку? Напишіть на support@coden-app.com.