令人惊讶的是,这个问题可以通过贪心算法解决。我也将其实现为记忆深度优先搜索。直到后来我才注意到搜索从未回溯,并且没有命中记忆缓存。只需要对问题的状态和部分解决方案进行两次检查,就可以知道是否应该进一步追求特定的解决方案分支。它们很容易用一对例子来说明。
首先,考虑这个测试用例:
Chest Number | Key Type To Open Chest | Key Types Inside
--------------+--------------------------+------------------
1 | 2 | 1
2 | 1 | 1 1
3 | 1 | 1
4 | 2 | 1
5 | 2 | 2
Initial keys: 1 1 2
这里总共只有两把类型 2 的钥匙:一把在 5 号箱子里,一把在你最初拥有的地方。但是,三个箱子需要使用类型 2 的钥匙才能打开。我们需要比现有更多的这种类型的钥匙,所以显然不可能打开所有的箱子。我们立即知道这个问题是不可能的。我称这个键为“全局约束”。我们只需要检查一次。我看到这个检查已经在你的程序中了。
仅通过这个检查和记忆的深度优先搜索(就像你的一样!),我的程序能够解决这个小问题,尽管速度很慢:大约花了一分钟。知道程序无法在足够的时间内解决大输入,我从小集合中查看了测试用例。一些测试用例很快就解决了,而另一些则需要很长时间。这是程序花了很长时间才找到解决方案的测试用例之一:
Chest Number | Key Type To Open Chest | Key Types Inside
--------------+--------------------------+------------------
1 | 1 | 1
2 | 6 |
3 | 3 |
4 | 1 |
5 | 7 | 7
6 | 5 |
7 | 2 |
8 | 10 | 10
9 | 8 |
10 | 3 | 3
11 | 9 |
12 | 7 |
13 | 4 | 4
14 | 6 | 6
15 | 9 | 9
16 | 5 | 5
17 | 10 |
18 | 2 | 2
19 | 4 |
20 | 8 | 8
Initial keys: 1 2 3 4 5 6 7 8 9 10
经过简单的检查,这个测试用例的结构是显而易见的。我们有 20 个箱子和 10 把钥匙。十种钥匙类型中的每一种都将打开两个箱子。在使用给定钥匙类型可打开的两个箱子中,一个包含另一个相同类型的钥匙,另一个根本不包含任何钥匙。解决方案很明显:对于每种类型的钥匙,我们必须首先打开将给我们另一把钥匙的箱子,以便能够打开同样需要该类型钥匙的第二个箱子。
该解决方案对人类来说是显而易见的,但是该程序需要很长时间才能解决它,因为它还没有任何方法来检测是否有任何无法再获取的密钥类型。“全局约束”涉及每种密钥的数量,而不是获取它们的顺序。这第二个约束关注的是可以获取密钥的顺序,而不是它们的数量。问题很简单:对于我需要的每种密钥类型,有什么方法我仍然可以得到它吗?
这是我为检查第二个约束而编写的代码:
# Verify that all needed key types may still be reachable
def still_possible(chests, keys, key_req, keys_inside):
keys = set(keys) # set of keys currently in my possession
chests = chests.copy() # set of not-yet-opened chests
# key_req is a dictionary mapping chests to the required key type
# keys_inside is a dict of Counters giving the keys inside each chest
def openable(chest):
return key_req[chest] in keys
# As long as there are chests that can be opened, keep opening
# those chests and take the keys. Here the key is not consumed
# when a chest is opened.
openable_chests = filter(openable, chests)
while openable_chests:
for chest in openable_chests:
keys |= set(keys_inside[chest])
chests.remove(chest)
openable_chests = filter(openable, chests)
# If any chests remain unopened, then they are unreachable no
# matter what order chests are opened, and thus we've gotten into
# a situation where no solution exists.
return not chests # true iff chests is empty
如果此检查失败,我们可以立即中止搜索分支。执行此检查后,我的程序运行得非常快,需要 10 秒而不是 1 分钟。此外,我注意到缓存命中数下降到零,而且,搜索从未回溯。我删除了记忆并将程序从递归形式转换为迭代形式。然后,Python 解决方案能够在大约 1.5 秒内解决“大”测试输入。使用优化编译的几乎相同的C++ 解决方案可在 0.25 秒内解决大量输入。
在Google 官方对该问题的分析中给出了这种迭代、贪心算法正确性的证明。