1. 问题解析:理解 Powerful Integers 的定义
Powerful Integers 问题要求我们找出所有可以表示为 x^i + y^j 的整数,其中 i 和 j 是非负整数,并且这个和不超过给定的 bound。为了更好地理解这个问题,让我们先拆解几个关键概念:
- x^i 和 y^j:这里的 x 和 y 是给定的整数基数,i 和 j 是指数。例如,当 x=2 时,2^0=1, 2^1=2, 2^2=4 等。
- bound:这是结果的上限,所有满足条件的 Powerful Integers 都必须小于或等于 bound。
- 唯一性:结果列表中的每个数字只能出现一次,即使它可以被多种不同的 i 和 j 组合表示。
1.1 边界情况分析
在解决这个问题时,我们需要特别注意几种边界情况:
- x 或 y 等于 1:当 x 或 y 为 1 时,任何次方的结果都是 1(因为 1^n = 1)。这会显著影响我们的计算方式。
- bound 小于 2:最小的 Powerful Integer 是 1^0 + 1^0 = 2。因此,如果 bound 小于 2,结果列表必然为空。
- x 或 y 为 0:虽然题目约束中 x 和 y 至少为 1,但在实际编码中,我们仍需确保程序能正确处理这种情况。
提示:在实际编码中,即使题目给出了输入约束,也应该考虑边界情况的处理,这能提高代码的健壮性。
2. 算法设计与实现思路
2.1 预处理 x 和 y 的幂
为了高效地找到所有满足条件的 Powerful Integers,我们需要预先计算 x 和 y 的所有可能的幂,这些幂不超过 bound。具体步骤如下:
- 计算 x 的幂:
- 从 x^0 = 1 开始,依次计算 x^1, x^2, ..., 直到 x^i > bound。
- 如果 x == 1,则只需要计算一次,因为 1 的任何次方都是 1。
- 计算 y 的幂:
- 同理,从 y^0 = 1 开始,依次计算 y^1, y^2, ..., 直到 y^j > bound。
- 如果 y == 1,同样只需要计算一次。
2.2 组合 x^i 和 y^j
在预处理完 x 和 y 的幂之后,我们需要遍历所有可能的 x^i 和 y^j 的组合,计算它们的和,并筛选出满足条件的 Powerful Integers:
- 双重循环遍历:外层循环遍历 x 的所有幂,内层循环遍历 y 的所有幂。
- 和的计算与筛选:对于每一对 (x^i, y^j),计算 sum = x^i + y^j。如果 sum <= bound,则将其加入结果列表。
- 去重处理:使用一个标记数组或哈希集合来记录已经出现过的 sum,避免重复。
2.3 内存管理与结果返回
在 C 语言中,动态内存管理是一个重要环节。我们需要:
- 动态分配结果数组:根据筛选后的 Powerful Integers 的数量,动态分配足够的内存空间。
- 释放临时内存:在返回结果之前,释放用于标记的临时数组,避免内存泄漏。
3. 代码实现与详细解析
以下是完整的 C 语言实现代码,我们将逐段解析其逻辑和实现细节。
c复制/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* powerfulIntegers(int x, int y, int bound, int* returnSize) {
if (bound < 2) { // smallest possible sum is 1^0 + 1^0 = 2
*returnSize = 0;
return NULL;
}
// Precompute powers of x and y up to `bound`
int xp[32], yp[32];
int xn = 0, yn = 0;
int val = 1;
while (val <= bound) {
xp[xn++] = val;
if (x == 1) break; // further powers would be the same
if (val > bound / x) break;
val *= x;
}
val = 1;
while (val <= bound) {
yp[yn++] = val;
if (y == 1) break;
if (val > bound / y) break;
val *= y;
}
// Use a boolean array to mark which sums have appeared (to avoid duplicates)
char *seen = (char *)calloc(bound + 1, sizeof(char));
if (!seen) {
*returnSize = 0;
return NULL; // allocation failure (unlikely on LeetCode)
}
int count = 0;
for (int i = 0; i < xn; ++i) {
for (int j = 0; j < yn; ++j) {
int sum = xp[i] + yp[j];
if (sum > bound) continue;
if (!seen[sum]) {
seen[sum] = 1;
count++;
}
}
}
int *res = (int *)malloc(sizeof(int) * count);
if (!res) {
free(seen);
*returnSize = 0;
return NULL;
}
// Collect sums in any order; here we choose increasing order
int idx = 0;
for (int s = 2; s <= bound; ++s) {
if (seen[s]) {
res[idx++] = s;
}
}
free(seen);
*returnSize = count;
return res;
}
3.1 预处理幂的计算
c复制int val = 1;
while (val <= bound) {
xp[xn++] = val;
if (x == 1) break; // further powers would be the same
if (val > bound / x) break;
val *= x;
}
- 初始化:从 val = 1(即 x^0)开始。
- 循环条件:val <= bound,确保幂不超过 bound。
- 特殊处理 x == 1:如果 x 为 1,则后续的幂都是 1,无需继续计算。
- 防止溢出:通过 val > bound / x 判断下一次乘法是否会超过 bound,避免整数溢出。
3.2 标记数组的使用
c复制char *seen = (char *)calloc(bound + 1, sizeof(char));
- 动态分配:使用 calloc 分配一个大小为 bound + 1 的数组,初始化为 0。
- 标记出现过的 sum:当 sum = x^i + y^j <= bound 时,将 seen[sum] 标记为 1。
3.3 结果收集与返回
c复制int idx = 0;
for (int s = 2; s <= bound; ++s) {
if (seen[s]) {
res[idx++] = s;
}
}
- 遍历标记数组:从 2 开始(最小的 Powerful Integer)到 bound,收集所有被标记的 sum。
- 动态分配结果数组:根据 count 的值分配恰好足够的内存空间,避免浪费。
4. 复杂度分析与优化思考
4.1 时间复杂度
- 幂的预处理:计算 x 的幂需要 O(log_x(bound)) 时间,同理计算 y 的幂需要 O(log_y(bound)) 时间。
- 双重循环:外层循环 O(log_x(bound)),内层循环 O(log_y(bound)),总时间为 O(log_x(bound) * log_y(bound))。
- 结果收集:O(bound) 时间遍历标记数组。
总体时间复杂度为 O(log_x(bound) * log_y(bound) + bound)。在 bound 较大时,结果收集的 O(bound) 可能成为瓶颈。
4.2 空间复杂度
- 幂的存储:O(log_x(bound) + log_y(bound)) 空间存储 x 和 y 的幂。
- 标记数组:O(bound) 空间。
- 结果数组:O(count) 空间,其中 count 是结果的数量。
总体空间复杂度为 O(bound)。
4.3 可能的优化方向
- 哈希集合替代标记数组:如果 bound 非常大,可以使用哈希集合来记录已经出现的 sum,减少空间占用。
- 提前终止循环:在内层循环中,如果 x^i + y^j > bound,可以提前终止内层循环,因为 y^j 是递增的。
- 并行计算:如果允许使用并行计算,可以并行处理不同的 x^i 和 y^j 的组合。
5. 常见问题与调试技巧
5.1 如何处理 x 或 y 为 1 的情况?
当 x 或 y 为 1 时,它们的任何次方都是 1。因此,我们只需要计算一次幂(即 1),并在后续的组合中重复使用它。代码中通过以下方式处理:
c复制if (x == 1) break;
5.2 如何避免整数溢出?
在计算 x^i 或 y^j 时,如果直接进行乘法运算,可能会导致整数溢出。为了避免这种情况,我们在每次乘法之前检查是否会超过 bound:
c复制if (val > bound / x) break;
5.3 如何调试错误的输出?
如果在实现过程中遇到错误的输出,可以按照以下步骤进行调试:
- 打印中间结果:在预处理幂的计算和双重循环中打印 xp、yp 和 sum 的值,确保它们符合预期。
- 检查边界条件:特别检查 bound < 2、x == 1 或 y == 1 的情况是否被正确处理。
- 验证去重逻辑:确保标记数组 seen 正确地记录了所有出现过的 sum。
5.4 为什么使用 calloc 而不是 malloc?
calloc 不仅分配内存,还会将内存初始化为 0,而 malloc 不会。在这里,我们需要 seen 数组初始化为 0,因此使用 calloc 更为方便和安全。
6. 个人实现中的经验与教训
在实际实现这个问题时,我遇到了一些典型的陷阱和挑战,以下是几点值得分享的经验:
- 边界条件的疏忽:最初我没有正确处理 bound < 2 的情况,导致程序返回了错误的结果。在编写代码时,务必仔细检查所有边界条件。
- 整数溢出的隐患:直接计算 x^i 和 y^j 可能会导致溢出,特别是在 bound 较大的情况下。通过预先检查 val > bound / x 或 val > bound / y,可以有效避免这个问题。
- 内存泄漏的风险:在动态分配内存时,必须确保在函数返回前释放所有临时分配的内存。例如,如果 res 分配失败,需要先释放 seen 再返回。
- 去重的重要性:最初我尝试使用排序和去重的方法,但后来发现使用标记数组更为高效。在类似的问题中,标记数组或哈希集合通常是更好的选择。
注意:在 LeetCode 等编程平台上,虽然内存管理的要求相对宽松,但在实际工程中,动态内存的分配和释放必须严格管理,避免内存泄漏和野指针问题。