DP学习笔记

经典案例

动态规划经常用来求最优解。如何判断一个题目是否能用DP来解决,可以通过画递归树来确定是否存在重复子问题,如果存在,则可以通过存储子问题的解去优化算法,还有就是知道子问题解后,能否得到当前的解。DP最关键的是整理出重叠子问题,从而得到动态转移方程或动态转移表。

如何得到状态转移方程?可以在假设知道子问题解的情况下,看看如何得到当前的解。这是问题的关键。而前提是要定义清楚dp[i]=k的含义。

以下是一些典型案例。

0-1背包问题

题目:

背包总重量W,n个物品,每个物品重量不等,不能分割,期望选择几件物品,装载到背包中,不超过总重量的前提下,让包里面物品总重量最大

分析:

dp[i][w]=true/false 表示第i个物品选或者不选时,是否能达到w重量。算法复杂度为O(n*w)

动态转移方程

选第i个物品时:dp[i][w+item[i]]=dp[i-1][w]

不选第i个物品时:dp[i][w] = dp[i-1][w]

 代码

//n表示一共有n个物品,w表示最多承受的重量
func  knapsack(items []int, n, w int) int {
    //初始化状态转移表
    states := make([][]bool, n)
    for i := 0; i < n; i++ {
        states[i] = make([]bool, w+1)
    }

    //初始化第0行的状态
    states[0][0] = false
    if items[0] <= w {
        states[0][items[0]] = true
    }

    //开始状态转移,从第一行开始
    for i := 1; i < n; i++ {
        //不选第i个
        for j := 0; j <= w; j++ {
            states[i][j] = states[i-1][j]
        }
        //选第i个
        for j := 0; j <= w-items[i]; j++ {
            states[i][j+items[i]] = states[i-1][j]
        }
    }

    for i := w; i >= 0; i-- {
        if states[n-1][i] {
            return i
        }
    }
    fmt.Println(states)
    return 0
}
View Code

股票买卖时机(买卖一次求最大profit)

题目:

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

分析:

首先将题目转化为每次交易的profit序列,有亏有赢,也就是对item[i]和item[i-1]做差,得到profit[i]序列,现在只需求连续求和运算,求出最大的和即可。

动态转移方程:

dp[i]=max(dp[i-1]+profit[i], profit[i])

代码:

if len(prices) <= 1 {
        return 0
    }

    diff := make([]int, len(prices)-1)
    for i:=1; i<len(prices); i++ {
        diff[i-1] = prices[i] - prices[i-1]
    }

    dp := make([]int, len(diff))
    dp[0] = diff[0]
    profit := diff[0]
    for i:=1; i<len(diff); i++ {
        dp[i] = Max(dp[i-1]+diff[i], diff[i])
        if profit < dp[i] {
            profit = dp[i]
        }
    }
    if profit < 0 {
        return 0
    }
    return profit
View Code 

莱特斯坦距离(编辑距离)

 题目:

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

分析:

编辑距离:将一个字符串转化成另一个字符串,需要的最少编辑次数(增加一个字符,删除一个字符,替换一个字符)这类二维的题目(涉及到两个字符串的题目)可以用表格来帮助推到状态转移方程。

a,b表示两个字符串,dp[i][j]=k表示a/b的莱特斯坦距离。如果a[i]=b[j],则dp[i][j]=dp[i-1][j-1];如果a[i]!=b[j],那么比较复杂,如果比如两个字符串为"a","c",那么改任意一个即可,所以可以表示为dp[i][j]=min(dp[i-1][j],d[i][j-1])+1,如果是"ac"和"ag",那么

if s1[i] == s2[j]:
    啥都别做(skip)
    i, j 同时向前移动
else:
三选一:
    插入(insert) dp[i][j]=dp[i][j-1]+1
    删除(delete)dp[i][j]=dp[i-1][j]+1
    替换(replace)dp[i][j]=dp[i-1][j-1]+1

注意,要考虑空串情况,比如a为"",b="abcf"这种情况。dp[i][j]=max{i,j}

状态转移方程

如果a[i]=b[j]:dp[i][j] = dp(i - 1, j - 1)

如果a[i]!=b[j]:dp[i][j] = min(dp(i, j - 1) + 1,  dp(i - 1, j) + 1, dp(i - 1, j - 1) + 1) 

代码:

func minDistance(a string, b string) int {
   lenA, lenB := len(a), len(b)
    if lenA == 0 || lenB == 0 {
        return int(math.Max(float64(lenA), float64(lenB)))
    }

    lev := make([][]int, lenA+1)
    for i := 0; i <= lenA; i++ {
        lev[i] = make([]int, lenB+1)
    }

    //初始化第一列
    for i := 0; i <= lenA; i++ {
        lev[i][0] = i
    }
    //初始化第一行
    for j := 0; j <= lenB; j++ {
        lev[0][j] = j
    }

    //计算
    for i := 1; i <= lenA; i++ {
        for j := 1; j <= lenB; j++ {
            if a[i-1] == b[j-1] {
                lev[i][j] = lev[i-1][j-1]
            } else {
                lev[i][j] = int(math.Min(math.Min(float64(lev[i-1][j]), float64(lev[i][j-1])), float64(lev[i-1][j-1]))) + 1
            }
        }
    }

    return lev[lenA][lenB]
}
View Code

最长公共子串

题目:

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

分析:

公共子串一定是连续的,所以dp[i][j]表示以a[i]和b[j]结尾的最长子串长度(如果不是以i,j结尾,那么dp[i+1][j+1]就不是子问题了)

动态转移方程:

情况一、a[i]=b[j]: dp[i][j] = dp[i-1][j-1]+1

情况二、a[i]不等于b[j]:dp [i][j]=0 (不等时得重新计算)

func longestCommonSubstring(a, b string) int {
    dp := make([][]int, len(a))
    for i, _ := range dp {
        dp[i] = make([]int, len(b))
    }

    //初始化第一列
    for i := 0; i < len(a); i++ {
        if a[i] == b[0] {
            dp[i][0] = 1
        } else {
            dp[i][0] = 0
        }
    }
    //初始化第一行
    for j := 0; j < len(b); j++ {
        if a[0] == b[j] {
            dp[0][j] = 1
        } else {
            dp[0][j] = 0
        }
    }

    longest := 0
    for i := 1; i < len(a); i++ {
        for j := 1; j < len(b); j++ {
            if a[i] == b[j] {
                dp[i][j] = dp[i-1][j-1] + 1
                longest = int(math.Max(float64(dp[i][j]), float64(longest)))
            } else {
                dp[i][j] = 0
            }
        }
    }

    return longest
}
View Code 

最长公共子序列

题目:

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

分析:

dp[i][j]=k表示长度为i的序列a和长度为j的序列b的最长公共子串的长度(子串不一定要以i和j结尾,这个很重要,和最长公共子串不一样);当知道之前状态时,能否得到当前的状态,如果能,则可以用动态规划。

动态转移方程:

情况一、a[i]=b[j]: dp[i][j] = dp[i-1][j-1]+1

情况二、a[i]不等于b[j]:dp [i][j]=max{dp[i-1][j], dp[i][j-1]}

//最长公共子序列,可以不连续
func longestCommonSubsequence(a, b string) int {
    lenA, lenB := len(a), len(b)
    dp := make([][]int, lenA)
    for i, _ := range dp {
        dp[i] = make([]int, lenB)
    }

    //初始化dp第0列和第0行
    for j := 0; j < lenB; j++ {
        if a[0] == b[j] || (j >= 1 && dp[0][j-1] == 1) {
            dp[0][j] = 1
        } else {
            dp[0][j] = 0
        }
    }
    for i := 0; i < lenA; i++ {
        if a[i] == b[0] || (i >= 1 && dp[i-1][0] == 1) {
            dp[i][0] = 1
        } else {
            dp[i][0] = 0
        }
    }

    for i := 1; i < lenA; i++ {
        for j := 1; j < lenB; j++ {
            if a[i] == b[j] {
                dp[i][j] = dp[i-1][j-1] + 1
            } else {
                dp[i][j] = int(math.Max(float64(dp[i-1][j]), float64(dp[i][j-1])))
            }
        }
    }
    return dp[lenA-1][lenB-1]
}
View Code

最长单调递增子串

题目

给定一个字符串(或数组),求最长的连续单调自增子串。

分析

定位dp[i]=k表示以item[i]结尾的最长单调自增子串长度为k。那么如果item[i+1]比前一个大,dp[i+1]=dp[i]+1,如果小于等于前一个,那么dp[i+1]=1

动态转移方程

如果item[i]>item[i-1],则dp[i]=dp[i-1]+1 

否则dp[i]=1

代码略。。。。

最长单调递增子序列

题目

给定一个无序的整数数组,找到其中最长上升子序列的长度。

分析

dp[i]=k表示以item[i]结尾的最长递增子序列的长度为k。当知道前i个dp的值后,怎么求第i+1个结尾的最长子序列长度呢:依此从后往前遍历,找到前i个中比item[i+1]小的item[j],求出dp[i]=dp[j]+1,取最大值为dp[i]。算法复杂度为O(n*n)

状态转移方程

dp[i]=max{1, dp[j]+1} (item[i]>item[j], 0<=j<i)

代码

func lengthOfLIS(nums []int) int {
    if nums == nil || len(nums) == 0 {
        return 0
    }
    //初始化dp为1
    dp := make([]int, len(nums))
    longest := 1

    for i := 0; i < len(nums); i++ {
        dp[i] = 1
        for j := i - 1; j >= 0; j-- {
            if nums[i] > nums[j] {
                dp[i] = int(math.Max(float64(dp[i]), float64(dp[j]+1)))
                if longest < dp[i] {
                    longest = dp[i]
                }
            }
        }
    }
    return longest
}
View Code

乘积最大子序列

题目:

给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。

分析:

数字每乘一次,有五种情况,从正变成负(极大到极小);从负变成正(极小变为极大);变为0(重新开始算);正数还是正数(变大);负数还是负数(变小);所以需要记录下最大值(正数),最小值(负数),如果是0,则重新开始算;所以需要两个dp数组用来记录以a[i]结束的最大值和最小值,防止乘后符号变化。

动态转移方程:

dpMax[i] = max{dp[i-1]*a[i], a[i], dpMin[i-1]*a[i]}

dpMin[i] = min{dp[i-1]*a[i], a[i], dpMin[i-1]*a[i]}

 

if len(nums) ==0 {
        return 0
    }
    dpMax := make([]int, len(nums))
    dpMin := make([]int, len(nums))
    dpMax[0] = nums[0]
    dpMin[0] = nums[0]
    max := nums[0]

    for i:=1; i<len(nums); i++ {
        dpMax[i] = getMax(dpMax[i-1]*nums[i], dpMin[i-1]*nums[i], nums[i])
        dpMin[i] = getMin(dpMax[i-1]*nums[i], dpMin[i-1]*nums[i], nums[i])
        if max < dpMax[i] {
            max = dpMax[i]
        }
    }

    return max
View Code

 

Coin Change (零钱兑换)

 题目:

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

 分析:

dp[n]=k表示总金额为n时,最少需要k枚硬币。

动态转移方程:

n=0时,dp[n]=0

n<0时,dp[n]=-1

n>=1时,dp[n]=dp[n-coin]+1, coin为所有的coin[]中的一个,每一个都要试一试。因此算法复杂度为amount*len(coins)

代码

func coinChange(coins []int, amount int) int {
    if amount == 0 {
        return 0
    }
    if amount <= 0 {
        return -1
    }
    dp := make([]int, amount+1)
    dp[0] = 0
    for i := 1; i <= amount; i++ {
        dp[i] = math.MaxInt32
    }

    for i := 1; i <= amount; i++ {
        for _, coin := range coins {
            if i-coin < 0 || dp[i-coin] == -1 {
                continue
            }
            if dp[i] > dp[i-coin]+1 {
                dp[i] = dp[i-coin] + 1
            }
        }
        if dp[i] == math.MaxInt32 {
            dp[i] = -1
        }
    }
    return dp[amount]
}
View Code 
posted @ 2020-03-14 09:31  guhowo  阅读(385)  评论(0编辑  收藏  举报