維基百科對動態規劃(Dynamic programming,簡稱DP)的定義是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。算法
斐波那契數列是一個典型的能夠把原問題分解爲相對簡單的子問題的方式求解複雜問題的例子。下面是斐波那契數列的數學定義:數組
根據這個數學定義,咱們能夠用遞歸的方式很輕鬆的實現這個算法。優化
package main import ( "fmt" "time" ) func fib(n int) int { if n <= 1 { return n } return fib(n-1) + fib(n-2) } func main() { start := time.Now().Unix() fib(45) end := time.Now().Unix() fmt.Println(end-start) }
上面代碼咱們求的是F(45),代碼很是的簡單,但發現計算時間達到了6秒左右,效率十分低下,下面是根據剛剛的代碼畫出的一個F(5)的樹狀圖:
從圖中能夠看出F(3)計算了2次,F(2)計算了3次,F(1)計算了4次,發生了不少重複計算,這也是形成效率低下的緣由,要優化的思路就是去除掉這些沒必要要的重複計算。如今咱們將每一個子問題的計算結果存儲起來,當再次碰到同一個子問題時,就能夠直接從以前存儲的結果中取值,就不用再次計算了。好比第一次碰到計算F(2)時,能夠用一個字典把F(2)的計算結果存儲起來,當再次碰到計算F(2)時就能夠直接從字典中取值,改造後的代碼以下:spa
package main import ( "fmt" "time" ) var m = map[int]int{0:0, 1:1} func fib(n int) int { if v, ok :=m[n]; ok { return v } m[n-1],m[n-2] =fib(n-1),fib(n-2) return m[n-1]+m[n-2] } func main() { start := time.Now().UnixNano() fib(45) end := time.Now().UnixNano() fmt.Println(end-start) }
通過改造後再計算F(45)不到1秒。一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表這也是動態規劃的重要內容。3d
因此動態規劃兩個最主要的點是:code
下面是用動態規劃的方法來解決 LeetCode 上一道名爲 House Robber 的題目:blog
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有必定的現金,影響你偷竊的惟一制約因素就是相鄰的房屋裝有相互連通的防盜系統,若是兩間相鄰的房屋在同一夜被小偷闖入,系統會自動報警。
給定一個表明每一個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的狀況下,可以偷竊到的最高金額。
假設如今各個房屋存放的金額分別爲二、七、九、三、1,求最大能偷竊到的金額。
咱們用 P(n) 表示爲總共有 n 間房屋時能偷取到的最大金額,用 r(n) 表示爲第 n 間房屋中存放的金額。 當 n 爲1時 P(1)=r(1),n 爲2時 P(2)=Max(r(1), r(2))。由於題目要求不能打劫相鄰兩間房,因此當有 n 間房時 P(n)=Max(P(n-2)+r(n), P(n-1))。用方程來表示就是:遞歸
P(1)=r(1) P(2)=Max(r(1), r(2)) P(n)=Max(P(n-2)+r(n), P(n-1))
因此這個問題就被分解成了若干個子問題,下面是其代碼實現:ip
package main import "fmt" var m = map[int]int{} func rob(arr []int) int { l := len(arr) if l <= 0 { return 0 } if v,ok:=m[l-1];ok{ return v } if l == 1 { m[0]=arr[0] return arr[0] } if l == 2 { if arr[0] >= arr[1] { m[1]=arr[0] return arr[0] } else { m[1]=arr[1] return arr[1] } } a, b:= rob(arr[:l-2])+arr[l-1],rob(arr[:l-1]) if a>=b{ m[l-1]=a } else { m[l-1]=b } return m[l-1] } func main() { arr := []int{2,7,9,3,1} m[0]=arr[0] ret :=rob(arr) fmt.Println(ret) }
上面的代碼就是咱們根據方程無腦寫出的算法就已經達到了偷竊最大金額的目的,但其實仍是有一些優化空間的,咱們要計算 P(n) 其實只須要記住以前的 P(n-2) 和 P(n-1)就夠了,但咱們其實將 P(1)、P(2)、...、P(n-2) 都記住了,帶來了一些內存浪費,之因此會有這個問題是由於咱們求解 P(n) 時會依次求解 P(n-1)、P(n-2)、...、P(1) 是一種自頂向下的求解方式,若是換成自底向上的求解方式能夠寫出以下代碼:內存
package main import "fmt" func rob(arr []int) int { pre1, pre2 := 0, 0 for _,v := range arr { if pre2+v >= pre1 { pre1,pre2 = pre2+v,pre1 } else { pre1,pre2= pre1,pre1 } } return pre1 } func main() { arr := []int{2,7,9,3,1} ret :=rob(arr) fmt.Println(ret) }
上面的變量 pre1 和 pre2 分別表示 P(n-1) 和 P(n-2),這樣經過自底向上的方式求出告終果,比自頂向下的方式更節省內存。
因此動態規劃須要記住的幾個關鍵點是將複雜問題拆分紅若干個子問題、記住子問題的結果、自頂向下、自底向上。
假若有10我的參與投票,有的人投給A,有的人投給B,有的人投給C,當咱們想要找出A、B、C誰得票最多時,咱們能夠將兩個不一樣的投票做爲一對進行刪除,直到不能再刪時而後再查看結果中還剩下的投票就是得票最多的那個。好比上述10我的的投票狀況是[A,B,C,C,B,A,A,A,B,A],下面是進行刪除的過程:
[A,B,C,C,B,A,A,A,B,A]==>[C,C,B,A,A,A,B,A] //A,B爲不一樣的投票因此能夠 //做爲一對進行刪除 [C,C,B,A,A,A,B,A]==>[C,A,A,A,B,A] //C,C爲相同的投票因此不刪除,而後 // 再依次向後查找發現C,B不一樣能夠刪除 [C,A,A,A,B,A]==>[A,A,B,A] [A,A,B,A]==>[A,A]
經過不斷的對不一樣的投票做爲一對進行刪除,投票結果中最後只剩下了[A,A],因此A就是得票最多的。摩爾投票法的核心就是將序列中兩個不一樣的元素進行抵消或刪除,序列最後剩下一個元素或多個相同的元素,那麼這個元素就是出現次數最多的元素。
求衆數就是摩爾投票法的一個典型運用場景,好比有下面這道算法題:
給定一個大小爲 n 的數組,找到其中的衆數。衆數是指在數組中出現次數大於 n/2 的元素。給定數組[2,2,1,1,1,2,2,4,5,2,3,2,2] 找出其衆數。
實現代碼以下:
package main import "fmt" func main() { arr := []int{2,2,1,1,1,2,2,4,5,2,3,2,2} maj, count := arr[0], 1 for i:=1;i<len(arr);i++ { if maj == arr[i] { count++ } else { if count == 0 { maj,count = arr[i],1 continue } count-- } } fmt.Println(maj) }
代碼中先假定數組的第一個元素就是衆數,並用一個變量 count 來記錄這個衆數出現的次數,當被迭代到的數與這個衆數相同時 count 就加1,不一樣時就作抵消操做,即 count 減1,當 count 爲0時,就將被迭代到的數設爲新的衆數並將 count 置1。
以上就是摩爾投票法的原理和應用。