Проблема Удовлетворения Ограничений
я пробиваюсь через Искусственный Интеллект: Современный Подход чтобы облегчить мою природную глупость. Пытаясь решить некоторые из упражнений, я столкнулся с проблемой "Кто владеет зеброй", упражнение 5.13 в Глава 5. Это было тема здесь на SO но ответы в основном касались вопроса "как бы вы решили это, если бы у вас был свободный выбор программного обеспечения для решения проблем свободен?"
Я признаю, что Prolog является очень подходящим языком программирования для такого рода проблем, и есть некоторые прекрасные пакеты, доступные, например, в Python, как показано в топ-ранжированном ответе, а также автономно. Увы, ничто из этого не помогает мне "выстоять" таким образом, как описано в книге.
в книге предлагается создать набор двойственных или, возможно, глобальных ограничений, а затем реализовать некоторые из упомянутых алгоритмов для поиска решения. Я имея много проблемы с набором ограничений, подходящих для моделирования проблемы. Я изучаю это самостоятельно, поэтому у меня нет доступа к профессору или TA, чтобы получить меня через горб - вот где я прошу вашей помощи.
я хотел построить двойные ограничения и начал с создания (логического эквивалента) 25 переменных:nationality1
, nationality2
, nationality3
, ... nationality5
, pet1
, pet2
, pet3
, ... pet5
, drink1
... drink5
и так далее, где цифра указывает на положение дома.
это прекрасно для построения унарных ограничений, например
норвежец живет в первом доме:
nationality1 = { :norway }.
но большинство ограничений представляют собой комбинацию двух таких переменных через общий номер дома, например
у шведа есть собака:
nationality[n] = { :sweden } AND pet[n] = { :dog }
здесь n
может варьироваться от 1 до 5, очевидно. Или иначе говоря:
nationality1 = { :sweden } AND pet1 = { :dog }
XOR nationality2 = { :sweden } AND pet2 = { :dog }
XOR nationality3 = { :sweden } AND pet3 = { :dog }
XOR nationality4 = { :sweden } AND pet4 = { :dog }
XOR nationality5 = { :sweden } AND pet5 = { :dog }
...что имеет решительно иное чувство к нему, чем" список кортежей", пропагандируемый книгой:
( X1, X2, X3 = { val1, val2, val3 }, { val4, val5, val6 }, ... )
я не ищу решение как таковое; я ищу начало о том, как моделировать эту проблему таким образом, чтобы это было совместимо с подходом книги. Любая помощь приветствуется.
4 ответов
существует несколько библиотек для решения CSP:
и есть еще много. Их можно использовать для эффективного решения ограничений.
с другой стороны, если вы хотите реализовать свой общий решатель ограничений, идея реализовать решатель CSP: построить граф ограничений, где узлы являются ограничением переменные и ограничения соединений. Для каждой переменной сохраните возможный домен и создайте механизм уведомления. Ограничения уведомляются при изменении связанных переменных, а затем запускают процесс распространения: просматривая текущие значения связанных переменных, уменьшают области возможных переменных.
размножение пример:
- переменные (с доменом): X - {1,2,3,4,5} - Y {1,2,3,4,5}
- Ограничение: X + Y
- когда ограничение распространяется, вы можете сделать вывод, что ни X, ни Y не могут быть 3, 4 или 5, потому что тогда ограничение потерпит неудачу, поэтому новые домены: X - {1,2} Y - {1,2}
- Теперь оба домена X и Y изменили ограничения прослушивания X и Y должны быть уведомлены о распространении.
возможно, что распространения недостаточно. В этом случае используется поиск backtracking/backjumping: мы пытаемся выбрать значение одной переменной, распространить изменения и т. д.
этот алгоритм считается довольно быстрым, в то время как его легко понять. У меня есть реализация, которая очень эффективно решает наши особые проблемы.
спасибо всем за полезную информацию!
Подсказка, в которой я действительно нуждался, пришла ко мне в пробке. Вместо того, чтобы присваивать национальности, домашних животных и т. д. к домам (переменные с именем country1
, country2
, pet1
, pet2
), что мне нужно было сделать, это назначить дома элементам домена! Пример:
(9) norway = 1 ; unary constraint: The Norwegian lives in the 1st house
(2) britain = dog ; binary constraint: Dog is in same house as the Brit
(4) green - ivory = 1 ; relative positions
это позволило мне найти простые формулировки для моих ограничений, например:
(def constraints
#{
[:con-eq :england :red]
[:con-eq :spain :dog]
[:abs-pos :norway 1]
[:con-eq :kools :yellow]
[:next-to :chesterfields :fox]
[:next-to :norway :blue]
[:con-eq :winston :snails]
[:con-eq :lucky :oj]
[:con-eq :ukraine :tea]
[:con-eq :japan :parliaments]
[:next-to :kools :horse]
[:con-eq :coffee :green]
[:right-of :green :ivory]
[:abs-pos :milk 3]
})
я еще не закончил (возня на этой только части время), но я опубликую полное решение, как только я его разработаю.
обновление: примерно через 2 недели я придумал рабочее решение в Clojure:
(ns houses
[:use [htmllog] clojure.set]
)
(comment
[ 1] The Englishman lives in the red house.
[ 2] The Spaniard owns the dog.
[ 3] The Norwegian lives in the first house on the left.
[ 4] Kools are smoked in the yellow house.
[ 5] The man who smokes Chesterfields lives in the house next to the man with the fox.
[ 6] The Norwegian lives next to the blue house.
[ 7] The Winston smoker owns snails.
[ 8] The Lucky Strike smoker drinks orange juice.
[ 9] The Ukrainian drinks tea.
[10] The Japanese smokes Parliaments.
[11] Kools are smoked in the house next to the house where the horse is kept.
[12] Coffee is drunk in the green house.
[13] The Green house is immediately to the right (your right) of the ivory house.
[14] Milk is drunk in the middle house.
“Where does the zebra live, and in which house do they drink water?”
)
(def positions #{1 2 3 4 5})
(def categories {
:country #{:england :spain :norway :ukraine :japan}
:color #{:red :yellow :blue :green :ivory}
:pet #{:dog :fox :snails :horse :zebra}
:smoke #{:chesterfield :winston :lucky :parliament :kool}
:drink #{:orange-juice :tea :coffee :milk :water}
})
(def constraints #{
; -- unary
'(at :norway 1) ; 3
'(at :milk 3) ; 14
; -- simple binary
'(coloc :england :red) ; 1
'(coloc :spain :dog) ; 2
'(coloc :kool :yellow) ; 4
'(coloc :winston :snails) ; 7
'(coloc :lucky :orange-juice) ; 8
'(coloc :ukraine :tea) ; 9
'(coloc :japan :parliament) ; 10
'(coloc :coffee :green) ; 12
; -- interesting binary
'(next-to :chesterfield :fox) ; 5
'(next-to :norway :blue) ; 6
'(next-to :kool :horse) ; 11
'(relative :green :ivory 1) ; 13
})
; ========== Setup ==========
(doseq [x (range 3)] (println))
(def var-cat ; map of variable -> group
; {:kool :smoke, :water :drink, :ivory :color, ...
(apply hash-map (apply concat
(for [cat categories vari (second cat)]
[vari (first cat)]))))
(prn "var-cat:" var-cat)
(def initial-vars ; map of variable -> positions
; {:kool #{1 2 3 4 5}, :water #{1 2 3 4 5}, :ivory #{1 2 3 4 5}, ...
(apply hash-map (apply concat
(for [v (keys var-cat)] [v positions]))))
(prn "initial-vars:" initial-vars)
(defn apply-unary-constraints
"This applies the 'at' constraint. Separately, because it only needs doing once."
[vars]
(let [update (apply concat
(for [c constraints :when (= (first c) 'at) :let [[v d] (rest c)]]
[v #{d}]))]
(apply assoc vars update)))
(def after-unary (apply-unary-constraints initial-vars))
(prn "after-unary:" after-unary)
(def binary-constraints (remove #(= 'at (first %)) constraints))
(prn "binary-constraints:" binary-constraints)
; ========== Utilities ==========
(defn dump-vars
"Dump map `vars` as a HTML table in the log, with `title`."
[vars title]
(letfn [
(vars-for-cat-pos [vars var-list pos]
(apply str (interpose "<br/>" (map name (filter #((vars %) pos) var-list)))))]
(log-tag "h2" title)
(log "<table border='1'>")
(log "<tr>")
(doall (map #(log-tag "th" %) (cons "house" positions)))
(log "</tr>")
(doseq [cat categories]
(log "<tr>")
(log-tag "th" (name (first cat)))
(doseq [pos positions]
(log-tag "td" (vars-for-cat-pos vars (second cat) pos)))
(log "</tr>")
)
(log "</table>")))
(defn remove-values
"Given a list of key/value pairs, remove the values from the vars named by key."
[vars kvs]
(let [names (distinct (map first kvs))
delta (for [n names]
[n (set (map second (filter #(= n (first %)) kvs)))])
update (for [kv delta
:let [[cname negative] kv]]
[cname (difference (vars cname) negative)])]
(let [vars (apply assoc vars (apply concat update))]
vars)))
(defn siblings
"Given a variable name, return a list of the names of variables in the same category."
[vname]
(disj (categories (var-cat vname)) vname))
(defn contradictory?
"Checks for a contradiction in vars, indicated by one variable having an empty domain."
[vars]
(some #(empty? (vars %)) (keys vars)))
(defn solved?
"Checks if all variables in 'vars' have a single-value domain."
[vars]
(every? #(= 1 (count (vars %))) (keys vars)))
(defn first-most-constrained
"Finds a variable having the smallest domain size > 1."
[vars]
(let [best-pair (first (sort (for [v (keys vars) :let [n (count (vars v))] :when (> n 1)] [n v])))]
(prn "best-pair:" best-pair)
(second best-pair)))
;========== Constraint functions ==========
(comment
These functions make an assertion about the domains in map 'bvars',
and remove any positions from it for which those assertions do not hold.
They all return the (hopefully modified) domain space 'bvars'.)
(declare bvars coloc next-to relative alldiff solitary)
(defn coloc
"Two variables share the same location."
[vname1 vname2]
(if (= (bvars vname1) (bvars vname2)) bvars
(do
(let [inter (intersection (bvars vname1) (bvars vname2))]
(apply assoc bvars [vname1 inter vname2 inter])))))
(defn next-to
"Two variables have adjoining positions"
[vname1 vname2]
; (prn "doing next-to" vname1 vname2)
(let [v1 (bvars vname1) v2 (bvars vname2)
bad1 (for [j1 v1 :when (not (or (v2 (dec j1)) (v2 (inc j1))))] [vname1 j1])
bad2 (for [j2 v2 :when (not (or (v1 (dec j2)) (v1 (inc j2))))] [vname2 j2])
allbad (concat bad1 bad2)]
(if (empty? allbad) bvars
(do
(remove-values bvars allbad)))))
(defn relative
"(position vname1) - (position vname2) = diff"
[vname1 vname2 diff]
(let [v1 (bvars vname1) v2 (bvars vname2)
bad1 (for [j1 v1 :when (not (v2 (- j1 diff)))] [vname1 j1])
bad2 (for [j2 v2 :when (not (v1 (+ j2 diff)))] [vname2 j2])
allbad (concat bad1 bad2)]
(if (empty? allbad) bvars
(do
(remove-values bvars allbad)))))
(defn alldiff
"If one variable of a category has only one location, no other variable in that category has it."
[]
(let [update (apply concat
(for [c categories v (val c) :when (= (count (bvars v)) 1) :let [x (first (bvars v))]]
(for [s (siblings v)]
[s x])))]
(remove-values bvars update)))
(defn solitary
"If only one variable of a category has a location, then that variable has no other locations."
[]
(let [loners (apply concat
(for [c categories p positions v (val c)
:when (and
((bvars v) p)
(> (count (bvars v)) 1)
(not-any? #((bvars %) p) (siblings v)))]
[v #{p}]))]
(if (empty? loners) bvars
(do
; (prn "loners:" loners)
(apply assoc bvars loners)))))
;========== Solving "engine" ==========
(open)
(dump-vars initial-vars "Initial vars")
(dump-vars after-unary "After unary")
(def rules-list (concat (list '(alldiff)) binary-constraints (list '(solitary))))
(defn apply-rule
"Applies the rule to the domain space and checks the result."
[vars rule]
(cond
(nil? vars) nil
(contradictory? vars) nil
:else
(binding [bvars vars]
(let [new-vars (eval rule)]
(cond
(contradictory new-vars) (do
(prn "contradiction after rule:" rule)
nil)
(= new-vars vars) vars ; no change
:else (do
(prn "applied:" rule)
(log-tag "p" (str "applied: " (pr-str rule)))
(prn "result: " new-vars)
new-vars))))))
(defn apply-rules
"Uses 'reduce' to sequentially apply all the rules from 'rules-list' to 'vars'."
[vars]
(reduce apply-rule vars rules-list))
(defn infer
"Repeatedly applies all rules until the var domains no longer change."
[vars]
(loop [vars vars]
(let [new-vars(apply-rules vars)]
(if (= new-vars vars) (do
(prn "no change")
vars)
(do (recur new-vars))))))
(def after-inference (infer after-unary))
(dump-vars after-inference "Inferred")
(prn "solved?" (solved? after-inference))
(defn backtrack
"solve by backtracking."
[vars]
(cond
(nil? vars) nil
(solved? vars) vars
:else
(let [fmc (first-most-constrained vars)]
(loop [hypotheses (seq (vars fmc))]
(if (empty? hypotheses) (do
(prn "dead end.")
(log-tag "p" "dead end.")
nil)
(let [hyp (first hypotheses) hyp-vars (assoc vars fmc #{hyp})]
(prn "hypothesis:" fmc hyp)
(log-tag "p" (str "hypothesis: " hyp))
(dump-vars hyp-vars (str "Hypothesis: " fmc " = " hyp))
(let [bt (backtrack (infer hyp-vars))]
(if bt (do
(prn "success!")
(dump-vars bt "Solved")
bt)
(recur (rest hypotheses))))))))))
(prn "first-most-constrained:" (first-most-constrained after-inference))
(def solution (backtrack after-inference))
(prn "solution:" solution)
(close)
(println "houses loaded.")
это 292 строки, но там много отладочного / диагностического кодирования. В целом, я очень рад, что мне удалось достаточно короткое решение в Clojure. Функциональное программирование создало небольшую проблему, но мне удалось сохранить довольно последовательный функционал стиль.
критика приветствуются однако!
для тех, кто заботится, вот решение:
house 1 2 3 4 5
country norway ukraine england spain japan
color yellow blue red ivory green
pet fox horse snails dog zebra
smoke kool chesterfield winston lucky parliament
drink water tea milk orange-juice coffee
предупреждение: Я не уверен, что это то, что вы ищете, потому что я не читал Искусственный Интеллект: Современный Подход, но я думаю, что то, что последует, тем не менее интересно.
Эди Вайц имеет интересная страница на этой загадке, с объяснил источник в Common Lisp и других источниках на C++ и Common Lisp без подробных комментариев. Я нашел исходного кода C++ Клаус Betzler особенно интересно (немного переформатировано для большей ясности):
// einstein.cpp (c) Klaus Betzler 20011218
// Klaus.Betzler@uos.de
// `Einstein's Riddle´, the rules:
// 1 The Brit lives in the red house
// 2 The Swede keeps dogs as pets
// 3 The Dane drinks tea
// 4 The green house is on the left of the white house
// 5 The green house's owner drinks coffee
// 6 The person who smokes Pall Mall rears birds
// 7 The owner of the yellow house smokes Dunhill
// 8 The man living in the centre house drinks milk
// 9 The Norwegian lives in the first house
// 10 The person who smokes Marlboro lives next to the one who keeps cats
// 11 The person who keeps horses lives next to the person who smokes Dunhill
// 12 The person who smokes Winfield drinks beer
// 13 The German smokes Rothmans
// 14 The Norwegian lives next to the blue house
// 15 The person who smokes Marlboro has a neigbor who drinks water
#undef WIN32 // #undef for Linux
#include <stdio.h>
#ifdef WIN32
#include <windows.h>
#endif
inline unsigned long BIT(unsigned n) {return 1<<n;}
const unsigned long
yellow = BIT( 0),
blue = BIT( 1),
red = BIT( 2),
green = BIT( 3),
white = BIT( 4),
norwegian = BIT( 5),
dane = BIT( 6),
brit = BIT( 7),
german = BIT( 8),
swede = BIT( 9),
water = BIT(10),
tea = BIT(11),
milk = BIT(12),
coffee = BIT(13),
beer = BIT(14),
dunhill = BIT(15),
marlboro = BIT(16),
pallmall = BIT(17),
rothmans = BIT(18),
winfield = BIT(19),
cat = BIT(20),
horse = BIT(21),
bird = BIT(22),
fish = BIT(23),
dog = BIT(24);
const char * Label[] = {
"Yellow", "Blue", "Red", "Green", "White",
"Norwegian","Dane", "Brit", "German", "Swede",
"Water", "Tea", "Milk", "Coffee", "Beer",
"Dunhill", "Marlboro","Pallmall","Rothmans","Winfield",
"Cat", "Horse", "Bird", "Fish", "Dog"
};
const unsigned long color = yellow +blue +red +green +white;
const unsigned long country = norwegian+dane +brit +german +swede;
const unsigned long drink = water +tea +milk +coffee +beer;
const unsigned long cigar = dunhill +marlboro+pallmall+rothmans+winfield;
const unsigned long animal = cat +horse +bird +fish +dog;
unsigned long house [5] = {norwegian, blue, milk, 0, 0}; // rules 8,9,14
unsigned long result[5];
const unsigned long comb[] = { // simple rules
brit+red, // 1
swede+dog, // 2
dane+tea, // 3
green+coffee, // 5
pallmall+bird, // 6
yellow+dunhill, // 7
winfield+beer, // 12
german+rothmans // 13
};
const unsigned long combmask[] = { // corresponding selection masks
country+color,
country+animal,
country+drink,
color+drink,
cigar+animal,
color+cigar,
cigar+drink,
country+cigar
};
inline bool SimpleRule(unsigned nr, unsigned which)
{
if (which<8) {
if ((house[nr]&combmask[which])>0)
return false;
else {
house[nr]|=comb[which];
return true;
}
}
else { // rule 4
if ((nr==4)||((house[nr]&green)==0))
return false;
else
if ((house[nr+1]&color)>0)
return false;
else {
house[nr+1]|=white;
return true;
}
}
}
inline void RemoveSimple(unsigned nr, unsigned which)
{
if (which<8)
house[nr]&=~comb[which];
else
house[nr+1]&=~white;
}
inline bool DunhillRule(unsigned nr, int side) // 11
{
if (((side==1)&&(nr==4))||((side==-1)&&(nr==0))||((house[nr]&dunhill)==0))
return false;
if ((house[nr+side]&animal)>0)
return false;
house[nr+side]|=horse;
return true;
}
inline void RemoveDunhill(unsigned nr, unsigned side)
{
house[nr+side]&=~horse;
}
inline bool MarlboroRule(unsigned nr) // 10 + 15
{
if ((house[nr]&cigar)>0)
return false;
house[nr]|=marlboro;
if (nr==0) {
if ((house[1]&(animal+drink))>0)
return false;
else {
house[1]|=(cat+water);
return true;
}
}
if (nr==4) {
if ((house[3]&(animal+drink))>0)
return false;
else {
house[3]|=(cat+water);
return true;
}
}
int i,k;
for (i=-1; i<2; i+=2) {
if ((house[nr+i]&animal)==0) {
house[nr+i]|=cat;
for (k=-1; k<2; k+=2) {
if ((house[nr+k]&drink)==0) {
house[nr+k]|=water;
return true;
}
}
}
}
return false;
}
void RemoveMarlboro(unsigned m)
{
house[m]&=~marlboro;
if (m>0)
house[m-1]&=~(cat+water);
if (m<4)
house[m+1]&=~(cat+water);
}
void Recurse(unsigned recdepth)
{
unsigned n, m;
for (n=0; n<5; n++) {
if (recdepth<9) { // simple rules
if (SimpleRule(n, recdepth)) {
Recurse(recdepth+1);
RemoveSimple(n, recdepth);
}
}
else { // Dunhill and Marlboro
for (int side=-1; side<2; side+=2)
if (DunhillRule(n, side)) {
for (m=0; m<5; m++)
if (MarlboroRule(m))
for (int r=0; r<5; r++)
result[r] = house[r];
else
RemoveMarlboro(m);
RemoveDunhill(n, side);
}
}
}
}
int main()
{
int index, i;
#ifdef WIN32
LARGE_INTEGER time0, time1, freq;
QueryPerformanceCounter(&time0);
#endif
Recurse(0);
#ifdef WIN32
QueryPerformanceCounter(&time1);
QueryPerformanceFrequency(&freq);
printf("\nComputation Time: %ld microsec\n\n",
(time1.QuadPart-time0.QuadPart)*1000000/freq.QuadPart);
#endif
if (result[0]==0) {
printf("No solution found !?!\n");
return 1;
}
for (i=0; i<5; i++)
if ((result[i]&animal)==0)
for (index=0; index<25; index++)
if (((result[i]&country)>>index)==1)
printf("Fish Owner is the %s !!!\n\n", Label[index]);
for (i=0; i<5; i++) {
printf("%d: ",i+1);
for (index=0; index<25; index++)
if (((result[i]>>index)&1)==1)
printf("%-12s",Label[index]);
printf("\n\n");
}
return 0;
}
вот как вы моделируете проблему удовлетворения двоичных ограничений
все подсказки, приведенные в загадке добавить ограничения. Без ограничений любая комбинация возможна.
Так что вы хотите сделать, это использовать ликвидация, что на самом деле является противоположным подходом к тому, что вы использовали в своих примерах. Вот как:
вам нужна матрица С одной строкой для каждой национальности и одним столбцом для каждого boolean атрибут ("живет в красном доме", "живет в синем доме", "имеет собаку",...)
каждая ячейка в этой матрице должна первоначально установите значение TRUE.
затем вы повторяете список ограничения и попытаться применить их к ваша матрица. Например, ключ "Англичанин живет в красном дом."устанавливает каждую из ячеек в столбец "красный дом" в FALSE, кроме для того, кто на английском национальность линия.
пропустить подсказки, которые ссылаются на атрибуты это еще не доказано. Например: "курильщик Winston владеет улитками.- ...ну, если еще не решено, кто курит Уинстона, а кто владеет улитками, то на время оставим это ограничение.
Это также, кстати, как вы решаете головоломки судоку и тому подобное.