Throw Hocroft-Karp onto shapeless recipes

This commit is contained in:
Desour 2023-02-19 20:35:23 +01:00 committed by DS
parent 50e91b882c
commit ccd696c49a
2 changed files with 156 additions and 23 deletions

@ -493,6 +493,113 @@ std::string CraftDefinitionShapeless::getName() const
return "shapeless"; 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<std::vector<u16>> &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<u16[]>(graph_size + 1); // for each u (or nil) the matched v (or nil)
auto pair_v = std::make_unique<u16[]>(graph_size + 1); // for each v (or nil) the matched u (or nil)
auto dist = std::make_unique<u16[]>(graph_size + 1); // for each u (or nil) the bfs distance
u16 num_matched;
std::queue<u16> 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 bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef) const
{ {
if (input.method != CRAFT_METHOD_NORMAL) if (input.method != CRAFT_METHOD_NORMAL)
@ -513,35 +620,61 @@ bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef)
return false; return false;
} }
// Sort input and recipe
std::sort(input_filtered.begin(), input_filtered.end());
std::vector<std::string> recipe_copy; std::vector<std::string> recipe_copy;
if (hash_inited) if (hash_inited) {
recipe_copy = recipe_names; recipe_copy = recipe_names;
else { } else {
recipe_copy = craftGetItemNames(recipe, gamedef); recipe_copy = craftGetItemNames(recipe, gamedef);
std::sort(recipe_copy.begin(), recipe_copy.end()); std::sort(recipe_copy.begin(), recipe_copy.end());
} }
// Try with all permutations of the recipe, // Split recipe in group and non-group
// start from the lexicographically first permutation (=sorted), std::vector<std::string> recipe_nogroup;
// recipe_names is pre-sorted std::vector<std::string> recipe_onlygroup;
do { std::partition_copy(recipe_copy.begin(), recipe_copy.end(),
// If all items match, the recipe matches std::back_inserter(recipe_onlygroup),
bool all_match = true; std::back_inserter(recipe_nogroup),
//dstream<<"Testing recipe (output="<<output<<"):"; [](const std::string &name) { return str_starts_with(name, "group:"); });
for (size_t i=0; i<recipe.size(); i++) {
//dstream<<" ("<<input_filtered[i]<<" == "<<recipe_copy[i]<<")";
if (!inputItemMatchesRecipe(input_filtered[i], recipe_copy[i],
gamedef->idef())) {
all_match = false;
break;
}
}
//dstream<<" -> match="<<all_match<<std::endl;
if (all_match)
return true;
} while (std::next_permutation(recipe_copy.begin(), recipe_copy.end()));
return false; // Filter out non-group recipe slots, using sorted merge.
// (This prefiltering is only a performance optimization and not strictly
// necessary.)
std::vector<std::string> 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<std::vector<u16>> bip_graph;
bip_graph.resize(graph_size);
for (u16 i = 0; i < graph_size; ++i) {
std::vector<u16> &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 CraftOutput CraftDefinitionShapeless::getOutput(const CraftInput &input, IGameDef *gamedef) const

@ -266,7 +266,7 @@ private:
std::string output; std::string output;
// Recipe list (itemstrings) // Recipe list (itemstrings)
std::vector<std::string> recipe; std::vector<std::string> recipe;
// Recipe list (item names) // Recipe list (item names), sorted
std::vector<std::string> recipe_names; std::vector<std::string> recipe_names;
// bool indicating if initHash has been called already // bool indicating if initHash has been called already
bool hash_inited = false; bool hash_inited = false;