126. 长方体层(Cuboid Layers)
在一个 \(3 \times 2 \times 1\) 的长方体上,覆盖每一个可见面所需的最少立方体数量是 \(22\)。
如果我们在这基础上添加第二层,则需要 \(46\) 个立方体来覆盖所有可见面;第三层需要 \(78\) 个立方体;第四层需要 \(118\) 个立方体。
然而,在 \(5 \times 1 \times 1\) 的长方体上,第一层也需要 \(22\) 个立方体;同样地,在 \(5 \times 3 \times 1\)、\(7 \times 2 \times 1\) 和 \(11 \times 1 \times 1\) 的长方体上,第一层都需要 \(46\) 个立方体。
我们定义 \(C(n)\) 为"在某一层中恰好包含 \(n\) 个立方体"的长方体个数。因此 \(C(22) = 2\),\(C(46) = 4\),\(C(78) = 5\),\(C(118) = 8\)。
事实证明,\(154\) 是使得 \(C(n) = 10\) 的最小的 \(n\)。
求使得 \(C(n) = 1000\) 的最小的 \(n\)。
一、数学背景
考虑一个尺寸为 \(a \times b \times c\)(\(a \le b \le c\))的长方体。初始时,其外表面由 \(2(ab + ac + bc)\) 个单位正方形组成。第一层在每一个暴露的面上放置一个单位立方体,因此第一层恰好需要 \(2(ab + ac + bc)\) 个立方体。
经过 \(k-1\) 层之后,立体形状的表面面积(以单位正方形计)具有封闭形式。第 \(k\) 层所需的立方体数量为:
公式验证(以 \(3 \times 2 \times 1\) 为例):
- \(k=1\):\(2(6+3+2) + 0 + 0 = 22\) ✓
- \(k=2\):\(22 + 4 \cdot 1 \cdot 6 + 4 \cdot 1 \cdot 0 = 22 + 24 = 46\) ✓
- \(k=3\):\(22 + 4 \cdot 2 \cdot 6 + 4 \cdot 2 \cdot 1 = 22 + 48 + 8 = 78\) ✓
- \(k=4\):\(22 + 4 \cdot 3 \cdot 6 + 4 \cdot 3 \cdot 2 = 22 + 72 + 24 = 118\) ✓
对于固定的 \((a,b,c)\),相邻层之间的增量构成等差数列:
首项(\(k=2\) 相对于 \(k=1\))为 \(4(a+b+c)\),公差为 \(8\)。
二、算法设计
采用直接枚举法:
- 迭代所有合理的 \((a,b,c,k)\) 组合,计算 \(L(a,b,c,k)\),统计每个 \(n\) 出现的次数 \(C(n)\)。
- 从小到大检查 \(C(n)\),找到第一个等于 \(1000\) 的 \(n\)。
枚举边界:
- \(a\) 的范围:由于 \(2ab \le L\)(\(k=1\) 且忽略非负项),\(a \le \sqrt{N/2}\)。
- \(b\) 的范围:在 \(a\) 固定且 \(c=b\) 时,\(L_{\min} = 4ab + 2b^2 \le N\),解得 \(b \le -a + \sqrt{a^2 + N/2}\)。
- \(c\) 的范围:\(k=1\) 时 \(L = 2ab + 2c(a+b) \le N\),解得 \(c \le (N/2 - ab)/(a+b)\)。
- \(k\) 的范围:对于固定的 \((a,b,c)\),使用递推式 \(L \gets L + \text{inc}\),\(\text{inc} \gets \text{inc} + 8\),直到 \(L > N\) 为止。
采用迭代加深策略:从 \(N=5000\) 开始,每次翻倍,直到找到答案。
三、复杂度分析
- 时间复杂度:枚举的三重循环主导开销,总体约为 \(O(N^{3/2} \log N)\),其中 \(N\) 为搜索上界。对于 \(N \approx 20000\),实际迭代次数约在百万量级,可在亚秒内完成。
- 空间复杂度:需要一个长度为 \(N+1\) 的计数数组,\(O(N)\)。对于 \(N \approx 20000\),内存占用约 \(160\) KB。
四、代码实现与说明
"""Project Euler 126 - Cuboid Layers."""
from time import perf_counter
def count_layers(max_n: int):
"""Count C(n) for all n from 1 to max_n via direct enumeration.
Iterates over a <= b <= c and k >= 1, computing L(a,b,c,k) and
incrementing the count for that value.
Args:
max_n: Maximum layer-cube count to consider.
Returns:
List C of length max_n+1 where C[n] = count of (a,b,c,k) producing n.
"""
C = [0] * (max_n + 1)
# a <= sqrt(max_n / 2) because 2ab <= L for any k >= 1, b >= a
max_a = int((max_n / 2) ** 0.5) + 1
for a in range(1, max_a + 1):
a2 = a * a
half_n = max_n / 2
# For fixed a, minimum L occurs when b=c and k=1:
# L_min = 2ab + 2b(a+b) = 4ab + 2b^2 <= max_n
# => b <= -a + sqrt(a^2 + max_n/2)
max_b = int(-a + (a2 + half_n) ** 0.5) + 1
if max_b < a:
continue
for b in range(a, max_b + 1):
ab = a * b
s_ab = a + b # a + b
# For k=1: L = 2ab + 2c(a+b) <= max_n
# => c <= (max_n/2 - ab) / (a+b)
max_c = (max_n // 2 - ab) // s_ab
if max_c < b:
continue
for c in range(b, max_c + 1):
s_abc = s_ab + c # a + b + c
# L for k=1
L = 2 * (ab + b * c + a * c)
if L > max_n:
continue
C[L] += 1
# For k >= 2: L(k) = L(k-1) + 4(a+b+c) + 8(k-2)
# First increment (k=1 -> k=2): inc = 4(a+b+c)
inc = 4 * s_abc
while True:
L += inc
if L > max_n:
break
C[L] += 1
inc += 8 # each subsequent step adds 8 more
return C
def solve(target: int = 1000) -> int:
"""Find the smallest n such that C(n) equals the target value.
Uses iterative deepening with a geometric progression on the search bound.
Args:
target: The desired value of C(n). Default 1000.
Returns:
The smallest positive integer n for which C(n) == target.
Raises:
RuntimeError: If not found within a reasonable bound.
"""
max_n = 5000
while max_n < 10 ** 8:
print(f"Searching up to n = {max_n}...")
C = count_layers(max_n)
for n in range(1, max_n + 1):
if C[n] == target:
return n
max_n *= 2
raise RuntimeError("Not found within search bound")
def verify_samples():
"""Verify C(n) values for the sample data given in the problem."""
C = count_layers(200)
samples = {22: 2, 46: 4, 78: 5, 118: 8, 154: 10}
for n, expected in samples.items():
actual = C[n]
status = "PASS" if actual == expected else f"FAIL (got {actual})"
print(f" C({n}) = {actual} {status}")
if __name__ == "__main__":
t0 = perf_counter()
print("Sample verification:")
verify_samples()
print()
result = solve(1000)
elapsed = perf_counter() - t0
print(f"\nAnswer: {result}")
print(f"Time: {elapsed:.3f}s")
常量与导入
- 仅导入
perf_counter用于计时,无外部依赖。
count_layers 函数
- 创建长度为
max_n + 1的计数数组C,C[n]表示使 \(L(a,b,c,k) = n\) 的 \((a,b,c,k)\) 组合数。 max_a上界推导:对于 \(k \ge 1\) 且 \(b \ge a\),有 \(2ab \le L\),因此 \(a \le \sqrt{N/2}\)。- 对每个 \(a\),通过二次不等式 \(4ab + 2b^2 \le N\) 导出
max_b上界。 - 对每个 \((a,b)\),由 \(k=1\) 时 \(L = 2ab + 2c(a+b) \le N\) 导出
max_c上界。 - 最内层先计算 \(k=1\) 时的 \(L\) 值并记录,然后利用递推公式沿 \(k\) 递增:每次
L += inc,inc += 8,线性更新避免每次重新计算完整公式。
solve 函数
- 采用迭代加深策略:从
max_n = 5000开始,每次翻倍,直到在计数数组中找到满足C[n] == target的最小 \(n\)。 - 若所有范围均未找到则抛出
RuntimeError。
verify_samples 函数
- 对题目给定的样本数据 \((22,2), (46,4), (78,5), (118,8), (154,10)\) 逐一验证,确保算法正确。