演算法基礎--遞迴和動態規劃

kirito_song發表於2018-12-14

演算法基礎--遞迴和動態規劃

本文主要作為自己的學習筆記,並不具備過多的指導意義。

暴力遞迴

  1. 把問題轉化為規模縮小了的同類問題的子問題

  2. 有明確的不需要繼續遞迴的條件

    base case


求n!的結果

非遞迴版本

從非依賴關係入手。明確的知曉n!=1×2×3×...×n,然後按照順序編寫演算法即可

func getFactorial1(n : Int) -> Int {
    var res = 1
    for i in 1..<n+1 {
        res = res * i
    }
    
    return res
}
複製程式碼

遞迴版本

從依賴關係入手。n已知,嘗試解決(n-1)!

func getFactorial2(n : Int) -> Int {
    if n == 1 {
        return 1
    }
    return n * getFactorial2(n: n-1)
}
複製程式碼

漢諾塔問題

列印N層漢諾塔從最左邊移動到最右邊的全部過程

每次一個,不能打壓小隻能小壓大

演算法基礎--遞迴和動態規劃

在第N層的問題上,需要完成以下三個狀態:

第N層的完成依賴N-1的完成,而第N-1層的完成又依賴N-1層的完成。

演算法基礎--遞迴和動態規劃

/// 移動1-N層漢諾塔
///
/// - Parameters:
///   - n: 需要移動到的層數
///   - form: 從哪根開始
///   - to: 從哪根結束
///   - help: 空那根
func hanoiGame(n : Int ,form :String ,to :String ,help :String) {
    if n == 1 {//只移動第一層,直接移動即可
        print("Move 1 from " + form + " to " + to)
    }else {
        hanoiGame(n: n-1, form: form, to: help, help: to)  //將第 1到n-1 層移動到 中間
        print("Move \(n) " + "from " + form + " to " + to) //將第 n 層移動到 最右
        hanoiGame(n: n-1, form: help, to: to, help: form) //將第 1到n-1 層移動到 最右
    }
}



hanoiGame(n: 3, form: "左", to: "右", help: "中")
//列印
Move 1 from 左 to 右
Move 2 from 左 to 中
Move 1 from 右 to 中
Move 3 from 左 to 右
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 右
複製程式碼

列印字串能組成的所有字串

輸入abc 列印:abc,ab,ac,a,bc,b,c

將字串轉化成陣列,每個位置都有兩個選擇:列印&&跳過。以此遞迴

程式碼

func printStr(str :String) {
    printAllSub(str: wordToArr(word: str), i: 0, res: "")
}

func printAllSub(str :[String] ,i :Int ,res :String) {
    if i == str.count {
        print(res)
    }else {
        printAllSub(str: str, i: i+1, res: res+str[i]) //列印當前位置
        printAllSub(str: str, i: i+1, res: res) //不列印當前位置
    }

}

func wordToArr(word:String) -> Array<String> {
    var res : [String]
    res = Array.init()
    if word.count == 0 {
        return res
    }
    let string = (word as NSString)
    for i in 0..<string.length {
        res.append(string.substring(with: NSMakeRange(i, 1)))
    }
    
    return res
}

複製程式碼

母牛數目問題

有一頭母牛,它每年年初生一頭小母牛。每頭小母牛從第四個年頭開始,每年年初也生一頭小母牛。請程式設計實現在第n年的時候,共有多少頭母牛?

當思維不夠直觀的時候,不妨列舉一下試試查詢規律

演算法基礎--遞迴和動態規劃

F(N) = F(N-1) + F(N-3)

第五年 = 第四年存活的 + A與第二年出生的B所生的兩個

需要注意:如果N-3為負數則不用計算,只計算母牛自己生的一個即可

func func(n : Int) -> Int {
    if n == 1 {
        return 1
    }
    if n - 3 <= 0 {
        return func1(n: n-1) + 1
    }else {
        return func1(n: n-1) + func1(n: n-3)
    }
}
複製程式碼

二維陣列--從左上角到右下角最大值

只能向右或向下走

經典的動態規劃題目,但我們可以先從遞迴做起

/// 二維陣列--從左上角到右下角最大值
///
/// - Parameters:
///   - matrix: 二維矩陣
///   - x: x軸座標
///   - y: y軸座標
/// - Returns: 當前點到右下角最小距離
func walk(matrix : [[Int]] ,x :Int ,y :Int) -> Int {
    if (x == matrix.count-1) && (y == matrix[0].count-1) { //已經到最後
        return matrix[x][y] //返回當前節點
    }
    
    if x == matrix.count-1 {  //已經到x軸末尾
        return matrix[x][y] + walk(matrix: matrix, x: x, y: y+1) //當前節點+y軸下一位
    }
    
    if y == matrix[0].count-1 { //已經到y軸末尾
        return matrix[x][y] + walk(matrix: matrix, x: x+1, y: y) //當前節點+x軸下一位
    }
    
    //當前節點+min(x軸下一位,y軸下一位)
    return matrix[x][y] + min(walk(matrix: matrix, x: x+1, y: y), walk(matrix: matrix, x: x, y: y+1))
}

複製程式碼

暴力遞迴的弊端

第一次進入walk(0,0)時,將會遞迴呼叫藍色位置walk(1,0)walk(0,1)

演算法基礎--遞迴和動態規劃

而在進入walk(1,0)時,又將遞迴呼叫walk(2,0)walk(1,1) 並且進入walk(0,1)時,又將遞迴呼叫walk(0,2)walk(1,1)

演算法基礎--遞迴和動態規劃

此時walk(1,1)將會執行兩次,其之後的遞迴計算也指數級的重複。

這就是動態規劃的意義,解決暴力遞迴重複執行的缺點進行優化


動態規劃

所有的動態規劃,都是從暴力遞迴嘗試優化(減少重複計算)而來

面試中,對於一個沒有見過的動態規劃。我們可以先寫出一個遞迴的嘗試版本,在驗證正確性之後嘗試改成動態規劃。

遞迴方法的後效性

如上文中所提到的暴力遞迴的弊端一樣:有些暴力遞迴會存在重複狀態,並且這些重複狀態的結果與到達其的路徑無關(狀態的引數確定,返回值則確定)。

什麼樣的問題可以改成動態規劃

對於無後效性遞迴,可以改成動態規劃的版本。

也有反例:比如漢諾塔問題,每一步列印都會對整體的列印結果造成影響。就叫有後效性遞迴,無法進行動態規劃。

無後效性遞迴如何改成動態規劃的通用方法

二維陣列--從左上角到右下角最大值題目為例:

  1. 分析可變引數,建立狀態表

    以每個狀態的return結果建立一個二維陣列。

  2. 找到自己需要的最終狀態位置(0,0)

    演算法基礎--遞迴和動態規劃

  3. 回到base case 中,對不被依賴的位置進行設定

    演算法基礎--遞迴和動態規劃

  4. 對普遍位置進行設定

    演算法基礎--遞迴和動態規劃

  5. 最終得到目標位置


陣列中元素是否能組成指定的和

先寫一個正常的暴力遞迴嘗試版本,與之前列印字串能組成的所有字串的問題基本一致

/// 陣列中元素是否能組成指定的和
///
/// - Parameters:
///   - arr: 陣列
///   - i: 當前位置
///   - sum: 已經求的和
///   - aim: 目標和
/// - Returns: 結果
func isSum(arr :[Int] ,i :Int ,sum :Int ,aim :Int) -> Bool {
    if i == arr.count { //陣列末尾已經嘗試結束
        return aim==sum //直接比對
    }
    
    let useC = isSum(arr: arr, i: i+1, sum: sum+arr[i], aim: aim) //嘗試新增當前位置
    
    let unuseC = isSum(arr: arr, i: i+1, sum: sum, aim: aim) //不新增當前位置
    
    return useC || unuseC
}
複製程式碼

如何轉變成動態規劃

  1. 簡化表示式,並建立動態規劃表

    只有兩個可變引數,可以簡化成F(i,sum)

    DP表的設計行為sum(最後一位為所有元素之和),列為i。 在程式碼上,將作為一個二維陣列存在

    演算法基礎--遞迴和動態規劃

  2. 確定目標位置

    演算法基礎--遞迴和動態規劃

  3. base case中找到不被依賴的位置 只有在F(N,Aim)時,aim==sum才會返回true

    演算法基礎--遞迴和動態規劃

  4. 對普遍位置進行設定 某一個位置F(i,sum1)的狀態依賴於F(i+1,sum1)F(i+1,sum1)+arr[i]F(i+1,sum1)+arr[i]又作為新的sum值Sum2存在於DP表內。 兩個位置有一個為Aim,則將返回true

    演算法基礎--遞迴和動態規劃

  5. 推回到最初位置


參考資料

左神牛課網演算法課

相關文章