高性能计算笔记:灯泡开关问题的数学优化与常数级解法
1. 问题背景与抽象
初始有 n 个处于关闭状态的灯泡,按如下规则依次操作开关:
- 第 1 轮:切换所有灯泡的开关状态;
- 第
i轮(i ≥ 2):每隔i-1个灯泡,切换第i、2i、3i...个灯泡的开关状态; - 执行完第
n轮操作后,求仍亮着的灯泡数量。
从实现角度,若直接模拟每一轮操作,时间复杂度约为 O(n log n)(n 轮操作,每轮最多操作 n/i 次),当 n 达到 10⁹ 等大规模时会出现显著性能瓶颈。
2. 核心数学推导
要实现常数级解法,需将"灯泡状态"转化为纯数学问题分析:
2.1 灯泡操作次数与约数的关联
第 k 个灯泡被操作的次数,等价于 k 的正约数个数。例如:
- 第 6 个灯泡的约数为 1、2、3、6,对应第 1/2/3/6 轮各操作 1 次,共 4 次;
- 灯泡初始为关闭状态,操作偶数次 则最终关闭,操作奇数次则最终亮起。
由此,问题可抽象为:求 1~n 中"正约数个数为奇数"的数字数量。
2.2 约数个数的奇偶性特征
普通正整数的约数呈"成对出现"的特征(如 6 的约数 1&6、2&3),因此约数个数为偶数;仅完全平方数 (如 4=2²、9=3²)存在"重复约数"(如 4 的约数 2 仅计数 1 次),其约数个数为奇数。
综上,原问题可进一步简化为:求 1~n 中完全平方数的数量------这是将 O(n log n) 复杂度优化为 O(1) 的核心数学依据。
3. 工程实现
基于上述推导,实现时需重点关注边界条件处理与类型转换的严谨性(面试中高频考察的工程能力点),以下是优化后的 C++ 实现:
cpp
class Solution {
public:
int bulbSwitch(int n) {
// 边界条件优先处理:无灯泡时结果为0,避免后续无效计算
if (n == 0) {
return 0;
}
// 计算n的平方根并向下取整,即为1~n中完全平方数的个数
// 显式类型转换避免double到int的隐式转换精度丢失(高性能场景的严谨性要求)
return static_cast<int>(sqrt(n));
}
};
实现关键要点
- 显式类型转换:
sqrt(n)返回double类型,直接隐式转换可能因浮点数精度问题导致错误(如n=25时sqrt(25)=5.0,n=26时sqrt(26)≈5.099,向下取整为 5),显式转换更符合工程规范; - 边界条件前置:在高性能计算中,提前处理边界可避免后续无效计算,符合"最小计算成本"原则。
4. 复杂度分析
- 时间复杂度 :
O(1)。仅执行一次平方根计算(CPU 原生指令级操作,属于常数时间),相比模拟法的O(n log n),复杂度呈数量级降低,尤其在n趋近于无穷大时性能优势显著; - 空间复杂度 :
O(1)。未使用任何额外辅助空间,符合"原地计算"的高性能优化思路。
5. 面试视角的思考与延伸
5.1 核心优化思路总结
高性能计算的核心并非"暴力优化代码细节",而是"先抽象问题本质,再选择最优算法":
- 第一步:将业务问题(灯泡开关操作)转化为数学问题(约数个数统计);
- 第二步:挖掘数学规律(完全平方数的约数个数特征);
- 第三步:基于规律设计常数级解法。
5.2 面试可能追问的延伸点
- 若
n=10⁹,模拟法为何不可行?
答:10⁹规模下,O(n log n)的操作次数约为10⁹×30=3×10¹⁰,远超 CPU 单次计算的处理能力(普通 CPU 每秒约执行10⁹次操作),而O(1)解法仅需 1 次平方根计算,可瞬间完成。 - 如何验证平方根向下取整的正确性?
答:举例验证,如n=10时,√10≈3.16,向下取整为 3,对应1²、2²、3²共 3 个完全平方数,与实际操作后亮着的灯泡数量一致。 - 类似的高性能优化案例?
答:如"两数之和"的哈希表优化(O(n²)→O(n))、"斐波那契数列"的矩阵快速幂优化(O(n)→O(log n)),核心均为"问题抽象 + 算法优化"。