z3 0.0.20180629 → 0.0.20220203
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -4
- data/Rakefile +15 -8
- data/examples/abc_path +187 -0
- data/examples/abc_path-1.txt +7 -0
- data/examples/algebra_problems +12 -12
- data/examples/aquarium +133 -0
- data/examples/aquarium-1.txt +11 -0
- data/examples/bridges +2 -2
- data/examples/cats_organized_neatly +133 -0
- data/examples/cats_organized_neatly-10.txt +15 -0
- data/examples/cats_organized_neatly-3.txt +8 -0
- data/examples/cats_organized_neatly-48.txt +32 -0
- data/examples/circuit_problems +4 -4
- data/examples/clogic_puzzle +2 -2
- data/examples/color_nonogram +150 -0
- data/examples/color_nonogram-1.txt +23 -0
- data/examples/crossflip +2 -4
- data/examples/dominion +153 -0
- data/examples/dominion-1.txt +8 -0
- data/examples/dominosa +133 -0
- data/examples/dominosa-1.txt +8 -0
- data/examples/eulero +99 -0
- data/examples/eulero-1.txt +5 -0
- data/examples/four_hackers_puzzle +2 -2
- data/examples/futoshiki +128 -0
- data/examples/futoshiki-1.txt +17 -0
- data/examples/kakurasu +73 -0
- data/examples/kakurasu-1.txt +2 -0
- data/examples/kakuro +2 -2
- data/examples/killer_sudoku +88 -0
- data/examples/killer_sudoku-1.txt +17 -0
- data/examples/killer_sudoku-2.txt +53 -0
- data/examples/kinematics_problems +20 -20
- data/examples/knights_puzzle +2 -2
- data/examples/kropki +100 -0
- data/examples/kropki-1.txt +13 -0
- data/examples/letter_connections +2 -2
- data/examples/light_up +2 -2
- data/examples/minisudoku +2 -2
- data/examples/miracle_sudoku +135 -0
- data/examples/miracle_sudoku-1.txt +9 -0
- data/examples/mortal_coil_puzzle +2 -2
- data/examples/nanro +245 -0
- data/examples/nanro-1.txt +8 -0
- data/examples/nine_clocks +106 -0
- data/examples/nonogram +2 -2
- data/examples/pyramid_nonogram +2 -2
- data/examples/regexp_crossword_solver +2 -2
- data/examples/regexp_solver +2 -2
- data/examples/renzoku +124 -0
- data/examples/renzoku-1.txt +17 -0
- data/examples/sandwich_sudoku +101 -0
- data/examples/sandwich_sudoku-1.txt +10 -0
- data/examples/selfref +2 -2
- data/examples/simple_regexp_parser.rb +58 -56
- data/examples/skyscrapers +118 -0
- data/examples/skyscrapers-1.txt +6 -0
- data/examples/skyscrapers-2.txt +11 -0
- data/examples/star_battle +134 -0
- data/examples/star_battle-1.txt +10 -0
- data/examples/stitches +180 -0
- data/examples/stitches-1.txt +11 -0
- data/examples/sudoku +2 -2
- data/examples/suguru +199 -0
- data/examples/suguru-1.txt +17 -0
- data/examples/verbal_arithmetic +2 -2
- data/examples/yajilin +268 -0
- data/examples/yajilin-1.txt +10 -0
- data/lib/z3/ast.rb +8 -0
- data/lib/z3/expr/expr.rb +16 -15
- data/lib/z3/low_level.rb +6 -2
- data/lib/z3/low_level_auto.rb +180 -36
- data/lib/z3/optimize.rb +4 -4
- data/lib/z3/very_low_level.rb +8 -5
- data/lib/z3/very_low_level_auto.rb +45 -9
- data/spec/expr_spec.rb +62 -0
- data/spec/integration/abc_path_spec.rb +21 -0
- data/spec/integration/aquarium_spec.rb +27 -0
- data/spec/integration/cats_organized_neatly_spec.rb +14 -0
- data/spec/integration/color_nonogram_spec.rb +28 -0
- data/spec/integration/dominion_spec.rb +14 -0
- data/spec/integration/dominosa_spec.rb +21 -0
- data/spec/integration/eulero_spec.rb +11 -0
- data/spec/integration/futoshiki_spec.rb +23 -0
- data/spec/integration/kakurasu_spec.rb +18 -0
- data/spec/integration/killer_sudoku_spec.rb +10 -0
- data/spec/integration/knights_puzzle_spec.rb +11 -11
- data/spec/integration/kropki_spec.rb +19 -0
- data/spec/integration/miracle_sudoku_spec.rb +15 -0
- data/spec/integration/mortal_coil_puzzle_spec.rb +8 -6
- data/spec/integration/nanro_spec.rb +39 -0
- data/spec/integration/nine_clocks_spec.rb +30 -0
- data/spec/integration/oneofus_spec.rb +7 -15
- data/spec/integration/regexp_crossword_solver_spec.rb +1 -1
- data/spec/integration/renzoku_spec.rb +23 -0
- data/spec/integration/sandwich_sudoku_spec.rb +15 -0
- data/spec/integration/skyscraper_spec.rb +10 -0
- data/spec/integration/star_battle_spec.rb +27 -0
- data/spec/integration/stitches_spec.rb +25 -0
- data/spec/integration/suguru_spec.rb +23 -0
- data/spec/integration/yajilin_spec.rb +25 -0
- data/spec/optimize_spec.rb +6 -4
- data/spec/set_expr_spec.rb +14 -8
- data/spec/solver_spec.rb +4 -5
- data/spec/spec_helper.rb +15 -0
- metadata +105 -25
data/examples/stitches
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require_relative "../lib/z3"
|
5
|
+
require "paint"
|
6
|
+
|
7
|
+
class StitchesSolver
|
8
|
+
def initialize(path)
|
9
|
+
data = Pathname(path).read
|
10
|
+
data = data.strip.split("\n").map do |line|
|
11
|
+
line.split
|
12
|
+
end
|
13
|
+
@col_counts = data.shift.map(&:to_i)
|
14
|
+
@row_counts = data.map(&:shift).map(&:to_i)
|
15
|
+
@xsize = @col_counts.size
|
16
|
+
@ysize = @row_counts.size
|
17
|
+
@data = data
|
18
|
+
raise unless data.size == @ysize
|
19
|
+
raise unless data.all?{|r| r.size == @ysize}
|
20
|
+
raise unless @col_counts.sum == @row_counts.sum
|
21
|
+
@solver = Z3::Solver.new
|
22
|
+
@groups = @data.flatten.uniq
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
assign_colors!
|
27
|
+
setup_cell_vars!
|
28
|
+
setup_stitch_vars!
|
29
|
+
|
30
|
+
setup_cell_assertions!
|
31
|
+
setup_stitch_assertions!
|
32
|
+
setup_cell_stitch_assertions!
|
33
|
+
|
34
|
+
if @solver.satisfiable?
|
35
|
+
@model = @solver.model
|
36
|
+
print_answer!
|
37
|
+
else
|
38
|
+
puts "failed to solve"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def assign_colors!
|
43
|
+
@fg = @groups.to_h{|g| [g, [rand(256),rand(256),rand(256)]] }
|
44
|
+
@bg = @groups.to_h{|g| [g, [rand(256),rand(256),rand(256)]] }
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_cell_vars!
|
48
|
+
@cell_vars = {}
|
49
|
+
@xsize.times do |x|
|
50
|
+
@ysize.times do |y|
|
51
|
+
v = Z3.Bool("c#{x},#{y}")
|
52
|
+
@cell_vars[[x,y]] = v
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def row_of_cell_vars(y)
|
58
|
+
@xsize.times.map{|x| @cell_vars[[x,y]].ite(1,0) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def col_of_cell_vars(x)
|
62
|
+
@ysize.times.map{|y| @cell_vars[[x,y]].ite(1,0) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup_cell_assertions!
|
66
|
+
@xsize.times do |x|
|
67
|
+
@solver.assert Z3.Add(*col_of_cell_vars(x)) == @col_counts[x]
|
68
|
+
end
|
69
|
+
|
70
|
+
@ysize.times do |y|
|
71
|
+
@solver.assert Z3.Add(*row_of_cell_vars(y)) == @row_counts[y]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_stitch_vars!
|
76
|
+
@hstitch = {}
|
77
|
+
@vstitch = {}
|
78
|
+
@stitch_groups = {}
|
79
|
+
|
80
|
+
(@xsize-1).times do |x|
|
81
|
+
@ysize.times do |y|
|
82
|
+
v = Z3.Bool("h#{x},#{y}")
|
83
|
+
g1 = @data[y][x]
|
84
|
+
g2 = @data[y][x+1]
|
85
|
+
pair = [g1,g2].sort.uniq
|
86
|
+
next if pair.size == 1
|
87
|
+
@stitch_groups[pair] ||= []
|
88
|
+
@stitch_groups[pair] << v
|
89
|
+
@hstitch[[x,y]] = v
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@xsize.times do |x|
|
94
|
+
(@ysize-1).times do |y|
|
95
|
+
v = Z3.Bool("v#{x},#{y}")
|
96
|
+
g1 = @data[y][x]
|
97
|
+
g2 = @data[y+1][x]
|
98
|
+
pair = [g1,g2].sort.uniq
|
99
|
+
next if pair.size == 1
|
100
|
+
@stitch_groups[pair] ||= []
|
101
|
+
@stitch_groups[pair] << v
|
102
|
+
@vstitch[[x,y]] = v
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
group_count = (2 * @stitch_groups.keys.size)
|
107
|
+
raise unless @col_counts.sum % group_count == 0
|
108
|
+
@stitch_count = @col_counts.sum / group_count
|
109
|
+
end
|
110
|
+
|
111
|
+
def setup_stitch_assertions!
|
112
|
+
@stitch_groups.each do |g, vars|
|
113
|
+
@solver.assert Z3.Add(*vars.map{|v| v.ite(1,0)}) == @stitch_count
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def setup_cell_stitch_assertions!
|
118
|
+
@xsize.times do |x|
|
119
|
+
@ysize.times do |y|
|
120
|
+
vars = [
|
121
|
+
@hstitch[[x,y]],
|
122
|
+
@hstitch[[x-1,y]],
|
123
|
+
@vstitch[[x,y]],
|
124
|
+
@vstitch[[x,y-1]],
|
125
|
+
].compact.map{|v| v.ite(1,0)}
|
126
|
+
if vars.empty?
|
127
|
+
@solver.assert !@cell_vars[[x,y]]
|
128
|
+
else
|
129
|
+
@solver.assert @cell_vars[[x,y]].ite(1,0) == Z3.Add(*vars)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def group_at(x,y)
|
136
|
+
return nil if x < 0 or y < 0
|
137
|
+
return nil if x >= @xsize or y >= @ysize
|
138
|
+
@data[y][x]
|
139
|
+
end
|
140
|
+
|
141
|
+
def paint_for(s,x1,y1,x2=x1,y2=y1)
|
142
|
+
g1 = group_at(x1,y1)
|
143
|
+
g2 = group_at(x2,y2)
|
144
|
+
if g1 == g2
|
145
|
+
Paint[s, @fg[g1], @bg[g1]]
|
146
|
+
else
|
147
|
+
s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def print_answer!
|
152
|
+
@ysize.times do |y|
|
153
|
+
@xsize.times do |x|
|
154
|
+
s = @model[@cell_vars[[x,y]]].to_b ? "*" : " "
|
155
|
+
print paint_for(s, x, y)
|
156
|
+
if @hstitch[[x,y]]
|
157
|
+
hs = @model[@hstitch[[x,y]]].to_b ? "-" : " "
|
158
|
+
else
|
159
|
+
hs = " "
|
160
|
+
end
|
161
|
+
print paint_for(hs, x, y, x+1, y)
|
162
|
+
end
|
163
|
+
print "\n"
|
164
|
+
next if y == @ysize - 1
|
165
|
+
@xsize.times do |x|
|
166
|
+
if @vstitch[[x,y]]
|
167
|
+
vs = @model[@vstitch[[x,y]]].to_b ? "|" : " "
|
168
|
+
else
|
169
|
+
vs = " "
|
170
|
+
end
|
171
|
+
print paint_for(vs, x, y, x, y+1)
|
172
|
+
print paint_for(" ", x, y, x+1, y+1)
|
173
|
+
end
|
174
|
+
print "\n"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
path = ARGV[0] || Pathname(__dir__) + "stitches-1.txt"
|
180
|
+
StitchesSolver.new(path).call
|
@@ -0,0 +1,11 @@
|
|
1
|
+
6 8 6 9 2 5 4 8 2 2
|
2
|
+
3 a a a a a a a b b b
|
3
|
+
6 a a c c c a a b c b
|
4
|
+
4 a f f c c b b b c b
|
5
|
+
6 f f f f c c c c c b
|
6
|
+
8 h h h f c h c b b b
|
7
|
+
4 g h h h h h c c b b
|
8
|
+
6 g g e e e h h h e b
|
9
|
+
8 g g g g e h h e e e
|
10
|
+
5 d d e e e e e e e e
|
11
|
+
2 d d d d e e e e e e
|
data/examples/sudoku
CHANGED
@@ -13,7 +13,7 @@ class SudokuSolver
|
|
13
13
|
@solver = Z3::Solver.new
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def call
|
17
17
|
@cells = (0..8).map do |j|
|
18
18
|
(0..8).map do |i|
|
19
19
|
cell_var(@data[j][i], i, j)
|
@@ -58,4 +58,4 @@ class SudokuSolver
|
|
58
58
|
end
|
59
59
|
|
60
60
|
path = ARGV[0] || Pathname(__dir__) + "sudoku-1.txt"
|
61
|
-
SudokuSolver.new(path).
|
61
|
+
SudokuSolver.new(path).call
|
data/examples/suguru
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require_relative "../lib/z3"
|
5
|
+
|
6
|
+
class Suguru
|
7
|
+
def initialize(path)
|
8
|
+
@data = Pathname(path).readlines.map(&:chomp)
|
9
|
+
@ysize = (@data.size - 1) / 2
|
10
|
+
@xsize = (@data[0].size - 1) / 2
|
11
|
+
raise unless @data.size == 2 * @ysize + 1
|
12
|
+
raise unless @data.all?{ |row| row.size == 2 * @xsize + 1 }
|
13
|
+
@solver = Z3::Solver.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
@connections = calculate_connections
|
18
|
+
@containers = calculate_containers
|
19
|
+
|
20
|
+
@ysize.times do |y|
|
21
|
+
@ysize.times do |x|
|
22
|
+
v = cell_data(x, y)
|
23
|
+
if v
|
24
|
+
@solver.assert cell(x, y) == v
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@containers.each do |name, cells|
|
30
|
+
@solver.assert Z3.Distinct(*cells)
|
31
|
+
cells.each do |c|
|
32
|
+
@solver.assert c >= 1
|
33
|
+
@solver.assert c <= cells.size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@ysize.times do |y|
|
38
|
+
@xsize.times do |x|
|
39
|
+
@solver.assert cell(x, y) != cell(x+1, y)
|
40
|
+
@solver.assert cell(x, y) != cell(x-1, y+1)
|
41
|
+
@solver.assert cell(x, y) != cell(x, y+1)
|
42
|
+
@solver.assert cell(x, y) != cell(x+1, y+1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if @solver.satisfiable?
|
47
|
+
@model = @solver.model
|
48
|
+
print_answer
|
49
|
+
else
|
50
|
+
puts "failed to solve"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def calculate_containers
|
57
|
+
cells = @xsize.times.flat_map{|x| @ysize.times.map{|y| [x,y]}}
|
58
|
+
|
59
|
+
containers = {}
|
60
|
+
cell_assignments = {}
|
61
|
+
cells.each do |c|
|
62
|
+
ct = c.join(",")
|
63
|
+
containers[ct] = [c]
|
64
|
+
cell_assignments[c] = ct
|
65
|
+
end
|
66
|
+
|
67
|
+
@connections.each do |c1, c2|
|
68
|
+
# Already in same container
|
69
|
+
ct1 = cell_assignments[c1]
|
70
|
+
ct2 = cell_assignments[c2]
|
71
|
+
next if ct1 == ct2
|
72
|
+
containers.delete(ct2).each do |c3|
|
73
|
+
cell_assignments[c3] = ct1
|
74
|
+
containers[ct1] << c3
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
containers.transform_values{|cells| cells.map{|x,y| cell(x,y)}}
|
79
|
+
end
|
80
|
+
|
81
|
+
def calculate_connections
|
82
|
+
result = []
|
83
|
+
|
84
|
+
@ysize.times do |y|
|
85
|
+
(0..@xsize-2).each do |x|
|
86
|
+
if cell_open_right?(x, y)
|
87
|
+
result << [[x,y], [x+1,y]]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@xsize.times do |x|
|
93
|
+
(0..@ysize-2).each do |y|
|
94
|
+
if cell_open_down?(x, y)
|
95
|
+
result << [[x,y], [x,y+1]]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
def cell_open_right?(x, y)
|
104
|
+
@data[2*y+1][2*x+2] == " "
|
105
|
+
end
|
106
|
+
|
107
|
+
def cell_open_down?(x, y)
|
108
|
+
@data[2*y+2][2*x+1] == " "
|
109
|
+
end
|
110
|
+
|
111
|
+
def cell_data(x, y)
|
112
|
+
v = @data[2*y+1][2*x+1]
|
113
|
+
return nil if v == " "
|
114
|
+
v.to_i
|
115
|
+
end
|
116
|
+
|
117
|
+
def cell(x, y)
|
118
|
+
Z3.Int("c#{x},#{y}")
|
119
|
+
end
|
120
|
+
|
121
|
+
def print_answer
|
122
|
+
output = @data.map(&:dup)
|
123
|
+
@ysize.times do |y|
|
124
|
+
@xsize.times do |x|
|
125
|
+
# This only works for 1-9, could use hex or something for more
|
126
|
+
value = @model[cell(x,y)].to_s
|
127
|
+
output[2*y+1][2*x+1, 1] = value
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
puts output
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
path = ARGV[0] || Pathname(__dir__) + "suguru-1.txt"
|
136
|
+
Suguru.new(path).call
|
137
|
+
|
138
|
+
|
139
|
+
__END__
|
140
|
+
|
141
|
+
# Based on https://play.google.com/store/apps/details?id=com.alexuvarov.android.kropki.puzzle&hl=en_GB
|
142
|
+
#
|
143
|
+
# NxN grid
|
144
|
+
# Each cell contains numbers 1 to N
|
145
|
+
# Each row and each column contains distinct numbers
|
146
|
+
# If there's black dot (o) between cells, one is twice the other
|
147
|
+
# If there's white dot (*) between cells, one is the other plus one
|
148
|
+
# If there's no dot, neither of these is true
|
149
|
+
# (for 1/2, dot can be of either color)
|
150
|
+
|
151
|
+
class Kropki
|
152
|
+
def call
|
153
|
+
# Cells contain numbers 1 - N
|
154
|
+
@size.times do |y|
|
155
|
+
@size.times do |x|
|
156
|
+
@solver.assert cell(x, y) >= 1
|
157
|
+
@solver.assert cell(x, y) <= @size
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Each row and column distinct
|
162
|
+
@size.times do |y|
|
163
|
+
@solver.assert Z3.Distinct(*@size.times.map{ |x| cell(x,y) })
|
164
|
+
end
|
165
|
+
@size.times do |x|
|
166
|
+
@solver.assert Z3.Distinct(*@size.times.map{ |y| cell(x,y) })
|
167
|
+
end
|
168
|
+
|
169
|
+
# Horizontal dots
|
170
|
+
@size.times do |y|
|
171
|
+
(0...@size).each do |x|
|
172
|
+
dot_constraints cell(x, y), cell(x+1, y), @data[2*y+1][2*x+2]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Vertical dots
|
177
|
+
@size.times do |x|
|
178
|
+
(0...@size).each do |y|
|
179
|
+
dot_constraints cell(x, y), cell(x, y+1), @data[2*y+2][2*x+1]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def dot_constraints(c1, c2, dot)
|
187
|
+
if dot == "o"
|
188
|
+
@solver.assert Z3.Or(c1 == c2 + 1, c2 == c1 + 1)
|
189
|
+
elsif dot == "*"
|
190
|
+
@solver.assert Z3.Or(c1 == c2 * 2, c2 == c1 * 2)
|
191
|
+
else
|
192
|
+
@solver.assert c1 != c2 + 1
|
193
|
+
@solver.assert c2 != c1 + 1
|
194
|
+
@solver.assert c1 != c2 * 2
|
195
|
+
@solver.assert c2 != c1 * 2
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
data/examples/verbal_arithmetic
CHANGED
@@ -16,7 +16,7 @@ class VerbalArithmetic
|
|
16
16
|
@c = c.chars.map{|v| @vars[v]}
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
19
|
+
def call
|
20
20
|
@solver.assert @a[0] != 0
|
21
21
|
@solver.assert @b[0] != 0
|
22
22
|
@solver.assert @c[0] != 0
|
@@ -44,4 +44,4 @@ class VerbalArithmetic
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
VerbalArithmetic.new("SEND", "MORE", "MONEY").
|
47
|
+
VerbalArithmetic.new("SEND", "MORE", "MONEY").call
|
data/examples/yajilin
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require_relative "../lib/z3"
|
5
|
+
|
6
|
+
class Yajilin
|
7
|
+
def initialize(path)
|
8
|
+
@data = Pathname(path)
|
9
|
+
.readlines
|
10
|
+
.map{|line|
|
11
|
+
line
|
12
|
+
.chomp
|
13
|
+
.split
|
14
|
+
.map{|x| x == "." ? nil : x}
|
15
|
+
}
|
16
|
+
@xsize = @data.size
|
17
|
+
@ysize = @data[0].size
|
18
|
+
raise unless @data.all?{|row| row.size == @xsize}
|
19
|
+
@solver = Z3::Solver.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_board?(x,y)
|
23
|
+
x >= 0 and y >= 0 and x < @xsize and y < @ysize
|
24
|
+
end
|
25
|
+
|
26
|
+
# true = white
|
27
|
+
# false = black
|
28
|
+
def cvar(x,y)
|
29
|
+
return unless on_board?(x,y)
|
30
|
+
Z3.Bool("c[#{x},#{y}]")
|
31
|
+
end
|
32
|
+
|
33
|
+
# true - connected
|
34
|
+
# right from (x,y)
|
35
|
+
def hvar(x,y)
|
36
|
+
return unless on_board?(x,y) and x != @xsize - 1
|
37
|
+
Z3.Bool("h[#{x},#{y}]")
|
38
|
+
end
|
39
|
+
|
40
|
+
# true - connected
|
41
|
+
# down from (x,y)
|
42
|
+
def vvar(x,y)
|
43
|
+
return unless on_board?(x,y) and y != @ysize - 1
|
44
|
+
Z3.Bool("v[#{x},#{y}]")
|
45
|
+
end
|
46
|
+
|
47
|
+
# Every cell has a number that shows its position within the loop
|
48
|
+
def nvar(x,y)
|
49
|
+
Z3.Int("n[#{x},#{y}]")
|
50
|
+
end
|
51
|
+
|
52
|
+
def hint_cell?(x,y)
|
53
|
+
!!@data[y][x]
|
54
|
+
end
|
55
|
+
|
56
|
+
def neighbours(x,y)
|
57
|
+
[
|
58
|
+
[x+1, y ],
|
59
|
+
[x-1, y ],
|
60
|
+
[x, y+1],
|
61
|
+
[x, y-1],
|
62
|
+
].select{|nx,ny|
|
63
|
+
on_board?(nx, ny) and !hint_cell?(nx,ny)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def neighbour_cvars(x,y)
|
68
|
+
neighbours(x,y)
|
69
|
+
.map{|nx,ny| cvar(nx, ny)}
|
70
|
+
end
|
71
|
+
|
72
|
+
def connections_at(x,y)
|
73
|
+
[
|
74
|
+
hvar(x,y),
|
75
|
+
hvar(x-1,y),
|
76
|
+
vvar(x,y),
|
77
|
+
vvar(x,y-1),
|
78
|
+
].compact
|
79
|
+
end
|
80
|
+
|
81
|
+
def count_loops_at(x,y)
|
82
|
+
Z3.Add(*connections_at(x,y).map{|n| n.ite(1,0)})
|
83
|
+
end
|
84
|
+
|
85
|
+
def assert_connections
|
86
|
+
each_xy do |x,y|
|
87
|
+
if hint_cell?(x, y)
|
88
|
+
@solver.assert count_loops_at(x,y) == 0
|
89
|
+
else
|
90
|
+
@solver.assert cvar(x,y).implies(count_loops_at(x,y) == 2)
|
91
|
+
@solver.assert (~cvar(x,y)).implies(count_loops_at(x,y) == 0)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def assert_no_black_multiples
|
97
|
+
each_xy do |x,y|
|
98
|
+
next if hint_cell?(x, y)
|
99
|
+
@solver.assert (~cvar(x,y)).implies(Z3.And(*neighbour_cvars(x,y)))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def stripe_left(x0,y)
|
104
|
+
(0...x0).map{|x| cvar(x,y) unless hint_cell?(x,y) }.compact
|
105
|
+
end
|
106
|
+
|
107
|
+
def stripe_right(x0,y)
|
108
|
+
(x0+1...@xsize).map{|x| cvar(x,y) unless hint_cell?(x,y) }.compact
|
109
|
+
end
|
110
|
+
|
111
|
+
def stripe_up(x,y0)
|
112
|
+
(0...y0).map{|y| cvar(x,y) unless hint_cell?(x,y) }.compact
|
113
|
+
end
|
114
|
+
|
115
|
+
def stripe_down(x,y0)
|
116
|
+
(y0+1...@ysize).map{|y| cvar(x,y) unless hint_cell?(x,y) }.compact
|
117
|
+
end
|
118
|
+
|
119
|
+
def assert_arrows
|
120
|
+
each_xy do |x,y|
|
121
|
+
if hint_cell?(x, y)
|
122
|
+
count = @data[y][x][0].to_i
|
123
|
+
dir = @data[y][x][1]
|
124
|
+
case dir
|
125
|
+
when "<", "←"
|
126
|
+
stripe = stripe_left(x,y)
|
127
|
+
when ">", "→"
|
128
|
+
stripe = stripe_right(x,y)
|
129
|
+
when "^", "↑"
|
130
|
+
stripe = stripe_up(x,y)
|
131
|
+
when "_", "↓"
|
132
|
+
stripe = stripe_down(x,y)
|
133
|
+
else
|
134
|
+
raise
|
135
|
+
end
|
136
|
+
|
137
|
+
@solver.assert Z3.Add(*stripe.map{|c| c.ite(0,1)}) == count
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def each_xy
|
143
|
+
@ysize.times do |y|
|
144
|
+
@xsize.times do |x|
|
145
|
+
yield(x,y)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def connections_and_nvars(x,y)
|
151
|
+
[
|
152
|
+
[x+1, y, hvar(x, y)],
|
153
|
+
[x-1, y, hvar(x-1, y)],
|
154
|
+
[x, y+1, vvar(x, y)],
|
155
|
+
[x, y-1, vvar(x, y-1)],
|
156
|
+
].select{|nx,ny,convar|
|
157
|
+
on_board?(nx, ny) and !hint_cell?(nx,ny)
|
158
|
+
}.map{|nx,ny,convar| [nvar(nx,ny), convar] }
|
159
|
+
end
|
160
|
+
|
161
|
+
# Loop will have Ns of 0+, all distinct and consecutive
|
162
|
+
# Non-loop will have Ns of MAX + cell_number
|
163
|
+
def assert_single_loop
|
164
|
+
# max_value = @xsize * @ysize + 10
|
165
|
+
max_value = 1000
|
166
|
+
|
167
|
+
each_xy do |x,y|
|
168
|
+
cell_idx = x + y * @xsize
|
169
|
+
@solver.assert nvar(x,y) >= 0
|
170
|
+
if hint_cell?(x,y)
|
171
|
+
@solver.assert nvar(x,y) == max_value + cell_idx
|
172
|
+
else
|
173
|
+
@solver.assert (~cvar(x,y)).implies( nvar(x,y) == max_value + cell_idx )
|
174
|
+
|
175
|
+
nval = connections_and_nvars(x,y)
|
176
|
+
.map{|n,c| c.ite(n+1, max_value) }
|
177
|
+
.reduce{|a,b| (a <= b).ite(a, b) }
|
178
|
+
|
179
|
+
@solver.assert cvar(x,y).implies((nvar(x,y) == 0) | (nvar(x,y) == nval) )
|
180
|
+
@solver.assert cvar(x,y).implies(nvar(x,y) < max_value)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# only one loop start
|
185
|
+
@solver.assert Z3.Add(
|
186
|
+
*enum_for(:each_xy).map{|x,y|
|
187
|
+
(nvar(x,y) == 0).ite(1,0)
|
188
|
+
}) == 1
|
189
|
+
end
|
190
|
+
|
191
|
+
def call
|
192
|
+
assert_connections
|
193
|
+
assert_no_black_multiples
|
194
|
+
assert_arrows
|
195
|
+
assert_single_loop
|
196
|
+
|
197
|
+
if @solver.satisfiable?
|
198
|
+
@model = @solver.model
|
199
|
+
print_answer
|
200
|
+
else
|
201
|
+
puts "failed to solve"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def hint_data(x,y)
|
206
|
+
u = @data[y][x]
|
207
|
+
n = u[0]
|
208
|
+
a = {
|
209
|
+
"<" => "←",
|
210
|
+
">" => "→",
|
211
|
+
"_" => "↓",
|
212
|
+
"^" => "↑",
|
213
|
+
}[u[1]] || u[1]
|
214
|
+
n + a
|
215
|
+
end
|
216
|
+
|
217
|
+
# TODO: make it nicer
|
218
|
+
def print_answer
|
219
|
+
@ysize.times do |y|
|
220
|
+
@xsize.times do |x|
|
221
|
+
if hint_cell?(x,y)
|
222
|
+
print hint_data(x,y)
|
223
|
+
# print "O "
|
224
|
+
else
|
225
|
+
c = @model[cvar(x,y)].to_b
|
226
|
+
if c
|
227
|
+
print "* "
|
228
|
+
else
|
229
|
+
print "# "
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
next if x == @xsize - 1
|
234
|
+
if @model[hvar(x,y)].to_b
|
235
|
+
print "-"
|
236
|
+
else
|
237
|
+
print " "
|
238
|
+
end
|
239
|
+
end
|
240
|
+
print "\n"
|
241
|
+
|
242
|
+
next if y == @ysize-1
|
243
|
+
@xsize.times do |x|
|
244
|
+
if @model[vvar(x,y)].to_b
|
245
|
+
print "| "
|
246
|
+
else
|
247
|
+
print " "
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
print "\n"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def print_debug_loop_info
|
256
|
+
print "\n"
|
257
|
+
@ysize.times do |y|
|
258
|
+
@xsize.times do |x|
|
259
|
+
n = @model[nvar(x,y)].to_i
|
260
|
+
print "%8d" % n
|
261
|
+
end
|
262
|
+
print "\n"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
path = ARGV[0] || Pathname(__dir__) + "yajilin-1.txt"
|
268
|
+
Yajilin.new(path).call
|