LeetCode 之 子序列问题

137 阅读5分钟

「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

300.最长递增子序列

dp[i]的定义:dp[i]表示 i 之前包括i的最长上升子序列的长度

状态转移方程:位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。

if (nums[i] > nums[j]) {
  dp[i] = max(dp[i], dp[j] + 1);
}
const lengthOfLIS = (nums) => {
  let dp = new Array(nums.length).fill(1);
  let result = 1;

  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    result = Math.max(result, dp[i])
  }
  return result
}

334. 递增的三元子序列

dp[i]的定义:dp[i]表示 i 之前包括i的最长上升子序列的长度

状态转移方程:位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。

if (nums[i] > nums[j]) {
  dp[i] = max(dp[i], dp[j] + 1);
}
const increasingTriplet = (nums) => {
  let dp = new Array(nums.length).fill(1);
  let result = 1;

  if (Array.from(new Set(nums)).length === 2) {
    return false
  }

  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    result = Math.max(result, dp[i])
    if (result >= 3) {
      return true
    }
  }
  return false
}

354. 俄罗斯套娃信封问题

dp[i]的定义:dp[i]表示 i 之前包括 i 的最长上升子序列的长度

状态转移方程:位置i的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。

if (envelopes[i][0] > envelopes[j][0] && envelopes[i][1] > envelopes[j][1]) {
  dp[i] = Math.max(dp[i], dp[j] + 1)
}
const maxEnvelopes = (envelopes) => {
  let result = 1;
  let dp = new Array(envelopes.length).fill(1);
  envelopes.sort((a, b) => a[0] - b[0])

  for (let i = 1; i < envelopes.length; i++) {
    for (let j = 0; j < i; j++) {
      if (envelopes[i][0] > envelopes[j][0] && envelopes[i][1] > envelopes[j][1]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    result = Math.max(result, dp[i]);
  }

  return result;
};

646. 最长数对链

dp[i]的定义:dp[i]表示 i 之前包括 i 的最长上升子序列的长度

状态转移方程:位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

if (pairs[i][0] > pairs[j][1]) {
  dp[i] = max(dp[i], dp[j] + 1);
}
const findLongestChain = (pairs) => {
  let dp = new Array(pairs.length).fill(1);
  let result = 1;
  pairs.sort((a, b) => a[0] - b[0])

  for (let i = 1; i < pairs.length; i++) {
    for (let j = 0; j < i; j++) {
      if (pairs[i][0] > pairs[j][1]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    result = Math.max(result, dp[i])
  }
  return result
}

674. 最长连续递增序列

dp[i]的定义:dp[i]表示以下标i为结尾的数组的连续递增的子序列长度。

状态转移方程:如果 nums[i + 1] > nums[i],那么以 i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。

if (nums[i + 1] > nums[i]) {
  dp[i + 1] = dp[i] + 1;
}
const findLengthOfLCIS = (nums) => {
  let dp = Array(nums.length).fill(1);

  for (let i = 0; i < nums.length - 1; i++) {
    if (nums[i + 1] > nums[i]) {
      dp[i + 1] = dp[i] + 1;
    }
  }

  return Math.max(...dp);
};

718. 最长重复子数组

dp[i][j]的定义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]

状态转移方程:根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。即当A[i - 1]B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

if (A[i - 1] === B[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1] + 1
}
const findLength = (A, B) => {
  const [m, n] = [A.length, B.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  let res = 0;

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (A[i - 1] === B[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1
      }
      res = Math.max(dp[i][j], res)
    }
  }
  return res;
}

673. 最长递增子序列的个数

const findNumberOfLIS = function (nums) {
  let n = nums.length, maxLen = 0, ans = 0;
  const dp = new Array(n).fill(0);
  const cnt = new Array(n).fill(0);
  for (let i = 0; i < n; ++i) {
    dp[i] = 1;
    cnt[i] = 1;
    for (let j = 0; j < i; ++j) {
      if (nums[i] > nums[j]) {
        if (dp[j] + 1 > dp[i]) {
          dp[i] = dp[j] + 1;
          cnt[i] = cnt[j]; // 重置计数
        } else if (dp[j] + 1 === dp[i]) {
          cnt[i] += cnt[j];
        }
      }
    }
    if (dp[i] > maxLen) {
      maxLen = dp[i];
      ans = cnt[i]; // 重置计数
    } else if (dp[i] === maxLen) {
      ans += cnt[i];
    }
  }
  return ans;
};

1143.最长公共子序列

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] 不相同

  • 如果 text1[i - 1]text2[j - 1] 相同,那么找到了一个公共元素,所以 dp[i][j] = dp[i - 1][j - 1] + 1

  • 如果 text1[i - 1]text2[j - 1] 不相同,那就看看 text1[0, i - 2]text2[0, j - 1] 的最长公共子序列和 text1[0, i - 1]text2[0, j - 2] 的最长公共子序列,取最大的。

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])
}
const longestCommonSubsequence = (text1, text2) => {
  const [m, n] = [text1.length, text2.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; 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])
      }
    }
  }
  return dp[m][n];
}

1035. 不相交的线

const maxUncrossedLines = (nums1, nums2) => {
  const [m, n] = [nums1.length, nums2.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (nums1[i - 1] === nums2[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])
      }
    }
  }
  return dp[m][n];
}

392. 判断子序列

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

状态转移方程:在确定递推公式的时候,首先要考虑如下两种操作:

  • s[i - 1] == t[j - 1]
    • t中找到了一个字符在s中也出现了,那么 dp[i][j] = dp[i - 1][j - 1] + 1,因为找到了一个相同的字符,相同子序列长度自然要在 dp[i-1][j-1] 的基础上加1
  • s[i - 1] != t[j - 1]
    • 相当于t要删除元素,继续匹配。此时相当于t要删除元素,t如果把当前元素 t[j - 1] 删除,那么 dp[i][j] 的数值就是 看 s[i - 1]t[j - 2] 的比较结果了,即:dp[i][j] = dp[i][j - 1]
if (s[i - 1] === t[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1] + 1
} else {
  dp[i][j] = dp[i][j - 1]
}
const isSubsequence = (s, t) => {
  const [m, n] = [s.length, t.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; 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[m][n] === m ? true : false;
}

115. 不同的子序列

dp[i][j]的定义:以 i-1 为结尾的s子序列中出现以 j-1 为结尾的t的个数为dp[i][j]

状态转移方程:

  • s[i - 1]t[j - 1] 相等:dp[i][j]可以有两部分组成。

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

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

    • 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]

if (s[i - 1] === t[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
} else {
  dp[i][j] = dp[i - 1][j]
}
const numDistinct = (s, t) => {
  const [m, n] = [s.length, t.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 0; i <= m; i++) {
    dp[i][0] = 1;
  }

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (s[i - 1] === t[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
      } else {
        dp[i][j] = dp[i - 1][j]
      }
    }
  }
  return dp[m][n]
}

583. 两个字符串的删除操作

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

最后当然是取最小值,所以当 word1[i - 1]word2[j - 1] 不相同的时候

if (word1[i - 1] === word2[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1]
} else {
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)
}
const minDistance = (word1, word2) => {
  const [m, n] = [word1.length, word2.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    dp[i][0] = i;
  }

  for (let j = 1; j <= n; j++) {
    dp[0][j] = j;
  }

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1]
      } else {
        dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2)
      }
    }
  }
  return dp[m][n]
}

72. 编辑距离

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

状态转移方程:

  • word1[i - 1] == word2[j - 1],不操作

  • word1[i - 1] != word2[j - 1]

    • word1 删除一个元素,那么就是以下标 i-2 为结尾的 word1j-1 为结尾的 word2 的最近编辑距离 再加上一个操作。dp[i][j] = dp[i - 1][j] + 1

    • word2 删除一个元素,那么就是以下标 i-1 为结尾的 word1j-2 为结尾的 word2 的最近编辑距离 再加上一个操作。dp[i][j] = dp[i][j - 1] + 1

    • 替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。即 dp[i][j] = dp[i - 1][j - 1] + 1

if (word1[i - 1] === word2[j - 1]) {
  dp[i][j] = dp[i - 1][j - 1]
} else {
  dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)
}
const minDistance = (word1, word2) => {
  const [m, n] = [word1.length, word2.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    dp[i][0] = i;
  }

  for (let j = 1; j <= n; j++) {
    dp[0][j] = j;
  }

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1]
      } else {
        dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)
      }
    }
  }
  return dp[m][n]
}

712. 两个字符串的最小ASCII删除和

const minimumDeleteSum = (word1, word2) => {
  const [m, n] = [word1.length, word2.length];
  const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    dp[i][0] = dp[i - 1][0] + word1[i - 1].charCodeAt();
  }

  for (let j = 1; j <= n; j++) {
    dp[0][j] = dp[0][j - 1] + word2[j - 1].charCodeAt();
  }

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1]
      } else {
        dp[i][j] = Math.min(dp[i - 1][j] + word1[i - 1].charCodeAt(), dp[i][j - 1] + word2[j - 1].charCodeAt(), dp[i - 1][j - 1] + (word1[i - 1].charCodeAt() + word2[j - 1].charCodeAt()))
      }
    }
  }
  return dp[m][n]
}

647. 回文子串

dp[i][j]的定义:表示区间范围[i,j] 的子串是否是回文子串,如果是 dp[i][j]true,否则为 false

状态转移方程:整体上是两种,就是 s[i]s[j] 相等,s[i]s[j] 不相等这两种。

  • 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+1j-1 区间,这个区间是不是回文就看 dp[i + 1][j - 1] 是否为 true
if (s[i] === s[j]) {
  if ((j - i) < 2) {
    dp[i][j] = true;
  } else {
    dp[i][j] = dp[i + 1][j - 1];
  }
  result += dp[i][j] ? 1 : 0;
}
const countSubstrings = (s) => {
  const m = s.length;
  let result = 0;
  const dp = new Array(m).fill(0).map(x => new Array(m).fill(0));

  for (let j = 0; j < m; j++) {
    for (let i = 0; i <= j; i++) {
      if (s[i] === s[j]) {
        if ((j - i) < 2) {
          dp[i][j] = true;
        } else {
          dp[i][j] = dp[i + 1][j - 1];
        }
        result += dp[i][j] ? 1 : 0;
      }
    }
  }

  return result;
}

516. 最长回文子序列

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] 看看哪一个可以组成最长的回文子序列。

    • 加入 s[j] 的回文子序列长度为 dp[i + 1][j]

    • 加入 s[i] 的回文子序列长度为 dp[i][j - 1]

那么 dp[i][j] 一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])

if (s[i] === s[j]) {
  dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
  dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1])
}
const longestPalindromeSubseq = (s) => {
  const m = s.length;
  const dp = new Array(m).fill(0).map(x => new Array(m).fill(0));

  for (let i = 0; i < m; i++) {
    dp[i][i] = 1;
  }

  for (let i = m - 1; i >= 0; i++) {
    for (let j = i + 1; j < m; j++) {
      if (s[i] === s[j]) {
        dp[i][j] = dp[i + 1][j - 1] + 2;
      } else {
        dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1])
      }
    }
  }

  return dp[0][m - 1];
}

5. 最长回文子串

const longestPalindrome = (s) => {
  let m = s.length;
  let result = '';
  const dp = new Array(m).fill(0).map(x => new Array(m).fill(0));

  for (let i = m - 1; i >= 0; i--) {
    for (let j = i; j < m; j++) {
      if (s[i] === s[j]) {
        if (j - i < 2 || dp[i + 1][j - 1]) {
          dp[i][j] = true
        }
      }
      if (dp[i][j] && j - i + 1 > result.length) {
        result = s.substring(i, j + 1);
      }
    }
  }
  return result;
};

53.最大子数组和

dp[i][j]的定义:包括下标i之前的最大连续子序列和为 dp[i]

状态转移方程:dp[i] 只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i] 加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和

一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i])

dp[i] = Math.max(dp[i - 1] + nums[i], nums[i])
const maxSubArray = (nums) => {
  let dp = new Array(nums.length).fill(0);
  dp[0] = nums[0];
  let result = nums[0];

  for (let i = 1; i < nums.length; i++) {
    dp[i] = Math.max(dp[i - 1] + nums[i], nums[i])
    result = Math.max(result, dp[i])
  }
  return result;
}

152. 乘积最大子数组

function maxProduct(nums) {
  if (nums.length == 0) return 0;
  let dpMax = [...nums], dpMin = [...nums];
  for (let i = 1; i < nums.length; i++) {
    // 比较i*最大值,i*最小值
    dpMax[i] = Math.max(dpMax[i - 1] * nums[i], Math.max(nums[i], dpMin[i - 1] * nums[i]));
    // 求最小值
    dpMin[i] = Math.min(dpMin[i - 1] * nums[i], Math.min(nums[i], dpMax[i - 1] * nums[i]));
  }
  let ans = dpMax[0];
  for (let i = 1; i < dpMax.length; i++) {
    ans = Math.max(ans, dpMax[i]);
  }
  return ans;
};

978. 最长湍流子数组

dp[i][j]的定义:i 位置结尾的最长连续子数组的长度

状态转移方程:

  • 定义 up[i] 表示以位置 i 结尾的,并且 arr[i - 1] < arr[i] 的最长湍流子数组长度。
  • 定义 down[i]表示以位置 i 结尾的,并且 arr[i - 1] > arr[i] 的最长湍流子数组长度。

up[i]down[i] 初始化都是 1,因为每个数字本身都是一个最小的湍流子数组。

up[i] = down[i - 1] + 1,当 arr[i - 1] < arr[i];
down[i] = up[i - 1] + 1,当 arr[i - 1] > arr[i];
const maxTurbulenceSize = (nums) => {
  let up = new Array(nums.length).fill(1);
  let down = new Array(nums.length).fill(1);

  let result = 1;

  for (let i = 1; i < nums.length; i++) {
    if (nums[i - 1] < nums[i]) {
      up[i] = down[i - 1] + 1
      result = Math.max(result, up[i])
    } else if (nums[i - 1] > nums[i]) {
      down[i] = up[i - 1] + 1
      result = Math.max(result, down[i])
    }
  }
  return result;
};

跟大家推荐代码随想录,真的非常好

参考文章:

LeetCode

代码随想录