动态规划和摩尔投票法
動態規劃
維基百科對動態規劃(Dynamic programming,簡稱DP)的定義是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。
斐波那契數列
斐波那契數列是一個典型的可以把原問題分解為相對簡單的子問題的方式求解復雜問題的例子。下面是斐波那契數列的數學定義:
- F(0)=0,F(1)=1
- F(2)=F(1)+F(0)=1+0=1
- F(n)=F(n-1)+F(n-2) (n>=2) 根據這個數學定義,我們可以用遞歸的方式很輕松的實現這個算法。
上面代碼我們求的是F(45),代碼非常的簡單,但發現計算時間達到了6秒左右,效率十分低下,下面是根據剛剛的代碼畫出的一個F(5)的樹狀圖:
從圖中可以看出F(3)計算了2次,F(2)計算了3次,F(1)計算了4次,發生了很多重復計算,這也是造成效率低下的原因,要優化的思路就是去除掉這些不必要的重復計算。現在我們將每個子問題的計算結果存儲起來,當再次碰到同一個子問題時,就可以直接從之前存儲的結果中取值,就不用再次計算了。比如第一次碰到計算F(2)時,可以用一個字典把F(2)的計算結果存儲起來,當再次碰到計算F(2)時就可以直接從字典中取值,改造后的代碼如下: package mainimport ("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秒。一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表這也是動態規劃的重要內容。
所以動態規劃兩個最主要的點是:
- 將一個復雜的問題分解為若干多個子問題。
- 將每個子問題的結果存儲起來,使每個子問題只解決一次。
House Robber
下面是用動態規劃的方法來解決 LeetCode 上一道名為 House Robber 的題目:
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。 給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。 假設現在各個房屋存放的金額分別為2、7、9、3、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)) 復制代碼所以這個問題就被分解成了若干個子問題,下面是其代碼實現:
package mainimport "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 mainimport "fmt"func rob(arr []int) int {pre1, pre2 := 0, 0for _,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就是得票最多的。摩爾投票法的核心就是將序列中兩個不同的元素進行抵消或刪除,序列最后剩下一個元素或多個相同的元素,那么這個元素就是出現次數最多的元素。
Majority Element
求眾數就是摩爾投票法的一個典型運用場景,比如有下面這道算法題:
給定一個大小為 n 的數組,找到其中的眾數。眾數是指在數組中出現次數大于 n/2 的元素。給定數組[2,2,1,1,1,2,2,4,5,2,3,2,2] 找出其眾數。
實現代碼如下:
package mainimport "fmt"func main() {arr := []int{2,2,1,1,1,2,2,4,5,2,3,2,2}maj, count := arr[0], 1for i:=1;i<len(arr);i++ {if maj == arr[i] {count++} else {if count == 0 {maj,count = arr[i],1continue}count--}}fmt.Println(maj) } 復制代碼代碼中先假定數組的第一個元素就是眾數,并用一個變量 count 來記錄這個眾數出現的次數,當被迭代到的數與這個眾數相同時 count 就加1,不同時就做抵消操作,即 count 減1,當 count 為0時,就將被迭代到的數設為新的眾數并將 count 置1。
以上就是摩爾投票法的原理和應用。
轉載于:https://juejin.im/post/5caffda35188251b0b7a6f1a
總結
以上是生活随笔為你收集整理的动态规划和摩尔投票法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Kubernetes1.5新特性(一):
- 下一篇: static/final/常量模式