Leetcode 279:完全平方數(最詳細解決方案!!!)
給定正整數 n,找到若干個完全平方數(比如 1, 4, 9, 16, ...
)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。
示例 1:
輸入: n = 12
輸出: 3
解釋: 12 = 4 + 4 + 4.
示例 2:
輸入: n = 13
輸出: 2
解釋: 13 = 4 + 9.
解題思路
很多人第一眼看到這個問題,想到的第一種做法就是使用貪心演算法,但是對於這個問題是不適用的,例如:
12 = 9 + 1 + 1 + 1
我們根據題目中完全平方數的個數最少,我們在什麼演算法中用到過最少這個關鍵字?啊哈!最短路徑問題。那麼和這個問題有什麼聯絡呢?
現在我們就可以用最短路徑演算法來解決這個問題。最短路徑演算法其實就是圖的廣度優先遍歷。例如對於上圖中的5
0
,我們的第一步有兩種走法,先走4
和先走1
。所以我們需要建立一個佇列或者棧,然後將第一步的走法壓入佇列或者棧中。如下(使用佇列, 我們同時記錄走的步數)
q : (4, 1) (1, 1)
我們將4
出隊,然後看4
的下一步怎麼走,發現只能走3
,所以我們將(3, 2)
入隊
q : (1, 1) (3, 2)
接著我們將(1, 1)
彈出,我們看1
的下一步怎麼走,發現只能走0
,這個時候我們發現已經到達了0
,那麼我們更新step+1
,然後出迴圈即可。以下是程式碼的全部過程:
class Solution:
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
q = list()
q.append([n, 0])
visited = [False for _ in range(n+1)]
visited[n] = True
while any(q):
num, step = q.pop(0)
i = 1
tNum = num - i**2
while tNum >= 0:
if tNum == 0:
return step + 1
if not visited[tNum]:
q.append((tNum, step + 1))
visited[tNum] = True
i += 1
tNum = num - i**2
但是這個解法不是最優的解法,但是我認為是一個不錯的思維方式。那麼更加快速的解法是什麼樣的呢?我們就要用到數學知識了,這裡使用的是四平方和定理。
Lagrange 四平方定理: 任何一個正整數都可以表示成不超過四個整數的平方之和。
那麼我們這個問題的解法就變得很簡單了,我們的結果只有1,2,3,4
,四種可能。
另外還有一個非常重要的推論
if and only if n is not of the form for integers a and b.
滿足四數平方和定理的數n(這裡要滿足由四個數構成,小於四個不行),必定滿足
根據這個重要的推論,我們可以非常迅速的寫出這樣的程式碼。
我們首先將輸入的n
迅速縮小。然後我們再判斷,這個縮小後的數是否可以通過兩個平方數的和或一個平方數
組成,不能的話我們返回3
,能的話我們返回平方數的個數
。
現在我們的問題已經縮減到了,怎麼判斷一個數是由一個還是由兩個平方數的和構成?對於這個問題,我們當然可以暴力破解。
class Solution:
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
while n % 4 == 0:
n /= 4
if n % 8 == 7:
return 4
a = 0
while a**2 <= n:
b = int((n - a**2)**0.5)
if a**2 + b**2 == n:
return (not not a) + (not not b)
a += 1
return 3
另外這個問題還有一種經典的解法,就是使用動態規劃。動態規劃的問題關鍵在於狀態轉移方程,這裡的思路還是和前面使用圖的廣度優先遍歷一樣。例如
我們要知道12
最少有多少個數構成,實際上如果我們走了一步的話,我們需要知道11、8、3
對應的步數,如果我們不走,我們就需要知道12
的步數,我們只要通過比較是走0
步小,還是走1步
哪個更小即可。通過一個式子表示就是
num[n] = min(num[n], num[n-i**2] + 1)
所以我們可以先定義一個n
大小的陣列(static型別),這裡我們要使陣列初始化為無窮大,在python
中我們可以使用float('inf')
class Solution:
_dp = list()
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
dp = self._dp
dp = [float('inf') for i in range(n + 1)]
dp[0] = 0
for i in range(n + 1):
j = 1
while i + j**2 <= n:
dp[i + j**2] = min(dp[i + j**2], dp[i] + 1)
j += 1
return dp[n]
但是這裡寫法我們在實際測試的時候超時了。
class Solution:
_dp = [0]
def numSquares(self, n):
dp = self._dp
while len(dp) <= n:
dp += list((min(dp[-i*i] for i in range(1, int(len(dp)**0.5+1))) + 1,))
return dp[n]
思路還是和上面一樣,但是這裡,
不是亂加的。為什麼要加?這是因為int
無法初始化list
,我們只有通過加上一個,
,將int
變成tuple
才可以初始化。這裡還有一個trick
,_dp
放到了全域性,這有什麼用?我們知道我們有很多的測試用例,而這麼多的測試用例中肯定有許多相似的測試用例,那麼這其中就會有很多相似的計算,如果我們將_dp
放到全域性,這樣做的話,我們就會節省很多的時間。
如有問題,希望大家指出!!!