Browse Source

Throw Hocroft-Karp onto shapeless recipes

Desour 1 year ago
parent
commit
ccd696c49a
2 changed files with 155 additions and 22 deletions
  1. 154 21
      src/craftdef.cpp
  2. 1 1
      src/craftdef.h

+ 154 - 21
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<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
 {
 	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<std::string> 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="<<output<<"):";
-		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;
-			}
+	// Split recipe in group and non-group
+	std::vector<std::string> recipe_nogroup;
+	std::vector<std::string> 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:"); });
+
+	// 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);
 		}
-		//dstream<<" -> match="<<all_match<<std::endl;
-		if (all_match)
-			return true;
-	} while (std::next_permutation(recipe_copy.begin(), recipe_copy.end()));
+	}
 
-	return false;
+	// 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

+ 1 - 1
src/craftdef.h

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