11#include <unordered_set>
28enum class AssignResult { success, no_solution, cutoff_reached };
66 bool operator==(
const BfsState &)
const =
default;
70 std::size_t operator()(
const BfsState &s)
const noexcept
72 std::size_t seed = std::hash<std::size_t>{}(s.next_tile_index);
74 seed ^= std::hash<ColorSet>{}(cs) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
80[[nodiscard]] std::array<SearchParams, 48> build_preset_matrix()
82 std::array<SearchParams, 48> matrix{};
85 constexpr std::array<std::size_t, 4> cutoffs = {1'000'000, 2'000'000, 4'000'000, 8'000'000};
87 for (std::size_t cutoff : cutoffs) {
90 matrix[idx++] = SearchParams{algo, cutoff, std::numeric_limits<std::size_t>::max(),
true};
93 for (std::size_t branches = 2; branches <= 6; ++branches) {
94 matrix[idx++] = SearchParams{algo, cutoff, branches,
false};
102[[nodiscard]] SearchContext build_search_context(
const PackingInput &input)
107 ctx.sorted_tiles.reserve(input.hints_.size() + input.tiles_.size());
108 for (
const auto &hint : input.hints_) {
109 ctx.sorted_tiles.push_back(hint);
111 for (
const auto &tile : input.tiles_) {
112 ctx.sorted_tiles.push_back(tile);
114 std::ranges::sort(ctx.sorted_tiles, [](
const PackableTile &a,
const PackableTile &b) {
116 if (a.color_count() != b.color_count()) {
117 return a.color_count() > b.color_count();
119 return a.id() < b.id();
123 PalettePool pool = input.pal_pool_;
127 for (
const auto &pal : prefilled_pals) {
128 ctx.initial_palette_colors.push_back(pal.color_set());
129 ctx.palette_capacities.push_back(input.pal_capacity_);
130 ctx.hardware_indices.push_back(pal.hardware_index());
134 for (std::size_t i = 0; i < prefilled_pals.size(); ++i) {
136 for (
const auto &prefilled_pal : input.prefilled_pals_) {
137 if (prefilled_pal.hardware_index() == ctx.hardware_indices[i]) {
138 const std::size_t unique_colors =
color_set_count(prefilled_pal.fixed_colors());
139 const std::size_t occupied = prefilled_pal.occupied_slots();
140 const std::size_t wasted = occupied - unique_colors;
141 ctx.palette_capacities[i] = input.pal_capacity_ - wasted;
147 while (pool.has_available_pal()) {
148 std::size_t hw_idx = pool.checkout();
149 ctx.initial_palette_colors.emplace_back();
150 ctx.palette_capacities.push_back(input.pal_capacity_);
151 ctx.hardware_indices.push_back(hw_idx);
155 if (input.shape_group_metadata_.has_value()) {
156 ctx.shape_group_metadata = &input.shape_group_metadata_.value();
159 ctx.sibling_color_sets.resize(ctx.sorted_tiles.size());
160 for (std::size_t i = 0; i < ctx.sorted_tiles.size(); ++i) {
161 const auto &tile_id = ctx.sorted_tiles[i].id();
162 auto group_it = ctx.shape_group_metadata->tile_id_to_group.find(tile_id);
163 if (group_it == ctx.shape_group_metadata->tile_id_to_group.end()) {
166 std::size_t group_idx = group_it->second;
167 const auto &members = ctx.shape_group_metadata->group_members[group_idx];
170 for (
const auto &sibling_id : members) {
171 if (sibling_id == tile_id) {
176 if (st.id() == sibling_id) {
177 ctx.sibling_color_sets[i].push_back(st.color_set());
197AssignResult assign_depth_first(
199 const SearchContext &ctx,
200 const SearchParams ¶ms,
202 std::size_t &explored_nodes)
205 if (explored_nodes > params.node_cutoff) {
206 return AssignResult::cutoff_reached;
211 return AssignResult::success;
215 const auto &tile_colors = tile.color_set();
243 std::size_t pal_index;
244 std::size_t isect_size;
245 std::size_t cs_count;
248 std::vector<Candidate> candidates;
253 if (u_size <= ctx.palette_capacities[i]) {
263 bool sibling =
false;
264 if (!ctx.sibling_color_sets.empty() &&
next_tile_index < ctx.sibling_color_sets.size()) {
273 candidates.push_back(Candidate{i, i_size, c_count, sibling});
278 std::ranges::sort(candidates, [](
const Candidate &a,
const Candidate &b) {
279 if (a.has_sibling != b.has_sibling) {
280 return !a.has_sibling;
282 if (a.isect_size != b.isect_size) {
283 return a.isect_size > b.isect_size;
285 return a.cs_count < b.cs_count;
289 if (params.smart_prune) {
290 for (std::size_t i = 0; i < candidates.size(); ++i) {
291 if (candidates[i].isect_size == 0) {
293 candidates.resize(i + 1);
300 if (candidates.size() > params.best_branches) {
301 candidates.resize(params.best_branches);
304 for (
const auto &cand : candidates) {
310 if (result != AssignResult::no_solution) {
318 return AssignResult::no_solution;
335AssignResult assign_breadth_first(
336 const std::vector<ColorSet> &initial_colors,
337 const SearchContext &ctx,
338 const SearchParams ¶ms,
339 std::size_t &explored_nodes,
340 std::vector<ColorSet> &solution)
342 std::deque<BfsState> high_queue;
343 std::deque<BfsState> low_queue;
344 std::unordered_set<BfsState, BfsStateHash> visited;
346 BfsState initial{initial_colors, 0};
347 visited.insert(initial);
348 high_queue.push_back(std::move(initial));
350 while (!high_queue.empty() || !low_queue.empty()) {
352 if (explored_nodes > params.node_cutoff) {
353 return AssignResult::cutoff_reached;
357 BfsState current = [&]() {
358 if (!high_queue.empty()) {
359 BfsState s = std::move(high_queue.front());
360 high_queue.pop_front();
363 BfsState s = std::move(low_queue.front());
364 low_queue.pop_front();
369 std::size_t tile_idx = current.next_tile_index;
370 while (tile_idx < ctx.sorted_tiles.size()) {
371 const auto &tc = ctx.sorted_tiles[tile_idx].color_set();
372 bool already_covered =
false;
375 already_covered =
true;
379 if (!already_covered) {
386 if (tile_idx >= ctx.sorted_tiles.size()) {
387 solution = std::move(current.palette_colors);
388 return AssignResult::success;
391 const auto &tile_colors = ctx.sorted_tiles[tile_idx].color_set();
395 std::size_t pal_index;
396 std::size_t isect_size;
397 std::size_t cs_count;
400 std::vector<Candidate> candidates;
401 candidates.reserve(current.palette_colors.size());
403 for (std::size_t i = 0; i < current.palette_colors.size(); ++i) {
404 std::size_t u_size =
union_size(tile_colors, current.palette_colors[i]);
405 if (u_size <= ctx.palette_capacities[i]) {
410 bool sibling =
false;
411 if (!ctx.sibling_color_sets.empty() && tile_idx < ctx.sibling_color_sets.size()) {
413 if (
is_subset(sibling_cs, current.palette_colors[i])) {
420 candidates.push_back(Candidate{i, i_size, c_count, sibling});
425 std::sort(candidates.begin(), candidates.end(), [](
const Candidate &a,
const Candidate &b) {
426 if (a.has_sibling != b.has_sibling) {
427 return !a.has_sibling;
429 if (a.isect_size != b.isect_size) {
430 return a.isect_size > b.isect_size;
432 return a.cs_count < b.cs_count;
436 if (params.smart_prune) {
437 for (std::size_t i = 0; i < candidates.size(); ++i) {
438 if (candidates[i].isect_size == 0) {
439 candidates.resize(i + 1);
446 if (candidates.size() > params.best_branches) {
447 candidates.resize(params.best_branches);
454 bool saw_intersection =
false;
456 for (
const auto &cand : candidates) {
458 next_state.palette_colors = current.palette_colors;
459 next_state.palette_colors[cand.pal_index] =
460 color_set_union(next_state.palette_colors[cand.pal_index], tile_colors);
461 next_state.next_tile_index = tile_idx + 1;
463 if (cand.isect_size > 0) {
464 saw_intersection =
true;
467 if (!visited.contains(next_state)) {
468 visited.insert(next_state);
469 if (saw_intersection && cand.isect_size == 0) {
470 low_queue.push_back(std::move(next_state));
473 high_queue.push_back(std::move(next_state));
479 return AssignResult::no_solution;
482[[nodiscard]] PackingOutput
483build_packing_output(
const std::vector<ColorSet> &solution_colors,
const SearchContext &ctx,
const PackingInput &input)
485 PackingOutput output;
488 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
489 PackedPalette pal{ctx.hardware_indices[i], ctx.palette_capacities[i]};
492 for (
const auto &prefilled : input.prefilled_pals_) {
493 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
495 PackableTile system_tile{
496 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
497 pal.add_tile(system_tile);
502 output.pals_.push_back(std::move(pal));
508 if (tile.is_prefilled_palette()) {
512 for (std::size_t i = 0; i < solution_colors.size(); ++i) {
513 if (
is_subset(tile.color_set(), solution_colors[i])) {
514 output.pals_[i].add_tile(tile);
515 output.tile_to_pal_[tile.id()] = ctx.hardware_indices[i];
524[[nodiscard]] PackingOutput build_empty_output(
const SearchContext &ctx,
const PackingInput &input)
526 PackingOutput output;
527 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
528 PackedPalette pal{ctx.hardware_indices[i], ctx.palette_capacities[i]};
530 for (
const auto &prefilled : input.prefilled_pals_) {
531 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
533 PackableTile system_tile{
534 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
535 pal.add_tile(system_tile);
540 output.pals_.push_back(std::move(pal));
545[[nodiscard]] std::string format_search_params_line(
const SearchParams ¶ms)
547 std::string branches_str = params.best_branches == std::numeric_limits<std::size_t>::max()
549 : std::to_string(params.best_branches);
551 "algorithm={}, node_cutoff={}, best_branches={}, smart_prune={}.",
555 params.smart_prune ?
"true" :
"false");
558void emit_success_remark(
const UserDiagnostics &diag,
const SearchParams ¶ms,
bool is_preset)
560 std::vector<std::string> lines;
562 lines.emplace_back(
"Backtracking search succeeded with preset config:");
565 lines.emplace_back(
"Backtracking search succeeded:");
567 lines.emplace_back(format_search_params_line(params));
568 diag.remark(
"backtracking-search", lines);
575 auto ctx = build_search_context(input);
577 if (ctx.sorted_tiles.empty()) {
578 return build_empty_output(ctx, input);
581 if (use_preset_matrix_) {
582 auto matrix = build_preset_matrix();
584 for (
const auto ¶ms : matrix) {
585 std::size_t explored = 0;
587 if (params.algorithm == SearchAlgorithm::dfs) {
588 auto colors = ctx.initial_palette_colors;
589 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
590 if (diag_ !=
nullptr) {
591 emit_success_remark(*diag_, params,
true);
593 return build_packing_output(colors, ctx, input);
597 std::vector<ColorSet> solution;
598 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
599 AssignResult::success) {
600 if (diag_ !=
nullptr) {
601 emit_success_remark(*diag_, params,
true);
603 return build_packing_output(solution, ctx, input);
609 "Backtracking strategy failed to find a valid palette assignment after all preset configurations."};
613 SearchParams params{algorithm_, node_cutoff_, best_branches_, smart_prune_};
614 std::size_t explored = 0;
616 if (algorithm_ == SearchAlgorithm::dfs) {
617 auto colors = ctx.initial_palette_colors;
618 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
619 if (diag_ !=
nullptr) {
620 emit_success_remark(*diag_, params,
false);
622 return build_packing_output(colors, ctx, input);
626 std::vector<ColorSet> solution;
627 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
628 AssignResult::success) {
629 if (diag_ !=
nullptr) {
630 emit_success_remark(*diag_, params,
false);
632 return build_packing_output(solution, ctx, input);
637 "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::vector< PackedPalette > initialize_packed_palettes(const std::set< PrefilledPalette > &prefilled_pals, PalettePool &pal_pool, std::size_t pal_capacity)
Initializes packed palettes from prefilled palettes.
std::size_t union_size(const ColorSet &a, const ColorSet &b)
Computes the union size of two ColorSets.