forked from TheAlgorithms/Python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
davisb_putnamb_logemannb_loveland.py
358 lines (309 loc) · 11.3 KB
/
davisb_putnamb_logemannb_loveland.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
#!/usr/bin/env python3
"""
Davis–Putnam–Logemann–Loveland (DPLL) algorithm is a complete, backtracking-based
search algorithm for deciding the satisfiability of propositional logic formulae in
conjunctive normal form, i.e, for solving the Conjunctive Normal Form SATisfiability
(CNF-SAT) problem.
For more information about the algorithm: https://en.wikipedia.org/wiki/DPLL_algorithm
"""
from __future__ import annotations
import random
from collections.abc import Iterable
class Clause:
"""
A clause represented in Conjunctive Normal Form.
A clause is a set of literals, either complemented or otherwise.
For example:
{A1, A2, A3'} is the clause (A1 v A2 v A3')
{A5', A2', A1} is the clause (A5' v A2' v A1)
Create model
>>> clause = Clause(["A1", "A2'", "A3"])
>>> clause.evaluate({"A1": True})
True
"""
def __init__(self, literals: list[str]) -> None:
"""
Represent the literals and an assignment in a clause."
"""
# Assign all literals to None initially
self.literals: dict[str, bool | None] = {literal: None for literal in literals}
def __str__(self) -> str:
"""
To print a clause as in Conjunctive Normal Form.
>>> str(Clause(["A1", "A2'", "A3"]))
"{A1 , A2' , A3}"
"""
return "{" + " , ".join(self.literals) + "}"
def __len__(self) -> int:
"""
To print a clause as in Conjunctive Normal Form.
>>> len(Clause([]))
0
>>> len(Clause(["A1", "A2'", "A3"]))
3
"""
return len(self.literals)
def assign(self, model: dict[str, bool | None]) -> None:
"""
Assign values to literals of the clause as given by model.
"""
for literal in self.literals:
symbol = literal[:2]
if symbol in model:
value = model[symbol]
else:
continue
if value is not None:
# Complement assignment if literal is in complemented form
if literal.endswith("'"):
value = not value
self.literals[literal] = value
def evaluate(self, model: dict[str, bool | None]) -> bool | None:
"""
Evaluates the clause with the assignments in model.
This has the following steps:
1. Return True if both a literal and its complement exist in the clause.
2. Return True if a single literal has the assignment True.
3. Return None(unable to complete evaluation) if a literal has no assignment.
4. Compute disjunction of all values assigned in clause.
"""
for literal in self.literals:
symbol = literal.rstrip("'") if literal.endswith("'") else literal + "'"
if symbol in self.literals:
return True
self.assign(model)
for value in self.literals.values():
if value in (True, None):
return value
return any(self.literals.values())
class Formula:
"""
A formula represented in Conjunctive Normal Form.
A formula is a set of clauses.
For example,
{{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1))
"""
def __init__(self, clauses: Iterable[Clause]) -> None:
"""
Represent the number of clauses and the clauses themselves.
"""
self.clauses = list(clauses)
def __str__(self) -> str:
"""
To print a formula as in Conjunctive Normal Form.
str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]))
"{{A1 , A2' , A3} , {A5' , A2' , A1}}"
"""
return "{" + " , ".join(str(clause) for clause in self.clauses) + "}"
def generate_clause() -> Clause:
"""
Randomly generate a clause.
All literals have the name Ax, where x is an integer from 1 to 5.
"""
literals = []
no_of_literals = random.randint(1, 5)
base_var = "A"
i = 0
while i < no_of_literals:
var_no = random.randint(1, 5)
var_name = base_var + str(var_no)
var_complement = random.randint(0, 1)
if var_complement == 1:
var_name += "'"
if var_name in literals:
i -= 1
else:
literals.append(var_name)
i += 1
return Clause(literals)
def generate_formula() -> Formula:
"""
Randomly generate a formula.
"""
clauses: set[Clause] = set()
no_of_clauses = random.randint(1, 10)
while len(clauses) < no_of_clauses:
clauses.add(generate_clause())
return Formula(clauses)
def generate_parameters(formula: Formula) -> tuple[list[Clause], list[str]]:
"""
Return the clauses and symbols from a formula.
A symbol is the uncomplemented form of a literal.
For example,
Symbol of A3 is A3.
Symbol of A5' is A5.
>>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])
>>> clauses, symbols = generate_parameters(formula)
>>> clauses_list = [str(i) for i in clauses]
>>> clauses_list
["{A1 , A2' , A3}", "{A5' , A2' , A1}"]
>>> symbols
['A1', 'A2', 'A3', 'A5']
"""
clauses = formula.clauses
symbols_set = []
for clause in formula.clauses:
for literal in clause.literals:
symbol = literal[:2]
if symbol not in symbols_set:
symbols_set.append(symbol)
return clauses, symbols_set
def find_pure_symbols(
clauses: list[Clause], symbols: list[str], model: dict[str, bool | None]
) -> tuple[list[str], dict[str, bool | None]]:
"""
Return pure symbols and their values to satisfy clause.
Pure symbols are symbols in a formula that exist only
in one form, either complemented or otherwise.
For example,
{ { A4 , A3 , A5' , A1 , A3' } , { A4 } , { A3 } } has
pure symbols A4, A5' and A1.
This has the following steps:
1. Ignore clauses that have already evaluated to be True.
2. Find symbols that occur only in one form in the rest of the clauses.
3. Assign value True or False depending on whether the symbols occurs
in normal or complemented form respectively.
>>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])
>>> clauses, symbols = generate_parameters(formula)
>>> pure_symbols, values = find_pure_symbols(clauses, symbols, {})
>>> pure_symbols
['A1', 'A2', 'A3', 'A5']
>>> values
{'A1': True, 'A2': False, 'A3': True, 'A5': False}
"""
pure_symbols = []
assignment: dict[str, bool | None] = {}
literals = []
for clause in clauses:
if clause.evaluate(model):
continue
for literal in clause.literals:
literals.append(literal)
for s in symbols:
sym = s + "'"
if (s in literals and sym not in literals) or (
s not in literals and sym in literals
):
pure_symbols.append(s)
for p in pure_symbols:
assignment[p] = None
for s in pure_symbols:
sym = s + "'"
if s in literals:
assignment[s] = True
elif sym in literals:
assignment[s] = False
return pure_symbols, assignment
def find_unit_clauses(
clauses: list[Clause], model: dict[str, bool | None]
) -> tuple[list[str], dict[str, bool | None]]:
"""
Returns the unit symbols and their values to satisfy clause.
Unit symbols are symbols in a formula that are:
- Either the only symbol in a clause
- Or all other literals in that clause have been assigned False
This has the following steps:
1. Find symbols that are the only occurrences in a clause.
2. Find symbols in a clause where all other literals are assigned False.
3. Assign True or False depending on whether the symbols occurs in
normal or complemented form respectively.
>>> clause1 = Clause(["A4", "A3", "A5'", "A1", "A3'"])
>>> clause2 = Clause(["A4"])
>>> clause3 = Clause(["A3"])
>>> clauses, symbols = generate_parameters(Formula([clause1, clause2, clause3]))
>>> unit_clauses, values = find_unit_clauses(clauses, {})
>>> unit_clauses
['A4', 'A3']
>>> values
{'A4': True, 'A3': True}
"""
unit_symbols = []
for clause in clauses:
if len(clause) == 1:
unit_symbols.append(next(iter(clause.literals.keys())))
else:
f_count, n_count = 0, 0
for literal, value in clause.literals.items():
if value is False:
f_count += 1
elif value is None:
sym = literal
n_count += 1
if f_count == len(clause) - 1 and n_count == 1:
unit_symbols.append(sym)
assignment: dict[str, bool | None] = {}
for i in unit_symbols:
symbol = i[:2]
assignment[symbol] = len(i) == 2
unit_symbols = [i[:2] for i in unit_symbols]
return unit_symbols, assignment
def dpll_algorithm(
clauses: list[Clause], symbols: list[str], model: dict[str, bool | None]
) -> tuple[bool | None, dict[str, bool | None] | None]:
"""
Returns the model if the formula is satisfiable, else None
This has the following steps:
1. If every clause in clauses is True, return True.
2. If some clause in clauses is False, return False.
3. Find pure symbols.
4. Find unit symbols.
>>> formula = Formula([Clause(["A4", "A3", "A5'", "A1", "A3'"]), Clause(["A4"])])
>>> clauses, symbols = generate_parameters(formula)
>>> soln, model = dpll_algorithm(clauses, symbols, {})
>>> soln
True
>>> model
{'A4': True}
"""
check_clause_all_true = True
for clause in clauses:
clause_check = clause.evaluate(model)
if clause_check is False:
return False, None
elif clause_check is None:
check_clause_all_true = False
continue
if check_clause_all_true:
return True, model
try:
pure_symbols, assignment = find_pure_symbols(clauses, symbols, model)
except RecursionError:
print("raises a RecursionError and is")
return None, {}
p = None
if len(pure_symbols) > 0:
p, value = pure_symbols[0], assignment[pure_symbols[0]]
if p:
tmp_model = model
tmp_model[p] = value
tmp_symbols = list(symbols)
if p in tmp_symbols:
tmp_symbols.remove(p)
return dpll_algorithm(clauses, tmp_symbols, tmp_model)
unit_symbols, assignment = find_unit_clauses(clauses, model)
p = None
if len(unit_symbols) > 0:
p, value = unit_symbols[0], assignment[unit_symbols[0]]
if p:
tmp_model = model
tmp_model[p] = value
tmp_symbols = list(symbols)
if p in tmp_symbols:
tmp_symbols.remove(p)
return dpll_algorithm(clauses, tmp_symbols, tmp_model)
p = symbols[0]
rest = symbols[1:]
tmp1, tmp2 = model, model
tmp1[p], tmp2[p] = True, False
return dpll_algorithm(clauses, rest, tmp1) or dpll_algorithm(clauses, rest, tmp2)
if __name__ == "__main__":
import doctest
doctest.testmod()
formula = generate_formula()
print(f"The formula {formula} is", end=" ")
clauses, symbols = generate_parameters(formula)
solution, model = dpll_algorithm(clauses, symbols, {})
if solution:
print(f"satisfiable with the assignment {model}.")
else:
print("not satisfiable.")