|
| 1 | +# Crystal port of simple-sat by Sahand Saba distributed under the MIT License. |
| 2 | +# - https://github.com/sahands/simple-sat/ |
| 3 | +# - https://sahandsaba.com/understanding-sat-by-implementing-a-simple-sat-solver-in-python.html |
| 4 | + |
| 5 | +module Shards |
| 6 | + class SAT |
| 7 | + alias Literal = Int32 |
| 8 | + alias Clause = Array(Literal) |
| 9 | + |
| 10 | + def self.from_io(io : IO) : SAT |
| 11 | + new.tap do |sat| |
| 12 | + while line = io.gets |
| 13 | + next if line.empty? || line.starts_with?('#') |
| 14 | + sat.add_clause(line) |
| 15 | + end |
| 16 | + end |
| 17 | + end |
| 18 | + |
| 19 | + def self.from_file(path : String) : SAT |
| 20 | + File.open(path) { |f| from_io(f) } |
| 21 | + end |
| 22 | + |
| 23 | + def initialize |
| 24 | + @clauses = Array(Clause).new |
| 25 | + @table = Hash(String, Literal).new |
| 26 | + @variables = Array(String).new |
| 27 | + end |
| 28 | + |
| 29 | + def add_clause(str : String) : Nil |
| 30 | + add_clause(str.split) |
| 31 | + end |
| 32 | + |
| 33 | + def add_clause(ary : Array(String)) : Nil |
| 34 | + clause = ary.map do |literal| |
| 35 | + negated = literal.starts_with?('~') ? 1 : 0 |
| 36 | + variable = literal[negated..-1] |
| 37 | + |
| 38 | + unless @table.has_key?(variable) |
| 39 | + @table[variable] = @variables.size |
| 40 | + @variables << variable |
| 41 | + end |
| 42 | + |
| 43 | + @table[variable] << 1 | negated |
| 44 | + end |
| 45 | + |
| 46 | + clause.uniq! |
| 47 | + @clauses << clause |
| 48 | + end |
| 49 | + |
| 50 | + protected def literal_to_s(literal : Literal) : String |
| 51 | + String.build { |str| literal_to_s(str, literal) } |
| 52 | + end |
| 53 | + |
| 54 | + protected def literal_to_s(io : IO, literal : Literal) : Nil |
| 55 | + io << '~' unless literal & 1 == 0 |
| 56 | + io << @variables[literal >> 1] |
| 57 | + end |
| 58 | + |
| 59 | + protected def clause_to_s(clause : Clause) : String |
| 60 | + String.build do |str| |
| 61 | + clause.each_with_index do |literal, index| |
| 62 | + str << ' ' unless index == 0 |
| 63 | + literal_to_s(str, literal) |
| 64 | + end |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | + protected def assignment_to_s(assignment : Array(Literal?), brief = false) |
| 69 | + String.build do |str| |
| 70 | + assignment.zip(@variables).compact_map do |(a, v)| |
| 71 | + if a == 0 |
| 72 | + next if brief |
| 73 | + str << '~' |
| 74 | + end |
| 75 | + str << v << ' ' |
| 76 | + end |
| 77 | + end |
| 78 | + end |
| 79 | + |
| 80 | + protected def to_assigment(assignment : Array(Literal?), brief = false) |
| 81 | + aa = Array(String).new(assignment.size) |
| 82 | + |
| 83 | + assignment.zip(@variables).each do |(a, v)| |
| 84 | + if a == 0 |
| 85 | + aa << "~#{v}" unless brief |
| 86 | + else |
| 87 | + aa << v |
| 88 | + end |
| 89 | + end |
| 90 | + |
| 91 | + aa |
| 92 | + end |
| 93 | + |
| 94 | + # Solves SAT and yields solutions. |
| 95 | + def solve(brief = true, verbose = false) : Nil |
| 96 | + watchlist = setup_watchlist |
| 97 | + assignment = Array(Literal?).new(@variables.size) { nil } |
| 98 | + |
| 99 | + solve(watchlist, assignment, 0, verbose) do |solution| |
| 100 | + yield to_assigment(solution, brief) |
| 101 | + end |
| 102 | + end |
| 103 | + |
| 104 | + # Iteratively solve SAT by assigning to variables d, d+1, ..., n-1. |
| 105 | + # Assumes variables 0, ..., d-1 are assigned so far. |
| 106 | + private def solve(watchlist, assignment, d, verbose) |
| 107 | + # The state list wil keep track of what values for which variables |
| 108 | + # we have tried so far. A value of 0 means nothing has been tried yet, |
| 109 | + # a value of 1 means False has been tried but not True, 2 means True |
| 110 | + # but not False, and 3 means both have been tried. |
| 111 | + n = @variables.size |
| 112 | + state = Array(Literal).new(n) { Literal.new(0) } |
| 113 | + |
| 114 | + loop do |
| 115 | + if d == n |
| 116 | + yield assignment |
| 117 | + d -= 1 |
| 118 | + next |
| 119 | + end |
| 120 | + |
| 121 | + # Let's try assigning a value to v. Here would be the place to insert |
| 122 | + # heuristics of which value to try first. |
| 123 | + tried_something = false |
| 124 | + |
| 125 | + {0, 1}.each do |a| |
| 126 | + if (state[d] >> a) & 1 == 0 |
| 127 | + STDERR.puts "Trying #{@variables[d]} = #{a}" if verbose |
| 128 | + |
| 129 | + tried_something = true |
| 130 | + |
| 131 | + # set the bit indicating a has been tried for d: |
| 132 | + state[d] |= 1 << a |
| 133 | + assignment[d] = a |
| 134 | + |
| 135 | + if !update_watchlist(watchlist, (d << 1) | a, assignment, verbose) |
| 136 | + assignment[d] = nil |
| 137 | + else |
| 138 | + d += 1 |
| 139 | + break |
| 140 | + end |
| 141 | + end |
| 142 | + end |
| 143 | + |
| 144 | + unless tried_something |
| 145 | + if d == 0 |
| 146 | + # can't backtrack further, no solutions: |
| 147 | + return |
| 148 | + end |
| 149 | + |
| 150 | + # backtrack: |
| 151 | + state[d] = 0 |
| 152 | + assignment[d] = nil |
| 153 | + d -= 1 |
| 154 | + end |
| 155 | + end |
| 156 | + end |
| 157 | + |
| 158 | + private def setup_watchlist |
| 159 | + watchlist = Array.new(@variables.size * 2) { Deque(Clause).new } |
| 160 | + @clauses.each { |clause| watchlist[clause[0]] << clause } |
| 161 | + watchlist |
| 162 | + end |
| 163 | + |
| 164 | + private def dump_watchlist(watchlist) |
| 165 | + STDERR.puts "Current watchlist:" |
| 166 | + |
| 167 | + watchlist.each_with_index do |w, l| |
| 168 | + STDERR << literal_to_s(l) << ": " |
| 169 | + STDERR.puts w.map { |c| clause_to_s(c) }.join(", ") |
| 170 | + end |
| 171 | + end |
| 172 | + |
| 173 | + # Updates the watch list after literal 'false_literal' was just assigned |
| 174 | + # `false`, by making any clause watching false_literal watch something else. |
| 175 | + # Returns False it is impossible to do so, meaning a clause is contradicted by |
| 176 | + # the current assignment. |
| 177 | + private def update_watchlist(watchlist, false_literal, assignment, verbose) |
| 178 | + while clause = watchlist[false_literal].first? |
| 179 | + found_alternative = false |
| 180 | + |
| 181 | + clause.each do |alternative| |
| 182 | + v = alternative >> 1 |
| 183 | + a = alternative & 1 |
| 184 | + av = assignment[v] |
| 185 | + |
| 186 | + if av.nil? || av == a ^ 1 |
| 187 | + found_alternative = true |
| 188 | + watchlist[false_literal].shift? |
| 189 | + watchlist[alternative] << clause |
| 190 | + break |
| 191 | + end |
| 192 | + end |
| 193 | + |
| 194 | + unless found_alternative |
| 195 | + if verbose |
| 196 | + dump_watchlist(watchlist) |
| 197 | + STDERR.puts "Current assignment: #{assignment_to_s(assignment)}" |
| 198 | + STDERR.puts "Clause #{clause_to_s(clause)} contradicted." |
| 199 | + end |
| 200 | + return false |
| 201 | + end |
| 202 | + end |
| 203 | + |
| 204 | + return true |
| 205 | + end |
| 206 | + end |
| 207 | +end |
0 commit comments