-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsolver.go
executable file
·221 lines (189 loc) · 6.62 KB
/
solver.go
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
package solveuranimateur
import (
"github.com/wkschwartz/pigosat"
)
func Solve() ([]Solution, Distribution) {
p := newProblem(constClues, Numbers, Persons)
// Compute all the solutions.
solutions := p.solve()
// Get the probability distribution.
distribution := p.getDistribution(solutions)
return solutions, distribution
}
type problem struct {
clues []clue
numbers numbersMap
persons personsMap
// Mapping of the persons indexes.
personLocalToGlobal map[localPersonIndex]PersonIndex
personGlobalToLocal map[PersonIndex]localPersonIndex
}
func newProblem(clues []clue, numbers numbersMap, persons personsMap) *problem {
p := &problem{
clues: clues,
numbers: numbers,
persons: persons,
personLocalToGlobal: make(map[localPersonIndex]PersonIndex),
personGlobalToLocal: make(map[PersonIndex]localPersonIndex),
}
p.mapPersons()
return p
}
// Solves the problem and returns all the solutions.
func (p *problem) solve() []Solution {
blocked := p.getBlockedAssociations()
var formula pigosat.Formula
formula = append(formula, p.getClausesBlockAssociations(blocked)...)
formula = append(formula, p.getClausesPersonExactlyOneNumber()...)
formula = append(formula, p.getClausesNumberAtMostOnePerson()...)
solutions := p.getAllSolutions(formula)
p.assertSolutions(solutions)
return solutions
}
// Associations associates persons to their number value.
type associations map[PersonIndex]NumberIndex
// Computes the blocked associations given the clues.
func (p *problem) getBlockedAssociations() (blocked []associations) {
for _, clue := range p.clues {
pers := clue.concerns()
p.numbers.iteratePermutations(len(pers), func(perm []NumberIndex) {
vals := make(map[PersonIndex]NumberValue, len(pers))
for i := 0; i < len(pers); i++ {
vals[pers[i]] = p.numbers[perm[i]]
}
if !clue.valid(vals) {
asso := make(associations, len(pers))
for i := 0; i < len(pers); i++ {
asso[pers[i]] = perm[i]
}
// The clue is not valid for this permutation, block it.
blocked = append(blocked, asso)
}
})
}
return blocked
}
type localPersonIndex int
// Computes a linear mapping of the persons mentioned in at least one of the clues.
func (p *problem) mapPersons() {
var localIndex localPersonIndex
for _, clue := range p.clues {
for _, globalIndex := range clue.concerns() {
if _, ok := p.personGlobalToLocal[globalIndex]; !ok {
// Add to map.
p.personGlobalToLocal[globalIndex] = localIndex
p.personLocalToGlobal[localIndex] = globalIndex
localIndex++
}
}
}
}
// Returns the clauses preventing the given associations.
func (p *problem) getClausesBlockAssociations(blocked []associations) (formula pigosat.Formula) {
for _, associations := range blocked {
clause := make(pigosat.Clause, 0, len(associations))
for personIndex, numberIndex := range associations {
clause = append(clause, -p.toLiteral(p.personGlobalToLocal[personIndex], numberIndex))
}
formula = append(formula, clause)
}
return formula
}
// Returns the clauses ensuring a person has exactly one number.
func (p *problem) getClausesPersonExactlyOneNumber() (formula pigosat.Formula) {
for _, personIndex := range p.personGlobalToLocal {
literals := make([]pigosat.Literal, len(p.numbers))
for numberIndex, _ := range p.numbers {
literals[numberIndex] = p.toLiteral(personIndex, numberIndex)
}
formula = append(formula, exactlyOneOf(literals)...)
}
return formula
}
// Returns the clauses ensuring a number belongs to at most one person.
func (p *problem) getClausesNumberAtMostOnePerson() (formula pigosat.Formula) {
for numberIndex, _ := range p.numbers {
literals := make([]pigosat.Literal, 0, len(p.personGlobalToLocal))
for _, relevantIndex := range p.personGlobalToLocal {
literals = append(literals, p.toLiteral(relevantIndex, numberIndex))
}
formula = append(formula, atMostOneOf(literals)...)
}
return formula
}
func (p *problem) toLiteral(person localPersonIndex, number NumberIndex) pigosat.Literal {
return pigosat.Literal(1 + int(person)*len(p.numbers) + int(number))
}
func (p *problem) fromLiteral(literal pigosat.Literal) (relevant localPersonIndex, number NumberIndex) {
relevant = localPersonIndex(int(literal-1) / len(p.numbers))
number = NumberIndex(int(literal-1) % len(p.numbers))
return
}
// Runs the SAT solver to compute the solutions given the clauses.
func (p *problem) getAllSolutions(clauses pigosat.Formula) (solutions []Solution) {
sat, _ := pigosat.New(nil)
defer sat.Delete()
sat.Add(clauses)
for rawSolution, status := sat.Solve(); status == pigosat.Satisfiable; rawSolution, status = sat.Solve() {
sol := make(Solution)
for literal, checked := range rawSolution {
if checked {
relevantIndex, numberIndex := p.fromLiteral(pigosat.Literal(literal))
sol[p.personLocalToGlobal[relevantIndex]] = numberIndex
}
}
solutions = append(solutions, sol)
sat.BlockSolution(rawSolution)
}
return solutions
}
// Checks the solutions against the clues. Panic if it fails.
func (p *problem) assertSolutions(solutions []Solution) {
vals := make(map[PersonIndex]NumberValue, len(p.personGlobalToLocal))
for _, solution := range solutions {
for personIndex, numberIndex := range solution {
vals[personIndex] = p.numbers[numberIndex]
}
for _, clue := range p.clues {
if !clue.valid(vals) {
panic("invalid solution")
}
}
}
}
type Solution map[PersonIndex]NumberIndex
// Compute the probability distribution over the provided solutions.
func (p problem) getDistribution(solutions []Solution) Distribution {
distribution := make(Distribution)
for _, solution := range solutions {
for personIndex, numberIndex := range solution {
if distribution[personIndex] == nil {
distribution[personIndex] = make([]float64, len(p.numbers))
}
distribution[personIndex][numberIndex] += 1
}
}
for personIndex, row := range distribution {
for numberIndex, cell := range row {
// Normalization over each person row.
distribution[personIndex][numberIndex] = cell / float64(len(solutions))
}
}
return distribution
}
// Return the formula which specifies that at most one of the provided literals must be true.
func atMostOneOf(literals []pigosat.Literal) pigosat.Formula {
formula := pigosat.Formula{}
iterateLiteralsCombinations(2, literals, func(combination []pigosat.Literal) {
clause := make(pigosat.Clause, 2)
for idx, literal := range combination {
clause[idx] = -literal
}
formula = append(formula, clause)
})
return formula
}
// Return the formula which specifies that exactly one of the provided literals must be true.
func exactlyOneOf(literals []pigosat.Literal) pigosat.Formula {
return append(atMostOneOf(literals), pigosat.Clause(literals))
}