mirror of
https://github.com/minetest/minetest.git
synced 2024-11-23 16:13:46 +01:00
Throw Hocroft-Karp onto shapeless recipes
This commit is contained in:
parent
50e91b882c
commit
ccd696c49a
177
src/craftdef.cpp
177
src/craftdef.cpp
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user