11#include <unordered_set>
28enum class AssignResult { success, no_solution, cutoff_reached };
63 bool operator==(
const BfsState &)
const =
default;
67 std::size_t operator()(
const BfsState &s)
const noexcept
69 std::size_t seed = std::hash<std::size_t>{}(s.next_tile_index);
71 seed ^= std::hash<ColorSet>{}(cs) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
77[[nodiscard]] std::array<SearchParams, 48> build_preset_matrix()
79 std::array<SearchParams, 48> matrix{};
82 constexpr std::array<std::size_t, 4> cutoffs = {1'000'000, 2'000'000, 4'000'000, 8'000'000};
84 for (std::size_t cutoff : cutoffs) {
87 matrix[idx++] = SearchParams{algo, cutoff, std::numeric_limits<std::size_t>::max(),
true};
90 for (std::size_t branches = 2; branches <= 6; ++branches) {
91 matrix[idx++] = SearchParams{algo, cutoff, branches,
false};
99[[nodiscard]] SearchContext build_search_context(
const PackingInput &input)
104 ctx.sorted_tiles.reserve(input.hints_.size() + input.tiles_.size());
105 for (
const auto &hint : input.hints_) {
106 ctx.sorted_tiles.push_back(hint);
108 for (
const auto &tile : input.tiles_) {
109 ctx.sorted_tiles.push_back(tile);
111 std::ranges::sort(ctx.sorted_tiles, [](
const PackableTile &a,
const PackableTile &b) {
113 if (a.color_count() != b.color_count()) {
114 return a.color_count() > b.color_count();
116 return a.id() < b.id();
120 PalettePool pool = input.palette_pool_;
124 for (
const auto &palette : prefilled_palettes) {
125 ctx.initial_palette_colors.push_back(palette.color_set());
126 ctx.palette_capacities.push_back(input.palette_capacity_);
127 ctx.hardware_indices.push_back(palette.hardware_index());
131 for (std::size_t i = 0; i < prefilled_palettes.size(); ++i) {
133 for (
const auto &prefilled_palette : input.prefilled_palettes_) {
134 if (prefilled_palette.hardware_index() == ctx.hardware_indices[i]) {
135 const std::size_t unique_colors =
color_set_count(prefilled_palette.fixed_colors());
136 const std::size_t occupied = prefilled_palette.occupied_slots();
137 const std::size_t wasted = occupied - unique_colors;
138 ctx.palette_capacities[i] = input.palette_capacity_ - wasted;
144 while (pool.has_available_palette()) {
145 std::size_t hw_idx = pool.checkout();
146 ctx.initial_palette_colors.emplace_back();
147 ctx.palette_capacities.push_back(input.palette_capacity_);
148 ctx.hardware_indices.push_back(hw_idx);
152 if (input.shape_group_metadata_.has_value()) {
153 ctx.shape_group_metadata = &input.shape_group_metadata_.value();
156 ctx.sibling_color_sets.resize(ctx.sorted_tiles.size());
157 for (std::size_t i = 0; i < ctx.sorted_tiles.size(); ++i) {
158 const auto &tile_id = ctx.sorted_tiles[i].id();
159 auto group_it = ctx.shape_group_metadata->tile_id_to_group.find(tile_id);
160 if (group_it == ctx.shape_group_metadata->tile_id_to_group.end()) {
163 std::size_t group_idx = group_it->second;
164 const auto &members = ctx.shape_group_metadata->group_members[group_idx];
167 for (
const auto &sibling_id : members) {
168 if (sibling_id == tile_id) {
173 if (st.id() == sibling_id) {
174 ctx.sibling_color_sets[i].push_back(st.color_set());
192AssignResult assign_depth_first(
194 const SearchContext &ctx,
195 const SearchParams ¶ms,
197 std::size_t &explored_nodes)
200 if (explored_nodes > params.node_cutoff) {
201 return AssignResult::cutoff_reached;
206 return AssignResult::success;
210 const auto &tile_colors = tile.color_set();
236 std::size_t palette_index;
237 std::size_t isect_size;
238 std::size_t cs_count;
241 std::vector<Candidate> candidates;
246 if (u_size <= ctx.palette_capacities[i]) {
254 bool sibling =
false;
255 if (!ctx.sibling_color_sets.empty() &&
next_tile_index < ctx.sibling_color_sets.size()) {
264 candidates.push_back(Candidate{i, i_size, c_count, sibling});
269 std::ranges::sort(candidates, [](
const Candidate &a,
const Candidate &b) {
270 if (a.has_sibling != b.has_sibling) {
271 return !a.has_sibling;
273 if (a.isect_size != b.isect_size) {
274 return a.isect_size > b.isect_size;
276 return a.cs_count < b.cs_count;
280 if (params.smart_prune) {
281 for (std::size_t i = 0; i < candidates.size(); ++i) {
282 if (candidates[i].isect_size == 0) {
284 candidates.resize(i + 1);
291 if (candidates.size() > params.best_branches) {
292 candidates.resize(params.best_branches);
295 for (
const auto &cand : candidates) {
301 if (result != AssignResult::no_solution) {
309 return AssignResult::no_solution;
324AssignResult assign_breadth_first(
325 const std::vector<ColorSet> &initial_colors,
326 const SearchContext &ctx,
327 const SearchParams ¶ms,
328 std::size_t &explored_nodes,
329 std::vector<ColorSet> &solution)
331 std::deque<BfsState> high_queue;
332 std::deque<BfsState> low_queue;
333 std::unordered_set<BfsState, BfsStateHash> visited;
335 BfsState initial{initial_colors, 0};
336 visited.insert(initial);
337 high_queue.push_back(std::move(initial));
339 while (!high_queue.empty() || !low_queue.empty()) {
341 if (explored_nodes > params.node_cutoff) {
342 return AssignResult::cutoff_reached;
346 BfsState current = [&]() {
347 if (!high_queue.empty()) {
348 BfsState s = std::move(high_queue.front());
349 high_queue.pop_front();
352 BfsState s = std::move(low_queue.front());
353 low_queue.pop_front();
358 std::size_t tile_idx = current.next_tile_index;
359 while (tile_idx < ctx.sorted_tiles.size()) {
360 const auto &tc = ctx.sorted_tiles[tile_idx].color_set();
361 bool already_covered =
false;
364 already_covered =
true;
368 if (!already_covered) {
375 if (tile_idx >= ctx.sorted_tiles.size()) {
376 solution = std::move(current.palette_colors);
377 return AssignResult::success;
380 const auto &tile_colors = ctx.sorted_tiles[tile_idx].color_set();
384 std::size_t palette_index;
385 std::size_t isect_size;
386 std::size_t cs_count;
389 std::vector<Candidate> candidates;
390 candidates.reserve(current.palette_colors.size());
392 for (std::size_t i = 0; i < current.palette_colors.size(); ++i) {
393 std::size_t u_size =
union_size(tile_colors, current.palette_colors[i]);
394 if (u_size <= ctx.palette_capacities[i]) {
399 bool sibling =
false;
400 if (!ctx.sibling_color_sets.empty() && tile_idx < ctx.sibling_color_sets.size()) {
402 if (
is_subset(sibling_cs, current.palette_colors[i])) {
409 candidates.push_back(Candidate{i, i_size, c_count, sibling});
414 std::sort(candidates.begin(), candidates.end(), [](
const Candidate &a,
const Candidate &b) {
415 if (a.has_sibling != b.has_sibling) {
416 return !a.has_sibling;
418 if (a.isect_size != b.isect_size) {
419 return a.isect_size > b.isect_size;
421 return a.cs_count < b.cs_count;
425 if (params.smart_prune) {
426 for (std::size_t i = 0; i < candidates.size(); ++i) {
427 if (candidates[i].isect_size == 0) {
428 candidates.resize(i + 1);
435 if (candidates.size() > params.best_branches) {
436 candidates.resize(params.best_branches);
443 bool saw_intersection =
false;
445 for (
const auto &cand : candidates) {
447 next_state.palette_colors = current.palette_colors;
448 next_state.palette_colors[cand.palette_index] =
449 color_set_union(next_state.palette_colors[cand.palette_index], tile_colors);
450 next_state.next_tile_index = tile_idx + 1;
452 if (cand.isect_size > 0) {
453 saw_intersection =
true;
456 if (!visited.contains(next_state)) {
457 visited.insert(next_state);
458 if (saw_intersection && cand.isect_size == 0) {
459 low_queue.push_back(std::move(next_state));
462 high_queue.push_back(std::move(next_state));
468 return AssignResult::no_solution;
471[[nodiscard]] PackingOutput
472build_packing_output(
const std::vector<ColorSet> &solution_colors,
const SearchContext &ctx,
const PackingInput &input)
474 PackingOutput output;
477 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
478 PackedPalette palette{ctx.hardware_indices[i], ctx.palette_capacities[i]};
481 for (
const auto &prefilled : input.prefilled_palettes_) {
482 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
484 PackableTile system_tile{
485 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
486 palette.add_tile(system_tile);
491 output.palettes_.push_back(std::move(palette));
497 if (tile.is_prefilled_palette()) {
501 for (std::size_t i = 0; i < solution_colors.size(); ++i) {
502 if (
is_subset(tile.color_set(), solution_colors[i])) {
503 output.palettes_[i].add_tile(tile);
504 output.tile_to_palette_[tile.id()] = ctx.hardware_indices[i];
513[[nodiscard]] PackingOutput build_empty_output(
const SearchContext &ctx,
const PackingInput &input)
515 PackingOutput output;
516 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
517 PackedPalette palette{ctx.hardware_indices[i], ctx.palette_capacities[i]};
519 for (
const auto &prefilled : input.prefilled_palettes_) {
520 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
522 PackableTile system_tile{
523 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
524 palette.add_tile(system_tile);
529 output.palettes_.push_back(std::move(palette));
534[[nodiscard]] std::string format_search_params_line(
const SearchParams ¶ms)
536 std::string branches_str = params.best_branches == std::numeric_limits<std::size_t>::max()
538 : std::to_string(params.best_branches);
540 "algorithm={}, node_cutoff={}, best_branches={}, smart_prune={}.",
544 params.smart_prune ?
"true" :
"false");
547void emit_success_remark(
const UserDiagnostics &diag,
const SearchParams ¶ms,
bool is_preset)
549 std::vector<std::string> lines;
551 lines.emplace_back(
"Backtracking search succeeded with preset config:");
554 lines.emplace_back(
"Backtracking search succeeded:");
556 lines.emplace_back(format_search_params_line(params));
557 diag.remark(
"backtracking-search", lines);
564 auto ctx = build_search_context(input);
566 if (ctx.sorted_tiles.empty()) {
567 return build_empty_output(ctx, input);
570 if (use_preset_matrix_) {
571 auto matrix = build_preset_matrix();
573 for (
const auto ¶ms : matrix) {
574 std::size_t explored = 0;
576 if (params.algorithm == SearchAlgorithm::dfs) {
577 auto colors = ctx.initial_palette_colors;
578 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
579 if (diag_ !=
nullptr) {
580 emit_success_remark(*diag_, params,
true);
582 return build_packing_output(colors, ctx, input);
586 std::vector<ColorSet> solution;
587 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
588 AssignResult::success) {
589 if (diag_ !=
nullptr) {
590 emit_success_remark(*diag_, params,
true);
592 return build_packing_output(solution, ctx, input);
598 "Backtracking strategy failed to find a valid palette assignment after all preset configurations."};
602 SearchParams params{algorithm_, node_cutoff_, best_branches_, smart_prune_};
603 std::size_t explored = 0;
605 if (algorithm_ == SearchAlgorithm::dfs) {
606 auto colors = ctx.initial_palette_colors;
607 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
608 if (diag_ !=
nullptr) {
609 emit_success_remark(*diag_, params,
false);
611 return build_packing_output(colors, ctx, input);
615 std::vector<ColorSet> solution;
616 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
617 AssignResult::success) {
618 if (diag_ !=
nullptr) {
619 emit_success_remark(*diag_, params,
false);
621 return build_packing_output(solution, ctx, input);
626 "Backtracking strategy failed to find a valid palette assignment with the configured parameters."};
std::vector< std::size_t > palette_capacities
std::vector< ColorSet > initial_palette_colors
std::size_t best_branches
SearchAlgorithm algorithm
std::vector< std::vector< ColorSet > > sibling_color_sets
Maps each sorted tile index to a list of sibling color sets from the same shape group.
std::vector< std::size_t > hardware_indices
std::size_t next_tile_index
const ShapeGroupMetadata * shape_group_metadata
Optional shape group metadata for sharing-aware candidate sorting.
std::vector< PackableTile > sorted_tiles
std::vector< ColorSet > palette_colors
A result type that maintains a chainable sequence of errors for debugging and error reporting.
bool is_subset(const ColorSet &a, const ColorSet &b)
Checks if one ColorSet is a subset of another.
ColorSet color_set_union(const ColorSet &a, const ColorSet &b)
Computes the union of two ColorSets.
std::size_t intersection_size(const ColorSet &a, const ColorSet &b)
Computes the intersection size between two ColorSets.
SearchAlgorithm
Search algorithm used by BacktrackingStrategy.
@ dfs
Depth-first search with in-place mutation and undo.
@ bfs
Breadth-first search with dual-queue heuristic and visited-state deduplication.
std::size_t color_set_count(const ColorSet &set)
Counts the number of colors in a ColorSet.
std::string to_string(const PrimaryPairingMode m)
Converts a PrimaryPairingMode to its canonical string representation.
std::size_t union_size(const ColorSet &a, const ColorSet &b)
Computes the union size of two ColorSets.
std::vector< PackedPalette > initialize_packed_palettes(const std::set< PrefilledPalette > &prefilled_palettes, PalettePool &palette_pool, std::size_t palette_capacity)
Initializes packed palettes from prefilled palettes.