multi-puzzle-solver 1.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multi_puzzle_solver-1.1.8.dist-info/METADATA +4326 -0
- multi_puzzle_solver-1.1.8.dist-info/RECORD +106 -0
- multi_puzzle_solver-1.1.8.dist-info/WHEEL +5 -0
- multi_puzzle_solver-1.1.8.dist-info/top_level.txt +1 -0
- puzzle_solver/__init__.py +184 -0
- puzzle_solver/core/utils.py +298 -0
- puzzle_solver/core/utils_ortools.py +333 -0
- puzzle_solver/core/utils_visualizer.py +575 -0
- puzzle_solver/puzzles/abc_view/abc_view.py +75 -0
- puzzle_solver/puzzles/aquarium/aquarium.py +97 -0
- puzzle_solver/puzzles/area_51/area_51.py +159 -0
- puzzle_solver/puzzles/battleships/battleships.py +139 -0
- puzzle_solver/puzzles/binairo/binairo.py +98 -0
- puzzle_solver/puzzles/binairo/binairo_plus.py +7 -0
- puzzle_solver/puzzles/black_box/black_box.py +243 -0
- puzzle_solver/puzzles/branches/branches.py +64 -0
- puzzle_solver/puzzles/bridges/bridges.py +104 -0
- puzzle_solver/puzzles/chess_range/chess_melee.py +6 -0
- puzzle_solver/puzzles/chess_range/chess_range.py +406 -0
- puzzle_solver/puzzles/chess_range/chess_solo.py +9 -0
- puzzle_solver/puzzles/chess_sequence/chess_sequence.py +262 -0
- puzzle_solver/puzzles/circle_9/circle_9.py +44 -0
- puzzle_solver/puzzles/clouds/clouds.py +81 -0
- puzzle_solver/puzzles/connect_the_dots/connect_the_dots.py +50 -0
- puzzle_solver/puzzles/cow_and_cactus/cow_and_cactus.py +66 -0
- puzzle_solver/puzzles/dominosa/dominosa.py +67 -0
- puzzle_solver/puzzles/filling/filling.py +94 -0
- puzzle_solver/puzzles/flip/flip.py +64 -0
- puzzle_solver/puzzles/flood_it/flood_it.py +174 -0
- puzzle_solver/puzzles/flood_it/parse_map/parse_map.py +197 -0
- puzzle_solver/puzzles/galaxies/galaxies.py +110 -0
- puzzle_solver/puzzles/galaxies/parse_map/parse_map.py +216 -0
- puzzle_solver/puzzles/guess/guess.py +232 -0
- puzzle_solver/puzzles/heyawake/heyawake.py +152 -0
- puzzle_solver/puzzles/hidden_stars/hidden_stars.py +52 -0
- puzzle_solver/puzzles/hidoku/hidoku.py +59 -0
- puzzle_solver/puzzles/inertia/inertia.py +121 -0
- puzzle_solver/puzzles/inertia/parse_map/parse_map.py +207 -0
- puzzle_solver/puzzles/inertia/tsp.py +400 -0
- puzzle_solver/puzzles/kakurasu/kakurasu.py +38 -0
- puzzle_solver/puzzles/kakuro/kakuro.py +81 -0
- puzzle_solver/puzzles/kakuro/krypto_kakuro.py +95 -0
- puzzle_solver/puzzles/keen/keen.py +76 -0
- puzzle_solver/puzzles/kropki/kropki.py +94 -0
- puzzle_solver/puzzles/light_up/light_up.py +58 -0
- puzzle_solver/puzzles/linesweeper/linesweeper.py +71 -0
- puzzle_solver/puzzles/link_a_pix/link_a_pix.py +91 -0
- puzzle_solver/puzzles/lits/lits.py +138 -0
- puzzle_solver/puzzles/magnets/magnets.py +96 -0
- puzzle_solver/puzzles/map/map.py +56 -0
- puzzle_solver/puzzles/mathema_grids/mathema_grids.py +119 -0
- puzzle_solver/puzzles/mathrax/mathrax.py +93 -0
- puzzle_solver/puzzles/minesweeper/minesweeper.py +123 -0
- puzzle_solver/puzzles/mosaic/mosaic.py +38 -0
- puzzle_solver/puzzles/n_queens/n_queens.py +71 -0
- puzzle_solver/puzzles/nonograms/nonograms.py +121 -0
- puzzle_solver/puzzles/nonograms/nonograms_colored.py +220 -0
- puzzle_solver/puzzles/norinori/norinori.py +96 -0
- puzzle_solver/puzzles/number_path/number_path.py +76 -0
- puzzle_solver/puzzles/numbermaze/numbermaze.py +97 -0
- puzzle_solver/puzzles/nurikabe/nurikabe.py +130 -0
- puzzle_solver/puzzles/palisade/palisade.py +91 -0
- puzzle_solver/puzzles/pearl/pearl.py +107 -0
- puzzle_solver/puzzles/pipes/pipes.py +82 -0
- puzzle_solver/puzzles/range/range.py +59 -0
- puzzle_solver/puzzles/rectangles/rectangles.py +128 -0
- puzzle_solver/puzzles/ripple_effect/ripple_effect.py +83 -0
- puzzle_solver/puzzles/rooms/rooms.py +75 -0
- puzzle_solver/puzzles/schurs_numbers/schurs_numbers.py +73 -0
- puzzle_solver/puzzles/shakashaka/shakashaka.py +201 -0
- puzzle_solver/puzzles/shingoki/shingoki.py +116 -0
- puzzle_solver/puzzles/signpost/signpost.py +93 -0
- puzzle_solver/puzzles/singles/singles.py +53 -0
- puzzle_solver/puzzles/slant/parse_map/parse_map.py +135 -0
- puzzle_solver/puzzles/slant/slant.py +111 -0
- puzzle_solver/puzzles/slitherlink/slitherlink.py +130 -0
- puzzle_solver/puzzles/snail/snail.py +97 -0
- puzzle_solver/puzzles/split_ends/split_ends.py +93 -0
- puzzle_solver/puzzles/star_battle/star_battle.py +75 -0
- puzzle_solver/puzzles/star_battle/star_battle_shapeless.py +7 -0
- puzzle_solver/puzzles/stitches/parse_map/parse_map.py +267 -0
- puzzle_solver/puzzles/stitches/stitches.py +96 -0
- puzzle_solver/puzzles/sudoku/sudoku.py +267 -0
- puzzle_solver/puzzles/suguru/suguru.py +55 -0
- puzzle_solver/puzzles/suko/suko.py +54 -0
- puzzle_solver/puzzles/tapa/tapa.py +97 -0
- puzzle_solver/puzzles/tatami/tatami.py +64 -0
- puzzle_solver/puzzles/tents/tents.py +80 -0
- puzzle_solver/puzzles/thermometers/thermometers.py +82 -0
- puzzle_solver/puzzles/towers/towers.py +89 -0
- puzzle_solver/puzzles/tracks/tracks.py +88 -0
- puzzle_solver/puzzles/trees_logic/trees_logic.py +48 -0
- puzzle_solver/puzzles/troix/dumplings.py +7 -0
- puzzle_solver/puzzles/troix/troix.py +75 -0
- puzzle_solver/puzzles/twiddle/twiddle.py +112 -0
- puzzle_solver/puzzles/undead/undead.py +130 -0
- puzzle_solver/puzzles/unequal/unequal.py +128 -0
- puzzle_solver/puzzles/unruly/unruly.py +54 -0
- puzzle_solver/puzzles/vectors/vectors.py +94 -0
- puzzle_solver/puzzles/vermicelli/vermicelli.py +74 -0
- puzzle_solver/puzzles/walls/walls.py +52 -0
- puzzle_solver/puzzles/yajilin/yajilin.py +87 -0
- puzzle_solver/puzzles/yin_yang/parse_map/parse_map.py +172 -0
- puzzle_solver/puzzles/yin_yang/yin_yang.py +103 -0
- puzzle_solver/utils/etc/parser/board_color_digit.py +497 -0
- puzzle_solver/utils/visualizer.py +155 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Callable, Any, Union
|
|
5
|
+
|
|
6
|
+
from ortools.sat.python import cp_model
|
|
7
|
+
from ortools.sat.python.cp_model import CpSolverSolutionCallback
|
|
8
|
+
|
|
9
|
+
from puzzle_solver.core.utils import Pos
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SingleSolution:
|
|
14
|
+
assignment: dict[Pos, Union[str, int]]
|
|
15
|
+
|
|
16
|
+
def get_hashable_solution(self) -> str:
|
|
17
|
+
if isinstance(self.assignment, list):
|
|
18
|
+
return json.dumps(self.assignment)
|
|
19
|
+
result = []
|
|
20
|
+
for pos, v in self.assignment.items():
|
|
21
|
+
result.append((pos.x, pos.y, v))
|
|
22
|
+
return json.dumps(result, sort_keys=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def and_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
|
|
26
|
+
model.AddBoolAnd(cs).OnlyEnforceIf(target) # target => (c1 ∧ ... ∧ cn)
|
|
27
|
+
model.AddBoolOr([target] + [c.Not() for c in cs]) # target ∨ ¬c1 ∨ ... ∨ ¬cn equivalent to (¬target => ¬(c1 ∧ ... ∧ cn))
|
|
28
|
+
# thus target <=> (c1 ∧ ... ∧ cn)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def or_constraint(model: cp_model.CpModel, target: cp_model.IntVar, cs: list[cp_model.IntVar]):
|
|
32
|
+
model.AddBoolOr(cs).OnlyEnforceIf(target) # target => (c1 ∨ ... ∨ cn)
|
|
33
|
+
model.AddBoolAnd([c.Not() for c in cs]).OnlyEnforceIf(target.Not()) # ¬target => ¬c1 ∧ ... ∧ ¬cn equivalent to (¬target => ¬(c1 ∨ ... ∨ cn))
|
|
34
|
+
# thus target <=> (c1 ∨ ... ∨ cn)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AllSolutionsCollector(CpSolverSolutionCallback):
|
|
38
|
+
def __init__(self,
|
|
39
|
+
board: Any,
|
|
40
|
+
board_to_solution: Callable[Any, SingleSolution],
|
|
41
|
+
max_solutions: Optional[int] = None,
|
|
42
|
+
callback: Optional[Callable[SingleSolution, None]] = None
|
|
43
|
+
):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.board = board
|
|
46
|
+
self.board_to_solution = board_to_solution
|
|
47
|
+
self.max_solutions = max_solutions
|
|
48
|
+
self.callback = callback
|
|
49
|
+
self.solutions = []
|
|
50
|
+
self.unique_solutions = set()
|
|
51
|
+
|
|
52
|
+
def on_solution_callback(self):
|
|
53
|
+
try:
|
|
54
|
+
result = self.board_to_solution(self.board, self)
|
|
55
|
+
result_json = result.get_hashable_solution()
|
|
56
|
+
if result_json in self.unique_solutions:
|
|
57
|
+
return
|
|
58
|
+
self.unique_solutions.add(result_json)
|
|
59
|
+
self.solutions.append(result)
|
|
60
|
+
if self.callback is not None:
|
|
61
|
+
self.callback(result)
|
|
62
|
+
if self.max_solutions is not None and len(self.solutions) >= self.max_solutions:
|
|
63
|
+
self.StopSearch()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(e)
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
def generic_solve_all(board: Any, board_to_solution: Callable[Any, SingleSolution], max_solutions: Optional[int] = None, callback: Optional[Callable[[SingleSolution], None]] = None, verbose: bool = True) -> list[SingleSolution]:
|
|
69
|
+
try:
|
|
70
|
+
solver = cp_model.CpSolver()
|
|
71
|
+
solver.parameters.enumerate_all_solutions = True
|
|
72
|
+
collector = AllSolutionsCollector(board, board_to_solution, max_solutions=max_solutions, callback=callback)
|
|
73
|
+
tic = time.time()
|
|
74
|
+
solver.solve(board.model, collector)
|
|
75
|
+
if verbose:
|
|
76
|
+
print("Solutions found:", len(collector.solutions))
|
|
77
|
+
print("status:", solver.StatusName())
|
|
78
|
+
toc = time.time()
|
|
79
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
80
|
+
return collector.solutions
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(e)
|
|
83
|
+
raise e
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def generic_unique_projections(board: Any, vars_to_forbid: list[cp_model.IntVar], board_to_solution: Callable[Any, SingleSolution], max_solutions: Optional[int] = None, callback: Optional[Callable[[SingleSolution], None]] = None, verbose: bool = True):
|
|
87
|
+
tic = time.time()
|
|
88
|
+
solutions = []
|
|
89
|
+
solver = cp_model.CpSolver()
|
|
90
|
+
stopped_early = False
|
|
91
|
+
try:
|
|
92
|
+
while True:
|
|
93
|
+
solver.solve(board.model)
|
|
94
|
+
if solver.StatusName() not in ['OPTIMAL', 'FEASIBLE']:
|
|
95
|
+
break
|
|
96
|
+
solution = board_to_solution(board, solver)
|
|
97
|
+
solutions.append(solution)
|
|
98
|
+
if callback is not None:
|
|
99
|
+
callback(solution)
|
|
100
|
+
if max_solutions is not None and len(solutions) >= max_solutions:
|
|
101
|
+
stopped_early = True
|
|
102
|
+
break
|
|
103
|
+
board.model.AddForbiddenAssignments(vars_to_forbid, [[solver.Value(v) for v in vars_to_forbid]])
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(e)
|
|
106
|
+
raise e
|
|
107
|
+
if verbose:
|
|
108
|
+
print(f"Solutions found: {len(solutions)}{' (stopped early)' if stopped_early else ''}")
|
|
109
|
+
if len(solutions) == 0:
|
|
110
|
+
status = solver.StatusName()
|
|
111
|
+
elif len(solutions) > 0 and stopped_early:
|
|
112
|
+
status = 'FEASIBLE'
|
|
113
|
+
elif len(solutions) > 0 and not stopped_early:
|
|
114
|
+
status = 'OPTIMAL'
|
|
115
|
+
else:
|
|
116
|
+
raise AssertionError("impossible state")
|
|
117
|
+
print("status:", status)
|
|
118
|
+
toc = time.time()
|
|
119
|
+
print(f"Time taken: {toc - tic:.2f} seconds")
|
|
120
|
+
return solutions
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def manhattan_distance(p1: Pos, p2: Pos) -> int:
|
|
124
|
+
return abs(p1.x - p2.x) + abs(p1.y - p2.y)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def force_connected_component(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.IntVar], is_neighbor: Callable[[Any, Any], bool] = None):
|
|
128
|
+
"""
|
|
129
|
+
Forces a single connected component of the given variables and any abstract function that defines adjacency.
|
|
130
|
+
Returns a dictionary of new variables that can be used to enforce the connected component constraint.
|
|
131
|
+
Total new variables: =4V [for N by M 2D grid total is 4NM]
|
|
132
|
+
"""
|
|
133
|
+
if is_neighbor is None:
|
|
134
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
135
|
+
|
|
136
|
+
vs = vars_to_force
|
|
137
|
+
v_count = len(vs)
|
|
138
|
+
if v_count <= 2: # graph must have at least 3 nodes to possibly be disconnected
|
|
139
|
+
return {}
|
|
140
|
+
# =V model variables, one for each variable
|
|
141
|
+
is_root: dict[Pos, cp_model.IntVar] = {} # =V, defines the unique
|
|
142
|
+
prefix_zero: dict[Pos, cp_model.IntVar] = {} # =V, used for picking the unique root
|
|
143
|
+
node_height: dict[Pos, cp_model.IntVar] = {} # =V, trickles down from the root
|
|
144
|
+
max_neighbor_height: dict[Pos, cp_model.IntVar] = {} # =V, the height of the tallest neighbor
|
|
145
|
+
prefix_name = "connected_component_"
|
|
146
|
+
# total = 4V [for N by M 2D grid total is 4NM]
|
|
147
|
+
|
|
148
|
+
keys_in_order = list(vs.keys()) # must enforce some ordering
|
|
149
|
+
|
|
150
|
+
for p in keys_in_order:
|
|
151
|
+
is_root[p] = model.NewBoolVar(f"{prefix_name}is_root[{p}]")
|
|
152
|
+
node_height[p] = model.NewIntVar(0, v_count, f"{prefix_name}node_height[{p}]")
|
|
153
|
+
max_neighbor_height[p] = model.NewIntVar(0, v_count, f"{prefix_name}max_neighbor_height[{p}]")
|
|
154
|
+
# Unique root: the smallest index i with x[i] = 1
|
|
155
|
+
# prefix_zero[i] = AND_{k < i} (not x[k])
|
|
156
|
+
prev_p = None
|
|
157
|
+
for p in keys_in_order:
|
|
158
|
+
b = model.NewBoolVar(f"{prefix_name}prefix_zero[{p}]")
|
|
159
|
+
prefix_zero[p] = b
|
|
160
|
+
if prev_p is None: # No earlier cells -> True
|
|
161
|
+
model.Add(b == 1)
|
|
162
|
+
else:
|
|
163
|
+
# b <-> (prefix_zero[i-1] & ~x[i-1])
|
|
164
|
+
and_constraint(model, b, [prefix_zero[prev_p], vs[prev_p].Not()])
|
|
165
|
+
prev_p = p
|
|
166
|
+
|
|
167
|
+
# x[i] & prefix_zero[i] -> root[i]
|
|
168
|
+
for p in keys_in_order:
|
|
169
|
+
and_constraint(model, is_root[p], [vs[p], prefix_zero[p]])
|
|
170
|
+
# Exactly one root:
|
|
171
|
+
model.Add(sum(is_root.values()) <= 1)
|
|
172
|
+
|
|
173
|
+
# For each node i, consider only neighbors
|
|
174
|
+
for i, pi in enumerate(keys_in_order):
|
|
175
|
+
# ps is list of neighbor heights
|
|
176
|
+
ps = [node_height[pj] for j, pj in enumerate(keys_in_order) if i != j and is_neighbor(pi, pj)]
|
|
177
|
+
model.AddMaxEquality(max_neighbor_height[pi], ps)
|
|
178
|
+
# if a node is active and its not root, its height is the height of the tallest neighbor - 1
|
|
179
|
+
model.Add(node_height[pi] == max_neighbor_height[pi] - 1).OnlyEnforceIf([vs[pi], is_root[pi].Not()])
|
|
180
|
+
model.Add(node_height[pi] == v_count).OnlyEnforceIf(is_root[pi])
|
|
181
|
+
model.Add(node_height[pi] == 0).OnlyEnforceIf(vs[pi].Not())
|
|
182
|
+
|
|
183
|
+
# final check: all active nodes have height > 0
|
|
184
|
+
for p in keys_in_order:
|
|
185
|
+
model.Add(node_height[p] > 0).OnlyEnforceIf(vs[p])
|
|
186
|
+
|
|
187
|
+
all_new_vars = {
|
|
188
|
+
"is_root": is_root,
|
|
189
|
+
"prefix_zero": prefix_zero,
|
|
190
|
+
"node_height": node_height,
|
|
191
|
+
"max_neighbor_height": max_neighbor_height,
|
|
192
|
+
}
|
|
193
|
+
return all_new_vars
|
|
194
|
+
|
|
195
|
+
def force_connected_component_using_demand(model: cp_model.CpModel, nodes: dict[Any, cp_model.IntVar], is_neighbor: Callable[[Any, Any], bool] = None):
|
|
196
|
+
"""
|
|
197
|
+
Forces a single connected component of the given variables and any abstract function that defines adjacency using demand variables.
|
|
198
|
+
Warning: This method will not have a unique feasible assignment of variable, thus this function MUST be used in conjunction with the unique projection method.
|
|
199
|
+
"""
|
|
200
|
+
if is_neighbor is None:
|
|
201
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
202
|
+
|
|
203
|
+
n = len(nodes)
|
|
204
|
+
if n <= 2: # graph must have at least 3 nodes to possibly be disconnected
|
|
205
|
+
return {}
|
|
206
|
+
|
|
207
|
+
neighs = {}
|
|
208
|
+
for ki in nodes.keys():
|
|
209
|
+
neighs[ki] = []
|
|
210
|
+
for kj in nodes.keys():
|
|
211
|
+
if is_neighbor(ki, kj):
|
|
212
|
+
neighs[ki].append(kj)
|
|
213
|
+
if sum(len(neighs[ki]) for ki in nodes.keys()) == 0: # no edges in the graph
|
|
214
|
+
model.Add(sum(nodes.values()) <= 1)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Parent choice variables:
|
|
218
|
+
# parent[i][j] = 1 if node i chooses neighbor j as parent (if either i or j is inactive, parent[i][j] must be 0)
|
|
219
|
+
# is_root[i] = 1 if node i is the (unique) root attached to the dummy source (if i is inactive, is_root[i] must be 0)
|
|
220
|
+
parent = {}
|
|
221
|
+
is_root = {}
|
|
222
|
+
for ki in nodes.keys():
|
|
223
|
+
for kj in neighs[ki]:
|
|
224
|
+
pij = model.NewBoolVar(f"par_{ki}_from_{kj}")
|
|
225
|
+
parent[(ki, kj)] = pij
|
|
226
|
+
# gate on endpoint selections
|
|
227
|
+
model.Add(pij <= nodes[ki])
|
|
228
|
+
model.Add(pij <= nodes[kj])
|
|
229
|
+
ps = model.NewBoolVar(f"par_{ki}_from_src")
|
|
230
|
+
is_root[ki] = ps
|
|
231
|
+
model.Add(ps <= nodes[ki]) # only active node may be root
|
|
232
|
+
# Exactly one parent if selected, none if root or inactive
|
|
233
|
+
model.Add(sum(parent[(ki, kj)] for kj in neighs[ki]) + ps == nodes[ki]) # ∀i: (Σ_{j∈N(i)} p[i→j]) + r[i] == v[i]
|
|
234
|
+
model.Add(sum(is_root.values()) <= 1) # at most one root
|
|
235
|
+
|
|
236
|
+
# Depth variables to break cycles and form a tree
|
|
237
|
+
# depth in [0, n-1]; root has depth 0; other selected nodes have >=1; unselected -> 0
|
|
238
|
+
depth = {ki: model.NewIntVar(0, n - 1, f"depth_{ki}") for ki in nodes.keys()}
|
|
239
|
+
for ki in nodes.keys():
|
|
240
|
+
vi = nodes[ki]
|
|
241
|
+
model.Add(depth[ki] == 0).OnlyEnforceIf(vi.Not()) # inactive => depth 0
|
|
242
|
+
model.Add(depth[ki] == 0).OnlyEnforceIf(is_root[ki]) # if root => depth 0
|
|
243
|
+
model.Add(depth[ki] >= 1).OnlyEnforceIf([vi, is_root[ki].Not()]) # if active and not root => depth >= 1
|
|
244
|
+
for kj in neighs[ki]: # every parent's depth < child's depth
|
|
245
|
+
pij = parent[(ki, kj)]
|
|
246
|
+
model.Add(depth[ki] >= depth[kj] + 1).OnlyEnforceIf(pij)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"parent": parent,
|
|
250
|
+
"is_root": is_root,
|
|
251
|
+
"depth": depth,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
def force_no_loops(model: cp_model.CpModel, vars_to_force: dict[Any, cp_model.IntVar], is_neighbor: Callable[[Any, Any], bool] = None):
|
|
255
|
+
"""
|
|
256
|
+
Forces no loops in the given variables and any abstract function that defines adjacency.
|
|
257
|
+
Returns a dictionary of new variables that can be used to enforce the no component constraint.
|
|
258
|
+
"""
|
|
259
|
+
if is_neighbor is None:
|
|
260
|
+
is_neighbor = lambda p1, p2: manhattan_distance(p1, p2) <= 1 # noqa: E731
|
|
261
|
+
|
|
262
|
+
vs = vars_to_force
|
|
263
|
+
v_count = len(vs)
|
|
264
|
+
is_root: dict[Pos, cp_model.IntVar] = {}
|
|
265
|
+
block_root: dict[Pos, cp_model.IntVar] = {}
|
|
266
|
+
node_height: dict[Pos, cp_model.IntVar] = {}
|
|
267
|
+
tree_edge: dict[tuple[Pos, Pos], cp_model.IntVar] = {} # tree_edge[p, q] means p is parent of q
|
|
268
|
+
prefix_name = "no_loops_"
|
|
269
|
+
|
|
270
|
+
parent_of = {p: [] for p in vs.keys()}
|
|
271
|
+
children_of = {p: [] for p in vs.keys()}
|
|
272
|
+
for p in vs.keys():
|
|
273
|
+
for q in vs.keys():
|
|
274
|
+
if p == q:
|
|
275
|
+
continue
|
|
276
|
+
if is_neighbor(p, q):
|
|
277
|
+
parent_of[q].append(p)
|
|
278
|
+
children_of[p].append(q)
|
|
279
|
+
|
|
280
|
+
keys_in_order = list(vs.keys()) # must enforce some ordering
|
|
281
|
+
node_to_idx: dict[Pos, int] = {p: i+1 for i, p in enumerate(keys_in_order)}
|
|
282
|
+
for p in keys_in_order:
|
|
283
|
+
# capacity = node_to_idx[p] + 1
|
|
284
|
+
# if p == Pos(0, 0):
|
|
285
|
+
# capacity = 10
|
|
286
|
+
# print(f'capacity for {p} = {capacity}')
|
|
287
|
+
node_height[p] = model.NewIntVar(0, v_count, f"{prefix_name}node_height[{p}]")
|
|
288
|
+
block_root[p] = model.NewIntVar(0, node_to_idx[p], f"{prefix_name}block_root[{p}]")
|
|
289
|
+
is_root[p] = model.NewBoolVar(f"{prefix_name}is_root[{p}]")
|
|
290
|
+
model.Add(is_root[p] == 0).OnlyEnforceIf([vs[p].Not()])
|
|
291
|
+
model.Add(node_height[p] == 0).OnlyEnforceIf([vs[p].Not()])
|
|
292
|
+
model.Add(node_height[p] == 1).OnlyEnforceIf([is_root[p]])
|
|
293
|
+
model.Add(block_root[p] == 0).OnlyEnforceIf([vs[p].Not()])
|
|
294
|
+
model.Add(block_root[p] == node_to_idx[p]).OnlyEnforceIf([is_root[p]])
|
|
295
|
+
|
|
296
|
+
for p in keys_in_order:
|
|
297
|
+
for q in children_of[p]:
|
|
298
|
+
tree_edge[(p, q)] = model.NewBoolVar(f"{prefix_name}tree_edge[{p} is parent of {q}]")
|
|
299
|
+
model.Add(tree_edge[(p, q)] == 0).OnlyEnforceIf([vs[p].Not()])
|
|
300
|
+
model.Add(tree_edge[(p, q)] == 0).OnlyEnforceIf([vs[q].Not()])
|
|
301
|
+
# a tree_edge[p, q] means p is parent of q thus h[q] = h[p] + 1
|
|
302
|
+
model.Add(node_height[q] == node_height[p] + 1).OnlyEnforceIf([tree_edge[(p, q)]])
|
|
303
|
+
model.Add(block_root[q] == block_root[p]).OnlyEnforceIf([tree_edge[(p, q)]])
|
|
304
|
+
|
|
305
|
+
for (p, q) in tree_edge:
|
|
306
|
+
if (q, p) in tree_edge:
|
|
307
|
+
model.Add(tree_edge[(p, q)] == 0).OnlyEnforceIf([tree_edge[(q, p)]])
|
|
308
|
+
model.Add(tree_edge[(p, q)] == 1).OnlyEnforceIf([tree_edge[(q, p)].Not(), vs[p], vs[q]])
|
|
309
|
+
|
|
310
|
+
for p in keys_in_order:
|
|
311
|
+
for p_child in children_of[p]:
|
|
312
|
+
# i am root thus I point to all my children
|
|
313
|
+
model.Add(tree_edge[(p, p_child)] == 1).OnlyEnforceIf([is_root[p], vs[p_child]])
|
|
314
|
+
for p_parent in parent_of[p]:
|
|
315
|
+
# i am root thus I have no parent
|
|
316
|
+
model.Add(tree_edge[(p_parent, p)] == 0).OnlyEnforceIf([is_root[p]])
|
|
317
|
+
# every active node has exactly 1 parent except root has none
|
|
318
|
+
model.AddExactlyOne([tree_edge[(p_parent, p)] for p_parent in parent_of[p]] + [vs[p].Not(), is_root[p]])
|
|
319
|
+
|
|
320
|
+
# now each subgraph has directions where each non-root points to a single parent (and its value is parent+1).
|
|
321
|
+
# to break cycles, every non-root active node must be > all neighbors that arent children
|
|
322
|
+
|
|
323
|
+
all_new_vars: dict[str, cp_model.IntVar] = {}
|
|
324
|
+
for k, v in is_root.items():
|
|
325
|
+
all_new_vars[f"{prefix_name}is_root[{k}]"] = v
|
|
326
|
+
for k, v in tree_edge.items():
|
|
327
|
+
all_new_vars[f"{prefix_name}tree_edge[{k[0]} is parent of {k[1]}]"] = v
|
|
328
|
+
for k, v in node_height.items():
|
|
329
|
+
all_new_vars[f"{prefix_name}node_height[{k}]"] = v
|
|
330
|
+
for k, v in block_root.items():
|
|
331
|
+
all_new_vars[f"{prefix_name}block_root[{k}]"] = v
|
|
332
|
+
|
|
333
|
+
return all_new_vars
|