Splňování omezujících podmínek: backtracking, propagace omezení a AC-3 na sudoku a mapě krajů
Proměnné, domény a podmínky, graf podmínek, backtracking, heuristiky MRV a LCV, dopředná kontrola, AC-3 a min-conflicts — na barvení krajů Česka, sudoku a osmi dámách.
Série Základy AI Díl 23 z 83
- Mapa umělé inteligence: co je co a jak to do sebe zapadá
- Stručná historie umělé inteligence: od Turinga po jazykové modely
- Data, model, trénink, inference: základní pojmy na jednom příkladu
- Jak číst matematický zápis: symboly, indexy, sumy a kvantifikátory
- Množiny a relace: stavebnice, ze které se skládají definice v AI
- Funkce a zobrazení: proč je každý model v AI funkce
- Logika a důkazy: pravidla, podle kterých se v AI uvažuje
- Grafy a stromy: struktura, na které stojí hledání i neuronové sítě
- Lineární algebra pro strojové učení: vektory, matice a proč AI hlavně násobí
- Derivace, gradient a řetízkové pravidlo: jak model zjistí, kterým směrem se učit
- Gradientní sestup: jak se model učí
- Co je pravděpodobnost: od hodu kostkou ke Kolmogorovovým axiomům
- Podmíněná pravděpodobnost a Bayesova věta: jak změnit odhad, když se dozvíte něco nového
- Náhodná veličina a distribuční funkce: jedna funkce, která o rozdělení řekne všechno
- Funkce hustoty a pravděpodobnostní funkce: proč má spojitá veličina v bodě pravděpodobnost nula
- Střední hodnota, rozptyl a kovariance: kde rozdělení leží, jak je široké a co s čím souvisí
- Přehled pravděpodobnostních rozdělení: Bernoulli, binomické, Poisson, rovnoměrné, normální a exponenciální
- Zákon velkých čísel a centrální limitní věta: proč se průměry chovají tak předvídatelně
- Statistika a odhady parametrů: maximální věrohodnost, MAP a intervaly spolehlivosti
- Teorie informace: entropie, křížová entropie a KL divergence — odkud se berou ztrátové funkce
- Prohledávání stavového prostoru: BFS, DFS, UCS a A* s animacemi
- Hry a protivník: minimax, alfa-beta ořezávání a Monte Carlo tree search
- Splňování omezujících podmínek: backtracking, propagace omezení a AC-3 na sudoku a mapě krajů
- Logika a znalostní systémy: dopředné a zpětné řetězení, rezoluce a expertní systémy
- Plánování: STRIPS, svět kostek, heuristiky z relaxace a plánování v AI agentech
- Lokální hledání a simulované žíhání: obchodní cestující přes krajská města
- Genetické algoritmy: populace, křížení, mutace a výběr na batohu na výlet
- Evoluční strategie a genetické programování: od pravidla jedné pětiny po Keplerův zákon
- Optimalizace mravenčí kolonií: jak feromonová stopa najde krátkou cestu
- Optimalizace hejnem částic: setrvačnost, vlastní zkušenost a zkušenost hejna
- Neuroevoluce: evoluce vah i tvaru neuronových sítí a algoritmus NEAT
- Fuzzy množiny a funkce příslušnosti: jak zapsat „trochu teplo“
- Fuzzy logika a pravděpodobnost: proč „trochu teplo“ neznamená „pravděpodobně teplo“
- Fuzzy logika: t-normy, implikace a pravidla KDYŽ–PAK
- Fuzzy řízení: Mamdani a Sugeno od ventilátoru po kyvadlo na vozíku
- Fuzzy shlukování: algoritmus fuzzy c-means a body mezi shluky
- Neuro-fuzzy systémy (ANFIS): fuzzy pravidla, která se učí z dat
- Markovovy řetězce: přechodová matice, stacionární rozdělení a PageRank
- Skryté Markovovy modely: Viterbiho algoritmus a nepoctivé kasino
- Bayesovské sítě: proč je tráva mokrá a co z toho plyne pro postřikovač
- Kalmanův filtr: jak z nepřesných měření odhadnout, kde auto je a jak rychle jede
- Druhy strojového učení: s učitelem, bez učitele, s částečným učitelem a další
- Přeučení a regularizace: proč model, který zná data nazpaměť, selže
- Vyhodnocení modelu: křížová validace, F1 a křivka ROC
- Shlukování a snížení dimenze: k-means, PCA a t-SNE
- Klasifikace a regrese: dvě úlohy učení s učitelem
- Lineární regrese: nejmenší čtverce geometricky i maticově
- Polynomiální a nelineární regrese: jak přímku ohnout
- Logistická regrese: sigmoida, šance a křížová entropie
- Vícetřídní klasifikace a softmax: od dvou tříd k mnoha
- Naivní Bayesův klasifikátor: spamový filtr, který funguje lépe, než by měl
- Ztrátové funkce: co přesně model minimalizuje
- Rozhodovací stromy a k nejbližších sousedů: modely bez vzorců
- Ansámbly: náhodný les a gradient boosting
- SVM a jádrový trik: nejširší hranice a jak ji ohnout
- Signál a vzorkování: proč platí Nyquistův teorém
- Fourierova transformace: rozklad signálu na frekvence
- Filtry a konvoluce: vyhlazení, hrany a odstranění šumu
- Zvuk a řeč: spektrogram, melová škála a MFCC
- Obraz jako signál: pixely, barvy, konvoluce a hrany
- Perceptron a umělý neuron: nejjednodušší neuronová síť a co neumí
- Neuronová síť od nuly: zpětné šíření chyby krok za krokem
- Aktivace, inicializace a normalizace: drobnosti, na kterých hluboká síť stojí
- Konvoluční sítě: jak síť poznává obrázky
- Rekurentní sítě: paměť pro posloupnosti a mizející gradient
- LSTM a GRU: brány, které rozhodují, co si síť zapamatuje
- Autoenkodéry: síť, která se učí data stlačit a zase rozbalit
- Embeddingy a word2vec: jak se slovo stane vektorem
- Mechanismus pozornosti: jak se model naučí, kam se dívat
- Transformery a velké jazykové modely: architektura krok za krokem
- Tokenizace: jak se text rozseká na kousky pro model
- Předtrénování a škálování: proč větší model a víc dat funguje
- Doladění a RLHF: jak se z doplňovače textu stane asistent
- Prompty, kontext a RAG: jak modelu dát správné informace
- Variační autoenkodéry (VAE): nové obrázky ze spojitého latentního prostoru
- Generativní adversariální sítě (GAN): padělatel proti detektivovi
- Difúzní modely: jak se z čistého šumu stane obrázek
- Markovovy rozhodovací procesy: stavy, akce, odměny a Bellmanovy rovnice
- Q-learning a DQN: učení hodnoty akcí od tabulky po neuronovou síť
- Policy gradient a PPO: učení přímo strategie
- Jak funguje AI agent: jazykový model ve smyčce s nástroji
- Programování s AI agenty: zadání, testy a kontrola
- Vyhodnocení a bezpečnost AI systémů: jak měřit a jak hlídat
Celá osnova série 83 z 83 témat hotovo
- Jak zapsat rozvrh, sudoku nebo barvení mapy jako proměnné, domény a podmínky.
- Jak backtracking s heuristikami a dopřednou kontrolou zkrátí hledání z milionů kroků na desítky.
- Jak AC-3 šíří škrtání po grafu podmínek a proč min-conflicts zvládne i milion dam.
Co se hodí znát předem: Prohledávání stavového prostoru: BFS, DFS, UCS a A* s animacemi, Grafy a stromy: struktura, na které stojí hledání i neuronové sítě
Rozvrh hodin, ve kterém žádný učitel neučí dvě třídy naráz. Sudoku. Rozmístění součástek na desce, směny v nemocnici, přidělení letadel k branám. Všechny tyhle úlohy mají společné to, že nehledáme cestu, ale přiřazení: každé proměnné dát hodnotu tak, aby platila všechna pravidla najednou. Takové úloze se říká úloha s omezujícími podmínkami a na rozdíl od obecného prohledávání má jednu velkou výhodu — pravidla jsou vidět, takže z nich algoritmus může usuzovat, ne jen zkoušet.
Navazuje na díly Prohledávání stavového prostoru a Grafy a stromy.
1. Co je úloha s omezujícími podmínkami
Definice — CSP
Nechť n ∈ ℕ, n ≥ 1. Úloha s omezujícími podmínkami (constraint satisfaction problem, CSP) je trojice (X, D, C), kde
- X = {X1, …, Xn} je množina n různých proměnných;
- D = (D1, …, Dn), kde pro každé i = 1, …, n je Di neprázdná množina — doména proměnné Xi, tj. množina hodnot, kterých smí Xi nabývat (v tomto dílu jsou všechny domény konečné);
- C je konečná množina podmínek. Každá podmínka je dvojice (S, R), kde rozsah S = (Xi1, …, Xir) je r-tice navzájem různých proměnných z X (r ≥ 1) a relace R ⊆ Di1 × … × Dir je množina povolených kombinací jejich hodnot.
Číslo r je arita podmínky. Podmínka s r = 2 je binární, například podmínka X1 ≠ X2 je dvojice ((X1, X2), {(x, y) ∈ D1 × D2 : x ≠ y}). CSP je binární, právě když jsou všechny jeho podmínky binární. Rozsah S budeme podle potřeby chápat i jako množinu svých proměnných (S ⊆ X).
Slovy: CSP tvoří konečně mnoho proměnných, ke každé množina přípustných hodnot a seznam pravidel, z nichž každé vyjmenovává povolené kombinace hodnot několika proměnných.
Proměnné jsou otázky, na které hledáte odpověď („jakou barvu dostane tenhle kraj?“). Doména jsou možné odpovědi („červená, zelená, modrá“). Podmínky jsou pravidla, která odpovědi musí dodržet („sousední kraje nesmějí mít stejnou barvu“).
Definice — přiřazení a řešení
Nechť (X, D, C) je CSP. Přiřazení je funkce a : dom(a) → D1 ∪ … ∪ Dn, kde dom(a) ⊆ X, taková, že
a(Xi) ∈ Di pro každé Xi ∈ dom(a).
Pro podmínku (S, R) ∈ C s rozsahem S = (Xi1, …, Xir) ⊆ dom(a) označme a|S = (a(Xi1), …, a(Xir)) hodnoty proměnných rozsahu. Přiřazení a je
- konzistentní, právě když pro každou podmínku (S, R) ∈ C s S ⊆ dom(a) platí a|S ∈ R;
- úplné, právě když dom(a) = X.
Řešení CSP je přiřazení, které je úplné a konzistentní. Slovy: řešení dá hodnotu každé proměnné a neporuší žádnou podmínku; konzistentní přiřazení neporuší žádnou podmínku, jejíž proměnné už všechny hodnotu mají.
Rozpracované sudoku, ve kterém zatím nic nekoliduje, je konzistentní, ale neúplné přiřazení. Vyplněné sudoku, ve kterém nic nekoliduje, je řešení.
Přiřazení a je tedy funkce z množiny proměnných dom(a) ⊆ X do jejich domén. Podmínku konzistence z definice lze zapsat jediným výrazem:
a je konzistentní ⇔ ∀(S, R) ∈ C : S ⊆ dom(a) ⇒ a|S ∈ R
- ∀(S, R) ∈ C
- projdeme každou podmínku — rozsah S a relaci R; stačí jediná porušená a přiřazení je špatně
- S ⊆ dom(a)
- soudíme jen podmínky, jejichž proměnné už všechny mají hodnotu; o podmínce s volnou proměnnou zatím rozhodnout nelze, a proto ji rozpracované přiřazení nemůže porušit
- a|S ∈ R
- hodnoty proměnných z rozsahu (zúžení a na S) musí tvořit kombinaci, kterou relace povoluje
Příklad — barvení krajů jako CSP
X = {KVK, ULK, …, MSK} je 14 krajů, každý má doménu Di = {1, 2, 3, 4} (čísla barev) a C obsahuje pro každou z 26 společných hranic jednu podmínku ((Xi, Xj), ≠), tedy relaci {(x, y) : x ≠ y} s dvanácti povolenými dvojicemi z 16.
Úplných přiřazení je |D1| · |D2| · … · |D14| = 414 = 268 435 456. Hledáme mezi nimi ta, která splní všech 26 podmínek.
Proč to nepojmout jako obyčejné prohledávání? Šlo by to — stav je rozpracované přiřazení, akce je „dej další proměnné hodnotu“. Jenže prohledávání by se na stav dívalo jako na černou skříňku. CSP do skříňky vidí: ví, která pravidla platí a kterých proměnných se týkají. Na tom stojí všechny triky v tomhle dílu.
2. Graf podmínek: barvení mapy krajů
Klasická úloha: obarvit mapu tak, aby žádné dva sousední státy neměly stejnou barvu. Vezmeme čtrnáct krajů Česka. Proměnné jsou kraje, doména je sada barev a pro každou společnou hranici je podmínka „různé barvy“.
Definice — graf podmínek
Nechť (X, D, C) je binární CSP s proměnnými X = {X1, …, Xn}. Jeho graf podmínek je neorientovaný graf (Grafy a stromy) G = (V, E), kde
V = X, E = {{Xi, Xj} : i ≠ j ∧ ∃R : ((Xi, Xj), R) ∈ C}.
Proměnné Xi a Xj jsou tedy spojeny hranou, právě když existuje podmínka, jejíž rozsah tvoří právě tyto dvě proměnné (v libovolném pořadí — proto stačí i, j projít oběma směry). Sousedy proměnné Xi v tomto grafu značíme N(Xi) = {Xj : {Xi, Xj} ∈ E}.
Každý kraj je kolečko a čára mezi dvěma kolečky znamená „tyhle dva spolu sousedí, nesmějí mít stejnou barvu“. Z mapy zbude jen to, na čem záleží: kdo s kým sousedí.
Se čtyřmi barvami to jde hladce. Se třemi to nejde vůbec — a graf ukáže proč. Středočeský kraj sousedí s Prahou a se sedmi kraji, které kolem něj tvoří uzavřený kruh. Kruh o lichém počtu vrcholů potřebuje sám o sobě tři barvy (dvě by se musely pravidelně střídat, a to u lichého počtu nevyjde). Středočeský kraj sousedí se všemi sedmi, takže na něj čtvrtá barva zbýt musí:
Věta — tři barvy na kraje nestačí
Nechť G = (V, E) je konečný neorientovaný graf a k ∈ ℕ, k ≥ 1. Obarvení grafu G k barvami je funkce c : V → {1, …, k} taková, že
∀{u, v} ∈ E : c(u) ≠ c(v).
Chromatické číslo grafu G je
χ(G) = min {k ∈ ℕ, k ≥ 1 : existuje obarvení G k barvami},
tedy nejmenší počet barev, kterými jdou vrcholy obarvit tak, aby každé dva sousední vrcholy měly různé barvy (pro konečný graf minimum existuje, |V| barev stačí vždy).
Lemma. Nechť G = (V, E) je graf, m ≥ 3 liché a v1, …, vm ∈ V vrcholy tvořící cyklus, tj. {vi, vi+1} ∈ E pro i = 1, …, m − 1 a {vm, v1} ∈ E. Pak neexistuje funkce c : {v1, …, vm} → {1, 2}, která by na těchto hranách dávala sousedům různé barvy. Důkaz: se dvěma barvami se podél cyklu barvy musí střídat; má-li v1 barvu 1, pak vi má barvu 1 právě pro lichá i. Pak má vm (liché m) barvu 1 stejně jako jeho soused v1. Spor. ∎
Věta. Nechť G je graf krajů: vrcholy jsou kraje Česka (14 vrcholů) a {u, v} ∈ E, právě když kraje u a v mají společnou hranici. Pak χ(G) = 4. Důkaz: obarvení čtyřmi barvami existuje (obrázek výše), takže χ(G) ≤ 4. Nechť by existovalo obarvení c třemi barvami. Prstenec 7 krajů kolem Středočeského je cyklus liché délky, takže podle lemmatu na něm c nevystačí s méně než třemi barvami — použije všechny tři. Středočeský kraj sousedí s každým krajem prstence, a c(STČ) se proto liší od všech tří barev, což v {1, 2, 3} nejde. Spor, tedy χ(G) ≥ 4. ∎
Že čtyři barvy stačí na každou mapu, je slavná věta o čtyřech barvách. Dokázali ji Appel a Haken v roce 1976 — jako první velkou matematickou větu s pomocí počítače, který prověřil skoro dva tisíce případů.
3. Backtracking
Definice — backtracking
Backtracking je prohledávání do hloubky, které v každém kroku přiřadí hodnotu jedné další proměnné. Pro přiřazení a, proměnnou Xi ∉ dom(a) a hodnotu v ∈ Di značíme a ∪ {Xi ↦ v} přiřazení a rozšířené o hodnotu v proměnné Xi.
Vstup: CSP (X, D, C) s konečnými doménami a konzistentní přiřazení a (na začátku prázdné, dom(a) = ∅).
Výstup: řešení CSP, které rozšiřuje a, nebo „neúspěch“, pokud takové řešení neexistuje.
Procedura BT(a):
- Je-li dom(a) = X, vrať a (úplné konzistentní přiřazení je řešení).
- Vyber proměnnou Xi ∈ X ∖ dom(a).
- Pro každou hodnotu v ∈ Di (v pevném pořadí): je-li a ∪ {Xi ↦ v} konzistentní, zavolej BT(a ∪ {Xi ↦ v}); vrátí-li toto volání řešení, vrať ho. Nekonzistentní hodnotu vůbec nezkoušej.
- Nevedla-li k řešení žádná hodnota, vrať „neúspěch“ — volající se tím vrátí k předchozí proměnné a zkusí její další hodnotu.
Zastavení: každé vnořené volání má o jednu přiřazenou proměnnou víc, takže hloubka rekurze je nejvýš n = |X|, a každé volání zkusí nejvýš |Di| hodnot. Strom volání je proto konečný a algoritmus vždy skončí.
Nechť d = maxi |Di|. V nejhorším případě projde backtracking O(dn) přiřazení. Obecný CSP je NP-těžký, takže v nejhorším případě lépe neumíme.
Vyplňujete políčko po políčku. Jakmile něco nesedí, nepokračujete — vrátíte se o krok, změníte poslední rozhodnutí a zkoušíte dál. Jako DFS z minulého dílu, jen s jednou drobností, která dělá obrovský rozdíl: pořadí proměnných na výsledku nezáleží, takže stačí rozhodovat o jedné proměnné na patro stromu.
def backtracking(prirazeni, csp):
if len(prirazeni) == len(csp.promenne):
return prirazeni # všechno má hodnotu: řešení
p = vyber_promennou(prirazeni, csp)
for h in csp.domena[p]:
if konzistentni(p, h, prirazeni, csp):
prirazeni[p] = h
vysledek = backtracking(prirazeni, csp)
if vysledek:
return vysledek
del prirazeni[p] # slepá ulička: vrať se
return None
Proč „jedna proměnná na patro“ dělá obrovský rozdíl? Obecné prohledávání by v každém stavu nabízelo všechny dvojice (nepřiřazená proměnná, hodnota). Totéž úplné přiřazení by pak šlo poskládat ve všech možných pořadích a strom by měl
n! · dn listů místo dn
- n!
- počet pořadí, ve kterých lze n proměnných přiřadit; na pořadí výsledek nezávisí (přiřazení je množina dvojic), takže všechna kromě jednoho jsou zbytečné kopie téže práce — backtracking je zahodí tím, že na každém patře rozhoduje o jediné proměnné
- dn
- počet různých úplných přiřazení: každá z n proměnných nezávisle jednu z d hodnot; tolik listů zbude, i kdyby backtracking nic neořezával
Na krajích se čtyřmi barvami udělá backtracking 14 přiřazení, jedno na kraj, a ani jednou se nevrací. Se třemi barvami musí dokázat, že řešení neexistuje — a to znamená vyzkoušet všechno: 93 přiřazení, než to vzdá. Začátek jeho běhu (odsazení je hloubka ve stromu, barvy jsou čísla 1–3, ✗ znamená, že pro kraj nezbyla žádná barva):
KVK = 1
ULK = 2
PLK = 3
PHA = 1
STČ ✗
PHA = 2
STČ = 1
LBK = 3
JHČ = 2
HKK = 2
VYS = 3
PAK ✗
PHA = 3
STČ = 1
…
Hned první slepá ulička je poučná: Středočeskému kraji vzaly všechny tři barvy Ústecký, Plzeňský a Praha. Backtracking se ale vrací jen k poslední proměnné — k Praze — a přebarvuje ji, i když skutečná potíž je v lichém prstenci. Praha se tak v běhu přebarvuje znovu a znovu. Tomuhle bezradnému přešlapování se říká thrashing a chytřejší volba proměnných ho výrazně omezí.
4. Kterou proměnnou a kterou hodnotu
Kostra backtrackingu nechává otevřené dvě otázky: kterou proměnnou vzít jako další a v jakém pořadí zkoušet její hodnoty. Na odpovědi záleží víc, než by se zdálo.
Definice — MRV, stupeň, LCV
Nechť (X, D, C) je binární CSP s grafem podmínek G a sousedy N(·), a je konzistentní přiřazení a U = X ∖ dom(a) ≠ ∅ množina nepřiřazených proměnných. Pro Xi ∈ U označme zbylé hodnoty
D′i = {v ∈ Di : a ∪ {Xi ↦ v} je konzistentní}.
- MRV (minimum remaining values) vybere jako další proměnnou některou z M = {Xi ∈ U : |D′i| = minXk ∈ U |D′k|}, tj. proměnnou s nejmenším počtem hodnot, které ještě nejsou v konfliktu s přiřazením.
- Stupňová heuristika rozhodne shodu: z M vybere proměnnou Xi s největším |N(Xi) ∩ U|, tj. s nejvíce podmínkami s dosud nepřiřazenými proměnnými.
- LCV (least constraining value): pro vybranou Xi a v ∈ D′i a každé Xj ∈ N(Xi) ∩ U označme D″j(v) = {w ∈ D′j : a ∪ {Xi ↦ v, Xj ↦ w} je konzistentní} a ztráta(v) = ∑Xj ∈ N(Xi) ∩ U |D′j ∖ D″j(v)|. LCV zkouší hodnoty v ∈ D′i vzestupně podle ztráty(v).
Slovy: MRV bere proměnnou, které zbývá nejméně možností, stupňová heuristika při shodě tu, která nejvíc omezuje ostatní, a LCV zkouší nejdřív hodnotu, která sousedům vyškrtne nejméně možností. U podmínky ≠ je D′j ∖ D″j(v) = {v} ∩ D′j, takže ztráta je prostě počet nepřiřazených sousedů, kterým v ještě zbývá.
Proměnnou berte tu nejtěžší: políčko sudoku, kam se vejde už jen jedno číslo, vyplňte hned — a když tam nejde nic, zjistíte to dřív, než postavíte nad chybou dalších dvacet pater. Hodnotu naopak berte tu nejvstřícnější, která ostatním nechá nejvíc prostoru. Proměnnou volíte tak, abyste rychle selhali, hodnotu tak, abyste uspěli.
U tří barev na krajích sníží MRV spolu s dopřednou kontrolou z další kapitoly počet přiřazení z 93 na 45 — začne u kraje, kterému zbývá nejméně barev, a spor najde dvakrát rychleji.
Na prázdné mapě mají všechny kraje po třech barvách a MRV nerozhodne. Stupňová heuristika by pak vzala Středočeský kraj, který má 8 sousedů — nejvíc ze všech; jeho barva hned omezí osm dalších krajů. Naše implementace při shodě bere prostě první kraj v pořadí od západu, tedy Karlovarský.
5. Dopředná kontrola
Definice — dopředná kontrola
Nechť (X, D, C) je binární CSP, ve kterém každé dvě proměnné spojuje nejvýš jedna podmínka (víc podmínek mezi touž dvojicí lze nahradit jednou s průnikem relací). Pro podmínku ((Xi, Xj), R) ∈ C značíme Rij = R a Rji = {(y, x) : (x, y) ∈ R}.
Nechť a je konzistentní přiřazení, D1, …, Dn aktuální (případně už zúžené) domény, Xi ∉ dom(a) a v ∈ Di. Dopředná kontrola po přiřazení Xi ↦ v nastaví Di ← {v} a pro každou proměnnou Xj ∈ N(Xi) ∖ dom(a)
Dj ← {w ∈ Dj : (v, w) ∈ Rij};
ostatní domény nemění. Je-li po zúžení Dj = ∅ pro některé j, přiřazení Xi ↦ v se okamžitě odvolá — nelze ho rozšířit na řešení, protože Xj by neměla žádnou přípustnou hodnotu.
Slovy: po každém přiřazení se ze všech nepřiřazených sousedů vyškrtnou hodnoty, které s přiřazenou hodnotou nejsou slučitelné, a prázdná doména znamená okamžitý návrat.
Napíšete do sudoku sedmičku a hned ji vyškrtnete z poznámek všech políček ve stejném řádku, sloupci a čtverci. Kdyby některému políčku tím nezbylo nic, víte, že sedmička byla chyba — ještě než se pustíte do dalšího políčka.
def dopredna(csp, domeny, prirazeni, p, h):
nove = dict(domeny)
nove[p] = [h]
for s in csp.sousede[p]:
if s in prirazeni:
continue
nove[s] = [x for x in nove[s] if csp.povoleno(p, h, s, x)]
if not nove[s]:
return None # sousedovi nic nezbylo: p = h odvolat
return nove
Backtracking pak místo původních domén předává do rekurze zúžené nove a MRV
už jen porovnává jejich délky. Takhle vypadá začátek běhu s dopřednou kontrolou a MRV
na krajích se třemi barvami. Ve sloupci vpravo jsou nepřiřazené kraje s nejmenší
doménou — z nich MRV vybere ten první:
| krok | přiřazení | domény sousedů po vyškrtnutí | nejmenší domény |
|---|---|---|---|
| 1 | KVK = 1 | PLK {2, 3}, ULK {2, 3} | ULK, PLK (2) |
| 2 | ULK = 2 | PLK {3}, STČ {1, 3}, LBK {1, 3} | PLK (1) |
| 3 | PLK = 3 | STČ {1}, JHČ {1, 2} | STČ (1) |
| 4 | STČ = 1 | PHA {2, 3}, LBK {3}, HKK {2, 3}, PAK {2, 3}, VYS {2, 3}, JHČ {2} | LBK, JHČ (1) |
| 5 | LBK = 3 | HKK {2} | JHČ, HKK (1) |
| 6 | JHČ = 2 | VYS {3}, JHM {1, 3} | HKK, VYS (1) |
| 7 | HKK = 2 | PAK {3} | VYS, PAK (1) |
| 8 | VYS = 3 | PAK ∅, JHM {1} | PAK = ∅ |
Po prvních třech krocích zbyla Středočeskému kraji jediná barva, a tak přijde na řadu dřív než Praha. Každý další vybraný kraj má už jen jednu možnost a v osmém kroku dopředná kontrola zjistí, že Pardubickému kraji nezůstalo nic — spor je odhalen, aniž by se Pardubický kraj vůbec zkoušel barvit. Celý prstenec se tak vynutí sám od sebe; volné rozhodnutí padlo jen u prvních dvou krajů.
Rozdíl je vidět na sudoku níže. Prostý backtracking políčka po řádcích potřebuje přes 14 milionů přiřazení. S dopřednou kontrolou jich je 11 400. A když k ní přidáme MRV, spadne to na 81 — 30 zadaných čísel a 51 doplněných, bez jediného návratu.
6. Hranová konzistence a AC-3
Dopředná kontrola se dívá jen o krok dál: na sousedy proměnné, kterou jsme právě přiřadili. Jde to důkladněji — vyškrtávat i mezi proměnnými, které ještě hodnotu nemají.
Definice — hranová konzistence
Nechť (X, D, C) je binární CSP se značením Rij z definice dopředné kontroly a nechť Xi, Xj ∈ X jsou proměnné spojené podmínkou, tj. {Xi, Xj} ∈ E. Proměnná Xi je hranově konzistentní vzhledem k Xj, právě když
∀x ∈ Di ∃y ∈ Dj : (x, y) ∈ Rij.
Uspořádaná dvojice (Xi, Xj) s {Xi, Xj} ∈ E se nazývá orientovaná hrana; každá hrana grafu podmínek dává dvě. CSP je hranově konzistentní, právě když je Xi hranově konzistentní vzhledem k Xj pro každou orientovanou hranu (Xi, Xj). Slovy: každá hodnota v doméně každé proměnné má u každého souseda aspoň jednu hodnotu, se kterou podmínku splní.
Každá hodnota musí mít u souseda aspoň jednoho „partnera“, se kterým může žít. Hodnota bez partnera je mrtvá — v žádném řešení se neobjeví — a můžete ji klidně škrtnout.
Zkonzistentnit jednu orientovanou hranu (operace zuz v kódu níže) znamená
ponechat jen hodnoty s partnerem:
Di ← { x ∈ Di : ∃y ∈ Dj : (x, y) ∈ Rij }
- Di ←
- doménu přepisujeme na místě; zmenšuje se jen doména Xi, hrana je orientovaná — opačný směr (Xj, Xi) je samostatná položka fronty
- x ∈ Di
- každou hodnotu soudíme zvlášť; nic nového nepřidáváme, takže doména může jen ubývat — proto AC-3 skončí
- ∃y ∈ Dj
- stačí jeden partner v aktuální doméně souseda; ptáme se, jestli x může přežít, ne jak
- (x, y) ∈ Rij
- dvojice musí podmínku mezi Xi a Xj splňovat — u barvení mapy prostě x ≠ y
Algoritmus — AC-3
Vstup: binární CSP (X, D, C) s konečnými doménami a grafem podmínek G = (X, E).
Výstup: „nepravda“, pokud některá doména zůstala prázdná (CSP pak nemá řešení); jinak „pravda“ a domény zúžené tak, že CSP je hranově konzistentní a má stejnou množinu řešení jako na vstupu.
- Vlož do fronty Q všechny orientované hrany (Xi, Xj), {Xi, Xj} ∈ E.
- Je-li Q prázdná, skonči s výsledkem „pravda“.
- Vyjmi z Q hranu (Xi, Xj) a proveď Di ← {x ∈ Di : ∃y ∈ Dj : (x, y) ∈ Rij} (rozbor výše).
- Pokud se Di zmenšila: je-li Di = ∅, skonči s výsledkem „nepravda“; jinak vlož do Q všechny hrany (Xk, Xi) pro Xk ∈ N(Xi), Xk ≠ Xj — jejich hodnoty mohly právě přijít o partnera.
- Pokračuj krokem 2.
Zastavení: hrany se do fronty vracejí jen tehdy, když některá doména ubude; domény jsou konečné a nikdy nerostou, takže po konečně mnoha krocích je fronta prázdná.
Pro c binárních podmínek a domény velikosti nejvýš d trvá AC-3 O(c · d3).
Škrtání se šíří jako vlna: když políčku ubude možnost, musí se znovu podívat i jeho sousedé, protože jim mohl zmizet partner. Vlna se zastaví, až už není co škrtnout. Tomu se říká propagace omezení.
from collections import deque
def ac3(csp):
fronta = deque((xi, xj) for xi in csp.promenne for xj in csp.sousede[xi])
while fronta:
xi, xj = fronta.popleft()
if zuz(csp, xi, xj):
if not csp.domena[xi]:
return False # prázdná doména: řešení neexistuje
for xk in csp.sousede[xi]:
if xk != xj:
fronta.append((xk, xi))
return True
def zuz(csp, xi, xj):
puvodne = len(csp.domena[xi])
csp.domena[xi] = [x for x in csp.domena[xi]
if any(csp.povoleno(xi, x, xj, y) for y in csp.domena[xj])]
return len(csp.domena[xi]) < puvodne
AC-3 krok za krokem
Tři proměnné X, Y, Z, každé dvě musí být různé, a domény DX = {1}, DY = {1, 2}, DZ = {1, 2, 3}. Fronta začíná šesti orientovanými hranami; řádek tabulky je jedno vyjmutí z fronty a jedno zúžení Di ← podle vzorce výše (sloupec „doména po“ má barvu stejného členu):
| krok | hrana | vyškrtnuto | doména po | do fronty | ve frontě |
|---|---|---|---|---|---|
| 1 | (X, Y) | — | {1} | — | 5 |
| 2 | (X, Z) | — | {1} | — | 4 |
| 3 | (Y, X) | 1 | {2} | (Z, Y) | 4 |
| 4 | (Y, Z) | — | {2} | — | 3 |
| 5 | (Z, X) | 1 | {2, 3} | (Y, Z) | 3 |
| 6 | (Z, Y) | 2 | {3} | (X, Z) | 3 |
| 7 | (Z, Y) | — | {3} | — | 2 |
| 8 | (Y, Z) | — | {2} | — | 1 |
| 9 | (X, Z) | — | {1} | — | 0 |
V kroku 3 přijde Y o jedničku, protože jediný možný partner u X je právě 1. Tím se do fronty vrátí hrana (Z, Y) — hodnota 2 v Z měla partnera právě v té vyškrtnuté jedničce. Hodnotu 2 ze Z ale škrtne už původní hrana (Z, Y) v kroku 6; vrácená kopie v kroku 7 nic nenajde. (Naše fronta není množina, takže v ní hrana může být dvakrát — na výsledku to nic nemění.) Po 9 krocích je fronta prázdná a domény jsou {1}, {2}, {3}: řešení bez jediného pokusu.
O( 2c · d · d2 ) = O(c · d3)
- 2c
- každá binární podmínka dává dvě orientované hrany, každou je potřeba zkontrolovat
- d
- kolikrát nejvýš se hrana (Xk, Xi) vrátí do fronty: jen když Di ubude, a to se stane nejvýš d-krát
- d2
- cena jedné kontroly hrany: pro každou z nejvýš d hodnot x hledáme partnera mezi nejvýš d hodnotami y
7. Sudoku: propagace v praxi
Sudoku je CSP jako z učebnice: 81 proměnných, domény {1, …, 9} a podmínka „různé hodnoty“ pro každou dvojici políček ve stejném řádku, sloupci nebo čtverci 3 × 3. Každé políčko tak má 20 sousedů v grafu podmínek.
Příklad — sudoku v číslech
Sousedé políčka: 8 dalších v řádku + 8 ve sloupci + 4 políčka čtverce 3 × 3, která neleží v jeho řádku ani sloupci (zbylá 4 už jsme započítali) = 20.
Podmínky: 81 · 20 = 1620 orientovaných hran, každá dvojice políček je v tom dvakrát, takže binárních podmínek je 1620/2 = 810.
Velikost domén na začátku: 30 zadaných políček s doménou {zadané číslo} a 51 prázdných s devíti hodnotami: 30 · 1 + 51 · 9 = 489 hodnot. Na konci má každé z 81 políček jednu hodnotu, takže AC-3 vyškrtlo 489 − 81 = 408 hodnot. Z fronty k tomu vzalo 9 372 hran.
Hrubá síla: vyzkoušet všechna doplnění prázdných políček znamená 951 ≈ 4,6 · 1048 přiřazení (dn s d = 9 a n = 51 prázdných políček; barva podle vzorce v kapitole Backtracking). Backtracking jich díky kontrole konzistence udělá „jen“ čtrnáct milionů.
Spusťme na zadání s 30 čísly samotné AC-3, bez jediného přiřazení naslepo. Vyškrtá 408 hodnot — a každému z 51 prázdných políček zůstane právě jedna. Sudoku je vyřešené čistou úvahou:
U lehkých sudoku to tak chodí; u těžších propagace někde uvízne a nastoupí hledání — backtracking, který po každém přiřazení znovu pustí propagaci. Tahle kombinace (udržování hranové konzistence, MAC) je základ většiny skutečných řešičů. Pro srovnání na stejném zadání:
| Algoritmus | Přiřazení | Návraty |
|---|---|---|
| Backtracking po řádcích | 14 740 819 | 14 740 738 |
| + dopředná kontrola | 11 400 | 11 319 |
| + dopředná kontrola + MRV | 81 | 0 |
| AC-3 bez hledání | 0 | 0 |
Od čtrnácti milionů k nule. Hrubá síla a chytrá úvaha řeší stejnou úlohu — jen jedna z nich čte pravidla.
8. Lokální hledání: min-conflicts
Úplně jiná strategie: nestavět řešení kousek po kousku, ale začít s úplným přiřazením, které porušuje spoustu podmínek, a postupně ho opravovat. Je to lokální hledání z dílu o prohledávání, jen účelová funkce je počet porušených podmínek.
Algoritmus — min-conflicts
Vstup: CSP (X, D, C) s konečnými doménami a největší počet kroků kmax ∈ ℕ.
Výstup: řešení CSP, nebo „neúspěch“.
Pro úplné přiřazení a, proměnnou Xi a hodnotu v ∈ Di nechť konflikty(Xi, v) je počet podmínek (S, R) ∈ C s Xi ∈ S, které porušuje přiřazení vzniklé z a změnou hodnoty Xi na v. Proměnná Xi je v konfliktu, právě když konflikty(Xi, a(Xi)) > 0.
- Zvol náhodné úplné přiřazení a (a(Xi) ∈ Di pro všechna i).
- Opakuj nejvýš kmax-krát: je-li a konzistentní (žádná proměnná není v konfliktu), vrať a. Jinak vyber náhodně proměnnou Xi v konfliktu a nastav a(Xi) ← v, kde v ∈ Di má nejmenší konflikty(Xi, v) (při shodě náhodně) — tedy hodnotu, se kterou porušuje nejméně podmínek.
- Po kmax krocích bez řešení vrať „neúspěch“.
Zastavení: nejpozději po kmax krocích. Algoritmus není úplný: „neúspěch“ neznamená, že řešení neexistuje.
Rozestavíte všechno najednou, jak to padne, a pak opravujete: vezmete jednu věc, která se s něčím hádá, a přesunete ji tam, kde se hádá nejmíň. Nepamatujete si žádný strom ani historii — jen aktuální stav a to, co v něm drhne.
Klasická zkouška je úloha n dam: rozestavit n šachových dam na šachovnici n × n tak, aby se žádné dvě neohrožovaly. Proměnná je řádek dámy v každém sloupci. Pro osm dam začal náš běh se šesti ohrožujícími se dvojicemi a skončil po 8 krocích:
Co přesně algoritmus minimalizuje? Dáma ve sloupci i stojí v řádku ri. Počet konfliktů, kdyby ji postavil do řádku v, je
konflikty(i, v) = ∑j ≠ i [ rj = v ∨ |rj − v| = |j − i| ]
- konflikty(i, v)
- cena tahu „dámu ze sloupce i do řádku v“; min-conflicts vybere v, pro které je nejmenší
- ∑j ≠ i [ … ]
- sečteme přes všechny ostatní dámy; hranatá závorka je 1, když podmínka platí, jinak 0 — počítáme porušené podmínky
- rj = v
- dáma j stojí ve stejném řádku; sloupec sdílet nemůže, to vylučuje už volba proměnných (jedna dáma na sloupec)
- |rj − v| = |j − i|
- dámy leží na stejné úhlopříčce: posun v řádcích se rovná posunu ve sloupcích
def min_conflicts(csp, kroku_max):
a = nahodne_uplne_prirazeni(csp)
for _ in range(kroku_max):
v_konfliktu = [x for x in csp.promenne if konflikty(csp, a, x, a[x]) > 0]
if not v_konfliktu:
return a # nic nedrhne: řešení
x = random.choice(v_konfliktu)
ceny = {v: konflikty(csp, a, x, v) for v in csp.domena[x]}
nejmene = min(ceny.values())
a[x] = random.choice([v for v in ceny if ceny[v] == nejmene])
return None # došla trpělivost
Náš běh tah po tahu. Sloupce i řádky číslujeme 1–8 (řádek 1 nahoře); „ceny“ jsou hodnoty konflikty(i, v) pro v = 1, …, 8 a tučně je nejmenší z nich (barvou odpovídají členu konflikty(i, v) ve vzorci výše). Na začátku stály dámy v řádcích 5 1 6 2 5 1 1 8.
| krok | sloupec | ceny pro řádky 1–8 | řádek | konfliktů po tahu |
|---|---|---|---|---|
| 1 | 8 | 4 3 1 0 2 2 1 1 | 8 → 4 | 5 |
| 2 | 7 | 2 3 2 1 4 2 1 0 | 1 → 8 | 3 |
| 3 | 1 | 3 3 0 2 2 2 0 2 | 5 → 3 | 1 |
| 4 | 2 | 1 3 2 3 3 1 1 2 | 1 → 7 | 1 |
| 5 | 3 | 3 1 3 3 2 1 2 2 | 6 → 2 | 1 |
| 6 | 3 | 3 1 3 3 2 1 2 2 | 2 → 2 | 1 |
| 7 | 4 | 2 1 3 2 3 2 1 2 | 2 → 7 | 1 |
| 8 | 2 | 2 3 3 2 3 0 1 2 | 7 → 6 | 0 |
V kroku 6 si algoritmus ze dvou stejně dobrých řádků vylosoval ten, ve kterém dáma už stála — tah nic nezměnil. V krocích 4 až 7 je nejmenší cena 1, teprve v osmém se ve vylosovaném sloupci najde řádek s nulou.
Všimněte si prostředních kroků: počet konfliktů se čtyři kroky drží na jedné — algoritmus přesouvá dámy „do strany“ po plošině, než najde tah, který spor odstraní. Překvapivé je, jak dobře to škáluje: milion dam vyřeší min-conflicts v průměru zhruba na padesát kroků. Stejným postupem se plánovala pozorování Hubbleova vesmírného dalekohledu (obojí popisují Minton, Johnston, Philips a Laird, 1992).
9. Omezující podmínky v praxi
- Rozvrhy a plánování. Školní rozvrhy, směny, přidělování učeben i výrobní plány se zadávají jako proměnné, domény a podmínky. Řešiče jako Google OR-Tools kombinují propagaci, hledání a lokální vylepšování.
- SAT řešiče řeší CSP, kde je každá proměnná pravda/nepravda. Ověřují návrhy čipů, hledají chyby v softwaru a správci balíčků (například v Linuxu) jimi skládají verze knihoven, které spolu fungují.
- Překladače přidělují proměnné programu do registrů procesoru barvením grafu: dvě proměnné, které žijí zároveň, nesmějí sdílet registr — přesně jako sousední kraje barvu.
- Konfigurátory v e-shopech a výrobě hlídají, aby šly vybrané součástky dohromady.
- AI agenti a jazykové modely v CSP často nevynikají: na sudoku nebo rozvrh se spolehlivěji hodí řešič. Dobrý agent proto úlohu nepřemýšlí „z hlavy“ — přeloží ji na proměnné a podmínky a zavolá na ni nástroj.
10. Shrnutí
- CSP je trojice (X, D, C): proměnné, domény a podmínky. Řešení je úplné přiřazení, které neporušuje žádnou podmínku.
- Backtracking přiřazuje proměnné jednu po druhé a vrací se ze slepých uliček; v nejhorším případě je exponenciální.
- Heuristiky MRV a LCV volí nejtěžší proměnnou a nejvstřícnější hodnotu; dopředná kontrola odhalí slepou uličku o krok dřív.
- AC-3 šíří škrtání po grafu podmínek, dokud každá hodnota nemá partnera; lehké sudoku vyřeší úplně bez hledání.
- Min-conflicts opravuje úplné přiřazení a zvládne i obrovské úlohy, jako je milion dam.
Chcete si to osahat? Nechte AI agenta napsat řešič sudoku s backtrackingem a pak do něj přidávejte dopřednou kontrolu, MRV a AC-3 — a počítejte, kolik přiřazení ubude.
Máte na to jiný názor? Napište na support@coden-app.com.