问题描述:【BFS、Math】279. Perfect Squares
解题思路:
这道题是给一个正整数 n,将其分解为平方数加法因子,使得分解后的因子最少。
这道题实际上和 Leetcode 【DP、BFS】322. Coin Change 很相似。我们将 <= n 的平方数因子当作硬币种类数,n 当作需要换的零钱,则可以使用相同的方法,即 DP 和 BFS 来求解。
方法1(DP,时间复杂度 O(n*sqrt(n)),TLE):
类似于 Leetcode 322,使用动态规划,时间复杂度 O(n*sqrt(n)),但是 DP 方法在这道题目中超时了,参考代码如下:
Python3 实现:
class Solution:
def numSquares(self, n: int) -> int:
dp = [i for i in range(n + 1)] # 注意dp的初始化为dp[i]=i
for i in range(2, int(math.sqrt(n)) + 1):
sqr = i * i # 平方数
for j in range(sqr, n+1):
dp[j] = min(dp[j], dp[j-sqr] + 1) # 更新dp[i]处的最小值
return dp[-1]
方法2(BFS,时间复杂度 O(n*sqrt(n)),AC):
类似于 Leetcode 322,使用广度优先遍历,时间复杂度 O(n*sqrt(n)),但是可以 AC。
Python3 实现:
class Solution:
def numSquares(self, n: int) -> int:
sn = [] # 保存平方数
for i in range(1, int(math.sqrt(n)) + 1):
sn.append(i * i)
visited = [False] * (n + 1) # 标记某个数字是否走过
visited[0] = True
q = collections.deque()
q.append((0, 0))
step = 0
while True:
while len(q) > 0 and q[0][1] == step:
x, _ = q.popleft() # 出队列
if x == n: # 程序出口
return step
for num in sn: # 对于每一种平方数选择
if x + num <= n and visited[x+num] == False:
q.append((x+num, step+1))
visited[x+num] = True
step += 1
return -1 # 不可达
方法3(Math,时间复杂度 O(sqrt(n)),AC):
其实这道题的最优解法是使用一个数学公式:四平方和定理。
四平方定理: 任何一个正整数都可以表示成不超过四个整数的平方之和。
那么这个问题的解法就变得很简单了,我们的结果只有 1、2、3、4 这四种可能。
另外还有一个非常重要的推论:
满足四数平方和定理的数 n(这里要满足由四个数构成,小于四个不行),必定满足 n = (4^a) * (8b + 7)
因此,我们可以得到此题解法:
- 先根据推论判断 n 由 4 个平方数组成的情况:如果 n 是 4 的倍数,则可以通过除以 4 将输入的 n 迅速缩小。然后判断缩小后的数除以 8 能否余 7,如果余 7,则返回 4。
- 再判断 n 由 1 个或 2 个平方数组成的情况:对缩小后的 n 可以使用暴力破解,来判断一个数是由 1 个还是由 2 个平方数的和构成。返回 1 或者 2。
- 那么,剩下的一种情况就是 n 由 3 个平方数组成的情况,返回 3 即可。
此题的时间复杂度主要消耗在判断 n 由 1 个或 2 个平方数组成的情况时,时间复杂度为 O(sqrt(n))。这里在编程时有一个点需要注意:比如 n = 25 这种,它应该返回 1(即 5 * 5),而不是返回 2 (即 3 * 3 + 4 * 4),因此我们的平方数要从大(int(sqrt(n)))到小(1)取,才能保证不会先出现 3 * 3 + 4 * 4 的情况
Python3 实现:
class Solution:
def numSquares(self, n: int) -> int:
while n % 4 == 0: #(4^a)*(8b+7)缩减为8b+7
n //= 4
if n % 8 == 7: # 如果余数为7,说明可以由4个平方数组成
return 4
for i in range(int(math.sqrt(n)), 0, -1): # 判断是否由1或2个平方数组成,从大的平方数取
if i * i == n: # 一个平方数满足
return 1
j = int(math.sqrt(n - i * i)) # 注意要取整
if i * i + j * j == n: # 两个平方数满足
return 2
return 3 # 剩下的就是由3个平方数组成
问题描述:【Math】343. Integer Break
解题思路:
这道题是给一个正整数 n,将其分解为若干正整数加法因子,使得分解后的因子乘积最大。
这道题看了数据范围,DFS 回溯法肯定不行,因此可以找规律。刚开始想到数字 12,发现 3 * 3 * 3 * 3
的分解结果要比 4 * 4 * 4
(或 2 * 2 * 2 * 2 * 2 * 2
) 的分解结果大,因此猜想分解中的因子应该尽可能多的包含 3 而不是 4(或者 2)。
可以依次写出前面几个数字的结果:
- dp[2]:1 * 1 = 1;
- dp[3]:1 * 2 = 2;
- dp[4]:2 * 2 = 4;
- dp[5]:2 * 3 = 6;
- dp[6]:3 * 3 = 9;
- dp[7]:3 * dp[4] = 3 * 2 * 2 = 12;
- dp[8]:3 * dp[5] = 3 * 2 * 3 * = 18;
- dp[9]:3 * dp[6] = 3 * 3 * 3 = 27;
- dp[10]:3 * dp[7] = 3 * 3 * 2 * 2 = 36;
- ...
由此可以发现规律:dp[n]:3 * dp[n-3]
因此,对于 n <= 6,直接返回上述结果;对于 n > 6,每次将结果乘以 3,然后将 n 减去 3,直到 n <= 6 为止,最后将累成的结果乘以 n 就是答案。
这种做法的时间复杂度为 O(n),空间复杂度为 O(1)。
Python3 实现:
class Solution:
def integerBreak(self, n: int) -> int:
ansTo6 = [0, 1, 1, 2, 4, 6, 9]
ans = 1
while n > 6:
ans *= 3 # 累乘结果
n -= 3
return ans * ansTo6[n] # 最后将结果乘以n
改进:
其实还可以对上述程序进行改进,获得时间复杂度为 O(1) 的做法。即根据对 3 取余判断其余数,然后根据余数的不同直接计算乘积结果。代码如下:
class Solution:
def integerBreak(self, n: int) -> int:
if n == 2: return 1
if n == 3: return 2
if n % 3 == 0: return 3 ** (n//3)
if n % 3 == 1: return 3 ** (n//3-1) * 4
if n % 3 == 2: return 3 ** (n//3) * 2