1. 程式人生 > >花式求解 LeetCode 279題-Perfect Squares

花式求解 LeetCode 279題-Perfect Squares

原文地址

https://www.jianshu.com/p/2925f4d7511b

迫於就業的壓力,不得不先放下 iOS 開發的學習,開始走上漫漫刷題路。

今天我想聊聊 LeetCode 上的第279題-Perfect Squares,花了挺長時間的,試了很多方法,作為一個演算法新手,個人感覺這題很好,對我的水平提升很有幫助。我在這裡和大家分享一下我的想法。下面是題目:

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ... ) which sum to n

.
For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13,return 2 because 13 = 4 + 9.

大致意思就是,“給一個正數 n, 找到和為 n 的平方數, 給出最少的平方數個數”。

BFS

我剛開始想到的是用 BFS,經過一番實踐,感覺程式碼是對的,但是 Time Limit Exceeded。畢竟用了 2 層迴圈。於是我就找了個字典(Dictionary)來存已經算過的節點,比如一個很大的數 n,有很大機率 n - i * i 這個節點和後面算出來的 m - j * j 是相等的。那麼就不再重新計算。但是,還是超時了。這部分程式碼2個小時前被我扔了,我就不在這裡重新寫了。

Lagrange's four-square theorem

這裡算是完全用數學知識解決了這個問題。不知道四平方和定理的請參考 wikipedia。話說童鞋們最好看英文版的 wiki,別翻譯成中文比較好。我也不說英文更專業,雖然好像就是這麼回事 == 因為有個公式非常重要,而解這題全靠這個公式:


這個定理就是講,任何數都可以由4個平方陣列成,即 n = a^2 + b^2 + c^2 + d^2,所以這題的答案已經限定在了 [1,4] 之間。

而上面這個公式的發明者-Adrien-Marie Legendre 又補充了這個定理:除了滿足以上這個公式的數以外的任何數都可以由3個平方陣列成。所以,這個答案又可以縮小範圍了。範圍都已經縮小到 [1,3] 了,我們開始求解。

先排除4個的情況:

    while myN & 3 == 0 {
        myN >>= 2
    }
<span class="hljs-keyword">if</span> myN % <span class="hljs-number">8</span> == <span class="hljs-number">7</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-number">4</span>
}

因為1和2的情況比較容易排除,先把1和2的排除。

    var index = Int(sqrt(Double(n)))
    while index > 0 {
        let tmp = Double(n - index * index)
        let sqrtTmp = Int(sqrt(tmp))
        if n == sqrtTmp * sqrtTmp + index * index {
            return sqrtTmp == 0 ? 1 : 2
        }
        index -= 1
    }

上面的程式碼就是說,如果一個數由2個平方陣列成,如果其中一個平方數是0,那麼就是1,如果不是0,那就是2。

剩下的就是3了,直接 return 3 就行了。在知道這個數學公式的情況下,這個方法還是很簡單的。

DP

我剛刷題沒幾天,對於 DP 的推理過程還不是很熟練,琢磨了好久。一旦琢磨出來了,又覺得好簡單,換一題,又可以琢磨一年。lol

初級的 DP 的使用方式差不多就是 Recursion + Memorization,就是遞迴和快取。這裡我們用一個數組來儲存已經算過的數的最少平方數的個數 (記作 minNum)。從1開始算(從0也沒事)。

這裡我們分2層來算,外層迴圈是計算從1到 n的各個數的最少平方數 minNum, 存入到陣列中,陣列的 index 表示數 n,裡面的 val 表示 minNum。關鍵是求每個數的 minNum。這裡我們用到遞迴,核心程式碼就是:

let tmp = val - i * i
minNum = min(minNum, tmp == 0 ? 1 : 1+sta.record[tmp])

tmp 表示 val 減去一個平方數剩下的數,如果 tmp == 0,就表示 val == i * i,即它由1個平方陣列成;如果 tmp != 0,就那麼我們就需要求以 tmp 為 val 的 minNum,也就是 tmp2 = tmp - i * i ,這個 tmp2 就相當於之前的 tmp。為了求 tmp 的 minNum,我們需要計算出 從1到 sqrt(val) 之間所有的可能值,然後取最小值。最後將那個最小值存放到陣列中。最終程式碼就是

func numSquares(n: Int) -> Int {
     var record = [0,1]
    while record.count <= n {
        var val = record.count, minNum = record.count
        for i in 1...Int(sqrt(Double(val))) {
            let tmp = val - i * i
            minNum = min(minNum, tmp == 0 ? 1 : 1+record[tmp])
        }
        record.append(minNum)
    }
    return record[n]
}

但是跑了之後又發現,我特喵的沒錯啊,怎麼時間又是這麼長,1400ms。如果拿個稍微大點的數放到 playground 裡跑一跑就會發現,迴圈次數還是挺多的。所以這裡就需要考慮到把陣列存成 static,而 swift 是沒法在 function 裡直接申明 static var n = 1 的,我們需要把 static 放在 class/struct 裡,參見 SO 大神的解答,還有官方 doc

可以把這個 struct 放在 class Solution 裡面,也可以放在外面,最後時間是 60ms 左右。從 1400 到 60,還是可以的。

struct sta {
    static var record = [0,1]
}

也許從短短這麼一篇文章你就已經看出來了一些 swift 語言的特點,最大的特點就是型別安全。求個根都要 Int(sqrt(Double(n))),我以前是用 C++ 的,遇到這種情況還是有點膈應的。但其實 swift 的優點絕對是可以讓我安全無視這些小麻煩的,其實習慣了之後就感覺是更方便,更安全了。

最後

每篇文章我都在用心寫,希望志同道合的童鞋能一起學習一起進步。如果喜歡我就請關注我哦,點個♥️表示鼓勵吧~

最近貌似 RESTful 很火,如果你對 MongoDB 或者 RESTful 感興趣,請看我的這篇文章,我用 MongoDB 作為後臺資料庫,用 AngularJS, Spark, Java 做了個網站 demo,建於 Heroku 上。每一種技術都是當下最流行的技術。

最後強烈推薦喜歡 swift,並想用 swift 寫演算法的童鞋,Swift Algorithm Club,你值得擁有。


歡迎轉載,轉載請註明出處

      </div>