算法连载(第六期)

236 阅读3分钟

hello大家好我是django,今天给大家带来的算法解析有《最长公共子序列》、《买卖股票的最佳时机》、《使用最小花费爬楼梯》、《翻转二叉树》、《回文子串》

最长公共子序列(动态规划)

  • 要求

最长公共子序列,英文为Longest Common Subsequence,缩写LCS,给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 例如,"ace" "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。 示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
  • 思路

本题采用动态规划进行解题,动态规划三要素:

最优子结构

假设有Xm<x1,x2,x3,xm>Yn<y1,y2,y3,yn>长度为mn的两个字符串,即我们需要计算得到的是LCS(m,n)的最长公共子序列。

  1. Xm == Yn时,则此时XmYn是最长子序列的最后一个字符,即有LCS(m,n) = LCS(m-1,n-1) + 1
  2. Xm != Yn时,有两种情况需要考虑。假设Xm-1 == Yn则最长子序列就在Xm-1Yn这个区间内查找,公式为LCS(m,n)=LCS(m-1,n) 。假设Xm == Yn-1则最长子序列就在XmYn-1这个区间内查找,公式为LCS(m,n)=LCS(m,n-1)

以上成功将原问题分成了子问题,并且子问题的最优解最终组成了原问题的最优解,所以符合最优子结构的性质

重叠子问题

根据最优子结构的分解,我们得到了三个公式:

LCS(m,n)=LCS(m-1,n-1) + 1
LCS(m,n)=LCS(m-1,n)
LCS(m,n)=LCS(m,n-1)

LCS(m-1,n)不是最后最大子序列的最后一项时,公式会继续分解为LCS(m-1,n)=LCS(m-2,n)LCS(m-1,n)=LCS(m-1,n-1)这里就会跟LCS(m,n)=LCS(m-1,n-1) + 1重叠。

无后效性

即已经得到的子问题不会受到后续的子问题的影响,反过来说就是后续的子问题的结果是根据之前的子问题的结构而确定的,而不管之前的值是如何得到的。

  • 实现
var longestCommonSubsequence = function (text1, text2) {
  const [m, n] = [text1.length, text2.length];
	// 创建动态规划二维图
  const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
  let maxCount = 0;

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      // 当i 和 j 为最长公共子序列的最后的字符时
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
			
      // 保存最唱子序列
      maxCount = Math.max(maxCount, dp[i][j]);
    }
  }
  return maxCount;
};

买卖股票的最佳时机

  • 要求

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

1 <= prices.length <= 105
0 <= prices[i] <= 104
  • 思路

假设我们是在股票最低点买入的,并且计算每一天获得的利润。记录最便宜的价格,记录最大收益

  • 实现
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    let miniPrice = Number.MAX_SAFE_INTEGER;
    let mProfit = 0;
    for(let i = 0; i < prices.length; i++) {
        if(miniPrice > prices[i]) {
            miniPrice = prices[i];
        } else if(mProfit < prices[i] - miniPrice) {
            mProfit = prices[i] - miniPrice;
        }
    }
    return mProfit
};

使用最小花费爬楼梯(动态规划)

  • 要求

数组的每个下标作为一个阶梯,第i 个阶梯对应着一个非负数的体力花费值cost[i](下标从0 开始)。 每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 01 的元素作为初始阶梯。

示例 1:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

示例 2:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

提示:

cost 的长度范围是 [2, 1000]。
cost[i] 将会是一个整型数据,范围为 [0, 999]
  • 思路

首先我们创建一个保留第i层需要消耗体力的数组dp[i], 可以从i = 0或者i = 1开始爬楼梯,即当i >= 2时dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

  • 实现
/**
 * @param {number[]} cost
 * @return {number}
 */
var minCostClimbingStairs = function(cost) {
    const n = cost.length;
    const dp = new Array(n + 1);
    dp[0] = dp[1] = 0;
    for (let i = 2; i <= n; i++) {
        dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
    }
    return dp[n];
};

翻转二叉树

  • 要求 翻转一棵二叉树。

示例:

输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1
  • 思路

递归遍历由下往上深度遍历,将节点的左节点和右节点进行交换

  • 实现
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    if(!root) return root
    if(root.left)invertTree(root.left)
    if(root.right) invertTree(root.right);
    const temp = root.left;
    root.left = root.right;
    root.right = temp;
    return root;
};

回文子串(动态规划)

  • 要求

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
  • 思路

使用动态规划进行解题,即有dp[i][j] = dp[i+1][j-1] && s[i] == s[j]

  • 实现
/**
 * @param {string} s
 * @return {number}
 */
var countSubstrings = function(s) {
    const m = s.length;
    const dp = new Array(m).fill(false).map(() => new Array(m).fill(false));
    let ansc = 0;
    for(let i = m - 1; i >= 0; i--) {
        for(let j = i; j < m; j++) {
            dp[i][j] = (j - i <= 1 || dp[i + 1][j - 1]) && s[i] === s[j];
            if(dp[i][j]) {
                ansc ++;
            }
        }
    }
    return ansc
};