zoukankan      html  css  js  c++  java
  • 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 
  • 相关阅读:
    【百度搜索研发部】以求医为例谈搜索引擎排序算法的基础原理(转)
    TF-IDF与余弦相似性的应用(三):自动摘要
    TF-IDF与余弦相似性的应用(一):自动提取关键词
    TF-IDF与余弦相似性的应用(二):找出相似文章
    技术向:一文读懂卷积神经网络CNN(转)
    [透析] 卷积神经网络CNN究竟是怎样一步一步工作的?(转)
    像素间的基本关系-距离(转)
    Swift学习笔记-字符串和字符(Strings and Characters)-比较字符串 (Comparing Strings)
    Swift学习笔记-基本运算符(Basic Operators)-空合运算符(Nil Coalescing Operator)
    Swift学习笔记-基本运算符(Basic Operators)-求余运算符
  • 原文地址:https://www.cnblogs.com/howo/p/12490608.html
Copyright © 2011-2022 走看看