動(dòng)態(tài)規(guī)劃

1、簡(jiǎn)介
動(dòng)態(tài)規(guī)劃(Dynamic Programming,DP)是求解決策過程最優(yōu)化的過程,把原始問題劃分成一系列子問題,以分治的方法找出最優(yōu)解。

如果沒有最優(yōu)子結(jié)構(gòu),就等同于回溯,比如斐波拉契數(shù)列;

如果局部最優(yōu)解就是全局最優(yōu)解,就等同于貪心算法,
比如錢幣的問題,如何用最少張紙幣匹配出某個(gè)數(shù)字?
如果用 10/5/1 這三種面額配 16 元,貪心算法用 10+5+1 湊出來是可以的;
但是如果用10/8/1 這三種面額匹配16元,貪心算法只能用 10+1+1+1+1+1+1,而顯然最優(yōu)解是 8+8,此時(shí)只能用動(dòng)態(tài)規(guī)劃,而貪心法比較有局限性。

2、題目
(1)給出一個(gè)三角形(用二維數(shù)組表示),找出自頂向下的最小路徑和。每一步只能移動(dòng)到下一行中相鄰的結(jié)點(diǎn)上(a[i][j] 的下一層相鄰結(jié)點(diǎn)有 a[i][j]a[i][j+1] )。
可以先用一個(gè)二維數(shù)組存儲(chǔ)中間結(jié)果,從最下面一層開始,逐步往上遞推:

package main
import "fmt"
func minimumTotal(triangle [][]int) int {
    x := len (triangle)
    m := make([][]int , x, x)

    for i:=0; i < x; i++ {
        m[i] = make([]int, len(triangle[i]), len(triangle[i]))
    }

    for i:=0; i < x; i++ {
        m[x-1][i] = triangle[x-1][i]
    }

    for i:= x - 2 ; i >= 0; i-- {
        for j:=0; j < len (triangle[i]) ; j++ {
            min := m[i+1][j]
            if m[i+1][j+1] < m[i+1][j] {
                min = m[i+1][j+1]
            }
            m[i][j] = triangle[i][j] + min
        }
    }
    return m[0][0]
}

func main() {
    triangle := [][]int {
        {2},
        {3, 4},
        {6, 5, 7},
        {4, 1, 8, 3} }
    fmt.Println(  minimumTotal( triangle )  )
}

上面申請(qǐng)了一個(gè)二維的切片存儲(chǔ)子結(jié)構(gòu)的結(jié)果,但是實(shí)際上只需要保存最近的一組數(shù)據(jù),歷史值已經(jīng)使用過不用再保存,所以只需要復(fù)用一個(gè)一維切片即可,可以減少存儲(chǔ)空間:

func minimumTotal(triangle [][]int) int {
    x := len (triangle)
    m := make([]int , x, x)

    for i:=0; i < x; i++ {
        m[i] = triangle[x-1][i]
    }

    for i:= x - 2 ; i >= 0; i-- {
        for j:=0; j < len (triangle[i]) ; j++ {
            min := m[j]
            if m[j+1] < m[j] {
                min = m[j+1]
            }
            m[j] = triangle[i][j] + min
        }
    }
    return m[0]
}

(2)給出一個(gè)一維整型數(shù)組,找出數(shù)組中乘積最大的連續(xù)子數(shù)組(子數(shù)組至少包含一個(gè)數(shù)字),返回乘積。
前面幾個(gè)元素的最大乘積記為 max ,當(dāng)前元素記為 nums[i]
如果 max > 0 && nums[i] > 0 ,則 max = max * nums[i] ,
如果 max <=0 0 && nums[i] > 0,則 max = nums[i],
這里雖然是求最大乘積,但是考慮到負(fù)數(shù),需要把最小乘積 min 也保存下來,所以這一題比上一題難的地方在于需要多保存一種狀態(tài):
如果 max > 0 && min <= 0 && nums[i] < 0,負(fù)負(fù)得正,則 max = min * nums[i]。
如果 max <= 0 && min <= 0 && nums[i] < 0,由于 min 的絕對(duì)值比較大,則 max = min * nums[i]。
簡(jiǎn)單的說就是每一輪循環(huán)都從 max * nums[i] 、min * nums[i]nums[i] 這三個(gè)值種找出最大值和最小值保存起來。

package main
import "fmt"

func maxMin(a int, b int, c int) (int,int) {
    var max, min int
    if a > b {
        max = a
        min = b
    } else {
        max = b
        min = a
    }
    if c > max {
        max = c
    } else if c < min {
        min = c
    }
    return max, min
}

func maxProduct(nums []int) int {
    n := len (nums)
    max := make([]int , n, n)
    min := make([]int , n, n)
    max[0], min[0] = nums[0], nums[0]
    result := nums[0]
    for i:=1; i < n; i++ {
        max[i], min[i] = maxMin(nums[i] * max[i-1], nums[i] * min[i-1], nums[i])
        if max[i] > result {
            result = max[i]
        }
    }
    return result
}

func main() {
    fmt.Println(  maxProduct( []int { -2, 3, -2 ,3} )  )
    fmt.Println(  maxProduct( []int { -2, 1, 2 ,3} )  )
    fmt.Println(  maxProduct( []int { -2, 0, -1} )  )
    fmt.Println(  maxProduct( []int { 0, 2} )  )
}

這里實(shí)際上只需要存儲(chǔ)當(dāng)前輪和前一輪的最大值、最小值即可,minmax 只需要定義為長(zhǎng)度為 2 的數(shù)組(或者直接用 2 個(gè)整型變量表示,比較具有可讀性):

func maxProduct(nums []int) int {
    var result, maxPre, minPre, maxCur, minCur int
    result, maxPre, minPre = nums[0], nums[0], nums[0]
    n := len (nums)
    for i:=1; i < n; i++ {
        maxCur, minCur = maxMin(nums[i] * maxPre, nums[i] * minPre, nums[i])
        minPre = minCur
        maxPre = maxCur
        if maxCur > result {
            result = maxCur
        }
    }
    return result
}

(3)最長(zhǎng)上升子序列
注意:子序列并不要求連續(xù)。
這里會(huì)有一個(gè)誤區(qū)是:直接用一層循環(huán),如果 nums[i] > nums[i-1]dp[i] = dp[i-1] + 1,這樣計(jì)算是錯(cuò)誤的,例如數(shù)組是1,3,1,2,3,后面的三個(gè)元素都沒能大于第二個(gè)元素3,會(huì)得到 2 這個(gè)錯(cuò)誤的答案。
所以狀態(tài)應(yīng)該定義成:dp[i] 表示以 nums[i] 結(jié)尾的上升子序列的長(zhǎng)度,用兩層循環(huán),計(jì)算 dp[i]時(shí),要把下標(biāo) [0][i] 都遍歷一遍;

package main
import "fmt"

func lengthOfLIS(nums []int) int {
    n := len(nums)
    if 0 == n {
        return 0
    }
    result := 1

    dp := make([]int, n, n)
    for i:=0; i < n; i++ {
        dp[i] = 1
    }

    for i:=0; i < n; i++ {
        for j:=0; j < i; j++ {
            if nums[j] < nums[i] {
                if dp[j] + 1 > dp[i] {
                    dp[i] = dp[j] + 1
                }
            }
        }
        if dp[i] > result {
            result = dp[i]
        }
    }
    return result
}

func main() {
    fmt.Println( lengthOfLIS( []int{}) )
    fmt.Println( lengthOfLIS( []int{1,2}) )
    fmt.Println( lengthOfLIS( []int{1,2,4,1,3,5}) )
    fmt.Println( lengthOfLIS( []int{2,1}) )
    fmt.Println( lengthOfLIS( []int{1,3,1,2,3}) )
}

(4)零錢問題
給定不同面額的硬幣(用整型數(shù)組表示) 和目標(biāo)金額。
計(jì)算湊成目標(biāo)金額所需最少的硬幣數(shù)。無解時(shí)返回 -1。
假設(shè)有1/2/5三種面額硬幣,則 f(n) = 1 + min{ f(n-1), f(n-2), f(n-5) }
遞歸寫法:

func coin(coins []int, amount int) int {
    if amount == 0 {
        return 0
    }
    if amount < 0 {
        return -1
    }

    min := -1
    for j := len(coins)-1; j >= 0; j-- {
        if amount == coins[j] {
            return 1
        }
        ret := coin(coins, amount-coins[j])
        if ret < min || min == -1 {
            min = ret
        }
    }
    if min == -1 {
        return -1
    }
    return min + 1
}

這種寫法有大量重復(fù)計(jì)算,速度非常慢,計(jì)算coin( []int{1, 2, 5}, n) 當(dāng)n=30還好,當(dāng)n=40時(shí)已經(jīng)有明顯卡頓,當(dāng)n=45時(shí)大約需要一分鐘。
下面改為非遞歸寫法:

func coinChange(coins []int, amount int) int {
    dp := make([]int, amount+1)
    for i := 1; i <= amount; i++ {
        dp[i] = -1
        for j:=len(coins)-1; j >= 0; j-- {
            if coins[j] > i || dp[i-coins[j]] == -1 {
                continue
            }
            if dp[i-coins[j]] + 1 < dp[i] || -1 == dp[i]{
                dp[i] = dp[i-coins[j]] + 1
            }
        }
    }
    return dp[amount]
}

(5)編輯距離
給出 2 個(gè)單詞,只能用 insert、delete、replace 三種操作,每次只能操作 1 個(gè)字符,目標(biāo)是使得 2 個(gè)單詞內(nèi)容相同,返回最少操作步數(shù)。
分析:設(shè)狀態(tài)為 dp[i][j] 表示單詞1 的前 i 個(gè)字符,最少花費(fèi)多少步可以得到單詞2 的前 j 個(gè)字符。
如果 word1[i-1] == word2[j-1]dp[i][j] = dp[i-1][j-1]
否則 為下列三者的最小值加一
dp[i][j] = min { insert, delete, replace } + 1
insert = dp[i-1][j]
delete = dp[i][j-1]
replace = dp[i-1][j-1]

func minDistance(word1 string, word2 string) int {
    n := len(word1)
    m := len(word2)
    dp := make([][]int, n+1)
    
    for i:=0; i <= n; i++ {
        dp[i] = make([]int, m+1)
        dp[i][0] = i   // init the special case
    }
    for i:=0; i <= m; i++ {
        dp[0][i] = i
    }
    
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            if word1[i-1] == word2[j-1] {
                dp[i][j] = dp[i-1][j-1]
            } else {
                min := dp[i-1][j-1] // replace
                if dp[i][j-1] < min { // delete
                    min = dp[i][j-1]
                }
                if dp[i-1][j] < min { // insert
                    min = dp[i-1][j]
                }
                dp[i][j] = min + 1
            }
        }
    }
    return dp[n][m]
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容