From ccd696c49af9e346c9408859b68ef9731c4a4b02 Mon Sep 17 00:00:00 2001 From: Desour Date: Sun, 19 Feb 2023 20:35:23 +0100 Subject: [PATCH] Throw Hocroft-Karp onto shapeless recipes --- src/craftdef.cpp | 177 +++++++++++++++++++++++++++++++++++++++++------ src/craftdef.h | 2 +- 2 files changed, 156 insertions(+), 23 deletions(-) diff --git a/src/craftdef.cpp b/src/craftdef.cpp index eb703fdbe..ac04716b4 100644 --- a/src/craftdef.cpp +++ b/src/craftdef.cpp @@ -493,6 +493,113 @@ std::string CraftDefinitionShapeless::getName() const return "shapeless"; } +constexpr u16 SHAPELESS_GROUPS_MAX = 30000; + +// Checks if there's a matching that matches all nodes in a given bipartite graph. +// bip_graph has graph_size nodes on each side. It is stored as list of lists of +// neighbors from one side. +// See https://en.wikipedia.org/w/index.php?title=Hopcroft-Karp_algorithm for +// details. +static bool hopcroft_karp_can_match_all(const std::vector> &bip_graph) +{ + assert(bip_graph.size() <= SHAPELESS_GROUPS_MAX); + u16 graph_size = bip_graph.size(); + const u16 nil = graph_size; // nil / dummy index + constexpr u16 inf = UINT16_MAX; // bigger than any path length (> SHAPELESS_GROUPS_MAX * 2) + + auto pair_u = std::make_unique(graph_size + 1); // for each u (or nil) the matched v (or nil) + auto pair_v = std::make_unique(graph_size + 1); // for each v (or nil) the matched u (or nil) + auto dist = std::make_unique(graph_size + 1); // for each u (or nil) the bfs distance + u16 num_matched; + std::queue queue{}; + + // calculates distances from unmatched nodes for augmentation paths until + // dummy is reached + // returns false if dummy can't be reached (and hence there are no further + // augmentation paths) + auto do_bfs = [&]() { + assert(queue.empty()); + + // enqueue all unmatched, give inf dist to the rest + for (u16 u = 0; u < graph_size; ++u) { + if (pair_u[u] == nil) { + dist[u] = 0; + queue.push(u); + } else { + dist[u] = inf; + } + } + dist[nil] = inf; + + while (!queue.empty()) { + u16 u = queue.front(); + queue.pop(); + + if (dist[u] < dist[nil]) { // if dummy not yet reached + for (u16 v : bip_graph[u]) { // for all adjanced of u + u16 u_back = pair_v[v]; + // if u_back unvisited, go there + if (dist[u_back] == inf) { + dist[u_back] = dist[u] + 1; + queue.push(u_back); + } + } + } + } + + return dist[nil] != inf; + }; + + // tries to find an augmenting path from u to the dummy + // if successful, swaps all edges along path and returns true + // otherwise returns false + auto do_dfs_raw = [&](u16 u, auto &&recurse) -> bool { + if (u == nil) // dummy => dest reached + return true; + + for (u16 v : bip_graph[u]) { // for all adjanced of u + u16 u_back = pair_v[v]; + // only walk according to bfs dists + if (dist[u_back] != dist[u] + 1) + continue; + + // if walk along u_back reached dummy, swap edges and backtrack + if (recurse(u_back, recurse)) { + pair_v[v] = u; + pair_u[u] = v; + return true; + } + } + + // unsuccessful path, don't walk here again + dist[u] = inf; + return false; + }; + + auto do_dfs = [&](u16 u) { + return do_dfs_raw(u, do_dfs_raw); + }; + + // everyone starts as matched to dummy + std::fill_n(&pair_u[0], graph_size + 1, nil); + std::fill_n(&pair_v[0], graph_size + 1, nil); + + num_matched = 0; + + while (do_bfs()) { + // try to match unmatched u nodes + for (u16 u = 0; u < graph_size; ++u) { + if (pair_u[u] == nil) { + if (do_dfs(u)) { + num_matched += 1; + } + } + } + } + + return num_matched == graph_size; +} + bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef) const { if (input.method != CRAFT_METHOD_NORMAL) @@ -513,35 +620,61 @@ bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef) return false; } + // Sort input and recipe + std::sort(input_filtered.begin(), input_filtered.end()); + std::vector recipe_copy; - if (hash_inited) + if (hash_inited) { recipe_copy = recipe_names; - else { + } else { recipe_copy = craftGetItemNames(recipe, gamedef); std::sort(recipe_copy.begin(), recipe_copy.end()); } - // Try with all permutations of the recipe, - // start from the lexicographically first permutation (=sorted), - // recipe_names is pre-sorted - do { - // If all items match, the recipe matches - bool all_match = true; - //dstream<<"Testing recipe (output="<idef())) { - all_match = false; - break; - } - } - //dstream<<" -> match="< recipe_nogroup; + std::vector recipe_onlygroup; + std::partition_copy(recipe_copy.begin(), recipe_copy.end(), + std::back_inserter(recipe_onlygroup), + std::back_inserter(recipe_nogroup), + [](const std::string &name) { return str_starts_with(name, "group:"); }); - return false; + // Filter out non-group recipe slots, using sorted merge. + // (This prefiltering is only a performance optimization and not strictly + // necessary.) + std::vector input_for_group; + std::set_difference(input_filtered.begin(), input_filtered.end(), + recipe_nogroup.begin(), recipe_nogroup.end(), + std::back_inserter(input_for_group)); + + // All non-group slots must be satisfied + if (input_filtered.size() - input_for_group.size() != recipe_nogroup.size()) + return false; + + // Find out which recipe slots each input item satisfies. This creates a + // bipartite graph + assert(recipe_onlygroup.size() == input_for_group.size()); + if (recipe_onlygroup.size() > SHAPELESS_GROUPS_MAX) { + // SHAPELESS_GROUPS_MAX is large enough that this should never happen by + // accident + errorstream << "Too many groups in shapless craft." << std::endl; + return false; + } + u16 graph_size = recipe_onlygroup.size(); + // bip_graph[i] are the group-slots that item i can satisfy + std::vector> bip_graph; + bip_graph.resize(graph_size); + for (u16 i = 0; i < graph_size; ++i) { + std::vector &neighbors_i = bip_graph[i]; + for (u16 j = 0; j < graph_size; ++j) { + if (inputItemMatchesRecipe(input_for_group[i], recipe_onlygroup[j], + gamedef->idef())) + neighbors_i.push_back(j); + } + } + + // Check if the maximum cardinality matching of bip_graph matches all items + return hopcroft_karp_can_match_all(bip_graph); } CraftOutput CraftDefinitionShapeless::getOutput(const CraftInput &input, IGameDef *gamedef) const diff --git a/src/craftdef.h b/src/craftdef.h index 97575f893..23a7d7ea0 100644 --- a/src/craftdef.h +++ b/src/craftdef.h @@ -266,7 +266,7 @@ private: std::string output; // Recipe list (itemstrings) std::vector recipe; - // Recipe list (item names) + // Recipe list (item names), sorted std::vector recipe_names; // bool indicating if initHash has been called already bool hash_inited = false;