1. 程式人生 > >Leetcode 279:完全平方數(最詳細解決方案!!!)

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 n=4a(8b+7)n = 4^a(8b + 7) for integers a and b.

滿足四數平方和定理的數n(這裡要滿足由四個數構成,小於四個不行),必定滿足 n=4a(8b+7)n = 4^a(8b + 7)

根據這個重要的推論,我們可以非常迅速的寫出這樣的程式碼。

我們首先將輸入的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放到全域性,這樣做的話,我們就會節省很多的時間。

如有問題,希望大家指出!!!