本文作者zzzz(@ustc-zzzz),使用CC-BY-SA 4.0协议授权。
合成表(又称配方)自然是 Minecraft 玩家非常熟悉且频繁使用的东西了,玩家会通过一个持有 3x3(有时为 2x2)物品槽的界面中添加特定的物品,从而在添加的物品满足一定条件时得到特定的输出。Minecraft 中的合成表分为有序合成和无序合成两种。
上图中上方是南瓜派的合成表,其中三个物品可以任意放置,而上图中下方是曲奇的合成表,只能通过把三个物品摆成一排合成得到,且三个物品的次序不能发生变化。
有序合成的实现相对简单不少,本篇文章主要关注的是无序合成的实现。
基于一对一的合成
我们可以把南瓜派的合成表简记为“[南瓜, 糖, 鸡蛋]”。这三种物品本身是没有次序的,但是物品输入一定存在一个遍历的次序。
这种情况下,匹配无序合成非常简单——遍历输入,然后逐个匹配配方中的元素即可。考虑“[鸡蛋, 南瓜, 糖]”这一输入次序:
- 首先匹配鸡蛋,配方便会剩下“[南瓜, 糖]”
- 然后匹配南瓜,配方便会只剩下“糖”
- 最后匹配糖,匹配大成功
这本质上和判定两个元素为 n 的集合是否等价是一样的。我们只需要通过稍高于 O(n) 的时间复杂度和 O(n) 的空间复杂度便可完成匹配。
Minecraft 1.12 及更早的版本使用的便是这一方法。这一方法在所有原版的无序合成中并未出现问题,而 1.12 也不允许玩家自定义合成表。但是基于 Forge 的 Mod 存在大量自定义合成表,因此有人率先发现了问题。
基于多对一的合成
然而现实并非那样简单。游戏中存在着很多按种类划分的物品,比如说蘑菇既可以是红蘑菇,也可以是棕蘑菇,木板有包括但不限于橡木、云杉等品种。Minecraft 允许在配方的某个成份中声明某种特定类型的物品,而满足条件的任意物品均可参与合成。
如果我们再使用之前的方式匹配,很快就会遇到麻烦。考虑“[木板, 橡木木板]”这样一个合成表,和“[橡木木板, 云杉木板]”这一输入次序:
- 首先匹配橡木木板,配方便会剩下“橡木木板”
- 然后匹配云杉木板,因“橡木木板”无法和云杉木板对应,匹配失败
这便是现实。只需要把“[橡木木板, 云杉木板]”这一输入次序颠倒过来便可成功完成匹配的合成表,在这种情况下竟然不能匹配了。事实上,Forge 的出现更是大大增加了这件事的复杂度。Forge 引入的矿物辞典机制使得游戏中几十种物品属于同一类的情况时常发生,而这些物品却会大量出现在合成表里。
我们现在手头有什么?
- 输入物品的集合(
Set[ItemStack]
) - 配方组成成分的集合(
Set[Ingredient]
) - 判断物品是否满足配方的函数(
(ItemStack, Ingredient) => Boolean
)
我们据此可以得到输入物品和配方组成成分之前的关系图。对相关算法了解的同学应该能够立刻意识到——这是二分图的最大匹配问题。
匹配与匈牙利算法
匈牙利算法是一种相对高效的解决最大匹配问题的算法。该算法对于两个元素为 n 的集合及 m 个边的情况下,能够通过 O(mn) 的时间复杂度解决问题。
匈牙利算法的核心是增广路径。科学家已经证明:当且仅当没有增广路径的时候,对应的是二分图的最大匹配。
增广路径的定义是二分图中第一个和最后一个边均为未匹配边,而中间路径中未匹配边和匹配边交替出现的路径。显然,增广路径中第一个和最后一个顶点均为未匹配点,而其中所有顶点均为匹配点。
匈牙利算法本质上是通过不断地寻找增广路径扩大匹配的数量。我们将增广路径中未匹配边和匹配边交换,便能立刻使已有的匹配数加一。
我们考虑“[红砂岩, 砂岩, 錾制砂岩, 切制砂岩]”这样一个合成表,和“[錾制红砂岩, 切制红砂岩, 切制砂岩, 砂岩]”这一输入次序。显然,只需要从二分图的一个部分遍历增广路径便可完成匹配,因此我们这里从第一个物品开始。
- 由于没有已匹配路径,因此我们选取的第一个增广路径只经过两个顶点:錾制红砂岩(物品)→红砂岩(配方)。我们将其标记为已匹配路径
- 然后我们分析切制红砂岩,我们能够找到一个较长的增广路径:切制红砂岩(物品)→红砂岩(配方)→錾制红砂岩(物品)→砂岩(配方)。我们进行一次变换,现在得到了两个匹配
- 然后我们分析切制砂岩,这次的增广路径更长:切制砂岩(物品)→砂岩(配方)→錾制红砂岩(物品)→红砂岩(配方)→切制红砂岩(物品)→切制砂岩(配方)。我们进行一次变换,现在得到了三个匹配
- 然后我们分析砂岩,我们得到了目前以来最长的增广路径:砂岩(物品)→砂岩(配方)→切制砂岩(物品)→切制砂岩(配方)→切制红砂岩(物品)→红砂岩(配方)→錾制红砂岩(物品)→錾制砂岩(配方),我们进行一次变换,得到了全部四个匹配
- 然后我们发现没有节点可以分析了,因此我们得到这是一个完全匹配,亦即所有的物品都匹配到了对应的配方,我们判定匹配成功。
Minecraft 在新版本内部便是使用的匈牙利算法。自然,对于二分图的最大匹配而言,有更高效的 Hopcroft-Karp 算法,不过 Minecraft 本体并未采用。笔者个人认为,九宫格的规模很小,使用 Hopcroft-Karp 算法可能也没有太大的必要。
虽然 Minecraft 从 1.13 开始允许玩家使用原版自定义合成表了,不过错误的实现只持续到了 Minecraft 1.12,因此对于原版玩家而言这是透明的。就 Forge 而言,他们在某个提交修复了这个问题。
鸣谢
- LexManos:Forge 项目的领导人,修复该问题的主要开发者
- Grum:Mojang 员工,为这个问题的解决方案提出了不可或缺的意见
- MinecraftForge#4516:正是这个 issue 发现并指出了无序合成的问题所在
- liach:正是他一针见血地指出了无序合成的问题可以归结为二分图的最大匹配问题