(LeetCode)动态规划问题学习笔记

由易到难涉及的几个例题:

  • LeetCode 198. House Robber 打家劫舍(Easy)
  • LeetCode 1143. Longest Common Subsequence 最长公共子序列(Medium)
  • LeetCode 72. Edit Distance 编辑距离(Hard)
    最后两个为二维动态规划问题

动态规划的解题方法:

  • 定义子问题:子问题是和原问题差不多的问题,但是并不完全一样,且规模较小。通常原问题可能是一个数组,在这个数组内需要进行某种有规律的运算,那定义到子问题可能就是将数组中的相邻几个元素或者将某一元素前面的所有元素拿出来作为一个新的元素,这个新元素可能是类似元组的一样的感觉,依次排列下去就又可以组成一个新的子问题数组。
  • 写出子问题的递推关系:子问题的递推关系往往是反直觉的,因此在代码中很难直观的看出有什么逻辑。通常都是整理出一个公式,然后对dp数组中的所有子问题通过这个公式进行递归或者遍历子集。(重要,这一步也是很好的锻炼逻辑思维,帮助拒绝直觉性的思维)
  • 确定 DP 数组的计算顺序:dp数组即子问题数组,计算方法包括自顶向下的递归和自底向上的循环方法。
  • 空间优化(可选):类似于斐波那契数列的递推,每次只需要3个变量就可以了,这样节省了存所有子问题数组的空间,下一个子问题可以通过上一个子问题得出。

**

打家劫舍:

**

  1. 定义子问题:
    -原问题:从全部房子中能偷到的最大金额,假设有n间房子
    -子问题:从k间房子中能偷到的最大金额,且k<=n,结果为f(k),当k等于n时结果就是原问题想要的
    -分析: 从一间房开始,当k每增加1,f(k+1)必然携带着f(k)有关的信息,不同的自变量带到同一个函数里肯定会有重合的部分,类似于自相关这样(瞎猜的)

  2. 子问题的递推关系:
    -寻找不同的自变量之间的关系:打家劫舍这个问题很明显,因为不能偷相邻的房子,所以偷前k间房子的时候1⃣️偷前k-1间房最后一间不偷2⃣️偷前k-2间房子加最后一间,然后再从这两种方案中选最大的那个。此时得出来的f(k)就能作为结果,去找f(k+1)的最大结果,规律就在这里诞生了。
    -公式: f ( k ) = { f ( k − 2 ) + H K − 1 f ( k − 1 ) \\f(k) = \lbrace ^{\\f(k-1)} _{\\f(k-2)+H_{K-1}} f(k)={f(k2)+HK1f(k1)

  3. 确定dp数组的计算顺序:该问题直接循环就可以解决

  4. 空间优化:我们不需要将dp数组里面每个子问题求出的结果都又放回dp数组中去,只需要当前k的最大值,k-1的结果和k-2的结果就可以了,当k=n的时候就返回结果。

  5. code:

func max_num(x int, y int) int{
    if x>y{
        return x
    }else {
        return y
    }
}

func rob(nums []int) int {
     var curr int //k-1
     var prev int  //k-2
     var tem int //k
     for _,v := range nums{
         tem = max_num(curr, prev+v)
         prev = curr
         curr = tem
     }
     return tem
}

最长公共子序列

原题目:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。若这两个字符串没有公共子序列,则返回 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence

1.定义子问题
 text1的前i个字符text1[0,i)和text2的前j个字符[0,j)之间的最长公共子序列的长度,是text1和text2求最长公共子序列的子问题。

2.写出子问题的递推关系
 这里不再贴出图来了,相关图在leetcode上有很多,也不列出公式仅用文字描述下。
 这里的递推关系跟一维其实差不多,还是从两个字符串数组上面着手,通过两个数组下标0进行不断的增长一直到达到两个数组的下标终点为止。
先排除下标为0的base case,当有一个数组下标为0的时候,结果肯定为0,这个应该很显而易见用脚想都知道了。
 从下标1开始,每当指针指到下标i和j时,肯定只会有两种情况:

  • 第一种是这两个下标找到的字符是相同的,那此时对应的dp二维数组中的的值应该比i-1和j-1找到的dp大一,因为此时找到了相同的字符。同时要注意的一点是,dp数组中的下标应该比两个字符串数组下标要领先1位,dp数组中的下标0对应的是两个字符串数组为空的情况,下标1对应两个字符串长度为1的情况。
  • 第二种是两个下标找到的字符不同,那此时就去dp二维数组中找当前i与j对应的子问题的上方和左手方的元素,拿出这两个元素中的最大值作为当前子问题的最大值。这里的直觉含义也很明显,可以想象一个二维方形矩阵,我当前两个字符没比对上,那我肯定继承前面最大的那个结果值(这里的结果值即最长公共子序列的长度,放在函数里看的话可以想象成前面的因变量作为后面的自变量,那此时必然带着前面的信息去作为后面的输入),然后这样一步步走下去,见到一样的就加一个,不一样的就拿前面的最大值(前面说的上方和左方元素),直到走到方形矩阵的右下角,over,拿到最大值。

3.DP 数组的子问题依赖方向是什么
 刚刚在第二点里面的说了,这里的方向就是从左上到右下,不断的把信息传递到右下角找到最大值。然后代码里面还是遍历两个字符串数组。

4.空间优化
 按正常的思路肯定是不断对dp二维数组进行填充,此时的空间复杂度是2次方级别。既然我们是带着某个最大值信息从左上角沿着矩阵一直游到右下角,那直觉的就可以想到这个信息我只要放在某个变量里然后一直携带下去就可以了,不需要把这个信息的所有过程都存着。当然这里时间还是一样的2次方级别改不了,游遍整个二维矩阵必须得花那么多时间。

5.code

func max_num(x int, y int) int {
    if x>y {
        return x
    }else{
        return y
    }
}


func longestCommonSubsequence(text1 string, text2 string) int {
    m := len(text1)
    n := len(text2)
    if m==0||n==0 {
        return 0
    }
    dp := make([]int, n+1)
    for i:=1;i<=m;i++{
        tem := 0
        for j:=1;j<=n;j++{
            var dp_j int
            if text2[j-1]==text1[i-1]{
                dp_j = tem + 1
            }else{
                dp_j = max_num(dp[j], dp[j-1])
            }
            tem = dp[j]
            dp[j] = dp_j
        }
    }
    return dp[n]
     
}

编辑距离

原题目:给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:插入一个字符;删除一个字符;替换一个字符
https://leetcode-cn.com/problems/edit-distance/

1.定义子问题
 这里与LCS的规划类似,给的也是两个字符串数组,因此很自然的将原问题为w1与w2的编辑距离问题转换为w1的子字符串与w2的子字符串的编辑距离子问题

2.写出子问题的递推关系
 推荐大佬的看图说话解释,很好的描述了编辑距离dp数组的内在逻辑关系https://leetcode-cn.com/problems/edit-distance/solution/edit-distance-by-ikaruga/
个人理解:这里的子问题递推与lcs差不多是相反的。编剧距离dp数组从左上角到右下角的值是通过看在原来的两个字符串数组中单个字符不相等来递增的,而lcs是相等则增加1。

3.依赖方向
 自底向上遍历,放在二维数组中看则是从左上到右下。

4.空间优化
。。。。

5.code

func num_min_two(x int, y int) int {
    if x>y{
        return y
    }else{
        return x
    }
}

func num_min_three(x int, y int, z int) int {
    return num_min_two(num_min_two(x, y), z)
}

func minDistance(word1 string, word2 string) int {
     m := len(word1)
     n := len(word2)
     var dp [][]int
     for x := 0; x <= m; x++ {  //循环为一维长度
         arr := make([]int, n+1) //创建一个一维切片
		 dp = append(dp, arr)    //把一维切片,当作一个整体传入二位切片中
	 }
     for i:=0;i<m+1;i++{
         for j:=0;j<n+1;j++{
             if i==0{
                 dp[i][j] = j
             }else if j==0{
                 dp[i][j] = i
             }else{
                 if word1[i-1]==word2[j-1]{
                     dp[i][j] = dp[i-1][j-1]  //两个字符相等则不用增加编辑距离
                 }else{
                     dp[i][j] = num_min_three(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])+1//两个字符不相等则在二维数组矩阵中找左上、左、上三个元素的最小值+1
                 }
             }
         }
     }

     return dp[m][n]
}

你可能感兴趣的:(算法,leetcode,数据结构)