Skip to content

Commit 17e9e6d

Browse files
Shivanirudhcclauss
authored andcommitted
DPLL algorithm (TheAlgorithms#3866)
* DPLL algorithm * Corrections complete * Formatting * Codespell hook * Corrections part 2 * Corrections v2 * Corrections v3 * Update and rename dpll.py to davis–putnam–logemann–loveland.py Co-authored-by: Christian Clauss <[email protected]>
1 parent dfdffc3 commit 17e9e6d

File tree

1 file changed

+357
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)