算法學習:經常使用設計思想

前面幾篇文章大概介紹了幾個經常使用的數據結構。算法

根據個人理解,數據結構幫助咱們對須要解決的問題進行描述,而算法就是咱們解決問題方案的具體描述。它包括對問題的分析及研究(創建描述問題的數學模型),而後根據一些策略和思想制定出解決問題的方案。bash

這篇文章講述了四個算法設計時的經常使用思想並給出了相應的例子:數據結構

  • 解空間內的窮舉
  • 貪婪法
  • 分治法
  • 動態規劃

解空間內的窮舉

這個名字是來自於《算法的樂趣》,其實就是窮舉法。這裏的解空間是全部可能的解的集合。加上解空間就是爲了說明:窮舉是在可能的解的集合中查找的,並非漫步目的的亂找。app

步驟:函數

  1. 肯定問題的解空間的範圍以及正確解的斷定條件
  2. 根據解空間的特色選取搜索策略。一一檢驗解空間中的候選解是否正確,必要時可輔助一些剪枝算法。

窮舉法能夠說是解決不少問題的 「通用算法」 了,可是窮舉法最大的問題就是問題的規模。因此,咱們須要一些策略來進行咱們的窮舉。優化

策略:ui

  • 盲目搜索: 在給定的解空間,按順序依次搜索全部的候選解。
  • 啓發式搜索: 在搜索過程當中,依據一些狀態評估的函數,優先對有可能演化出解的節點進行搜索。
  • 剪枝策略: 若是一些節點能夠根據提供的信息明確地被斷定爲不可能演化出解,那麼就能夠跳過此狀態節點。

舉例: 在一個籠子裏關着若干只雞和若干只兔,從上面數共有35個頭;從下面數共有94只腳。問籠中雞和兔的數量各是多少?spa

// head = 35,  foot = 94
func getNumberBy(head: Int, foot: Int) -> (Int, Int)? {
    var numOfRabbit: Int = 1
    var numOfChicken: Int = head - numOfRabbit
    
    while (numOfRabbit*4 + numOfChicken*2) != foot {
        numOfRabbit += 1
        numOfChicken = head - numOfRabbit
        
        if numOfRabbit > head {
            return nil
        }
    }
    
    return (numOfRabbit, numOfChicken)
}
複製代碼

貪婪法

又稱貪心算法(greedy algorithm),是尋找最優解問題的經常使用方法。這種方法模式通常將求解過程分爲若干個步驟,在每一個步驟都應用貪心原則,選取當前狀態下最好的或最優的選擇(局部最有利的選擇),並以此但願最後堆疊的結果也是最好或最優的解。設計

大多數狀況下,因爲貪婪法在選擇策略上的「短視」,會錯過真正的最優解,可是貪婪法簡單高效,省去了爲了尋找最優解可能須要的窮舉操做,能夠獲得與最優解比較接近的近似最優解。code

基本思想:

  1. 創建對問題精確描述的數學模型,包括定義最優解的模型。
  2. 將問題分解爲一系列子問題,同時定義子問題的最優解結構。
  3. 應用貪心原則肯定每一個子問題的局部最優解,並根據最優解的模型,用子問題的局部最優解堆疊出全局最優解。

舉例: 如今咱們有一個揹包,裏面能夠裝下 150 單位重量的物體,如今咱們有一系列重量的東西,怎麼樣的組合讓揹包裝下最多的東西?

let weight = [35, 30, 60, 50, 40, 10, 25] // 重量

// total = 150
func greedy(total: Int) -> [Int] {
    var result: [Int] = []
    
    let tempArr = weight.sorted()
    
    var temp = 0
    for index in 0..<weight.count {
        temp = temp + tempArr[index]
        if temp < total {
            result.append(tempArr[index])
        }
    }
    
    return result
}
複製代碼

分治法

分治法的設計思想是將沒法着手解決的大問題分解成一系列規模較小的相同問題。而後逐個解決小問題,即分而治之。分治法產生的子問題與原始問題相同,只是規模減少,反覆使用分治方法,可使得子問題的規模不斷減少,直到可以被直接求解爲止。

基本思想:

  1. 分解: 將問題分解爲若干個規模較小,相互獨立且與原問題形式相同的子問題,確保各個子問題的解具備相同的結構。
  2. 解決: 若是上一步分解獲得的子問題能夠解決,則直接解決,不然,對每一個子問題使用和上一步相同的方法再次分解,而後求解分解後的子問題,這個過程多是個遞歸的過程。
  3. 合併: 將上一步解決的各個子問題的解經過某種規則合併起來,獲得原問題的解。

舉例: 一我的在 1~100 中隨機選取一個數,如何才能以最少的次數猜到這個數字?每次猜想後,都會得知猜想結果小了、大了或正確。

// 一個典型的二分搜索
// source = [1, 2, 3, ... 100], target 爲須要猜想的數字
func binarySearch(source: [Int], target: Int) -> Int? {
    guard source.count != 0 else {
        return nil
    }
    
    var low: Int = 0
    var high: Int = source.count - 1
    var mid: Int = 0
    
    var count = 0
    
    while low <= high {
        count += 1;
        
        mid = low + (high - low) / 2 
        
        if target < source[mid] {
            high = mid - 1
        } else if target > source[mid] {
            low = mid + 1
        } else {
            return mid
        }
    }
    return nil
}
複製代碼

動態規劃

解決多階段決策問題經常使用的最優化理論。原理是把多階段決策過程轉化爲一系列的單階段決策問題,利用各個階段之間的遞推關係,逐個肯定每一個階段的最優化決策,最終堆疊出多階段決策的最優化決策結果。

須要知足的條件:

  • 最優化原理:

    無論以前決策是不是最優決策,都必須保證從如今開始決策是在以前決策基礎上的最優決策。

  • 無後向性:

    當各個階段的子問題肯定之後,對於某個特定階段的子問題來講,它以前的各個階段的子問題的決策隻影響該階段的決策,對該階段以後的決策不產生影響。也就是說,每一個階段的決策僅受以前決策的影響,可是不影響以後各階段的決策。

  • 有重疊子問題:

    即子問題之間是不獨立的,一個子問題在下一階段決策中可能被屢次使用到。(該性質並非動態規劃適用的必要條件,可是若是沒有這條性質,動態規劃算法同其餘算法相比就不具有優點)

動態規劃算法與分治法最大的差異是:適合於用動態規劃法求解的問題,經分解後獲得的子問題每每不是互相獨立的(即下一個子階段的求解是創建在上一個子階段的解的基礎上,進行進一步的求解)。

基本思想:

  1. 定義最優子問題:

    肯定問題的優化目標以及決策最優解,並對決策過程劃分階段。

  2. 定義狀態:

    對起始狀態施加決策,使得狀態發生改變,獲得決策的結果狀態。狀態的定義是創建在子問題定義的基礎上的,所以狀態必須知足 「無後效性」。

  3. 定義決策和狀態轉換方程:

    決策就是能使狀態發生轉變的選擇動做。狀態轉換方程是根據上一階段的狀態和決策來導出本階段的狀態的方程。

  4. 肯定邊界條件:

    邊界條件其實就是狀態轉移方程的終止條件。

舉例:

有n級臺階,一我的每次上一級或者兩級,問有多少種走完n級臺階的方法?

分析:

動態規劃的實現的關鍵在於能不能準確合理的用動態規劃表來抽象出實際問題。在這個問題上,咱們讓f(n)表示走上n級臺階的方法數。

那麼當 n 爲 1 時,f(n) = 1, n 爲 2 時,f(n) = 2, 就是說當臺階只有一級的時候,方法數是一種,臺階有兩級的時候,方法數爲 2。那麼當咱們要走上 n 級臺階,必然是從 n-1 級臺階邁一步或者是從 n-2 級臺階邁兩步,因此到達 n 級臺階的方法數必然是到達 n-1 級臺階的方法數加上到達 n-2 級臺階的方法數之和。即 f(n) = f(n-1) + f(n-2)。

// n 爲須要走的臺階總數
func calculateStep(n: Int) -> Int {
    //若是爲第一級臺階或者第二級臺階 則直接返回n
    if n < 1 {
        return 0
    }
    
    if n == 1 || n ==  2 {
        return n
    }
    
    var a = 1
    var b = 2
    var temp = 0
    
    for _ in 3..<n+1 {
        temp = a + b
        a = b
        b = temp
    }
    return temp
}
複製代碼
相關文章
相關標籤/搜索