Constraint Satisfaction Problems and Functional Backtracking Search
Recently I have reviewed my notes on the online course BerkeleyX: CS188.1x, which I took years ago (I can highly recommend the course, it is of outstanding didactic quality). As I reviewed the topic Constraint Satisfaction Problems, I was wondering how the imperative pseudo code for backtracking search formulated in the script could be translated into a functional programming language.
Translating the pseudo code to real code
For this I translated the pseudo code in Python first. This seemed to me to require the least amount of effort and in fact the result is very similar to the pseudo code from the script:
counter = 0
DOMAINS = "DOMAINS"
VARIABLES = "VARIABLES"
CONSTRAINTS = "CONSTRAINTS"
FAILURE = "FAILURE"
def is_complete(assignment):
return None not in (assignment.values())
def select_unassigned_variable(variables, assignment):
for var in variables:
if assignment[var] is None:
return var
def is_consistent(assignment, constraints):
global counter
counter += 1
for constraint_violated in constraints:
if constraint_violated(assignment):
return False
return True
def init_assignment(csp):
assignment = {}
for var in csp[VARIABLES]:
assignment[var] = None
return assignment
def recursive_backtracking(assignment, csp):
if is_complete(assignment):
return assignment
var = select_unassigned_variable(csp[VARIABLES], assignment)
for value in csp[DOMAINS]:
assignment[var] = value
if is_consistent(assignment, csp[CONSTRAINTS]):
result = recursive_backtracking(assignment, csp)
if result != FAILURE:
return result
assignment[var] = None
return FAILURE
Testing the code translation
To test the code, we solve the problem of coloring the map of Australia. The constraints in this problem are simply that no two adjacent states can be the same color:
counter = 0
def eq(a, b): return a is not None and b is not None and a == b
def wa_nt(asmt): return eq(asmt["WA"], asmt["NT"])
def wa_sa(asmt): return eq(asmt["WA"], asmt["SA"])
def nt_sa(asmt): return eq(asmt["NT"], asmt["SA"])
def nt_q(asmt): return eq(asmt["NT"], asmt["Q"])
def sa_q(asmt): return eq(asmt["SA"], asmt["Q"])
def sa_nsw(asmt): return eq(asmt["SA"], asmt["NSW"])
def sa_v(asmt): return eq(asmt["SA"], asmt["V"])
def q_nsw(asmt): return eq(asmt["Q"], asmt["NSW"])
def v_t(asmt): return eq(asmt["V"], asmt["T"])
my_csp = {VARIABLES: ["WA", "NT", "Q", "NSW", "V", "SA", "T"],
DOMAINS: ["red", "green", "blue"],
CONSTRAINTS: [wa_nt, wa_sa, nt_sa, nt_q, sa_q, sa_nsw, sa_v, q_nsw, v_t]}
result = recursive_backtracking(init_assignment(my_csp), my_csp)
[counter, result]
After checking all constraints 12 times, we find a valid coloring.
A functional solution
Since the python code makes heavy use of jumping out of a method via the return
clause, a direct translation in functional code is difficult. Furthermore, the recursive call to recursive_backtracking
is not at a tail-recursive position, which is ok for pseudo code, but - depending on the size of the CSP - might be a problem for production code. In functional languages however, it is recommended to prefer Higher Order Functions over recursion, anyway.
For the functional solution, the general idea is to generate a lazy sequence of CSPs, which contain the (partial) assignment from colors to states. Every next CSP is either one step closer to the complete assignment, or - if a constraint was violated - the next branch of the backtracked assignment. We check each element of that sequence and stop as soon as an element is complete.
My functional language of choice is Clojure. In my opinion, the signal-to-noise ratio is very high in Clojure, so the algorithm gets very clear.
(def counter (atom 0))
(defn init [domains variables constraints]
{:domains domains
:variables variables
:any-constraints-violated? (apply some-fn constraints)
:assignment (zipmap variables (repeat nil))})
(defn complete? [csp]
(every? some? (vals (:assignment csp))))
(defn consistent? [{:keys [any-constraints-violated?]
:as csp}]
(swap! counter inc)
(not (any-constraints-violated? csp)))
(defn select-unassigned-variable [csp]
(first (filter (comp nil?
(partial get-in csp)
(partial conj [:assignment]))
(:variables csp))))
(defn next-csps [csp]
(map (fn [d]
(assoc-in csp
[:assignment (select-unassigned-variable csp)]
d))
(:domains csp)))
(defn backtracking-seq [csps]
(lazy-seq (if-let [[$first & $rest] (seq csps)]
(if (consistent? $first)
(cons $first
(backtracking-seq (concat (next-csps $first)
$rest)))
(backtracking-seq $rest)))))
(defn backtracking [csp]
(if-let [result (first (filter complete?
(backtracking-seq (next-csps csp))))]
result
:failure))
Testing the functional solution
(defn constraint [state-a state-b]
(fn [csp]
(if-let [a (get-in csp [:assignment state-a])]
(= a (get-in csp [:assignment state-b]))
false)))
(reset! counter 0)
(let [csp (init {:red :green :blue}
[:WA :NT :Q :NSW :V :SA :T]
{(constraint :WA :NT)
(constraint :WA :SA)
(constraint :NT :SA)
(constraint :NT :Q)
(constraint :SA :Q)
(constraint :SA :NSW)
(constraint :SA :V)
(constraint :Q :NSW)
(constraint :V :T)})
result (backtracking csp)]
[counter result])
Again, we find a complete assignment after checking all constraints 12 times.