HackerRank #Lego Blocks

题目来自 HackerRank1,给定了任意数量的四种砖块,高度都是 1,宽度分别为 1、2、3、4。需要用这些砖块填充一面 height x width 的墙,当然不能有重叠、不能有空缺。

限制是,墙的中间不允许存在一条竖线分开整面墙,不然左右两部分没有固定,整面墙不安全。

要求计算出共有多少种符合条件的拼接方式,结果可能比较大,对 10^9 + 7 取 MOD。

1"""
21 <= height, width <= 1000
3"""
4def legoBlocks(height: int, width: int) -> int:
5    pass

这种组合问题,尤其是只考虑高度为 1 时的组合,是典型的一个问题拆分成重复子问题,是 DP 没错。

如果没有不允许出现竖线的限制,我们可以直接算出 1 x width 的组合数 M,多层之间是独立的,最终结果就是 M^height 了。

但是这个竖线的限制很麻烦,竖线这里是对应总 height 的 global 状态,我们怎么在处理每一行的时候,基于这个 global 状态做状态转移呢?

从另一个 LeetCode 的题目 #554 Brick Wall2 找到了一点灵感。
这道题中,已经有了一个 height x width 的墙,我们希望在 [1, width - 1] 的范围里垂直切割,求破坏的砖块的最小数量。
解法中有一个小小的逆向思维,总共 height 层的切割过程中要么刚好穿过砖块的缝隙,要么破坏了砖块,那咱们忽略有没有破坏砖块了,只统计有没有穿过缝隙。

对于现在这道 Lego 题,咱们也逆向思维从竖线考虑。希望找到所有安全的组合数,我已经知道所有组合的总数量了,那就只用算出不安全的组合数,做个减法。

怎么算不安全的组合数呢?我们累计整面墙以坐标 a 对应的竖线拆开的组合数。如果 a 的位置有竖线,左边的宽度是 a,右边的宽度是 width - a,不安全的总数就是左边的组合数 x 右边的组合数,我们从总数量中减去这部分不安全的数量。

1┌──────│───────┐
2│      │       │
3│      │       │
4└──────│───────┘
5       a

如果最终组合里只有一条竖线,是没有问题的,但是如果有多条竖线,会导致同样的组合被减了多次。比如下面的例子,当我遍历到 a 时,右边的组合中包含了有 b 的情况,当遍历到 b 时,左边组合中包含有 a 的情况。

1┌────│────│────┐
2│    │    │    │
3│    │    │    │
4└────│────│────┘
5     a    b

对于遍历过程中唯一的坐标 x,怎么去除这个重复呢?我们可以从 2 个角度理解:

好了,我们重新梳理一遍流程。

  1. 我们希望求 height x width 对应的所有安全组合数。
  2. 不方便直接计算安全的组合数,我们选择用所有组合数减去不安全的组合数。
  3. 对于 1 x width 的组合数,是一维 DP 问题。
  4. 高度为 height 的所有组合数,就是高度为 1 的结果 M^height。
  5. 需要减去所有不安全的组合数,为了避免多次扣除,我们遍历 [1, width - 1]。
    对于每一个位置 x,我们希望求左边都是安全的,右边是任意的,对应的所有组合数,从总数中扣除。

那么就可以 coding 了:

 1from functools import cache
 2
 3MOD = 1_000_000_007
 4
 5@cache
 6def singleRowCount(width: int) -> int:
 7    if width == 0:
 8        return 1
 9    res = 0
10    for i in range(1, 5):
11        if width >= i:
12            res += singleRowCount(width - i)
13    return res
14
15@cache
16def unsafeCount(height: int, width: int) -> int:
17    return pow(singleRowCount(width), height, MOD)
18
19@cache
20def safeCount(height: int, width: int) -> int:
21    if width == 1:
22        return 1
23    res = unsafeCount(height, width)
24    # no break in left range, any in right range
25    for i in range(1, width):
26        res -= safeCount(height, i) * unsafeCount(height, width - i)
27        res %= MOD
28    return res
29
30def legoBlocks(height, width):
31    return safeCount(height, width)

AC 之后的碎碎念是: