面试准备

最近改动

更新题目 拆成 4 的幂:最少拆几个

题目 visa-split-into-powers-of-four · 修改(rev 3) · cc · 2026-10-26 23:11

body

@@ -1,5 +1,7 @@
 ## 题目
 
-给一个整数数组。对每个整数 x,求最小的正整数 k,使 x 能写成 k 个数之和:其中**一个**是 1、2 或 3,其余 k − 1 个都是 2 的偶数次方、但不含 2^0(也就是 4、16、64……,可以重复用)。做不到返回 -1。
+给一个整数数组。对每个整数 x,求最小的正整数 k,使 x 能写成 k 个数之和:其中**一个**是 1、2 或 3,其余 k − 1 个都是 2 的偶数次方、但不含 2^0(也就是 4、16、64……,可以重复用)。做不到返回 -1。题面没写整个数组的返回格式,下面按逐个求、返回等长数组写。
+
+例子:x = 30 时,30 = 2 + 16 + 4 + 4 + 4,共 5 项,答案是 5。x = 8 时做不到,答案是 -1(原因见下一节)。
 
 ## 面经里怎么考的
@@ -9,31 +11,93 @@
 ## 思路
 
-2 的偶数次方去掉 1 以后就是 4 的幂:4、16、64……它们都是 4 的倍数。所以 x 减去那个 1、2 或 3(记作 r)之后剩下的 m 必须是 4 的倍数,r 只有一种取法(x 除以 4 余几就是几);x 本身是 4 的倍数时没有合法的 r,返回 -1。
+2 的偶数次方写成 2^(2j),也就是 4^j。题目去掉了 j = 0(即 2^0 = 1),所以能用的数正好是 4 的正整数次幂:4、16、64……下面把那一项 1、2 或 3 叫做「小项」,记作 r,把 x 减去小项以后剩下的部分记作 m = x − r。
 
-剩下的 m 要用最少个数的 4 的幂凑出来。把 m 写成 4 进制,每一位上的数字就是要用几个对应的幂;4 进制各位数字之和就是最少个数(如果某一位用了 4 个以上,四个合成一个更高的幂会更少,所以 4 进制写法已经最省)。答案是 1 加上这个数位和。
+小项只有一种取法。4、16、64 都是 4 的倍数,它们的和 m 也一定是 4 的倍数(m = 0 也算,这时 k − 1 = 0)。m = x − r 是 4 的倍数,等价于 r 和 x 除以 4 的余数相同。r 只能在 1、2、3 里选,所以 r 就是 `x % 4`。由此得到两种无解的情况:
+
+- x 是 4 的倍数时,余数是 0,1、2、3 里没有合法的小项,返回 -1。比如 x = 8,减 1、2、3 分别剩 7、6、5,都不是 4 的倍数。
+- x ≤ 0 时,k 项都是正数,和不可能 ≤ 0,返回 -1。
+
+x 是 1、2、3 时,m = 0,只用小项一项,答案是 1。
+
+> [!say] Every allowed power is a power of four, so they all sum to a multiple of four. That forces the small term to be x mod 4, and if x itself is a multiple of four there is no valid small term, so the answer is minus one.
+
+剩下的 m 用最少几个 4 的幂凑出来,答案是 m 的 4 进制各位数字之和。把 m 写成 4 进制,第 j 位的数字 d_j 表示用 d_j 个 4^j,这样凑出来的个数就是各位数字之和。它是最少的,推导分两步:
+
+1. 任取一种凑法,如果某个 4^j 用了 4 个或更多,把其中 4 个换成 1 个 4^(j+1),和不变、个数少 3。反复换下去个数一直变少,所以最少的凑法里每个 4^j 最多用 3 个。
+2. 每个 4^j 最多用 3 个的凑法,就是 m 的 4 进制写法,而一个数的 4 进制写法只有一种。所以最少的凑法只能是 4 进制写法。
+
+m 是 4 的倍数,4 进制最低位(4^0 那一位)一定是 0,正好对上「不能用 1」这条限制。
+
+例子:x = 30,30 % 4 = 2,小项取 2,m = 28。28 = 1 × 16 + 3 × 4 + 0 × 1,4 进制是 130,数字和 1 + 3 + 0 = 4,答案 1 + 4 = 5,和题目一节的拆法一致。
+
+> [!say] For the remaining part, the fewest powers of four that add up to m is the digit sum of m in base four. If any power were used four or more times, I could merge four copies into the next power and save three terms, so an optimal answer uses each power at most three times, which is exactly the base-four representation.
 
 ## 代码
 
 ```python
-def min_parts(x):
-    for r in (1, 2, 3):
-        m = x - r
-        if m >= 0 and m % 4 == 0:
-            k = 1                    # r 自己算一个
-            while m:
-                k += m % 4           # 4 进制的每一位
-                m //= 4
-            return k
-    return -1
+from __future__ import annotations   # 让 list[int] 这种注解在 Python 3.8 及以下也能用
+
+
+def base4_digit_sum(m: int) -> int:
+    """m >= 0,返回 m 写成 4 进制后各位数字之和。"""
+    total = 0
+    while m > 0:
+        m, digit = divmod(m, 4)
+        total += digit
+    return total
+
+
+def min_parts(x: int) -> int:
+    """x 最少拆成几项(一项取 1、2、3,其余取 4 的正整数次幂),做不到返回 -1。"""
+    if x <= 0:
+        return -1                      # 每一项都是正数,和不可能 <= 0
+    small = x % 4                      # 那一项 1、2、3 只能取 x 除以 4 的余数
+    if small == 0:
+        return -1                      # x 是 4 的倍数:没有合法的那一项
+    return 1 + base4_digit_sum(x - small)
+
+
+def solve(nums: list[int]) -> list[int]:
+    """对数组里每个数分别求答案,返回等长数组。"""
+    return [min_parts(x) for x in nums]
 ```
 
-本机和暴力搜索在 1 到 299 上对拍过。
+Python 的整数不会溢出,x 很大时也不用特殊处理。
+
+> [!optional] 用 Java 或 C++ 写时,x 超过 2^31 − 1 就要用 64 位整数(`long` / `long long`)。
+
+2026-10-26 在云端用 python3 跑了三组,代码和上面逐字一致:
+
+- **和暴力对拍**:暴力解是完全背包(硬币取 4、16、64……求凑出 x − r 的最少硬币数,再枚举 r = 1、2、3),不复用 4 进制的推导。x 取 -5 到 2000 共 2006 个值,`min_parts` 和暴力全部一致。
+- **一种错误写法对照**:把「4 进制各位数字之和」写成「4 进制非零位的个数」,也就是把 2 × 4 当成一项。错误写法只换掉 `base4_digit_sum`,`min_parts` 其余部分不变:
+
+  ```python
+  def base4_digit_sum(m: int) -> int:   # 错误写法:数的是非零位的个数
+      count = 0
+      while m > 0:
+          m, digit = divmod(m, 4)
+          if digit != 0:
+              count += 1
+      return count
+  ```
+
+  同一组输入的结果:
+
+  | x | 1 | 4 | 5 | 9 | 13 | 30 | 47 | 63 | 0 | -3 |
+  | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+  | 错误写法 | 1 | -1 | 2 | 2 | 2 | 3 | 3 | 3 | -1 | -1 |
+  | 正确写法 | 1 | -1 | 2 | 3 | 4 | 5 | 6 | 7 | -1 | -1 |
+  | 暴力 | 1 | -1 | 2 | 3 | 4 | 5 | 6 | 7 | -1 | -1 |
+
+  x = 9 时 m = 8,4 进制是 20,非零位只有 1 个,错误写法得 2。可是 8 本身不是 4 的幂,要写成 4 + 4,正确答案是 1 + 2 = 3。在 -5 到 2000 上,错误写法有 1404 个值和暴力不一致。
+- **大输入**:`min_parts(10**18 + 1)` 返回 38,单次调用约 2 微秒(重复调用 10 万次取平均)。`min_parts(4**31 - 1)` 返回 91(m 的 4 进制是 30 个 3 后面跟一个 0)。
+
+## 复杂度
+
+每个数 x 的循环次数等于 m 的 4 进制位数,约 log₄ x,时间 O(log x),额外空间 O(1)。整个数组是 O(n log X),X 是数组里的最大值。
 
 ## 易错点
 
-- 「2 的偶数次方」不含 2^0 = 1,所以只能是 4 的幂;看清这一句题就简单了。
-- 题面没说 4 的幂能不能重复用,上面按可以重复写;不能重复时,4 进制某位大于 1 就无解。
-
-## 英文讲法
-
-> [!say] Every allowed power is a power of four, so x minus the small term must be a multiple of four, and the fewest powers of four that sum to m is just the digit sum of m in base four.
+- 「2 的偶数次方」不含 2^0 = 1,所以能用的只有 4 的幂。看清这一句,题就变成了上面的取余加数位和(见思路一节第一段)。
+- 答案按 4 进制各位数字之和算。只数非零位会少算,因为一位上的 2 或 3 代表要用 2 个或 3 个同样的幂(见代码一节的对照表)。
+- 题面没说 4 的幂能不能重复用,上面按可以重复写。如果不能重复,m 的 4 进制某一位大于 1 就无解,答案改成「有某位 ≥ 2 时返回 -1,否则 1 加非零位个数」(2026-10-26 和穷举所有子集的暴力在 -3 到 1499 上对拍过,全部一致)。

meta

@@ -1 +1,5 @@
-{}
+{
+ "depth_guide": "2026-10-23",
+ "rewritten_by": "cloud:20261026-1620",
+ "rewritten_at": "2026-10-27"
+}