Bootstrap

完全平方数问题:DFS + 记忆化搜索优化解法

1. 题目描述

给定一个整数 n,求 最少需要多少个完全平方数的和 才能表示 n

完全平方数 是指某个整数的平方,例如 1, 4, 9, 16, 25 等。

示例 1:

输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

2. 解题思路(DFS + 记忆化搜索)

本题类似于 背包问题,我们可以使用 DFS(深度优先搜索)+ 记忆化搜索 来进行求解。

思路分析

  • 递归搜索所有可能的完全平方数组合。

  • 由于可能存在大量重复计算,因此使用 记忆化搜索(Memoization) 进行优化。

  • 递归状态 dfs(i, j)

    • i 代表当前可使用的最大完全平方数 i^2

    • j 代表当前剩余的目标值。

    • 目标是找到 最少 需要多少个平方数。

代码实现

import java.util.Arrays;

class Solution {
    // 记忆化数组 memo[i][j] 代表使用前 i 个平方数凑出 j 的最少数量
    private static final int[][] memo = new int[101][10001];

    static {
        for (int[] row : memo) {
            Arrays.fill(row, -1);
        }
    }

    private static int dfs(int i, int j) {
        if (i == 0) {
            return j == 0 ? 0 : Integer.MAX_VALUE;
        }
        if (memo[i][j] != -1) {
            return memo[i][j];
        }
        if (j < i * i) {
            return memo[i][j] = dfs(i - 1, j);
        }
        return memo[i][j] = Math.min(dfs(i - 1, j), dfs(i, j - i * i) + 1);
    }

    public int numSquares(int n) {
        return dfs((int) Math.sqrt(n), n);
    }
}

代码解析

  1. 初始化 memo 记忆化数组

    • memo[i][j] 代表 使用前 i 个平方数 组合成 j 需要的最小数量。

    • memo 预设为 -1,表示未计算。

  2. 递归 dfs(i, j) 逻辑

    • 终止条件

      • i == 0j == 0,返回 0(无需任何数)。

      • i == 0j != 0,返回 Integer.MAX_VALUE(无解)。

    • 记忆化查询:如果 memo[i][j] 已计算,则直接返回。

    • 不选 i^2 的情况:递归 dfs(i - 1, j)

    • 选择 i^2 的情况:递归 dfs(i, j - i^2) + 1

    • 返回最小值,即 min(不选, 选)

  3. numSquares(n) 主函数

    • Math.sqrt(n) 获取最大可选完全平方数。

    • 调用 dfs(i, n) 计算最优解。


3. 其他解法对比

方法 1:动态规划(DP)

思路

  • 定义 dp[j]:表示和为 j 的最少完全平方数个数。

  • 状态转移方程:

    dp[j] = Math.min(dp[j], dp[j - k^2] + 1)

    其中 k^2 ≤ j

代码实现:

public int numSquares(int n) {
    int[] dp = new int[n + 1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;
    
    for (int i = 1; i * i <= n; i++) {
        int square = i * i;
        for (int j = square; j <= n; j++) {
            dp[j] = Math.min(dp[j], dp[j - square] + 1);
        }
    }
    return dp[n];
}

时间复杂度O(n√n)

方法 2:数学解法(拉格朗日四平方和定理)

定理:任何正整数 n,最少可以由 4 个或更少的完全平方数组成。

优化步骤

  1. n 是完全平方数,返回 1

  2. 检查 n 是否可以由 2 个平方数组成,如 n = 13

  3. 检查 n 是否满足 n = 4^a * (8b + 7),如果是则返回 4

  4. 否则返回 3

代码实现:

public int numSquares(int n) {
    while (n % 4 == 0) {
        n /= 4;
    }
    if (n % 8 == 7) {
        return 4;
    }
    for (int i = 0; i * i <= n; i++) {
        int j = (int) Math.sqrt(n - i * i);
        if (i * i + j * j == n) {
            return i == 0 || j == 0 ? 1 : 2;
        }
    }
    return 3;
}

时间复杂度O(√n),更优!


4. 总结

方法时间复杂度适用情况
DFS + 记忆化搜索O(n√n)适用于小规模 n,但递归深度大,可能超时
动态规划O(n√n)适用于中等规模 n,但需要额外 O(n) 空间
数学优化O(√n)适用于所有 n,最优方法

推荐解法:

  • 如果 n 较小,DFS + 记忆化搜索或 DP 都可以。

  • 如果 n 较大,数学方法最优。

如果觉得这篇文章对你有帮助,记得 点赞👍、收藏⭐、关注📌 哦!

作者:gentle_ice 🚀 CSDN 

;