516. 最长回文子序列(Python)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了516. 最长回文子序列(Python)相关的知识,希望对你有一定的参考价值。

参考技术A 难度:★★★☆☆
类型:字符串
方法:动态规划

力扣链接请移步 本题传送门
更多力扣中等题的解决方案请移步 力扣中等题目录

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

示例 1:

输入:

"bbbab"
输出:

4
一个可能的最长回文子序列为 "bbbb"。

示例 2:

输入:

"cbbd"
输出:

2
一个可能的最长回文子序列为 "bb"。

提示:

1 <= s.length <= 1000
s 只包含小写英文字母

这道题使用动态规划来做。

定义:设输入字符串s的长度为n,设dp矩阵为nxn维,dp[i][j]表示以下标i开始,以下标j结尾的子串中最长回文子序列的长度。i<=j
初始化:设置dp矩阵为对角阵,即对角线处的所有元素为1,其他元素填充为0,因为当i=j时,选中的子串只包含一个字符。
递推公式:分两种情况,
第一,从下标i+1到j-1的串左右两头均各增加一个字符,如果新增的两个字符一致,也就是s[i]==s[j],那么将从下标i+1到j-1的串对应的最长回文子序列的长度加2即可,也就是dp[i][j]=dp[i+1]dp[j-1]+2
第二,如果新增的两个字符不等,也就是s[i]!=s[j],那么选取dp[i+1][j]和dp[i][j-1]中更大的数作为dp[i][j]即可。
这里需要注意的是,为了保证每次递推都可以取到该有的值,外层遍历应该逆序,内层遍历应该顺序进行。
返回值:最终返回dp[0][n-1]即可。

如有疑问或建议,欢迎评论区留言~

有关更多力扣中等题的python解决方案,请移步 力扣中等题解析

算法 ---- 子序列系列问题题解(子序列编辑距离回文系列问题)

子序列题解问题

子序列(不连续)

300. 最长递增子序列

/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i]: 表示包含下标i在内的最长递增子序列
 *  2. 确定递推公式
 *     位置i的最长上升子序列等于j从0到i-1各个位置的最长上升子序列+1的最大值
 *     if(nums[i] > nums[j])
 *        dp[i] = Math.max(dp[i], dp[j]+1)
 *  3. 初始化
 *     每一个dp[i]至少都是1
 *  4. 确定遍历顺序
 *     i是从前往后遍历
 *     j是从0开始到i-1遍历
 *  5. 举例推导dp数组
 *     [0, 1, 0, 3, 2]
 *  i=1  1  2  1  1  1
 *  i=2  1  2  1  1  1
 *  i=3  1  2  1  3  1
 *  i=4  1  2  1  3  3
 *
 *  时间: O(n^2)
 *  空间: O(n)
 */
public int lengthOfLIS(int[] nums) 
   int[] dp = new int[nums.length];
   Arrays.fill(dp, 1);
   int res = 1;
   for (int i = 1; i < nums.length; i++) 
      for (int j = 0; j < i; j++) 
         if(nums[i] > nums[j])
            dp[i] = Math.max(dp[i], dp[j]+1);
      
      // 取长的子序列
      res = Math.max(res, dp[i]);
   
   return res;

/**
 *  思路: 贪心
 *  
 *  二分插入法
 *
 *  1. 对原序列进行遍历,将每位元素二分插入dp数组中
 *     - 如果dp数组中元素都比它小,将它插到最后
 *     - 否则,用它覆盖掉比它大的元素中最小的那个
 *
 *  2. dp数组未必是真实的最长上升子序列,但长度是对的
 *
 *  时间: O(nlogn)
 *  空间: O(n)
 */
public int lengthOfLIS(int[] nums) 
   // dp数组是递增的
   int[] dp = new int[nums.length];
   dp[0] = nums[0];
   // dp数组最后一个元素的下标,即dp中最大元素的下标
   int end = 0;
   for (int i = 1; i < nums.length; i++) 
      int num = nums[i];

      // 当前元素大于dp数组最后一个元素,则直接插入到后面
      if(num > dp[end]) 
         dp[++end] = num;
         continue;
      

      // 二分找到比num元素大的元素中最小的那个
      int left = 0;
      int right = end;
      while(left < right) 
         int mid = left + (right - left) / 2;
         if(dp[mid] < num) 
            left = mid + 1;
          else 
            // 注意这里的right = mid
            right = mid;
         
      
      dp[left] = num;
   

   return end+1;

1143. 最长公共子序列

/**
 *  思路: 动态规划
 *
 *  1. 确定dp数组以及下标含义
 *     dp[i][j]表示s1包含下标i在内的子串和s2包含下标j在内的子串的最长公共子序列
 *  2. 确定递推公式
 *     if(s1[i] == s2[j]) 
 *           dp[i][j] = dp[i-1][j-1] + 1
 *      else 
 *        dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
 *     
 *
 *  3. 初始化
 *     i=0时, dp[0][j] = 0
 *     j=0时, dp[i][0] = 0
 *  4. 遍历顺序
 *     外层遍历s1,内层遍历s2
 *
 *  时间: O(n * m)
 *  空间: O(n * m)
 */
/*public int longestCommonSubsequence(String text1, String text2) 
   int m = text1.length();
   int n = text2.length();
   int[][] dp = new int[m+1][n+1];

   for (int i = 1; i <= m; i++) 
      for (int j = 1; j <= n; j++) 
         if(text1.charAt(i-1) == text2.charAt(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];
*/

// 动态规划 + 空间优化
// 时间: O(n * m)
// 空间: O(n)
public int longestCommonSubsequence(String text1, String text2) 
   int m = text1.length();
   int n = text2.length();
   int[] dp = new int[n+1];
   int leftUp = 0;
   for (int i = 1; i <= m; i++) 
      for (int j = 1; j <= n; j++) 
         int temp = dp[j];
         if(text1.charAt(i-1) == text2.charAt(j-1)) 
            dp[j] = leftUp + 1;
          else 
            dp[j] = Math.max(dp[j], dp[j-1]);
         
         leftUp = temp;
      
      leftUp = 0;
   

   return dp[n];

1035. 不相交的线

/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i][j]表示s1中包含下标i在内的元素和s2中包含下标j在内的元素的最多相同元素
 *  2. 确定递推公式
 *     if(nums[i] == nums[j]) 
 *        dp[i][j] = dp[i-1][j-1] + 1
 *      else 
 *        dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
 *     
 *  3. 初始化
 *     i=0时,dp[0][j] = 0
 *     j=0时,dp[i][0] = 0
 *  4. 遍历顺序
 *     外层先遍历nums1,内层遍历nums2
 *
 *  时间: O(n*m)
 *  空间: O(n*m)
 *
 */
/*public int maxUncrossedLines(int[] nums1, int[] nums2) 
   int n = nums1.length;
   int m = nums2.length;
   int[][] dp = new int[n+1][m+1];

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; 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[n][m];
*/

// 动态规划 + 空间优化
// 时间: O(n*m)
// 空间: O(m)
public int maxUncrossedLines(int[] nums1, int[] nums2) 
   int n = nums1.length;
   int m = nums2.length;
   int[] dp = new int[m+1];
   int leftUp = 0;

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; j++) 
         int temp = dp[j];
         if(nums1[i-1] == nums2[j-1]) 
            dp[j] = leftUp + 1;
          else 
            dp[j] = Math.max(dp[j], dp[j-1]);
         
         leftUp = temp;
      
      leftUp = 0;
   

   return dp[m];

子序列(连续)

674. 最长连续递增序列

贪心

/**
 *  思路: 贪心
 *  1. 遍历数组,统计连续增长的子序列长度
 *  2. 一旦遇到不连续的元素,则重新开始统计
 *
 *  时间: O(n)
 *  空间: O(1)
 */
public int findLengthOfLCIS(int[] nums) 
   int res = 1;
   int lenOfChild = 1;
   for (int i = 1; i < nums.length; i++) 
      if(nums[i] > nums[i-1]) 
         lenOfChild++;
       else 
         // 重新开始统计
         lenOfChild = 1;
      
      res = Math.max(res, lenOfChild);
   
   return res;

动态规划

/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i]表示包含下标i在内的最长连续递增序列
 *  2. 确定递推公式
 *     if(nums[i] == nums[i-1]) 
 *        dp[i] = dp[i-1] + 1;
 *      else 
 *        dp[i] = 1;
 *     
 *  3. 初始化
 *     dp[0] = 1
 *  4. 遍历顺序
 *     从前往后遍历
 *
 *  时间: O(n)
 *  空间: O(n)
 */
/*public int findLengthOfLCIS(int[] nums) 
   int[] dp = new int[nums.length];

   dp[0] = 1;

   int res = 1;

   for (int i = 1; i < nums.length; i++) 
      if(nums[i] > nums[i-1]) 
         dp[i] = dp[i-1] + 1;
       else 
         dp[i] = 1;
      
      res = Math.max(res, dp[i]);
   

   return res;
*/

// 动态规划 + 空间优化
// 时间: O(n)
// 空间: O(1)
public int findLengthOfLCIS(int[] nums) 
   int dp = 1;

   int res = 1;

   for (int i = 1; i < nums.length; i++) 
      if(nums[i] > nums[i-1]) 
         dp = dp + 1;
       else 
         dp = 1;
      
      res = Math.max(res, dp);
   

   return res;

718. 最长重复子数组

/**
 *  思路: 动态规划
 *  1. dp[i][j] : 表示nums1中包含下标i在内的数组元素和nums2中包含下标j在内的数组元素最长重复子数组(连续的)
 *  2. if(nums1[i-1] == nums2[j-1]) 
 *      dp[i][j] = dp[i-1][j-1] + 1;
 *  
 *  3. dp[0][j] = 0, dp[i][0] = 0
 *  4. 外层遍历nums1,内层遍历nums2
 *
 *  时间: O(n*m)
 *  空间: O(n*m)
 */
/*public int findLength(int[] nums1, int[] nums2) 
   int n = nums1.length;
   int m = nums2.length;

   int[][] dp = new int[n+1][m+1];

   int res = 0;

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; j++) 
         if(nums1[i-1] == nums2[j-1]) 
            dp[i][j] = dp[i-1][j-1] + 1;
         

         res = Math.max(res, dp[i][j]);
      
   
   return res;
*/

// 动态规划 + 空间优化
// 时间: O(n*m) 空间: O(m)
// 注意: nums2应该从后往前遍历, 如果从前往后遍历, 会出现覆盖问题
public int findLength(int[] nums1, int[] nums2) 
   int n = nums1.length;
   int m = nums2.length;

   int[] dp = new int[m+1];

   int res = 0;

   for (int i = 1; i <= n; i++) 
      for (int j = m; j > 0; j--) 
         if(nums1[i-1] == nums2[j-1]) 
            dp[j] = dp[j-1] + 1;
          else 
            // 注意这里不相等要等于0
            dp[j] = 0;
         
         res = Math.max(res, dp[j]);
      
   

   return res;

53. 最大子序和

贪心

/**
 *  思路: 贪心
 *  遍历数组,如果出现sumOfChild < 0, 则开始重新计算连续子序和
 *  注意sumOfChild要写在第一步进行判断,这样当出现[-2,-1,-4]时, 每次重新计算连续子序和, 都能把当前元素纳入计算范围
 *
 *  时间: O(n)
 *  空间: O(1)
 */
public int maxSubArray(int[] nums) 
   int sumOfChild = nums[0];

   int res = nums[0];

   for (int i = 1; i < nums.length; i++) 
      if(sumOfChild < 0) 
         sumOfChild = 0;
      
      sumOfChild += nums[i];
      res = Math.max(res, sumOfChild);
   

   return res;

动态规划

/**
 *  思路: 动态规划
 *  1. dp[i]表示包含下标i在内的最大子序和
 *  2. if(dp[i-1] < 0) 
 *         dp[i] = nums[i];
 *      else 
 *         dp[i] = dp[i-1] + nums[i];
 *     
 *  3. 初始化
 *     dp[0] = nums[0]
 *  4. 遍历顺序
 *     从前往后遍历
 *
 *  时间: O(n)
 *  空间: O(n)
 */
/*public int maxSubArray(int[] nums) 
   int[] dp = new int[nums.length];
   dp[0] = nums[0];
   int res = nums[0];

   for (int i = 1; i < nums.length; i++) 
      if(dp[i-1] < 0) 
         dp[i] = nums[i];
       else 
         dp[i] = dp[i-1] + nums[i];
      
      res = Math.max(res, dp[i]);
   

   return res;
*/

// 动态规划 + 空间优化
// 时间: O(n)
// 空间: O(1)
public int maxSubArray(int[] nums) 
   int dp = nums[0];
   int res = nums[0];

   for (int i = 1; i < nums.length; i++) 
      if(dp < 0) 
         dp = nums[i];
       else 
         dp = dp + nums[i];
      
      res = Math.max(res, dp);
   

   return res;

编辑距离

392. 判断子序列

双指针法

/**
 * 思路: 双指针法
 *  1. 指针i指向s, 指向j指向t
 *  2. 不断移动指针j,一直遍历到结束,除非i指针已到达s末尾
 *  3. 若指针i和指针j指向的字符匹配时,则i++
 *
 *  时间: O(n+m)
 *  空间: O(1)
 */
public boolean isSubsequence(String s, String t) 
   int n = s.length();
   int m = t.length();

   int i = 0;
   int j = 0;

   while(i < n && j < m) 
      if(s.charAt(i) == t.charAt(j)) 
         i++;
      
      if(i == s.length()) 
         break;
      
      j++;
   

   return i == n;

动态规划

/**
 *  思路: 动态规划
 *  1. dp[i][j]: 表示s包含下标i在内的子串和t包含下标j在内的子串的相同子序列长度
 *  2. if(s[i] == t[j]) 
 *            dp[i][j] = dp[i-1][j-1] + 1;
 *      else 
 *            // 不匹配时,相当于t要删除元素,继续匹配
 *         dp[i][j] = dp[i][j-1];
 *     
 *  3. dp[0][j] = 0, dp[i][0] = 0
 *  4. 外层遍历s,内层遍历t
 *
 *  时间: O(n * m)
 *  空间: O(n * m)
 */
public boolean isSubsequence(String s, String t) 
   int n = s.length();
   int m = t.length();

   int[][] dp = new int[n+1][m+1];

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; j++) 
         if(s.charAt(i-1) == t.charAt(j-1)) 
            dp[i][j] = dp[i-1][j-1] + 1;
          else 
            dp[i][j] = dp[i][j-1];
         
      
   

   return dp[n][m] == n;

115. 不同的子序列

/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i][j] : 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
 *  2. 确定递推公式
 *     if(s[i] == t[j]) 
 *        dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
 *        // dp[i-1][j-1]表示匹配s[i]和t[j], dp[i-1][j]表示不匹配s[i]和t[j]
 *        // 比如baegg 和 bag, 求dp[5][3]时, 匹配最后一个字符,s可以组成bag, 不匹配最后一个字符,s也可以组成bag
 *      else 
 *        dp[i][j] = dp[i-1][j];
 *     
 *
 *  3. 确定遍历顺序
 *     外层遍历s,内层遍历t
 *
 *  4. 初始化
 *     i=0时, 除了dp[0][0]外, dp[0][j]一定都为0, 因为空字符串s无论如何都无法组成t
 *     j=0时, dp[i][0]一定都为1, 因为字符串s可以删除全部字符,组成空字符串t
 *
 *  时间: O(n*m)
 *  空间: O(n*m)
 */
/*public int numDistinct(String s, String t) 
   int n = s.length();
   int m = t.length();

   int[][] dp = new int[n+1][m+1];

   for (int i = 0; i < n; i++) 
      dp[i][0] = 1;
   

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; j++) 
         if(s.charAt(i-1) == t.charAt(j-1)) 
            dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
          else 
            dp[i][j] = dp[i-1][j];
         
      
   

   return dp[n][m];
*/

// 动态规划 + 空间压缩
// 时间: O(n * m)
// 空间: O(m)
public int numDistinct(String s, String t) 
   int n = s.length();
   int m = t.length();

   int[] dp = new int[m+1];

   dp[0] = 1;
   int leftUp = dp[0];

   for (int i = 1; i <= n; i++) 
      for (int j = 1; j <= m; j++) 
         int temp = dp[j];
         if(s.charAt(i-1) == t.charAt(j-1)) 
            dp[j] = leftUp + dp[j];
         
         leftUp = temp;
      
      leftUp = dp[0];
   

   return dp[m];

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

/**
 *  思路: 动态规划
 *
 *  1. 确定dp数组以及下标含义
 *     dp[i][j]:表示word1[i]和word2[j]达到相等,所需要删除元素的最少次数
 *  2. 确定递推公式
 *     if(word1[i] == word2[j]) 
 *        dp[i][j] = dp[i-1][j-1];
 *      else 
 *        // 分三种情况
 *        // ① 删word1[i],最少操作数 = dp[i-1][j] + 1
 *        // ② 删word2[j],最少操作数 = dp[i][j-1] + 1
 *        // ③ 同时删word1[i]和word2[j],最少操作数 = dp[i-1][j-1] + 2
 *        dp[i][j] = Math.min(Math.min(dp[i-1][j]+1, dp[i][j-1]+1),dp[i-1][j-1]+2);
 *     
 *  3. 初始化
 *     dp[0][j] = j;
 *     dp[i][0] = i;
 *  4. 遍历顺序
 *     先遍历word1,后遍历word2
 *
 *  5. 举例推导dp数组
 *     比如"sea"和"eat"
 *      e a t
 *    0 1 2 3
 *  s 1 2 3 4
 *  e 2 1 2 3
 *  a 3 2 1 2
 *
 *  时间: O(n*m)
 *  空间: O(n*m)
 */
/*public int minDistance(String word1, String word2) 
   int n = word1.length();
   int m = word2.length();

   int[][] dp = new int[n+1][m+1];

   for (int i = 0; i <= n; i++) 
      dp[i][0] = i;
   
   for (int j = 0; j <= m; j++) 
      dp[0][j] = j;
   

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

   return dp[n][m];
*/

// 动态规划 + 空间优化
// 时间: O(n*m)
// 空间: O(m)
public int minDistance(String word1, String word2) 
   int n = word1.length();
   int m = word2.length();

   int[] dp = new int[m+1];

   for (int i = 0; i <= m; i++) 
      dp[i] = i;
   

   int leftUp = dp[0];

   for (int i = 1; i <= n; i++) 
      dp以上是关于516. 最长回文子序列(Python)的主要内容,如果未能解决你的问题,请参考以下文章

516. 最长回文子序列

LeetCode 第516题:最长回文子序列

java 516.最长的回文子序列(#1).java

golang必备算法动态规划 Letecode 516.最长回文子序列

516 Longest Palindromic Subsequence 最长回文子序列

Leetcode 516 最长回文子序列