本次四道题
完全平方数
零钱兑换
单词拆分
最长递增子序列
279. 完全平方数

class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
// 初始化:最坏情况全是1,即 dp[i] = i
for (int i = 1; i <= n; i++) {
dp[i] = i;
}
// 遍历所有完全平方数作为"硬币"
for (int j = 1; j * j <= n; j++) {
int square = j * j;
for (int i = square; i <= n; i++) {
dp[i] = Math.min(dp[i], dp[i - square] + 1);
}
}
return dp[n];
}
}
解题思路1:动态规划
这是一道典型的 "最少硬币" 类问题,我们可以用动态规划来求解:
-
定义状态 :
dp[i]表示和为i的完全平方数的最少数量。 -
初始化:
-
dp[0] = 0,因为和为 0 不需要任何数。 -
其余
dp[i]初始化为一个较大的值(如i,最坏情况是全用 1),表示初始不可达。
-
-
状态转移 :对于每个数
i,我们尝试减去所有小于等于i的完全平方数j*j,并取最小值:dp[i]=min(dp[i], dp[i−j2]+1) -
结果 :
dp[n]即为所求。
class Solution {
public int numSquares(int n) {
if (isPerfectSquare(n)) {
return 1;
}
if (checkAnswer4(n)) {
return 4;
}
for (int i = 1; i * i <= n; i++) {
int j = n - i * i;
if (isPerfectSquare(j)) {
return 2;
}
}
return 3;
}
// 判断是否为完全平方数
public boolean isPerfectSquare(int x) {
int y = (int) Math.sqrt(x);
return y * y == x;
}
// 判断是否能表示为 4^k*(8m+7)
public boolean checkAnswer4(int x) {
while (x % 4 == 0) {
x /= 4;
}
return x % 8 == 7;
}
}
解题思路2:四平方和定理
任意一个正整数都可以表示为至多 4 个完全平方数的和 。因此,本题的答案只可能是 1、2、3 或 4。
我们可以根据以下规则快速判断结果:
-
结果为 1 :当且仅当
n本身是一个完全平方数。 -
结果为 4 :当且仅当
n满足n % 8 == 7。 -
结果为 2 :当且仅当
n可以表示为两个完全平方数之和,即存在整数a和b,使得n = a² + b²。 -
结果为 3:如果以上情况都不满足,则结果为 3。
322. 零钱兑换

public class Solution {
public int coinChange(int[] coins, int amount) {
// 定义dp数组,dp[i]代表凑成金额i所需的最少硬币数
int[] dp = new int[amount + 1];
// 初始化:将所有值设为一个大于最大可能硬币数(amount)的数,代表初始不可达
java.util.Arrays.fill(dp, amount + 1);
// 金额0需要0枚硬币
dp[0] = 0;
// 遍历从1到目标金额的每一个金额
for (int i = 1; i <= amount; i++) {
// 对于每一种硬币,尝试使用它来凑成当前金额
for (int coin : coins) {
// 如果当前硬币的面额小于等于当前金额,才可以使用
if (coin <= i) {
// 状态转移方程:取最小值
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// 如果dp[amount]仍然大于amount,说明无法凑成,返回-1;否则返回结果
return dp[amount] > amount ? -1 : dp[amount];
}
}
解题思路1:动态规划
-
状态定义 :
dp[i]表示凑成总金额i所需要的最少硬币个数。 -
初始化:
-
dp[0] = 0,因为凑成金额 0 不需要任何硬币。 -
其余
dp[i]初始化为一个很大的值(比如amount + 1),表示初始状态下无法凑成该金额。
-
-
状态转移 :对于每个金额
i,遍历所有硬币面额coin:-
如果
coin <= i,则dp[i] = min(dp[i], dp[i - coin] + 1)。 -
这个公式的含义是,凑成
i的最少硬币数,等于凑成i - coin的最少硬币数再加 1(即加上当前这枚硬币)。
-
-
结果判断 :如果最终
dp[amount]仍然大于amount,说明无法凑成该金额,返回-1;否则返回dp[amount]。
解题思路2:记忆化搜素(记忆化递归)略
139. 单词拆分

public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// 1. 将字典转成HashSet,加速单词查找
Set<String> wordSet = new HashSet<>(wordDict);
int n = s.length();
// 2. 定义dp数组:dp[i]表示s的前i个字符能否被拆分
boolean[] dp = new boolean[n + 1];
// 3. 初始化:空字符串可以被拆分
dp[0] = true;
// 4. 遍历结束位置i(表示前i个字符)
for (int i = 1; i <= n; i++) {
// 5. 遍历起始位置j,尝试拆分出子串s[j..i-1]
for (int j = 0; j < i; j++) {
// 核心条件:前j个字符可拆分 + 子串在字典中
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break; // 找到一种拆分方式即可,无需继续遍历j
}
}
}
// 6. 返回整个字符串的拆分结果
return dp[n];
}
}
解题思路1:动态规划
-
状态定义 :定义布尔数组
dp[],其中dp[i]表示字符串s的前i个字符 (即s[0..i-1])能否被字典拆分。 -
初始化 :
dp[0] = true,表示空字符串可以被拆分(作为递归的基准条件)。 -
状态转移:
-
遍历字符串的每个结束位置
i(从 1 到字符串长度)。 -
对于每个
i,向前遍历起始位置j(从 0 到i)。 -
若
dp[j]为true(前j个字符可拆分),且子串s[j..i-1]存在于字典中,则dp[i] = true。
-
-
结果 :返回
dp[s.length()],即整个字符串是否可拆分。
优化点
使用 HashSet 存储字典,将单词查找的时间复杂度从 O(n) 降为 O(1),提升效率。
300. 最长递增子序列

public class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
// 初始化,每个元素自身长度为1
Arrays.fill(dp, 1);
int maxLen = 1;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLen = Math.max(maxLen, dp[i]);
}
return maxLen;
}
}
解题思路1:动态规划(O (n²))
思路:
-
定义
dp[i]表示以nums[i]结尾的最长递增子序列的长度。 -
初始时,每个元素自身就是一个长度为 1 的子序列,所以
dp[i] = 1。 -
对于每个
i,遍历j从0到i-1:- 如果
nums[j] < nums[i],则dp[i] = max(dp[i], dp[j] + 1)。
- 如果
-
最终答案就是
dp数组中的最大值。
public class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int[] tails = new int[nums.length];
int size = 0;
for (int num : nums) {
int left = 0, right = size;
// 二分查找第一个 >= num 的位置
while (left < right) {
int mid = (left + right) / 2;
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
tails[left] = num;
if (left == size) {
size++;
}
}
return size;
}
}
解题思路2:贪心 + 二分查找(O (n log n))
维护一个数组 tails,其中 tails[i] 表示长度为 i+1 的递增子序列的最小可能末尾值。
遍历数组 nums:
-
如果当前数大于
tails的最后一个元素,直接加入tails。 -
否则,用二分查找找到
tails中第一个大于等于当前数的位置,用当前数替换它。
tails 的长度就是最长递增子序列的长度。

