Porytiles
Loading...
Searching...
No Matches
backtracking_strategy.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <deque>
7#include <format>
8#include <limits>
9#include <map>
10#include <string>
11#include <unordered_set>
12#include <vector>
13
23
24namespace porytiles {
25
26namespace {
27
28enum class AssignResult { success, no_solution, cutoff_reached };
29
30struct SearchParams {
32 std::size_t node_cutoff;
33 std::size_t best_branches; // SIZE_MAX = unlimited
35};
36
37struct SearchContext {
38 std::vector<PackableTile> sorted_tiles;
39 std::vector<ColorSet> initial_palette_colors;
40 std::vector<std::size_t> palette_capacities;
41 std::vector<std::size_t> hardware_indices;
42
49 const ShapeGroupMetadata *shape_group_metadata = nullptr;
50
57 std::vector<std::vector<ColorSet>> sibling_color_sets;
58};
59
60struct BfsState {
61 std::vector<ColorSet> palette_colors;
62 std::size_t next_tile_index{};
63 bool operator==(const BfsState &) const = default;
64};
65
66struct BfsStateHash {
67 std::size_t operator()(const BfsState &s) const noexcept
68 {
69 std::size_t seed = std::hash<std::size_t>{}(s.next_tile_index);
70 for (const auto &cs : s.palette_colors) {
71 seed ^= std::hash<ColorSet>{}(cs) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
72 }
73 return seed;
74 }
75};
76
77[[nodiscard]] std::array<SearchParams, 48> build_preset_matrix()
78{
79 std::array<SearchParams, 48> matrix{};
80 std::size_t idx = 0;
81
82 constexpr std::array<std::size_t, 4> cutoffs = {1'000'000, 2'000'000, 4'000'000, 8'000'000};
83
84 for (std::size_t cutoff : cutoffs) {
85 for (auto algo : {SearchAlgorithm::dfs, SearchAlgorithm::bfs}) {
86 // Configuration 1: unlimited branches with smart pruning
87 matrix[idx++] = SearchParams{algo, cutoff, std::numeric_limits<std::size_t>::max(), true};
88
89 // Configurations 2-6: limited branches without smart pruning
90 for (std::size_t branches = 2; branches <= 6; ++branches) {
91 matrix[idx++] = SearchParams{algo, cutoff, branches, false};
92 }
93 }
94 }
95
96 return matrix;
97}
98
99[[nodiscard]] SearchContext build_search_context(const PackingInput &input)
100{
101 SearchContext ctx;
102
103 // Combine hints and regular tiles, sorted descending by color count (FFD)
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);
107 }
108 for (const auto &tile : input.tiles_) {
109 ctx.sorted_tiles.push_back(tile);
110 }
111 std::ranges::sort(ctx.sorted_tiles, [](const PackableTile &a, const PackableTile &b) {
112 // Descending by color count, tiebreak by ID for determinism
113 if (a.color_count() != b.color_count()) {
114 return a.color_count() > b.color_count();
115 }
116 return a.id() < b.id();
117 });
118
119 // Initialize palettes from prefilled + available pool slots
120 PalettePool pool = input.palette_pool_;
121 auto prefilled_palettes = initialize_packed_palettes(input.prefilled_palettes_, pool, input.palette_capacity_);
122
123 // Build palette arrays: first prefilled, then empty slots from 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());
128 }
129
130 // Compute effective capacities for prefilled palettes
131 for (std::size_t i = 0; i < prefilled_palettes.size(); ++i) {
132 // Account for wasted slots from duplicate colors in prefilled palettes
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;
139 break;
140 }
141 }
142 }
143
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);
149 }
150
151 // Populate sharing metadata if available
152 if (input.shape_group_metadata_.has_value()) {
153 ctx.shape_group_metadata = &input.shape_group_metadata_.value();
154
155 // Build sibling color sets for each sorted tile
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()) {
161 continue;
162 }
163 std::size_t group_idx = group_it->second;
164 const auto &members = ctx.shape_group_metadata->group_members[group_idx];
165
166 // Find sibling color sets from the sorted_tiles vector
167 for (const auto &sibling_id : members) {
168 if (sibling_id == tile_id) {
169 continue;
170 }
171 // Look up sibling in sorted_tiles to get its color set
172 for (const auto &st : ctx.sorted_tiles) {
173 if (st.id() == sibling_id) {
174 ctx.sibling_color_sets[i].push_back(st.color_set());
175 break;
176 }
177 }
178 }
179 }
180 }
181
182 return ctx;
183}
184
185// DFS with in-place mutation and undo. Porytiles1 copied the entire palette vector for each branch;
186// we save and restore only the single modified ColorSet, reducing per-node allocation overhead.
187//
188// Candidate palettes are sorted by intersection_size (descending), then color_set_count (ascending).
189// This "best-fit" heuristic tries palettes with the most color overlap first, preferring emptier
190// palettes as a tiebreaker. smart_prune caps candidates after the first zero-intersection palette,
191// and best_branches limits total branching factor.
192AssignResult assign_depth_first(
193 std::vector<ColorSet> &palette_colors,
194 const SearchContext &ctx,
195 const SearchParams &params,
196 std::size_t next_tile_index,
197 std::size_t &explored_nodes)
198{
199 ++explored_nodes;
200 if (explored_nodes > params.node_cutoff) {
201 return AssignResult::cutoff_reached;
202 }
203
204 // Base case: all tiles assigned
205 if (next_tile_index >= ctx.sorted_tiles.size()) {
206 return AssignResult::success;
207 }
208
209 const auto &tile = ctx.sorted_tiles[next_tile_index];
210 const auto &tile_colors = tile.color_set();
211
212 // Authoritative subset shortcut (improvement over Porytiles1).
213 //
214 // If the tile's colors are already a subset of some palette, the tile is satisfied without adding
215 // any new colors. We recurse immediately and return the result directly (no fallthrough to the
216 // candidate loop).
217 //
218 // Why authoritative? If skipping the tile fails (remaining tiles can't be packed), trying explicit
219 // candidate assignments can only make things worse:
220 // - Assigning to the covering palette is a no-op (union doesn't change its ColorSet), so we'd
221 // re-explore the exact same subtree that already failed.
222 // - Assigning to a different palette ADDS colors to it, strictly reducing its remaining capacity.
223 //
224 // A non-authoritative version that fell through to candidates caused exponential blowup: at each
225 // of K levels with a subset match, the same subtree was explored twice (once via shortcut, once
226 // via the covering palette as first candidate), yielding O(2^K) redundant work. For tilesets like
227 // gTileset_General with many shared colors, K is large enough to make the search hang indefinitely.
228 for (std::size_t i = 0; i < palette_colors.size(); ++i) {
229 if (is_subset(tile_colors, palette_colors[i])) {
230 return assign_depth_first(palette_colors, ctx, params, next_tile_index + 1, explored_nodes);
231 }
232 }
233
234 // Build candidate list: (palette_index, intersection_size, color_set_count, has_sibling)
235 struct Candidate {
236 std::size_t palette_index;
237 std::size_t isect_size;
238 std::size_t cs_count;
239 bool has_sibling;
240 };
241 std::vector<Candidate> candidates;
242 candidates.reserve(palette_colors.size());
243
244 for (std::size_t i = 0; i < palette_colors.size(); ++i) {
245 std::size_t u_size = union_size(tile_colors, palette_colors[i]);
246 if (u_size <= ctx.palette_capacities[i]) {
247 std::size_t i_size = intersection_size(tile_colors, palette_colors[i]);
248 std::size_t c_count = color_set_count(palette_colors[i]);
249
250 // Heuristic: check if this palette likely contains a sibling by testing whether any sibling's color set
251 // is a subset of the palette's accumulated colors. This is an approximation; false positives are possible
252 // when unrelated tiles contribute the same colors. False positives only cause suboptimal candidate ordering
253 // (deprioritizing a palette unnecessarily), not incorrect packing.
254 bool sibling = false;
255 if (!ctx.sibling_color_sets.empty() && next_tile_index < ctx.sibling_color_sets.size()) {
256 for (const auto &sibling_cs : ctx.sibling_color_sets[next_tile_index]) {
257 if (is_subset(sibling_cs, palette_colors[i])) {
258 sibling = true;
259 break;
260 }
261 }
262 }
263
264 candidates.push_back(Candidate{i, i_size, c_count, sibling});
265 }
266 }
267
268 // Sort: no_sibling < has_sibling, then descending by intersection_size, then ascending by color_set_count
269 std::ranges::sort(candidates, [](const Candidate &a, const Candidate &b) {
270 if (a.has_sibling != b.has_sibling) {
271 return !a.has_sibling;
272 }
273 if (a.isect_size != b.isect_size) {
274 return a.isect_size > b.isect_size;
275 }
276 return a.cs_count < b.cs_count;
277 });
278
279 // Smart prune: cap candidates after first zero-intersection palette
280 if (params.smart_prune) {
281 for (std::size_t i = 0; i < candidates.size(); ++i) {
282 if (candidates[i].isect_size == 0) {
283 // Keep this one but remove the rest after it
284 candidates.resize(i + 1);
285 break;
286 }
287 }
288 }
289
290 // Apply best_branches limit
291 if (candidates.size() > params.best_branches) {
292 candidates.resize(params.best_branches);
293 }
294
295 for (const auto &cand : candidates) {
296 // Save/restore single palette (Porytiles1 copied the entire vector per branch)
297 ColorSet saved = palette_colors[cand.palette_index];
298 palette_colors[cand.palette_index] = color_set_union(palette_colors[cand.palette_index], tile_colors);
299
300 auto result = assign_depth_first(palette_colors, ctx, params, next_tile_index + 1, explored_nodes);
301 if (result != AssignResult::no_solution) {
302 return result;
303 }
304
305 // Restore state (backtrack)
306 palette_colors[cand.palette_index] = saved;
307 }
308
309 return AssignResult::no_solution;
310}
311
312// BFS with dual-queue heuristic and visited-state deduplication (matching Porytiles1's approach).
313//
314// Two queues partition the frontier: high_queue for states reached via overlapping assignments
315// (intersection > 0), and low_queue for states reached via zero-overlap assignments. The high_queue
316// is always drained first, focusing exploration on promising branches before resorting to "waste"
317// assignments that consume fresh palette capacity.
318//
319// Improvement over Porytiles1: when ALL candidates for a tile have zero intersection (no palette has
320// any color overlap), Porytiles1 routed them to the high queue via a `sawAssignmentWithIntersection`
321// flag that stayed false. We preserve this behavior: zero-intersection candidates only go to the low
322// queue after we've seen at least one candidate with overlap. This prevents starvation when a tile
323// has entirely unique colors (common for early tiles assigned to empty palettes).
324AssignResult assign_breadth_first(
325 const std::vector<ColorSet> &initial_colors,
326 const SearchContext &ctx,
327 const SearchParams &params,
328 std::size_t &explored_nodes,
329 std::vector<ColorSet> &solution)
330{
331 std::deque<BfsState> high_queue;
332 std::deque<BfsState> low_queue;
333 std::unordered_set<BfsState, BfsStateHash> visited;
334
335 BfsState initial{initial_colors, 0};
336 visited.insert(initial);
337 high_queue.push_back(std::move(initial));
338
339 while (!high_queue.empty() || !low_queue.empty()) {
340 ++explored_nodes;
341 if (explored_nodes > params.node_cutoff) {
342 return AssignResult::cutoff_reached;
343 }
344
345 // Dequeue: prefer high_queue (overlap assignments)
346 BfsState current = [&]() {
347 if (!high_queue.empty()) {
348 BfsState s = std::move(high_queue.front());
349 high_queue.pop_front();
350 return s;
351 }
352 BfsState s = std::move(low_queue.front());
353 low_queue.pop_front();
354 return s;
355 }();
356
357 // Skip tiles whose colors are already subsets (advance next_tile_index)
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;
362 for (const auto &pc : current.palette_colors) {
363 if (is_subset(tc, pc)) {
364 already_covered = true;
365 break;
366 }
367 }
368 if (!already_covered) {
369 break;
370 }
371 ++tile_idx;
372 }
373
374 // Base case: all tiles assigned
375 if (tile_idx >= ctx.sorted_tiles.size()) {
376 solution = std::move(current.palette_colors);
377 return AssignResult::success;
378 }
379
380 const auto &tile_colors = ctx.sorted_tiles[tile_idx].color_set();
381
382 // Build candidates
383 struct Candidate {
384 std::size_t palette_index;
385 std::size_t isect_size;
386 std::size_t cs_count;
387 bool has_sibling;
388 };
389 std::vector<Candidate> candidates;
390 candidates.reserve(current.palette_colors.size());
391
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]) {
395 std::size_t i_size = intersection_size(tile_colors, current.palette_colors[i]);
396 std::size_t c_count = color_set_count(current.palette_colors[i]);
397
398 // Check if this palette already contains a sibling
399 bool sibling = false;
400 if (!ctx.sibling_color_sets.empty() && tile_idx < ctx.sibling_color_sets.size()) {
401 for (const auto &sibling_cs : ctx.sibling_color_sets[tile_idx]) {
402 if (is_subset(sibling_cs, current.palette_colors[i])) {
403 sibling = true;
404 break;
405 }
406 }
407 }
408
409 candidates.push_back(Candidate{i, i_size, c_count, sibling});
410 }
411 }
412
413 // Sort: no_sibling < has_sibling, then descending by intersection_size, ascending by color_set_count
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;
417 }
418 if (a.isect_size != b.isect_size) {
419 return a.isect_size > b.isect_size;
420 }
421 return a.cs_count < b.cs_count;
422 });
423
424 // Smart prune
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);
429 break;
430 }
431 }
432 }
433
434 // Apply best_branches limit
435 if (candidates.size() > params.best_branches) {
436 candidates.resize(params.best_branches);
437 }
438
439 // Track whether we've seen any candidate with color overlap, matching Porytiles1's
440 // dual-queue heuristic: when NO candidate has intersection, all go to high_queue
441 // (they're the only options). Only after seeing an intersection candidate do
442 // zero-intersection candidates go to low_queue.
443 bool saw_intersection = false;
444
445 for (const auto &cand : candidates) {
446 BfsState next_state;
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;
451
452 if (cand.isect_size > 0) {
453 saw_intersection = true;
454 }
455
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));
460 }
461 else {
462 high_queue.push_back(std::move(next_state));
463 }
464 }
465 }
466 }
467
468 return AssignResult::no_solution;
469}
470
471[[nodiscard]] PackingOutput
472build_packing_output(const std::vector<ColorSet> &solution_colors, const SearchContext &ctx, const PackingInput &input)
473{
474 PackingOutput output;
475
476 // Create PackedPalettes
477 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
478 PackedPalette palette{ctx.hardware_indices[i], ctx.palette_capacities[i]};
479
480 // Add system tile for prefilled palettes
481 for (const auto &prefilled : input.prefilled_palettes_) {
482 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
483 color_set_count(prefilled.fixed_colors()) > 0) {
484 PackableTile system_tile{
485 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
486 palette.add_tile(system_tile);
487 break;
488 }
489 }
490
491 output.palettes_.push_back(std::move(palette));
492 }
493
494 // Assign each tile to the first palette whose solution colors are a superset
495 for (const auto &tile : ctx.sorted_tiles) {
496 // Skip prefilled palette system tiles (already added above)
497 if (tile.is_prefilled_palette()) {
498 continue;
499 }
500
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];
505 break;
506 }
507 }
508 }
509
510 return output;
511}
512
513[[nodiscard]] PackingOutput build_empty_output(const SearchContext &ctx, const PackingInput &input)
514{
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]};
518
519 for (const auto &prefilled : input.prefilled_palettes_) {
520 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
521 color_set_count(prefilled.fixed_colors()) > 0) {
522 PackableTile system_tile{
523 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
524 palette.add_tile(system_tile);
525 break;
526 }
527 }
528
529 output.palettes_.push_back(std::move(palette));
530 }
531 return output;
532}
533
534[[nodiscard]] std::string format_search_params_line(const SearchParams &params)
535{
536 std::string branches_str = params.best_branches == std::numeric_limits<std::size_t>::max()
537 ? "unlimited"
538 : std::to_string(params.best_branches);
539 return std::format(
540 "algorithm={}, node_cutoff={}, best_branches={}, smart_prune={}.",
541 to_string(params.algorithm),
542 params.node_cutoff,
543 branches_str,
544 params.smart_prune ? "true" : "false");
545}
546
547void emit_success_remark(const UserDiagnostics &diag, const SearchParams &params, bool is_preset)
548{
549 std::vector<std::string> lines;
550 if (is_preset) {
551 lines.emplace_back("Backtracking search succeeded with preset config:");
552 }
553 else {
554 lines.emplace_back("Backtracking search succeeded:");
555 }
556 lines.emplace_back(format_search_params_line(params));
557 diag.remark("backtracking-search", lines);
558}
559
560} // namespace
561
562ChainableResult<PackingOutput> BacktrackingStrategy::pack(const PackingInput &input) const
563{
564 auto ctx = build_search_context(input);
565
566 if (ctx.sorted_tiles.empty()) {
567 return build_empty_output(ctx, input);
568 }
569
570 if (use_preset_matrix_) {
571 auto matrix = build_preset_matrix();
572
573 for (const auto &params : matrix) {
574 std::size_t explored = 0;
575
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);
581 }
582 return build_packing_output(colors, ctx, input);
583 }
584 }
585 else {
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);
591 }
592 return build_packing_output(solution, ctx, input);
593 }
594 }
595 }
596
597 return FormattableError{
598 "Backtracking strategy failed to find a valid palette assignment after all preset configurations."};
599 }
600
601 // Single-config mode: run one search with the configured parameters
602 SearchParams params{algorithm_, node_cutoff_, best_branches_, smart_prune_};
603 std::size_t explored = 0;
604
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);
610 }
611 return build_packing_output(colors, ctx, input);
612 }
613 }
614 else {
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);
620 }
621 return build_packing_output(solution, ctx, input);
622 }
623 }
624
625 return FormattableError{
626 "Backtracking strategy failed to find a valid palette assignment with the configured parameters."};
627}
628
629} // namespace porytiles
std::size_t node_cutoff
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
bool smart_prune
std::vector< ColorSet > palette_colors
A result type that maintains a chainable sequence of errors for debugging and error reporting.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
bool is_subset(const ColorSet &a, const ColorSet &b)
Checks if one ColorSet is a subset of another.
Definition color_set.cpp:49
ColorSet color_set_union(const ColorSet &a, const ColorSet &b)
Computes the union of two ColorSets.
Definition color_set.cpp:20
std::size_t intersection_size(const ColorSet &a, const ColorSet &b)
Computes the intersection size between two ColorSets.
Definition color_set.cpp:58
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.
Definition color_set.cpp:44
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.
Definition color_set.cpp:63
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.
Input data aggregate for the low-level palette packing algorithm.