1. 问题背景与定义
旋转数字问题最初出现在LeetCode第788号题目中,属于字符串处理与数学结合的经典面试题型。题目要求我们统计在1到N的范围内,有多少个数是"好数"(good number)。这里的"好数"定义非常特殊:当这个数的每一位数字旋转180度后,得到的数字与原来不同且仍然是一个有效的数字。
数字旋转规则如下:
- 0,1,8旋转后仍然是自身
- 2旋转后变成5
- 5旋转后变成2
- 6旋转后变成9
- 9旋转后变成6
- 3,4,7旋转后无法形成有效数字
举个例子:
- 数字"2"旋转后变成"5",属于好数
- 数字"12"旋转后变成"15",也属于好数
- 数字"10"旋转后仍然是"10",不属于好数(因为旋转前后相同)
- 数字"34"旋转后无效(因为包含3),不属于好数
2. 解题思路分析
2.1 暴力解法可行性评估
最直观的解法是遍历1到N的每个数字,检查其是否满足好数条件。对于每个数字:
- 将其转换为字符串
- 检查每个字符是否属于可旋转数字(0,1,2,5,6,8,9)
- 如果全部字符都可旋转,则进行旋转操作
- 比较旋转前后的数字是否不同
这种方法的时间复杂度是O(N*L),其中L是数字的平均位数。当N较大时(比如N=1e5),这种解法在面试中可能不够高效。
2.2 数学性质观察
通过观察可以发现,好数必须满足两个条件:
- 不包含任何无效数字(3,4,7)
- 至少包含一个可变化数字(2,5,6,9)
这提示我们可以将问题分解为:
- 首先排除包含3,4,7的数字
- 然后在剩余数字中找出至少包含一个2,5,6,9的数字
2.3 动态规划思路
这个问题可以建模为数字的排列组合问题,适合用动态规划解决。我们可以定义:
- dp[i][eq][hasGood] 表示处理到第i位时的状态
- eq表示当前数字是否等于N的前i位
- hasGood表示是否已经包含可变化数字
通过状态转移,我们可以高效计算出好数的数量,而不需要逐个检查。
3. 具体实现方案
3.1 暴力解法实现
虽然暴力解法不是最优解,但作为基础实现,可以帮助理解问题:
python复制def rotatedDigits(N):
count = 0
rotate_map = {'0':'0', '1':'1', '2':'5', '5':'2', '6':'9', '8':'8', '9':'6'}
invalid = {'3','4','7'}
for num in range(1, N+1):
s = str(num)
rotated = []
valid = True
for c in s:
if c in invalid:
valid = False
break
rotated.append(rotate_map[c])
if valid and ''.join(rotated) != s:
count += 1
return count
3.2 动态规划解法
更高效的动态规划实现:
python复制def rotatedDigits(N):
s = str(N)
n = len(s)
memo = {}
def dp(i, eq, hasGood):
if i == n:
return 1 if hasGood else 0
if (i, eq, hasGood) in memo:
return memo[(i, eq, hasGood)]
limit = int(s[i]) if eq else 9
res = 0
for d in range(0, limit + 1):
if d in {3,4,7}:
continue
new_eq = eq and (d == limit)
new_hasGood = hasGood or (d in {2,5,6,9})
res += dp(i+1, new_eq, new_hasGood)
memo[(i, eq, hasGood)] = res
return res
return dp(0, True, False)
3.3 数学组合解法
利用数字排列组合的性质:
python复制def rotatedDigits(N):
A = list(map(int, str(N)))
good = {0,1,2,5,6,8,9}
change = {2,5,6,9}
from functools import lru_cache
@lru_cache(maxsize=None)
def dfs(pos, tight, hasChange):
if pos == len(A):
return 1 if hasChange else 0
limit = A[pos] if tight else 9
res = 0
for d in range(0, limit + 1):
if d not in good:
continue
new_tight = tight and (d == limit)
new_hasChange = hasChange or (d in change)
res += dfs(pos + 1, new_tight, new_hasChange)
return res
return dfs(0, True, False)
4. 复杂度分析与优化
4.1 时间复杂度对比
- 暴力解法:O(N*L),其中L是数字的平均位数
- 动态规划:O(L102*2) = O(L),其中L是数字的位数
- 数学组合:与动态规划类似,也是O(L)
当N很大时(如1e9),暴力解法会非常慢,而后两种方法仍然高效。
4.2 空间优化技巧
动态规划解法可以使用滚动数组优化空间:
- 只需要保存当前位和上一位的状态
- 可以将空间复杂度从O(L)降低到O(1)
4.3 预处理加速
对于多次查询的情况,可以预处理所有数字的旋转结果,建立哈希表:
- 预处理时间O(N)
- 查询时间O(1)
- 适合需要多次查询的场景
5. 边界条件与测试用例
5.1 常见测试用例
python复制测试用例1:
输入:10
输出:4
解释:好数为2,5,6,9
测试用例2:
输入:20
输出:9
解释:好数为2,5,6,9,12,15,16,19,20
测试用例3:
输入:100
输出:40
5.2 特殊边界情况
- N=1:输出0
- N=2:输出1(只有2)
- 包含多个无效数字:如347
- 全部由0,1,8组成:如108
- 大数测试:如1e5
5.3 测试代码示例
python复制def test_rotatedDigits():
assert rotatedDigits(1) == 0
assert rotatedDigits(2) == 1
assert rotatedDigits(10) == 4
assert rotatedDigits(20) == 9
assert rotatedDigits(100) == 40
print("所有测试用例通过")
6. 面试技巧与常见问题
6.1 面试官可能追问的问题
- 如何优化暴力解法?
- 为什么动态规划适合这个问题?
- 如何处理大数情况(如N=1e9)?
- 如何验证你的解法是否正确?
- 这个问题的实际应用场景是什么?
6.2 回答技巧
- 先给出暴力解法,再逐步优化
- 画图说明数字旋转的规则
- 举例说明动态规划的状态转移
- 讨论时间空间复杂度的权衡
6.3 常见错误
- 忽略旋转后必须不同的条件
- 忘记处理包含无效数字的情况
- 动态规划状态设计不完整
- 边界条件处理不当(如N=0)
7. 实际应用与扩展
7.1 实际应用场景
- 数字显示系统的校验(如七段显示器)
- 密码学中的数字变换
- 游戏中的数字谜题设计
7.2 问题变种
- 统计旋转后仍然是有效数字(不要求不同)的数量
- 找出第K个好数
- 区间查询(如L到R之间的好数数量)
- 多位数字旋转后形成回文数
7.3 进阶挑战
- 实现O(1)空间复杂度的解法
- 处理超大范围(如1e100)
- 并行化计算方案
8. 个人实现心得
在实际实现过程中,我发现动态规划的状态设计是关键。最初我忽略了"hasGood"这个状态,导致计算结果包含那些全部由0,1,8组成的数字。通过添加这个状态,问题得到了正确解决。
另一个收获是数字旋转问题的处理技巧。将数字转换为字符串处理比数学运算更直观,特别是在处理旋转映射时。建立明确的旋转映射字典可以让代码更清晰。
对于大数测试用例,预处理方法确实能提高查询效率,这在多次查询的场景下非常有用。我建议在面试中可以先提一下这个优化方向,即使时间不够实现,也能展示你的思考全面性。