Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aimacode
GitHub Repository: aimacode/aima-python
Path: blob/master/csp.py
615 views
1
"""CSP (Constraint Satisfaction Problems) problems and solvers. (Chapter 6)"""
2
3
import itertools
4
import random
5
import re
6
import string
7
from collections import defaultdict, Counter
8
from functools import reduce
9
from operator import eq, neg
10
11
from sortedcontainers import SortedSet
12
13
import search
14
from utils import argmin_random_tie, count, first, extend
15
16
17
class CSP(search.Problem):
18
"""This class describes finite-domain Constraint Satisfaction Problems.
19
A CSP is specified by the following inputs:
20
variables A list of variables; each is atomic (e.g. int or string).
21
domains A dict of {var:[possible_value, ...]} entries.
22
neighbors A dict of {var:[var,...]} that for each variable lists
23
the other variables that participate in constraints.
24
constraints A function f(A, a, B, b) that returns true if neighbors
25
A, B satisfy the constraint when they have values A=a, B=b
26
27
In the textbook and in most mathematical definitions, the
28
constraints are specified as explicit pairs of allowable values,
29
but the formulation here is easier to express and more compact for
30
most cases (for example, the n-Queens problem can be represented
31
in O(n) space using this notation, instead of O(n^4) for the
32
explicit representation). In terms of describing the CSP as a
33
problem, that's all there is.
34
35
However, the class also supports data structures and methods that help you
36
solve CSPs by calling a search function on the CSP. Methods and slots are
37
as follows, where the argument 'a' represents an assignment, which is a
38
dict of {var:val} entries:
39
assign(var, val, a) Assign a[var] = val; do other bookkeeping
40
unassign(var, a) Do del a[var], plus other bookkeeping
41
nconflicts(var, val, a) Return the number of other variables that
42
conflict with var=val
43
curr_domains[var] Slot: remaining consistent values for var
44
Used by constraint propagation routines.
45
The following methods are used only by graph_search and tree_search:
46
actions(state) Return a list of actions
47
result(state, action) Return a successor of state
48
goal_test(state) Return true if all constraints satisfied
49
The following are just for debugging purposes:
50
nassigns Slot: tracks the number of assignments made
51
display(a) Print a human-readable representation
52
"""
53
54
def __init__(self, variables, domains, neighbors, constraints):
55
"""Construct a CSP problem. If variables is empty, it becomes domains.keys()."""
56
super().__init__(())
57
variables = variables or list(domains.keys())
58
self.variables = variables
59
self.domains = domains
60
self.neighbors = neighbors
61
self.constraints = constraints
62
self.curr_domains = None
63
self.nassigns = 0
64
65
def assign(self, var, val, assignment):
66
"""Add {var: val} to assignment; Discard the old value if any."""
67
assignment[var] = val
68
self.nassigns += 1
69
70
def unassign(self, var, assignment):
71
"""Remove {var: val} from assignment.
72
DO NOT call this if you are changing a variable to a new value;
73
just call assign for that."""
74
if var in assignment:
75
del assignment[var]
76
77
def nconflicts(self, var, val, assignment):
78
"""Return the number of conflicts var=val has with other variables."""
79
80
# Subclasses may implement this more efficiently
81
def conflict(var2):
82
return var2 in assignment and not self.constraints(var, val, var2, assignment[var2])
83
84
return count(conflict(v) for v in self.neighbors[var])
85
86
def display(self, assignment):
87
"""Show a human-readable representation of the CSP."""
88
# Subclasses can print in a prettier way, or display with a GUI
89
print(assignment)
90
91
# These methods are for the tree and graph-search interface:
92
93
def actions(self, state):
94
"""Return a list of applicable actions: non conflicting
95
assignments to an unassigned variable."""
96
if len(state) == len(self.variables):
97
return []
98
else:
99
assignment = dict(state)
100
var = first([v for v in self.variables if v not in assignment])
101
return [(var, val) for val in self.domains[var]
102
if self.nconflicts(var, val, assignment) == 0]
103
104
def result(self, state, action):
105
"""Perform an action and return the new state."""
106
(var, val) = action
107
return state + ((var, val),)
108
109
def goal_test(self, state):
110
"""The goal is to assign all variables, with all constraints satisfied."""
111
assignment = dict(state)
112
return (len(assignment) == len(self.variables)
113
and all(self.nconflicts(variables, assignment[variables], assignment) == 0
114
for variables in self.variables))
115
116
# These are for constraint propagation
117
118
def support_pruning(self):
119
"""Make sure we can prune values from domains. (We want to pay
120
for this only if we use it.)"""
121
if self.curr_domains is None:
122
self.curr_domains = {v: list(self.domains[v]) for v in self.variables}
123
124
def suppose(self, var, value):
125
"""Start accumulating inferences from assuming var=value."""
126
self.support_pruning()
127
removals = [(var, a) for a in self.curr_domains[var] if a != value]
128
self.curr_domains[var] = [value]
129
return removals
130
131
def prune(self, var, value, removals):
132
"""Rule out var=value."""
133
self.curr_domains[var].remove(value)
134
if removals is not None:
135
removals.append((var, value))
136
137
def choices(self, var):
138
"""Return all values for var that aren't currently ruled out."""
139
return (self.curr_domains or self.domains)[var]
140
141
def infer_assignment(self):
142
"""Return the partial assignment implied by the current inferences."""
143
self.support_pruning()
144
return {v: self.curr_domains[v][0]
145
for v in self.variables if 1 == len(self.curr_domains[v])}
146
147
def restore(self, removals):
148
"""Undo a supposition and all inferences from it."""
149
for B, b in removals:
150
self.curr_domains[B].append(b)
151
152
# This is for min_conflicts search
153
154
def conflicted_vars(self, current):
155
"""Return a list of variables in current assignment that are in conflict"""
156
return [var for var in self.variables
157
if self.nconflicts(var, current[var], current) > 0]
158
159
160
# ______________________________________________________________________________
161
# Constraint Propagation with AC3
162
163
164
def no_arc_heuristic(csp, queue):
165
return queue
166
167
168
def dom_j_up(csp, queue):
169
return SortedSet(queue, key=lambda t: neg(len(csp.curr_domains[t[1]])))
170
171
172
def AC3(csp, queue=None, removals=None, arc_heuristic=dom_j_up):
173
"""[Figure 6.3]"""
174
if queue is None:
175
queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]}
176
csp.support_pruning()
177
queue = arc_heuristic(csp, queue)
178
checks = 0
179
while queue:
180
(Xi, Xj) = queue.pop()
181
revised, checks = revise(csp, Xi, Xj, removals, checks)
182
if revised:
183
if not csp.curr_domains[Xi]:
184
return False, checks # CSP is inconsistent
185
for Xk in csp.neighbors[Xi]:
186
if Xk != Xj:
187
queue.add((Xk, Xi))
188
return True, checks # CSP is satisfiable
189
190
191
def revise(csp, Xi, Xj, removals, checks=0):
192
"""Return true if we remove a value."""
193
revised = False
194
for x in csp.curr_domains[Xi][:]:
195
# If Xi=x conflicts with Xj=y for every possible y, eliminate Xi=x
196
# if all(not csp.constraints(Xi, x, Xj, y) for y in csp.curr_domains[Xj]):
197
conflict = True
198
for y in csp.curr_domains[Xj]:
199
if csp.constraints(Xi, x, Xj, y):
200
conflict = False
201
checks += 1
202
if not conflict:
203
break
204
if conflict:
205
csp.prune(Xi, x, removals)
206
revised = True
207
return revised, checks
208
209
210
# Constraint Propagation with AC3b: an improved version
211
# of AC3 with double-support domain-heuristic
212
213
def AC3b(csp, queue=None, removals=None, arc_heuristic=dom_j_up):
214
if queue is None:
215
queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]}
216
csp.support_pruning()
217
queue = arc_heuristic(csp, queue)
218
checks = 0
219
while queue:
220
(Xi, Xj) = queue.pop()
221
# Si_p values are all known to be supported by Xj
222
# Sj_p values are all known to be supported by Xi
223
# Dj - Sj_p = Sj_u values are unknown, as yet, to be supported by Xi
224
Si_p, Sj_p, Sj_u, checks = partition(csp, Xi, Xj, checks)
225
if not Si_p:
226
return False, checks # CSP is inconsistent
227
revised = False
228
for x in set(csp.curr_domains[Xi]) - Si_p:
229
csp.prune(Xi, x, removals)
230
revised = True
231
if revised:
232
for Xk in csp.neighbors[Xi]:
233
if Xk != Xj:
234
queue.add((Xk, Xi))
235
if (Xj, Xi) in queue:
236
if isinstance(queue, set):
237
# or queue -= {(Xj, Xi)} or queue.remove((Xj, Xi))
238
queue.difference_update({(Xj, Xi)})
239
else:
240
queue.difference_update((Xj, Xi))
241
# the elements in D_j which are supported by Xi are given by the union of Sj_p with the set of those
242
# elements of Sj_u which further processing will show to be supported by some vi_p in Si_p
243
for vj_p in Sj_u:
244
for vi_p in Si_p:
245
conflict = True
246
if csp.constraints(Xj, vj_p, Xi, vi_p):
247
conflict = False
248
Sj_p.add(vj_p)
249
checks += 1
250
if not conflict:
251
break
252
revised = False
253
for x in set(csp.curr_domains[Xj]) - Sj_p:
254
csp.prune(Xj, x, removals)
255
revised = True
256
if revised:
257
for Xk in csp.neighbors[Xj]:
258
if Xk != Xi:
259
queue.add((Xk, Xj))
260
return True, checks # CSP is satisfiable
261
262
263
def partition(csp, Xi, Xj, checks=0):
264
Si_p = set()
265
Sj_p = set()
266
Sj_u = set(csp.curr_domains[Xj])
267
for vi_u in csp.curr_domains[Xi]:
268
conflict = True
269
# now, in order to establish support for a value vi_u in Di it seems better to try to find a support among
270
# the values in Sj_u first, because for each vj_u in Sj_u the check (vi_u, vj_u) is a double-support check
271
# and it is just as likely that any vj_u in Sj_u supports vi_u than it is that any vj_p in Sj_p does...
272
for vj_u in Sj_u - Sj_p:
273
# double-support check
274
if csp.constraints(Xi, vi_u, Xj, vj_u):
275
conflict = False
276
Si_p.add(vi_u)
277
Sj_p.add(vj_u)
278
checks += 1
279
if not conflict:
280
break
281
# ... and only if no support can be found among the elements in Sj_u, should the elements vj_p in Sj_p be used
282
# for single-support checks (vi_u, vj_p)
283
if conflict:
284
for vj_p in Sj_p:
285
# single-support check
286
if csp.constraints(Xi, vi_u, Xj, vj_p):
287
conflict = False
288
Si_p.add(vi_u)
289
checks += 1
290
if not conflict:
291
break
292
return Si_p, Sj_p, Sj_u - Sj_p, checks
293
294
295
# Constraint Propagation with AC4
296
297
def AC4(csp, queue=None, removals=None, arc_heuristic=dom_j_up):
298
if queue is None:
299
queue = {(Xi, Xk) for Xi in csp.variables for Xk in csp.neighbors[Xi]}
300
csp.support_pruning()
301
queue = arc_heuristic(csp, queue)
302
support_counter = Counter()
303
variable_value_pairs_supported = defaultdict(set)
304
unsupported_variable_value_pairs = []
305
checks = 0
306
# construction and initialization of support sets
307
while queue:
308
(Xi, Xj) = queue.pop()
309
revised = False
310
for x in csp.curr_domains[Xi][:]:
311
for y in csp.curr_domains[Xj]:
312
if csp.constraints(Xi, x, Xj, y):
313
support_counter[(Xi, x, Xj)] += 1
314
variable_value_pairs_supported[(Xj, y)].add((Xi, x))
315
checks += 1
316
if support_counter[(Xi, x, Xj)] == 0:
317
csp.prune(Xi, x, removals)
318
revised = True
319
unsupported_variable_value_pairs.append((Xi, x))
320
if revised:
321
if not csp.curr_domains[Xi]:
322
return False, checks # CSP is inconsistent
323
# propagation of removed values
324
while unsupported_variable_value_pairs:
325
Xj, y = unsupported_variable_value_pairs.pop()
326
for Xi, x in variable_value_pairs_supported[(Xj, y)]:
327
revised = False
328
if x in csp.curr_domains[Xi][:]:
329
support_counter[(Xi, x, Xj)] -= 1
330
if support_counter[(Xi, x, Xj)] == 0:
331
csp.prune(Xi, x, removals)
332
revised = True
333
unsupported_variable_value_pairs.append((Xi, x))
334
if revised:
335
if not csp.curr_domains[Xi]:
336
return False, checks # CSP is inconsistent
337
return True, checks # CSP is satisfiable
338
339
340
# ______________________________________________________________________________
341
# CSP Backtracking Search
342
343
# Variable ordering
344
345
346
def first_unassigned_variable(assignment, csp):
347
"""The default variable order."""
348
return first([var for var in csp.variables if var not in assignment])
349
350
351
def mrv(assignment, csp):
352
"""Minimum-remaining-values heuristic."""
353
return argmin_random_tie([v for v in csp.variables if v not in assignment],
354
key=lambda var: num_legal_values(csp, var, assignment))
355
356
357
def num_legal_values(csp, var, assignment):
358
if csp.curr_domains:
359
return len(csp.curr_domains[var])
360
else:
361
return count(csp.nconflicts(var, val, assignment) == 0 for val in csp.domains[var])
362
363
364
# Value ordering
365
366
367
def unordered_domain_values(var, assignment, csp):
368
"""The default value order."""
369
return csp.choices(var)
370
371
372
def lcv(var, assignment, csp):
373
"""Least-constraining-values heuristic."""
374
return sorted(csp.choices(var), key=lambda val: csp.nconflicts(var, val, assignment))
375
376
377
# Inference
378
379
380
def no_inference(csp, var, value, assignment, removals):
381
return True
382
383
384
def forward_checking(csp, var, value, assignment, removals):
385
"""Prune neighbor values inconsistent with var=value."""
386
csp.support_pruning()
387
for B in csp.neighbors[var]:
388
if B not in assignment:
389
for b in csp.curr_domains[B][:]:
390
if not csp.constraints(var, value, B, b):
391
csp.prune(B, b, removals)
392
if not csp.curr_domains[B]:
393
return False
394
return True
395
396
397
def mac(csp, var, value, assignment, removals, constraint_propagation=AC3b):
398
"""Maintain arc consistency."""
399
return constraint_propagation(csp, {(X, var) for X in csp.neighbors[var]}, removals)
400
401
402
# The search, proper
403
404
405
def backtracking_search(csp, select_unassigned_variable=first_unassigned_variable,
406
order_domain_values=unordered_domain_values, inference=no_inference):
407
"""[Figure 6.5]"""
408
409
def backtrack(assignment):
410
if len(assignment) == len(csp.variables):
411
return assignment
412
var = select_unassigned_variable(assignment, csp)
413
for value in order_domain_values(var, assignment, csp):
414
if 0 == csp.nconflicts(var, value, assignment):
415
csp.assign(var, value, assignment)
416
removals = csp.suppose(var, value)
417
if inference(csp, var, value, assignment, removals):
418
result = backtrack(assignment)
419
if result is not None:
420
return result
421
csp.restore(removals)
422
csp.unassign(var, assignment)
423
return None
424
425
result = backtrack({})
426
assert result is None or csp.goal_test(result)
427
return result
428
429
430
# ______________________________________________________________________________
431
# Min-conflicts Hill Climbing search for CSPs
432
433
434
def min_conflicts(csp, max_steps=100000):
435
"""Solve a CSP by stochastic Hill Climbing on the number of conflicts."""
436
# Generate a complete assignment for all variables (probably with conflicts)
437
csp.current = current = {}
438
for var in csp.variables:
439
val = min_conflicts_value(csp, var, current)
440
csp.assign(var, val, current)
441
# Now repeatedly choose a random conflicted variable and change it
442
for i in range(max_steps):
443
conflicted = csp.conflicted_vars(current)
444
if not conflicted:
445
return current
446
var = random.choice(conflicted)
447
val = min_conflicts_value(csp, var, current)
448
csp.assign(var, val, current)
449
return None
450
451
452
def min_conflicts_value(csp, var, current):
453
"""Return the value that will give var the least number of conflicts.
454
If there is a tie, choose at random."""
455
return argmin_random_tie(csp.domains[var], key=lambda val: csp.nconflicts(var, val, current))
456
457
458
# ______________________________________________________________________________
459
460
461
def tree_csp_solver(csp):
462
"""[Figure 6.11]"""
463
assignment = {}
464
root = csp.variables[0]
465
X, parent = topological_sort(csp, root)
466
467
csp.support_pruning()
468
for Xj in reversed(X[1:]):
469
if not make_arc_consistent(parent[Xj], Xj, csp):
470
return None
471
472
assignment[root] = csp.curr_domains[root][0]
473
for Xi in X[1:]:
474
assignment[Xi] = assign_value(parent[Xi], Xi, csp, assignment)
475
if not assignment[Xi]:
476
return None
477
return assignment
478
479
480
def topological_sort(X, root):
481
"""Returns the topological sort of X starting from the root.
482
483
Input:
484
X is a list with the nodes of the graph
485
N is the dictionary with the neighbors of each node
486
root denotes the root of the graph.
487
488
Output:
489
stack is a list with the nodes topologically sorted
490
parents is a dictionary pointing to each node's parent
491
492
Other:
493
visited shows the state (visited - not visited) of nodes
494
495
"""
496
neighbors = X.neighbors
497
498
visited = defaultdict(lambda: False)
499
500
stack = []
501
parents = {}
502
503
build_topological(root, None, neighbors, visited, stack, parents)
504
return stack, parents
505
506
507
def build_topological(node, parent, neighbors, visited, stack, parents):
508
"""Build the topological sort and the parents of each node in the graph."""
509
visited[node] = True
510
511
for n in neighbors[node]:
512
if not visited[n]:
513
build_topological(n, node, neighbors, visited, stack, parents)
514
515
parents[node] = parent
516
stack.insert(0, node)
517
518
519
def make_arc_consistent(Xj, Xk, csp):
520
"""Make arc between parent (Xj) and child (Xk) consistent under the csp's constraints,
521
by removing the possible values of Xj that cause inconsistencies."""
522
# csp.curr_domains[Xj] = []
523
for val1 in csp.domains[Xj]:
524
keep = False # Keep or remove val1
525
for val2 in csp.domains[Xk]:
526
if csp.constraints(Xj, val1, Xk, val2):
527
# Found a consistent assignment for val1, keep it
528
keep = True
529
break
530
531
if not keep:
532
# Remove val1
533
csp.prune(Xj, val1, None)
534
535
return csp.curr_domains[Xj]
536
537
538
def assign_value(Xj, Xk, csp, assignment):
539
"""Assign a value to Xk given Xj's (Xk's parent) assignment.
540
Return the first value that satisfies the constraints."""
541
parent_assignment = assignment[Xj]
542
for val in csp.curr_domains[Xk]:
543
if csp.constraints(Xj, parent_assignment, Xk, val):
544
return val
545
546
# No consistent assignment available
547
return None
548
549
550
# ______________________________________________________________________________
551
# Map Coloring CSP Problems
552
553
554
class UniversalDict:
555
"""A universal dict maps any key to the same value. We use it here
556
as the domains dict for CSPs in which all variables have the same domain.
557
>>> d = UniversalDict(42)
558
>>> d['life']
559
42
560
"""
561
562
def __init__(self, value): self.value = value
563
564
def __getitem__(self, key): return self.value
565
566
def __repr__(self): return '{{Any: {0!r}}}'.format(self.value)
567
568
569
def different_values_constraint(A, a, B, b):
570
"""A constraint saying two neighboring variables must differ in value."""
571
return a != b
572
573
574
def MapColoringCSP(colors, neighbors):
575
"""Make a CSP for the problem of coloring a map with different colors
576
for any two adjacent regions. Arguments are a list of colors, and a
577
dict of {region: [neighbor,...]} entries. This dict may also be
578
specified as a string of the form defined by parse_neighbors."""
579
if isinstance(neighbors, str):
580
neighbors = parse_neighbors(neighbors)
581
return CSP(list(neighbors.keys()), UniversalDict(colors), neighbors, different_values_constraint)
582
583
584
def parse_neighbors(neighbors):
585
"""Convert a string of the form 'X: Y Z; Y: Z' into a dict mapping
586
regions to neighbors. The syntax is a region name followed by a ':'
587
followed by zero or more region names, followed by ';', repeated for
588
each region name. If you say 'X: Y' you don't need 'Y: X'.
589
>>> parse_neighbors('X: Y Z; Y: Z') == {'Y': ['X', 'Z'], 'X': ['Y', 'Z'], 'Z': ['X', 'Y']}
590
True
591
"""
592
dic = defaultdict(list)
593
specs = [spec.split(':') for spec in neighbors.split(';')]
594
for (A, Aneighbors) in specs:
595
A = A.strip()
596
for B in Aneighbors.split():
597
dic[A].append(B)
598
dic[B].append(A)
599
return dic
600
601
602
australia_csp = MapColoringCSP(list('RGB'), """SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: """)
603
604
usa_csp = MapColoringCSP(list('RGBY'),
605
"""WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;
606
UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX AZ;
607
ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;
608
TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;
609
LA: MS; WI: MI IL; IL: IN KY; IN: OH KY; MS: TN AL; AL: TN GA FL;
610
MI: OH IN; OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;
611
PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CT NJ;
612
NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;
613
HI: ; AK: """)
614
615
france_csp = MapColoringCSP(list('RGBY'),
616
"""AL: LO FC; AQ: MP LI PC; AU: LI CE BO RA LR MP; BO: CE IF CA FC RA
617
AU; BR: NB PL; CA: IF PI LO FC BO; CE: PL NB NH IF BO AU LI PC; FC: BO
618
CA LO AL RA; IF: NH PI CA BO CE; LI: PC CE AU MP AQ; LO: CA AL FC; LR:
619
MP AU RA PA; MP: AQ LI AU LR; NB: NH CE PL BR; NH: PI IF CE NB; NO:
620
PI; PA: LR RA; PC: PL CE LI AQ; PI: NH NO CA IF; PL: BR NB CE PC; RA:
621
AU BO FC PA LR""")
622
623
624
# ______________________________________________________________________________
625
# n-Queens Problem
626
627
628
def queen_constraint(A, a, B, b):
629
"""Constraint is satisfied (true) if A, B are really the same variable,
630
or if they are not in the same row, down diagonal, or up diagonal."""
631
return A == B or (a != b and A + a != B + b and A - a != B - b)
632
633
634
class NQueensCSP(CSP):
635
"""
636
Make a CSP for the nQueens problem for search with min_conflicts.
637
Suitable for large n, it uses only data structures of size O(n).
638
Think of placing queens one per column, from left to right.
639
That means position (x, y) represents (var, val) in the CSP.
640
The main structures are three arrays to count queens that could conflict:
641
rows[i] Number of queens in the ith row (i.e. val == i)
642
downs[i] Number of queens in the \ diagonal
643
such that their (x, y) coordinates sum to i
644
ups[i] Number of queens in the / diagonal
645
such that their (x, y) coordinates have x-y+n-1 = i
646
We increment/decrement these counts each time a queen is placed/moved from
647
a row/diagonal. So moving is O(1), as is nconflicts. But choosing
648
a variable, and a best value for the variable, are each O(n).
649
If you want, you can keep track of conflicted variables, then variable
650
selection will also be O(1).
651
>>> len(backtracking_search(NQueensCSP(8)))
652
8
653
"""
654
655
def __init__(self, n):
656
"""Initialize data structures for n Queens."""
657
CSP.__init__(self, list(range(n)), UniversalDict(list(range(n))),
658
UniversalDict(list(range(n))), queen_constraint)
659
660
self.rows = [0] * n
661
self.ups = [0] * (2 * n - 1)
662
self.downs = [0] * (2 * n - 1)
663
664
def nconflicts(self, var, val, assignment):
665
"""The number of conflicts, as recorded with each assignment.
666
Count conflicts in row and in up, down diagonals. If there
667
is a queen there, it can't conflict with itself, so subtract 3."""
668
n = len(self.variables)
669
c = self.rows[val] + self.downs[var + val] + self.ups[var - val + n - 1]
670
if assignment.get(var, None) == val:
671
c -= 3
672
return c
673
674
def assign(self, var, val, assignment):
675
"""Assign var, and keep track of conflicts."""
676
old_val = assignment.get(var, None)
677
if val != old_val:
678
if old_val is not None: # Remove old val if there was one
679
self.record_conflict(assignment, var, old_val, -1)
680
self.record_conflict(assignment, var, val, +1)
681
CSP.assign(self, var, val, assignment)
682
683
def unassign(self, var, assignment):
684
"""Remove var from assignment (if it is there) and track conflicts."""
685
if var in assignment:
686
self.record_conflict(assignment, var, assignment[var], -1)
687
CSP.unassign(self, var, assignment)
688
689
def record_conflict(self, assignment, var, val, delta):
690
"""Record conflicts caused by addition or deletion of a Queen."""
691
n = len(self.variables)
692
self.rows[val] += delta
693
self.downs[var + val] += delta
694
self.ups[var - val + n - 1] += delta
695
696
def display(self, assignment):
697
"""Print the queens and the nconflicts values (for debugging)."""
698
n = len(self.variables)
699
for val in range(n):
700
for var in range(n):
701
if assignment.get(var, '') == val:
702
ch = 'Q'
703
elif (var + val) % 2 == 0:
704
ch = '.'
705
else:
706
ch = '-'
707
print(ch, end=' ')
708
print(' ', end=' ')
709
for var in range(n):
710
if assignment.get(var, '') == val:
711
ch = '*'
712
else:
713
ch = ' '
714
print(str(self.nconflicts(var, val, assignment)) + ch, end=' ')
715
print()
716
717
718
# ______________________________________________________________________________
719
# Sudoku
720
721
722
def flatten(seqs):
723
return sum(seqs, [])
724
725
726
easy1 = '..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3..'
727
harder1 = '4173698.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......'
728
729
_R3 = list(range(3))
730
_CELL = itertools.count().__next__
731
_BGRID = [[[[_CELL() for x in _R3] for y in _R3] for bx in _R3] for by in _R3]
732
_BOXES = flatten([list(map(flatten, brow)) for brow in _BGRID])
733
_ROWS = flatten([list(map(flatten, zip(*brow))) for brow in _BGRID])
734
_COLS = list(zip(*_ROWS))
735
736
_NEIGHBORS = {v: set() for v in flatten(_ROWS)}
737
for unit in map(set, _BOXES + _ROWS + _COLS):
738
for v in unit:
739
_NEIGHBORS[v].update(unit - {v})
740
741
742
class Sudoku(CSP):
743
"""
744
A Sudoku problem.
745
The box grid is a 3x3 array of boxes, each a 3x3 array of cells.
746
Each cell holds a digit in 1..9. In each box, all digits are
747
different; the same for each row and column as a 9x9 grid.
748
>>> e = Sudoku(easy1)
749
>>> e.display(e.infer_assignment())
750
. . 3 | . 2 . | 6 . .
751
9 . . | 3 . 5 | . . 1
752
. . 1 | 8 . 6 | 4 . .
753
------+-------+------
754
. . 8 | 1 . 2 | 9 . .
755
7 . . | . . . | . . 8
756
. . 6 | 7 . 8 | 2 . .
757
------+-------+------
758
. . 2 | 6 . 9 | 5 . .
759
8 . . | 2 . 3 | . . 9
760
. . 5 | . 1 . | 3 . .
761
>>> AC3(e) # doctest: +ELLIPSIS
762
(True, ...)
763
>>> e.display(e.infer_assignment())
764
4 8 3 | 9 2 1 | 6 5 7
765
9 6 7 | 3 4 5 | 8 2 1
766
2 5 1 | 8 7 6 | 4 9 3
767
------+-------+------
768
5 4 8 | 1 3 2 | 9 7 6
769
7 2 9 | 5 6 4 | 1 3 8
770
1 3 6 | 7 9 8 | 2 4 5
771
------+-------+------
772
3 7 2 | 6 8 9 | 5 1 4
773
8 1 4 | 2 5 3 | 7 6 9
774
6 9 5 | 4 1 7 | 3 8 2
775
>>> h = Sudoku(harder1)
776
>>> backtracking_search(h, select_unassigned_variable=mrv, inference=forward_checking) is not None
777
True
778
"""
779
780
R3 = _R3
781
Cell = _CELL
782
bgrid = _BGRID
783
boxes = _BOXES
784
rows = _ROWS
785
cols = _COLS
786
neighbors = _NEIGHBORS
787
788
def __init__(self, grid):
789
"""Build a Sudoku problem from a string representing the grid:
790
the digits 1-9 denote a filled cell, '.' or '0' an empty one;
791
other characters are ignored."""
792
squares = iter(re.findall(r'\d|\.', grid))
793
domains = {var: [ch] if ch in '123456789' else '123456789'
794
for var, ch in zip(flatten(self.rows), squares)}
795
for _ in squares:
796
raise ValueError("Not a Sudoku grid", grid) # Too many squares
797
CSP.__init__(self, None, domains, self.neighbors, different_values_constraint)
798
799
def display(self, assignment):
800
def show_box(box): return [' '.join(map(show_cell, row)) for row in box]
801
802
def show_cell(cell): return str(assignment.get(cell, '.'))
803
804
def abut(lines1, lines2): return list(
805
map(' | '.join, list(zip(lines1, lines2))))
806
807
print('\n------+-------+------\n'.join(
808
'\n'.join(reduce(
809
abut, map(show_box, brow))) for brow in self.bgrid))
810
811
812
# ______________________________________________________________________________
813
# The Zebra Puzzle
814
815
816
def Zebra():
817
"""Return an instance of the Zebra Puzzle."""
818
Colors = 'Red Yellow Blue Green Ivory'.split()
819
Pets = 'Dog Fox Snails Horse Zebra'.split()
820
Drinks = 'OJ Tea Coffee Milk Water'.split()
821
Countries = 'Englishman Spaniard Norwegian Ukranian Japanese'.split()
822
Smokes = 'Kools Chesterfields Winston LuckyStrike Parliaments'.split()
823
variables = Colors + Pets + Drinks + Countries + Smokes
824
domains = {}
825
for var in variables:
826
domains[var] = list(range(1, 6))
827
domains['Norwegian'] = [1]
828
domains['Milk'] = [3]
829
neighbors = parse_neighbors("""Englishman: Red;
830
Spaniard: Dog; Kools: Yellow; Chesterfields: Fox;
831
Norwegian: Blue; Winston: Snails; LuckyStrike: OJ;
832
Ukranian: Tea; Japanese: Parliaments; Kools: Horse;
833
Coffee: Green; Green: Ivory""")
834
for type in [Colors, Pets, Drinks, Countries, Smokes]:
835
for A in type:
836
for B in type:
837
if A != B:
838
if B not in neighbors[A]:
839
neighbors[A].append(B)
840
if A not in neighbors[B]:
841
neighbors[B].append(A)
842
843
def zebra_constraint(A, a, B, b, recurse=0):
844
same = (a == b)
845
next_to = abs(a - b) == 1
846
if A == 'Englishman' and B == 'Red':
847
return same
848
if A == 'Spaniard' and B == 'Dog':
849
return same
850
if A == 'Chesterfields' and B == 'Fox':
851
return next_to
852
if A == 'Norwegian' and B == 'Blue':
853
return next_to
854
if A == 'Kools' and B == 'Yellow':
855
return same
856
if A == 'Winston' and B == 'Snails':
857
return same
858
if A == 'LuckyStrike' and B == 'OJ':
859
return same
860
if A == 'Ukranian' and B == 'Tea':
861
return same
862
if A == 'Japanese' and B == 'Parliaments':
863
return same
864
if A == 'Kools' and B == 'Horse':
865
return next_to
866
if A == 'Coffee' and B == 'Green':
867
return same
868
if A == 'Green' and B == 'Ivory':
869
return a - 1 == b
870
if recurse == 0:
871
return zebra_constraint(B, b, A, a, 1)
872
if ((A in Colors and B in Colors) or
873
(A in Pets and B in Pets) or
874
(A in Drinks and B in Drinks) or
875
(A in Countries and B in Countries) or
876
(A in Smokes and B in Smokes)):
877
return not same
878
raise Exception('error')
879
880
return CSP(variables, domains, neighbors, zebra_constraint)
881
882
883
def solve_zebra(algorithm=min_conflicts, **args):
884
z = Zebra()
885
ans = algorithm(z, **args)
886
for h in range(1, 6):
887
print('House', h, end=' ')
888
for (var, val) in ans.items():
889
if val == h:
890
print(var, end=' ')
891
print()
892
return ans['Zebra'], ans['Water'], z.nassigns, ans
893
894
895
# ______________________________________________________________________________
896
# n-ary Constraint Satisfaction Problem
897
898
class NaryCSP:
899
"""
900
A nary-CSP consists of:
901
domains : a dictionary that maps each variable to its domain
902
constraints : a list of constraints
903
variables : a set of variables
904
var_to_const: a variable to set of constraints dictionary
905
"""
906
907
def __init__(self, domains, constraints):
908
"""Domains is a variable:domain dictionary
909
constraints is a list of constraints
910
"""
911
self.variables = set(domains)
912
self.domains = domains
913
self.constraints = constraints
914
self.var_to_const = {var: set() for var in self.variables}
915
for con in constraints:
916
for var in con.scope:
917
self.var_to_const[var].add(con)
918
919
def __str__(self):
920
"""String representation of CSP"""
921
return str(self.domains)
922
923
def display(self, assignment=None):
924
"""More detailed string representation of CSP"""
925
if assignment is None:
926
assignment = {}
927
print(assignment)
928
929
def consistent(self, assignment):
930
"""assignment is a variable:value dictionary
931
returns True if all of the constraints that can be evaluated
932
evaluate to True given assignment.
933
"""
934
return all(con.holds(assignment)
935
for con in self.constraints
936
if all(v in assignment for v in con.scope))
937
938
939
class Constraint:
940
"""
941
A Constraint consists of:
942
scope : a tuple of variables
943
condition: a function that can applied to a tuple of values
944
for the variables.
945
"""
946
947
def __init__(self, scope, condition):
948
self.scope = scope
949
self.condition = condition
950
951
def __repr__(self):
952
return self.condition.__name__ + str(self.scope)
953
954
def holds(self, assignment):
955
"""Returns the value of Constraint con evaluated in assignment.
956
957
precondition: all variables are assigned in assignment
958
"""
959
return self.condition(*tuple(assignment[v] for v in self.scope))
960
961
962
def all_diff_constraint(*values):
963
"""Returns True if all values are different, False otherwise"""
964
return len(values) is len(set(values))
965
966
967
def is_word_constraint(words):
968
"""Returns True if the letters concatenated form a word in words, False otherwise"""
969
970
def isw(*letters):
971
return "".join(letters) in words
972
973
return isw
974
975
976
def meet_at_constraint(p1, p2):
977
"""Returns a function that is True when the words meet at the positions (p1, p2), False otherwise"""
978
979
def meets(w1, w2):
980
return w1[p1] == w2[p2]
981
982
meets.__name__ = "meet_at(" + str(p1) + ',' + str(p2) + ')'
983
return meets
984
985
986
def adjacent_constraint(x, y):
987
"""Returns True if x and y are adjacent numbers, False otherwise"""
988
return abs(x - y) == 1
989
990
991
def sum_constraint(n):
992
"""Returns a function that is True when the the sum of all values is n, False otherwise"""
993
994
def sumv(*values):
995
return sum(values) is n
996
997
sumv.__name__ = str(n) + "==sum"
998
return sumv
999
1000
1001
def is_constraint(val):
1002
"""Returns a function that is True when x is equal to val, False otherwise"""
1003
1004
def isv(x):
1005
return val == x
1006
1007
isv.__name__ = str(val) + "=="
1008
return isv
1009
1010
1011
def ne_constraint(val):
1012
"""Returns a function that is True when x is not equal to val, False otherwise"""
1013
1014
def nev(x):
1015
return val != x
1016
1017
nev.__name__ = str(val) + "!="
1018
return nev
1019
1020
1021
def no_heuristic(to_do):
1022
return to_do
1023
1024
1025
def sat_up(to_do):
1026
return SortedSet(to_do, key=lambda t: 1 / len([var for var in t[1].scope]))
1027
1028
1029
class ACSolver:
1030
"""Solves a CSP with arc consistency and domain splitting"""
1031
1032
def __init__(self, csp):
1033
"""a CSP solver that uses arc consistency
1034
* csp is the CSP to be solved
1035
"""
1036
self.csp = csp
1037
1038
def GAC(self, orig_domains=None, to_do=None, arc_heuristic=sat_up):
1039
"""
1040
Makes this CSP arc-consistent using Generalized Arc Consistency
1041
orig_domains: is the original domains
1042
to_do : is a set of (variable,constraint) pairs
1043
returns the reduced domains (an arc-consistent variable:domain dictionary)
1044
"""
1045
if orig_domains is None:
1046
orig_domains = self.csp.domains
1047
if to_do is None:
1048
to_do = {(var, const) for const in self.csp.constraints for var in const.scope}
1049
else:
1050
to_do = to_do.copy()
1051
domains = orig_domains.copy()
1052
to_do = arc_heuristic(to_do)
1053
checks = 0
1054
while to_do:
1055
var, const = to_do.pop()
1056
other_vars = [ov for ov in const.scope if ov != var]
1057
new_domain = set()
1058
if len(other_vars) == 0:
1059
for val in domains[var]:
1060
if const.holds({var: val}):
1061
new_domain.add(val)
1062
checks += 1
1063
# new_domain = {val for val in domains[var]
1064
# if const.holds({var: val})}
1065
elif len(other_vars) == 1:
1066
other = other_vars[0]
1067
for val in domains[var]:
1068
for other_val in domains[other]:
1069
checks += 1
1070
if const.holds({var: val, other: other_val}):
1071
new_domain.add(val)
1072
break
1073
# new_domain = {val for val in domains[var]
1074
# if any(const.holds({var: val, other: other_val})
1075
# for other_val in domains[other])}
1076
else: # general case
1077
for val in domains[var]:
1078
holds, checks = self.any_holds(domains, const, {var: val}, other_vars, checks=checks)
1079
if holds:
1080
new_domain.add(val)
1081
# new_domain = {val for val in domains[var]
1082
# if self.any_holds(domains, const, {var: val}, other_vars)}
1083
if new_domain != domains[var]:
1084
domains[var] = new_domain
1085
if not new_domain:
1086
return False, domains, checks
1087
add_to_do = self.new_to_do(var, const).difference(to_do)
1088
to_do |= add_to_do
1089
return True, domains, checks
1090
1091
def new_to_do(self, var, const):
1092
"""
1093
Returns new elements to be added to to_do after assigning
1094
variable var in constraint const.
1095
"""
1096
return {(nvar, nconst) for nconst in self.csp.var_to_const[var]
1097
if nconst != const
1098
for nvar in nconst.scope
1099
if nvar != var}
1100
1101
def any_holds(self, domains, const, env, other_vars, ind=0, checks=0):
1102
"""
1103
Returns True if Constraint const holds for an assignment
1104
that extends env with the variables in other_vars[ind:]
1105
env is a dictionary
1106
Warning: this has side effects and changes the elements of env
1107
"""
1108
if ind == len(other_vars):
1109
return const.holds(env), checks + 1
1110
else:
1111
var = other_vars[ind]
1112
for val in domains[var]:
1113
# env = dict_union(env, {var:val}) # no side effects
1114
env[var] = val
1115
holds, checks = self.any_holds(domains, const, env, other_vars, ind + 1, checks)
1116
if holds:
1117
return True, checks
1118
return False, checks
1119
1120
def domain_splitting(self, domains=None, to_do=None, arc_heuristic=sat_up):
1121
"""
1122
Return a solution to the current CSP or False if there are no solutions
1123
to_do is the list of arcs to check
1124
"""
1125
if domains is None:
1126
domains = self.csp.domains
1127
consistency, new_domains, _ = self.GAC(domains, to_do, arc_heuristic)
1128
if not consistency:
1129
return False
1130
elif all(len(new_domains[var]) == 1 for var in domains):
1131
return {var: first(new_domains[var]) for var in domains}
1132
else:
1133
var = first(x for x in self.csp.variables if len(new_domains[x]) > 1)
1134
if var:
1135
dom1, dom2 = partition_domain(new_domains[var])
1136
new_doms1 = extend(new_domains, var, dom1)
1137
new_doms2 = extend(new_domains, var, dom2)
1138
to_do = self.new_to_do(var, None)
1139
return self.domain_splitting(new_doms1, to_do, arc_heuristic) or \
1140
self.domain_splitting(new_doms2, to_do, arc_heuristic)
1141
1142
1143
def partition_domain(dom):
1144
"""Partitions domain dom into two"""
1145
split = len(dom) // 2
1146
dom1 = set(list(dom)[:split])
1147
dom2 = dom - dom1
1148
return dom1, dom2
1149
1150
1151
class ACSearchSolver(search.Problem):
1152
"""A search problem with arc consistency and domain splitting
1153
A node is a CSP"""
1154
1155
def __init__(self, csp, arc_heuristic=sat_up):
1156
self.cons = ACSolver(csp)
1157
consistency, self.domains, _ = self.cons.GAC(arc_heuristic=arc_heuristic)
1158
if not consistency:
1159
raise Exception('CSP is inconsistent')
1160
self.heuristic = arc_heuristic
1161
super().__init__(self.domains)
1162
1163
def goal_test(self, node):
1164
"""Node is a goal if all domains have 1 element"""
1165
return all(len(node[var]) == 1 for var in node)
1166
1167
def actions(self, state):
1168
var = first(x for x in state if len(state[x]) > 1)
1169
neighs = []
1170
if var:
1171
dom1, dom2 = partition_domain(state[var])
1172
to_do = self.cons.new_to_do(var, None)
1173
for dom in [dom1, dom2]:
1174
new_domains = extend(state, var, dom)
1175
consistency, cons_doms, _ = self.cons.GAC(new_domains, to_do, self.heuristic)
1176
if consistency:
1177
neighs.append(cons_doms)
1178
return neighs
1179
1180
def result(self, state, action):
1181
return action
1182
1183
1184
def ac_solver(csp, arc_heuristic=sat_up):
1185
"""Arc consistency (domain splitting interface)"""
1186
return ACSolver(csp).domain_splitting(arc_heuristic=arc_heuristic)
1187
1188
1189
def ac_search_solver(csp, arc_heuristic=sat_up):
1190
"""Arc consistency (search interface)"""
1191
from search import depth_first_tree_search
1192
solution = None
1193
try:
1194
solution = depth_first_tree_search(ACSearchSolver(csp, arc_heuristic=arc_heuristic)).state
1195
except:
1196
return solution
1197
if solution:
1198
return {var: first(solution[var]) for var in solution}
1199
1200
1201
# ______________________________________________________________________________
1202
# Crossword Problem
1203
1204
1205
csp_crossword = NaryCSP({'one_across': {'ant', 'big', 'bus', 'car', 'has'},
1206
'one_down': {'book', 'buys', 'hold', 'lane', 'year'},
1207
'two_down': {'ginger', 'search', 'symbol', 'syntax'},
1208
'three_across': {'book', 'buys', 'hold', 'land', 'year'},
1209
'four_across': {'ant', 'big', 'bus', 'car', 'has'}},
1210
[Constraint(('one_across', 'one_down'), meet_at_constraint(0, 0)),
1211
Constraint(('one_across', 'two_down'), meet_at_constraint(2, 0)),
1212
Constraint(('three_across', 'two_down'), meet_at_constraint(2, 2)),
1213
Constraint(('three_across', 'one_down'), meet_at_constraint(0, 2)),
1214
Constraint(('four_across', 'two_down'), meet_at_constraint(0, 4))])
1215
1216
crossword1 = [['_', '_', '_', '*', '*'],
1217
['_', '*', '_', '*', '*'],
1218
['_', '_', '_', '_', '*'],
1219
['_', '*', '_', '*', '*'],
1220
['*', '*', '_', '_', '_'],
1221
['*', '*', '_', '*', '*']]
1222
1223
words1 = {'ant', 'big', 'bus', 'car', 'has', 'book', 'buys', 'hold',
1224
'lane', 'year', 'ginger', 'search', 'symbol', 'syntax'}
1225
1226
1227
class Crossword(NaryCSP):
1228
1229
def __init__(self, puzzle, words):
1230
domains = {}
1231
constraints = []
1232
for i, line in enumerate(puzzle):
1233
scope = []
1234
for j, element in enumerate(line):
1235
if element == '_':
1236
var = "p" + str(j) + str(i)
1237
domains[var] = list(string.ascii_lowercase)
1238
scope.append(var)
1239
else:
1240
if len(scope) > 1:
1241
constraints.append(Constraint(tuple(scope), is_word_constraint(words)))
1242
scope.clear()
1243
if len(scope) > 1:
1244
constraints.append(Constraint(tuple(scope), is_word_constraint(words)))
1245
puzzle_t = list(map(list, zip(*puzzle)))
1246
for i, line in enumerate(puzzle_t):
1247
scope = []
1248
for j, element in enumerate(line):
1249
if element == '_':
1250
scope.append("p" + str(i) + str(j))
1251
else:
1252
if len(scope) > 1:
1253
constraints.append(Constraint(tuple(scope), is_word_constraint(words)))
1254
scope.clear()
1255
if len(scope) > 1:
1256
constraints.append(Constraint(tuple(scope), is_word_constraint(words)))
1257
super().__init__(domains, constraints)
1258
self.puzzle = puzzle
1259
1260
def display(self, assignment=None):
1261
for i, line in enumerate(self.puzzle):
1262
puzzle = ""
1263
for j, element in enumerate(line):
1264
if element == '*':
1265
puzzle += "[*] "
1266
else:
1267
var = "p" + str(j) + str(i)
1268
if assignment is not None:
1269
if isinstance(assignment[var], set) and len(assignment[var]) == 1:
1270
puzzle += "[" + str(first(assignment[var])).upper() + "] "
1271
elif isinstance(assignment[var], str):
1272
puzzle += "[" + str(assignment[var]).upper() + "] "
1273
else:
1274
puzzle += "[_] "
1275
else:
1276
puzzle += "[_] "
1277
print(puzzle)
1278
1279
1280
# ______________________________________________________________________________
1281
# Kakuro Problem
1282
1283
1284
# difficulty 0
1285
kakuro1 = [['*', '*', '*', [6, ''], [3, '']],
1286
['*', [4, ''], [3, 3], '_', '_'],
1287
[['', 10], '_', '_', '_', '_'],
1288
[['', 3], '_', '_', '*', '*']]
1289
1290
# difficulty 0
1291
kakuro2 = [
1292
['*', [10, ''], [13, ''], '*'],
1293
[['', 3], '_', '_', [13, '']],
1294
[['', 12], '_', '_', '_'],
1295
[['', 21], '_', '_', '_']]
1296
1297
# difficulty 1
1298
kakuro3 = [
1299
['*', [17, ''], [28, ''], '*', [42, ''], [22, '']],
1300
[['', 9], '_', '_', [31, 14], '_', '_'],
1301
[['', 20], '_', '_', '_', '_', '_'],
1302
['*', ['', 30], '_', '_', '_', '_'],
1303
['*', [22, 24], '_', '_', '_', '*'],
1304
[['', 25], '_', '_', '_', '_', [11, '']],
1305
[['', 20], '_', '_', '_', '_', '_'],
1306
[['', 14], '_', '_', ['', 17], '_', '_']]
1307
1308
# difficulty 2
1309
kakuro4 = [
1310
['*', '*', '*', '*', '*', [4, ''], [24, ''], [11, ''], '*', '*', '*', [11, ''], [17, ''], '*', '*'],
1311
['*', '*', '*', [17, ''], [11, 12], '_', '_', '_', '*', '*', [24, 10], '_', '_', [11, ''], '*'],
1312
['*', [4, ''], [16, 26], '_', '_', '_', '_', '_', '*', ['', 20], '_', '_', '_', '_', [16, '']],
1313
[['', 20], '_', '_', '_', '_', [24, 13], '_', '_', [16, ''], ['', 12], '_', '_', [23, 10], '_', '_'],
1314
[['', 10], '_', '_', [24, 12], '_', '_', [16, 5], '_', '_', [16, 30], '_', '_', '_', '_', '_'],
1315
['*', '*', [3, 26], '_', '_', '_', '_', ['', 12], '_', '_', [4, ''], [16, 14], '_', '_', '*'],
1316
['*', ['', 8], '_', '_', ['', 15], '_', '_', [34, 26], '_', '_', '_', '_', '_', '*', '*'],
1317
['*', ['', 11], '_', '_', [3, ''], [17, ''], ['', 14], '_', '_', ['', 8], '_', '_', [7, ''], [17, ''], '*'],
1318
['*', '*', '*', [23, 10], '_', '_', [3, 9], '_', '_', [4, ''], [23, ''], ['', 13], '_', '_', '*'],
1319
['*', '*', [10, 26], '_', '_', '_', '_', '_', ['', 7], '_', '_', [30, 9], '_', '_', '*'],
1320
['*', [17, 11], '_', '_', [11, ''], [24, 8], '_', '_', [11, 21], '_', '_', '_', '_', [16, ''], [17, '']],
1321
[['', 29], '_', '_', '_', '_', '_', ['', 7], '_', '_', [23, 14], '_', '_', [3, 17], '_', '_'],
1322
[['', 10], '_', '_', [3, 10], '_', '_', '*', ['', 8], '_', '_', [4, 25], '_', '_', '_', '_'],
1323
['*', ['', 16], '_', '_', '_', '_', '*', ['', 23], '_', '_', '_', '_', '_', '*', '*'],
1324
['*', '*', ['', 6], '_', '_', '*', '*', ['', 15], '_', '_', '_', '*', '*', '*', '*']]
1325
1326
1327
class Kakuro(NaryCSP):
1328
1329
def __init__(self, puzzle):
1330
variables = []
1331
for i, line in enumerate(puzzle):
1332
# print line
1333
for j, element in enumerate(line):
1334
if element == '_':
1335
var1 = str(i)
1336
if len(var1) == 1:
1337
var1 = "0" + var1
1338
var2 = str(j)
1339
if len(var2) == 1:
1340
var2 = "0" + var2
1341
variables.append("X" + var1 + var2)
1342
domains = {}
1343
for var in variables:
1344
domains[var] = set(range(1, 10))
1345
constraints = []
1346
for i, line in enumerate(puzzle):
1347
for j, element in enumerate(line):
1348
if element != '_' and element != '*':
1349
# down - column
1350
if element[0] != '':
1351
x = []
1352
for k in range(i + 1, len(puzzle)):
1353
if puzzle[k][j] != '_':
1354
break
1355
var1 = str(k)
1356
if len(var1) == 1:
1357
var1 = "0" + var1
1358
var2 = str(j)
1359
if len(var2) == 1:
1360
var2 = "0" + var2
1361
x.append("X" + var1 + var2)
1362
constraints.append(Constraint(x, sum_constraint(element[0])))
1363
constraints.append(Constraint(x, all_diff_constraint))
1364
# right - line
1365
if element[1] != '':
1366
x = []
1367
for k in range(j + 1, len(puzzle[i])):
1368
if puzzle[i][k] != '_':
1369
break
1370
var1 = str(i)
1371
if len(var1) == 1:
1372
var1 = "0" + var1
1373
var2 = str(k)
1374
if len(var2) == 1:
1375
var2 = "0" + var2
1376
x.append("X" + var1 + var2)
1377
constraints.append(Constraint(x, sum_constraint(element[1])))
1378
constraints.append(Constraint(x, all_diff_constraint))
1379
super().__init__(domains, constraints)
1380
self.puzzle = puzzle
1381
1382
def display(self, assignment=None):
1383
for i, line in enumerate(self.puzzle):
1384
puzzle = ""
1385
for j, element in enumerate(line):
1386
if element == '*':
1387
puzzle += "[*]\t"
1388
elif element == '_':
1389
var1 = str(i)
1390
if len(var1) == 1:
1391
var1 = "0" + var1
1392
var2 = str(j)
1393
if len(var2) == 1:
1394
var2 = "0" + var2
1395
var = "X" + var1 + var2
1396
if assignment is not None:
1397
if isinstance(assignment[var], set) and len(assignment[var]) == 1:
1398
puzzle += "[" + str(first(assignment[var])) + "]\t"
1399
elif isinstance(assignment[var], int):
1400
puzzle += "[" + str(assignment[var]) + "]\t"
1401
else:
1402
puzzle += "[_]\t"
1403
else:
1404
puzzle += "[_]\t"
1405
else:
1406
puzzle += str(element[0]) + "\\" + str(element[1]) + "\t"
1407
print(puzzle)
1408
1409
1410
# ______________________________________________________________________________
1411
# Cryptarithmetic Problem
1412
1413
# [Figure 6.2]
1414
# T W O + T W O = F O U R
1415
two_two_four = NaryCSP({'T': set(range(1, 10)), 'F': set(range(1, 10)),
1416
'W': set(range(0, 10)), 'O': set(range(0, 10)), 'U': set(range(0, 10)), 'R': set(range(0, 10)),
1417
'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2))},
1418
[Constraint(('T', 'F', 'W', 'O', 'U', 'R'), all_diff_constraint),
1419
Constraint(('O', 'R', 'C1'), lambda o, r, c1: o + o == r + 10 * c1),
1420
Constraint(('W', 'U', 'C1', 'C2'), lambda w, u, c1, c2: c1 + w + w == u + 10 * c2),
1421
Constraint(('T', 'O', 'C2', 'C3'), lambda t, o, c2, c3: c2 + t + t == o + 10 * c3),
1422
Constraint(('F', 'C3'), eq)])
1423
1424
# S E N D + M O R E = M O N E Y
1425
send_more_money = NaryCSP({'S': set(range(1, 10)), 'M': set(range(1, 10)),
1426
'E': set(range(0, 10)), 'N': set(range(0, 10)), 'D': set(range(0, 10)),
1427
'O': set(range(0, 10)), 'R': set(range(0, 10)), 'Y': set(range(0, 10)),
1428
'C1': set(range(0, 2)), 'C2': set(range(0, 2)), 'C3': set(range(0, 2)),
1429
'C4': set(range(0, 2))},
1430
[Constraint(('S', 'E', 'N', 'D', 'M', 'O', 'R', 'Y'), all_diff_constraint),
1431
Constraint(('D', 'E', 'Y', 'C1'), lambda d, e, y, c1: d + e == y + 10 * c1),
1432
Constraint(('N', 'R', 'E', 'C1', 'C2'), lambda n, r, e, c1, c2: c1 + n + r == e + 10 * c2),
1433
Constraint(('E', 'O', 'N', 'C2', 'C3'), lambda e, o, n, c2, c3: c2 + e + o == n + 10 * c3),
1434
Constraint(('S', 'M', 'O', 'C3', 'C4'), lambda s, m, o, c3, c4: c3 + s + m == o + 10 * c4),
1435
Constraint(('M', 'C4'), eq)])
1436
1437