算法-动态规划-子序列等问题

133 阅读2分钟

最长递增子序列

  • 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序

  • if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

  • 注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值

  • 双指针 i遍历s j遍历0-i

  • dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度

func lengthOfLIS(nums []int) int {
    //双指针+动态规划
    //dp[i]表示i下标的位置,最长递增子串长度
    //用j去遍历0-i-1区间,如果满足 nums[i]>nums[j] 那么dp更新
    dp := make([]int,len(nums))
    for i:=0;i<len(nums);i++{
        dp[i] = 1
    }
    res := 0
    for i:=0;i<len(nums);i++{
        for j:=0;j<i;j++{
            if nums[i]>nums[j]{
                dp[i] = max(dp[i],dp[j]+1)
            }
        }
        res = max(res,dp[i])
    }
    return res
	
}

最长连续递增序列

  • 连续

  • dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]

  • 因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i]

func findLengthOfLCIS(nums []int) int {
    //dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。
    dp := make([]int,len(nums))
    for i:=0;i<len(nums);i++{
        dp[i] = 1
    }
    res := 0
    for i:=0;i<len(nums)-1;i++{
        if nums[i]<nums[i+1]{
            dp[i+1] = dp[i]+1
        }    
    }

    for i:=0;i<len(nums);i++{
        res = max(res,dp[i])

    }
    return res
}

最长重复子数组

  • 子数组,其实就是连续子序列

  • dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串

  • 即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

  • dp[i][0] 和dp[0][j]初始化为0。

func findLength(nums1 []int, nums2 []int) int {
    //dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
    //地推公式 即当A[i - 1]B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
    m,n := len(nums1),len(nums2)
    res := 0
    dp := make([][]int,m+1)
    for i:=0;i<=m;i++{
        dp[i] = make([]int,n+1)
    }

    for i:=1;i<=m;i++{
        for j:=1;j<=n;j++{
            if nums1[i-1]==nums2[j-1]{
                dp[i][j] = dp[i-1][j-1]+1
            }
             res = max(res,dp[i][j])
        }
    }

    return res 
}

最长公共子序列

  • 区别在于这里不要求是连续的了

  • dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]

  • text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同

image.png

func longestCommonSubsequence(text1 string, text2 string) int {
    dp:=make([][]int,len(text1)+1)
    for i:=0;i<len(dp);i++{
        dp[i] = make([]int,len(text2)+1)
    }
    res := 0
    for i:=1;i<=len(text1);i++{
        for j:=1;j<=len(text2);j++{
            if text1[i-1] == text2[j-1]{
                dp[i][j] = dp[i-1][j-1] + 1
            }else{
                dp[i][j] = max(dp[i-1][j],dp[i][j-1])
            }
            res = max(res,dp[i][j])
        }
    }

    return res

}

不相交的线

  • 直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
func maxUncrossedLines(nums1 []int, nums2 []int) int {
  //本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!
  dp := make([][]int,len(nums1)+1)
  for i:=0;i<=len(nums1);i++{
      dp[i] = make([]int,len(nums2)+1)
  }
    res := 0
  for i:=1;i<=len(nums1);i++{
      for j:=1;j<=len(nums2);j++{
          if nums1[i-1]==nums2[j-1]{
              dp[i][j] = dp[i-1][j-1] + 1
          }else{
              dp[i][j] = max(dp[i][j-1],dp[i-1][j])
          }
          res = max(res,dp[i][j])
      }
    }

    return res
}

最大子序和

  • 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
贪心
func maxSubArray(nums []int) int {
    maxSum := nums[0]
    for i := 1; i < len(nums); i++ {
        if nums[i] + nums[i-1] > nums[i] {
            nums[i] += nums[i-1]
        }
        if nums[i] > maxSum {
            maxSum = nums[i]
        }
    }
    return maxSum
}
  • dp[i]:包括下标i之前的最大连续子序列和为dp[i]

  • dp[i]只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和

  • nums[i],即:从头开始计算当前连续子序列和

func maxSubArray(nums []int) int {
    if len(nums) == 1{
        return nums[0]
    }
    //dp[i]表示子数组的和
    dp := make([]int,len(nums))
    dp[0] = nums[0]
    res := nums[0]
    for i:=1;i<len(dp);i++{
        dp[i] = max(dp[i-1]+nums[i],nums[i])
        res =max(res,dp[i])
    }
    return res
}

判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

  • dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

  • if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;

  • if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];

  • 就是说不相等的时候要去取t的上一个状态的值

  • 最后 dp == len(s)说明相同子序列的长度等于s的长度,说明他就是t的子序列

func isSubsequence(s string, t string) bool {
    //dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
	dp := make([][]int,len(s)+1)
	for i:=0;i<=len(s);i++ {
		dp[i] = make([]int,len(t)+1)
	}
    for i:=1;i<len(dp);i++{
        for j:=1;j<len(dp[i]);j++{
            if s[i-1] == t[j-1]{
                dp[i][j] = dp[i-1][j-1]+1
            }else{
                dp[i][j] = dp[i][j-1]
            }
        }
    }

	return dp[len(s)][len(t)] == len(s)
}

不同子序列

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 s大 t小

  • 这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。

  • 本题相当于只有删除操作,

    • s[i - 1] 与 t[j - 1]相等
  • s[i - 1] 与 t[j - 1] 不相等

当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。

  • 一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。

  • 一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。

  • 就是说你相等了 你可以选择的机会就多了,可以选择相等时候的状态,把相等的删了,也可把s的最新的删了, dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

  • 当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]。。,。 所以递推公式为:dp[i][j] = dp[i - 1][j];

两个字符串的删除操作

  • 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

  • 和上一题相比,其实就是两个字符串都可以删除,整体思路不变

  • dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。

  • 当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];

  • 当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:

    • 情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1

    • 情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1

    • 情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2

  • dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});

func minDistance1(word1 string, word2 string) int {
    //dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
    dp := make([][]int,len(word1)+1)
    for i:=0;i<len(dp);i++{
        dp[i] = make([]int,len(word2)+1)
    } 
    //初始化
    for i:=0;i<len(dp);i++{
        dp[i][0] = i
    }
    for j:=0;j<len(dp[0]);j++{
        dp[0][j] = j
    }
//当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];
//当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:
//情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1
//情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
//情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
//那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});

    for i:=1;i<len(dp);i++{
        for j:=1;j<len(dp[0]);j++{
            if word1[i-1] == word2[j-1]{
                dp[i][j] = dp[i-1][j-1]
            }else{
                dp[i][j] = min(dp[i-1][j]+1,min(dp[i][j-1]+1,dp[i-1][j-1]+2))
            }
        }
    }
    return dp[len(word1)][len(word2)]


}

编辑距离

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

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

  • dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增
    删
    换
  • dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
  • 替换 dp[i - 1][j - 1] + 1 理解成i-2 和 j-1都相等了,下面做一次替换就行了
func minDistance(word1,word2 string)int{
    m,n:=len(word1),len(word2)
    dp := make([][]int,len(word1)+1)
    for i:=0;i<len(dp);i++{
        dp[i] = make([]int,len(word2)+1)
    }

    for i:=0;i<len(dp);i++{
        dp[i][0] = i
    }
    for i:=0;i<len(dp[0]);i++{
        dp[0][i] = i
    }

    for i:=1;i<len(dp);i++{
        for j:=1;j<len(dp[i]);j++{
            if word1[i-1] == word2[j-1]{
                dp[i][j] = dp[i-1][j-1]
            }else{
                dp[i][j] = Min(dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1)
            }
        }
    }
    return dp[m][n]
}

回文子串---连续

  • 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

  • 当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。

  • 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况

    • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
    • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
    • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  • 递归顺序看dp公式,大for反序小for正序

func countSubstrings(s string)int{
    ans := 0
    dp := make([][]bool,len(s))
    for i:=0;i<len(dp);i++{
        dp[i] = make([]bool,len(s))
    }
    for i:=len(s)-1;i>=0;i--{
        for j:=i;j<len(s);j++{
            if s[i] != s[j]{
                dp[i][j] = false
            }else{
                if j-i<=2{
                    dp[i][j] = true    
                }else if j-i > 2 && dp[i+1][j-1]{
                    dp[i][j] = true
                }
            }
            if dp[i][j]{
                ans++
            }
        }
    }
    return ans
}

最长回文序列---可以不连续

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。

  • dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
  • 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
  • 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
  • dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
func countSubstrings(s string)int{
    ans := 0
    dp := make([][]bool,len(s))

    for i:=0;i<len(dp);i++{
        dp[i] = make([]bool,len(s))
    }

    for i:=len(s)-1;i>=0;i--{
        for j:=i;j<len(s);j++{
            if s[i] != s[j]{
                dp[i][j] = false
            }else{
                if j-i<=2{
                    dp[i][j] = true    
                }else if j-i > 2 && dp[i+1][j-1]{
                    dp[i][j] = true
                }
            }
            if dp[i][j]{
                ans++
            }

        }
    }

    return ans
}

20210127151521432.jpg