#!/usr/bin/env python # coding: utf-8 # # Constraint Satisfaction Problems # --- # # Heuristics for Arc-Consistency Algorithms # # ## Introduction # A ***Constraint Satisfaction Problem*** is a triple $(X,D,C)$ where: # - $X$ is a set of variables $X_1, …, X_n$; # - $D$ is a set of domains $D_1, …, D_n$, one for each variable and each of which consists of a set of allowable values $v_1, ..., v_k$; # - $C$ is a set of constraints that specify allowable combinations of values. # # A CSP is called *arc-consistent* if every value in the domain of every variable is supported by all the neighbors of the variable while, is called *inconsistent*, if it has no solutions.
# ***Arc-consistency algorithms*** remove all unsupported values from the domains of variables making the CSP *arc-consistent* or decide that a CSP is *inconsistent* by finding that some variable has no supported values in its domain.
# Heuristics significantly enhance the efficiency of the *arc-consistency algorithms* improving their average performance in terms of *consistency-checks* which can be considered a standard measure of goodness for such algorithms. *Arc-heuristic* operate at arc-level and selects the constraint that will be used for the next check, while *domain-heuristics* operate at domain-level and selects which values will be used for the next support-check. # In[1]: from csp import * # ## Domain-Heuristics for Arc-Consistency Algorithms # In [[1]](#cite-van2002domain) are investigated the effects of a *domain-heuristic* based on the notion of a *double-support check* by studying its average time-complexity. # # The objective of *arc-consistency algorithms* is to resolve some uncertainty; it has to be know, for each $v_i \in D_i$ and for each $v_j \in D_j$, whether it is supported. # # A *single-support check*, $(v_i, v_j) \in C_{ij}$, is one in which, before the check is done, it is already known that either $v_i$ or $v_j$ are supported. # # A *double-support check* $(v_i, v_j) \in C_{ij}$, is one in which there is still, before the check, uncertainty about the support-status of both $v_i$ and $v_j$. # # If a *double-support check* is successful, two uncertainties are resolved. If a *single-support check* is successful, only one uncertainty is resolved. A good *arc-consistency algorithm*, therefore, would always choose to do a *double-support check* in preference of a *single-support check*, because the cormer offers the potential higher payback. # # The improvement with *double-support check* is that, where possible, *consistency-checks* are used to find supports for two values, one value in the domain of each variable, which were previously known to be unsupported. It is motivated by the insight that *in order to minimize the number of consistency-checks it is necessary to maximize the number of uncertainties which are resolved per check*. # ### AC-3b: an improved version of AC-3 with Double-Support Checks # As shown in [[2]](#cite-van2000improving) the idea is to use *double-support checks* to improve the average performance of `AC3` which does not exploit the fact that relations are bidirectional and results in a new general purpose *arc-consistency algorithm* called `AC3b`. # In[2]: get_ipython().run_line_magic('psource', 'AC3') # In[3]: get_ipython().run_line_magic('psource', 'revise') # At any stage in the process of making 2-variable CSP *arc-consistent* in `AC3b`: # - there is a set $S_i^+ \subseteq D_i$ whose values are all known to be supported by $X_j$; # - there is a set $S_i^? = D_i \setminus S_i^+$ whose values are unknown, as yet, to be supported by $X_j$. # # The same holds if the roles for $X_i$ and $X_j$ are exchanged. # # In order to establish support for a value $v_i^? \in S_i^?$ it seems better to try to find a support among the values in $S_j^?$ first, because for each $v_j^? \in S_j^?$ the check $(v_i^?,v_j^?) \in C_{ij}$ is a *double-support check* and it is just as likely that any $v_j^? \in S_j^?$ supports $v_i^?$ than it is that any $v_j^+ \in S_j^+$ does. Only if no support can be found among the elements in $S_j^?$, should the elements $v_j^+$ in $S_j^+$ be used for *single-support checks* $(v_i^?,v_j^+) \in C_{ij}$. After it has been decided for each value in $D_i$ whether it is supported or not, either $S_x^+ = \emptyset$ and the 2-variable CSP is *inconsistent*, or $S_x^+ \neq \emptyset$ and the CSP is *satisfiable*. In the latter case, the elements from $D_i$ which are supported by $j$ are given by $S_x^+$. The elements in $D_j$ which are supported by $x$ are given by the union of $S_j^+$ with the set of those elements of $S_j^?$ which further processing will show to be supported by some $v_i^+ \in S_x^+$. # In[4]: get_ipython().run_line_magic('psource', 'AC3b') # In[5]: get_ipython().run_line_magic('psource', 'partition') # `AC3b` is a refinement of the `AC3` algorithm which consists of the fact that if, when arc $(i,j)$ is being processed and the reverse arc $(j,i)$ is also in the queue, then consistency-checks can be saved because only support for the elements in $S_j^?$ has to be found (as opposed to support for all the elements in $D_j$ in the # `AC3` algorithm).
# `AC3b` inherits all its properties like $\mathcal{O}(ed^3)$ time-complexity and $\mathcal{O}(e + nd)$ space-complexity fron `AC3` and where $n$ denotes the number of variables in the CSP, $e$ denotes the number of binary constraints and $d$ denotes the maximum domain-size of the variables. # ## Arc-Heuristics for Arc-Consistency Algorithms # Many *arc-heuristics* can be devised, based on three major features of CSPs: # - the number of acceptable pairs in each constraint (the *constraint size* or *satisfiability*); # - the *domain size*; # - the number of binary constraints that each variable participates in, equal to the *degree* of the node of that variable in the constraint graph. # # Simple examples of heuristics that might be expected to improve the efficiency of relaxation are: # - ordering the list of variable pairs by *increasing* relative *satisfiability*; # - ordering by *increasing size of the domain* of the variable $v_j$ relaxed against $v_i$; # - ordering by *descending degree* of node of the variable relaxed. # # In
[[3]](#cite-wallace1992ordering) are investigated the effects of these *arc-heuristics* in an empirical way, experimenting the effects of them on random CSPs. Their results demonstrate that the first two, later called `sat up` and `dom j up` for n-ary and binary CSPs respectively, significantly reduce the number of *consistency-checks*. # In[6]: get_ipython().run_line_magic('psource', 'dom_j_up') # In[7]: get_ipython().run_line_magic('psource', 'sat_up') # ## Experimental Results # For the experiments below on binary CSPs, in addition to the two *arc-consistency algorithms* already cited above, `AC3` and `AC3b`, the `AC4` algorithm was used.
# The `AC4` algorithm runs in $\mathcal{O}(ed^2)$ worst-case time but can be slower than `AC3` on average cases. # In[8]: get_ipython().run_line_magic('psource', 'AC4') # ### Sudoku # #### Easy Sudoku # In[9]: sudoku = Sudoku(easy1) sudoku.display(sudoku.infer_assignment()) # In[10]: get_ipython().run_line_magic('time', '_, checks = AC3(sudoku, arc_heuristic=no_arc_heuristic)') f'AC3 needs {checks} consistency-checks' # In[11]: sudoku = Sudoku(easy1) get_ipython().run_line_magic('time', '_, checks = AC3b(sudoku, arc_heuristic=no_arc_heuristic)') f'AC3b needs {checks} consistency-checks' # In[12]: sudoku = Sudoku(easy1) get_ipython().run_line_magic('time', '_, checks = AC4(sudoku, arc_heuristic=no_arc_heuristic)') f'AC4 needs {checks} consistency-checks' # In[13]: sudoku = Sudoku(easy1) get_ipython().run_line_magic('time', '_, checks = AC3(sudoku, arc_heuristic=dom_j_up)') f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[14]: sudoku = Sudoku(easy1) get_ipython().run_line_magic('time', '_, checks = AC3b(sudoku, arc_heuristic=dom_j_up)') f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks' # In[15]: sudoku = Sudoku(easy1) get_ipython().run_line_magic('time', '_, checks = AC4(sudoku, arc_heuristic=dom_j_up)') f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[16]: backtracking_search(sudoku, select_unassigned_variable=mrv, inference=forward_checking) sudoku.display(sudoku.infer_assignment()) # #### Harder Sudoku # In[17]: sudoku = Sudoku(harder1) sudoku.display(sudoku.infer_assignment()) # In[18]: get_ipython().run_line_magic('time', '_, checks = AC3(sudoku, arc_heuristic=no_arc_heuristic)') f'AC3 needs {checks} consistency-checks' # In[19]: sudoku = Sudoku(harder1) get_ipython().run_line_magic('time', '_, checks = AC3b(sudoku, arc_heuristic=no_arc_heuristic)') f'AC3b needs {checks} consistency-checks' # In[20]: sudoku = Sudoku(harder1) get_ipython().run_line_magic('time', '_, checks = AC4(sudoku, arc_heuristic=no_arc_heuristic)') f'AC4 needs {checks} consistency-checks' # In[21]: sudoku = Sudoku(harder1) get_ipython().run_line_magic('time', '_, checks = AC3(sudoku, arc_heuristic=dom_j_up)') f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[22]: sudoku = Sudoku(harder1) get_ipython().run_line_magic('time', '_, checks = AC3b(sudoku, arc_heuristic=dom_j_up)') f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks' # In[23]: sudoku = Sudoku(harder1) get_ipython().run_line_magic('time', '_, checks = AC4(sudoku, arc_heuristic=dom_j_up)') f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[24]: backtracking_search(sudoku, select_unassigned_variable=mrv, inference=forward_checking) sudoku.display(sudoku.infer_assignment()) # ### 8 Queens # In[27]: chess = NQueensCSP(8) chess.display(chess.infer_assignment()) # In[28]: get_ipython().run_line_magic('time', '_, checks = AC3(chess, arc_heuristic=no_arc_heuristic)') f'AC3 needs {checks} consistency-checks' # In[30]: chess = NQueensCSP(8) get_ipython().run_line_magic('time', '_, checks = AC3b(chess, arc_heuristic=no_arc_heuristic)') f'AC3b needs {checks} consistency-checks' # In[32]: chess = NQueensCSP(8) get_ipython().run_line_magic('time', '_, checks = AC4(chess, arc_heuristic=no_arc_heuristic)') f'AC4 needs {checks} consistency-checks' # In[34]: chess = NQueensCSP(8) get_ipython().run_line_magic('time', '_, checks = AC3(chess, arc_heuristic=dom_j_up)') f'AC3 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[36]: chess = NQueensCSP(8) get_ipython().run_line_magic('time', '_, checks = AC3b(chess, arc_heuristic=dom_j_up)') f'AC3b with DOM J UP arc heuristic needs {checks} consistency-checks' # In[38]: chess = NQueensCSP(8) get_ipython().run_line_magic('time', '_, checks = AC4(chess, arc_heuristic=dom_j_up)') f'AC4 with DOM J UP arc heuristic needs {checks} consistency-checks' # In[39]: backtracking_search(chess, select_unassigned_variable=mrv, inference=forward_checking) chess.display(chess.infer_assignment()) # For the experiments below on n-ary CSPs, due to the n-ary constraints, the `GAC` algorithm was used.
# The `GAC` algorithm has $\mathcal{O}(er^2d^t)$ time-complexity and $\mathcal{O}(erd)$ space-complexity where $e$ denotes the number of n-ary constraints, $r$ denotes the constraint arity and $d$ denotes the maximum domain-size of the variables. # In[40]: get_ipython().run_line_magic('psource', 'ACSolver.GAC') # ### Crossword # In[41]: crossword = Crossword(crossword1, words1) crossword.display() words1 # In[36]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(crossword).GAC(arc_heuristic=no_heuristic)') f'GAC needs {checks} consistency-checks' # In[42]: crossword = Crossword(crossword1, words1) get_ipython().run_line_magic('time', '_, _, checks = ACSolver(crossword).GAC(arc_heuristic=sat_up)') f'GAC with SAT UP arc heuristic needs {checks} consistency-checks' # In[43]: crossword.display(ACSolver(crossword).domain_splitting()) # ### Kakuro # #### Easy Kakuro # In[44]: kakuro = Kakuro(kakuro2) kakuro.display() # In[45]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC(arc_heuristic=no_heuristic)') f'GAC needs {checks} consistency-checks' # In[46]: kakuro = Kakuro(kakuro2) get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)') f'GAC with SAT UP arc heuristic needs {checks} consistency-checks' # In[47]: kakuro.display(ACSolver(kakuro).domain_splitting()) # #### Medium Kakuro # In[48]: kakuro = Kakuro(kakuro3) kakuro.display() # In[49]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC(arc_heuristic=no_heuristic)') f'GAC needs {checks} consistency-checks' # In[50]: kakuro = Kakuro(kakuro3) get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)') f'GAC with SAT UP arc heuristic needs {checks} consistency-checks' # In[51]: kakuro.display(ACSolver(kakuro).domain_splitting()) # #### Harder Kakuro # In[52]: kakuro = Kakuro(kakuro4) kakuro.display() # In[53]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC()') f'GAC needs {checks} consistency-checks' # In[54]: kakuro = Kakuro(kakuro4) get_ipython().run_line_magic('time', '_, _, checks = ACSolver(kakuro).GAC(arc_heuristic=sat_up)') f'GAC with SAT UP arc heuristic needs {checks} consistency-checks' # In[55]: kakuro.display(ACSolver(kakuro).domain_splitting()) # ### Cryptarithmetic Puzzle # $$ # \begin{array}{@{}r@{}} # S E N D \\ # {} + M O R E \\ # \hline # M O N E Y # \end{array} # $$ # In[57]: cryptarithmetic = NaryCSP( {'S': set(range(1, 10)), 'M': set(range(1, 10)), 'E': set(range(0, 10)), 'N': set(range(0, 10)), 'D': set(range(0, 10)), 'O': set(range(0, 10)), 'R': set(range(0, 10)), 'Y': set(range(0, 10)), 'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2)), 'C4': set(range(0, 2))}, [Constraint(('S', 'E', 'N', 'D', 'M', 'O', 'R', 'Y'), all_diff), Constraint(('D', 'E', 'Y', 'C1'), lambda d, e, y, c1: d + e == y + 10 * c1), Constraint(('N', 'R', 'E', 'C1', 'C2'), lambda n, r, e, c1, c2: c1 + n + r == e + 10 * c2), Constraint(('E', 'O', 'N', 'C2', 'C3'), lambda e, o, n, c2, c3: c2 + e + o == n + 10 * c3), Constraint(('S', 'M', 'O', 'C3', 'C4'), lambda s, m, o, c3, c4: c3 + s + m == o + 10 * c4), Constraint(('M', 'C4'), eq)]) # In[52]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(cryptarithmetic).GAC(arc_heuristic=no_heuristic)') f'GAC needs {checks} consistency-checks' # In[58]: get_ipython().run_line_magic('time', '_, _, checks = ACSolver(cryptarithmetic).GAC(arc_heuristic=sat_up)') f'GAC with SAT UP arc heuristic needs {checks} consistency-checks' # In[59]: assignment = ACSolver(cryptarithmetic).domain_splitting() from IPython.display import Latex display(Latex(r'\begin{array}{@{}r@{}} ' + '{}{}{}{}'.format(assignment['S'], assignment['E'], assignment['N'], assignment['D']) + r' \\ + ' + '{}{}{}{}'.format(assignment['M'], assignment['O'], assignment['R'], assignment['E']) + r' \\ \hline ' + '{}{}{}{}{}'.format(assignment['M'], assignment['O'], assignment['N'], assignment['E'], assignment['Y']) + ' \end{array}')) # ## References # #
[[1]](#ref-1) Van Dongen, Marc RC. 2002. _Domain-heuristics for arc-consistency algorithms_. # # [[2]](#ref-2) Van Dongen, MRC and Bowen, JA. 2000. _Improving arc-consistency algorithms with double-support checks_. # # [[3]](#ref-3) Wallace, Richard J and Freuder, Eugene Charles. 1992. _Ordering heuristics for arc consistency algorithms_.