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

子序列(不连续)

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[0] = i;
      for (int j = 1; j <= m; j++) {
         int temp = dp[j];
         if(word1.charAt(i-1) == word2.charAt(j-1)) {
            dp[j] = leftUp;
         } else {
            dp[j] = Math.min(Math.min(dp[j]+1, dp[j-1]+1),leftUp+2);
         }
         leftUp = temp;
      }
      leftUp = dp[0];
   }

   return dp[m];
}

72. 编辑距离

/**
 *  思路: 动态规划
 *  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添加一个元素,使word1[i] == word2[j], 即dp[i][j] = dp[i-1][j] + 1
 *              也可以是word2添加一个元素,使word2[j] == word1[i], 即dp[i][j] = dp[i][j-1] + 1
 *      // 删除情况, 添加元素本身就已经包含删除的情况了,比如word2添加一个元素,相当于word1删除一个元素
 *      // 比如 word1="ad", word2="a", word1删除"d",word2添加一个元素"d",最后变成word1="a",word2="ad", 最终的操作数是一样的
 *              dp数组如下所示
 *               a                a  d
    *           0  1              0 1  2
 *         a 1  0    =====>  a 1 0  1
 *         d 2  1
 *
 *         dp[2][1]   ==   dp[1][2]
 *
 *      // 更改情况, wrod1[i]更改成与word2[j]相同的字符, dp[i][j] = dp[i-1][j-1] + 1
 *
 *      综上, dp[i][j] = Math.min(Math.min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+1)
 *     }
 *
 *  3. 初始化
 *     i=0时, dp[0][j] = j;
 *     j=0时, dp[i][0] = i;
 *
 *  4. 遍历顺序
 *     外层遍历word1,内层遍历word2
 *
 *  5. 举例推导dp数组
 *     word1="horse", word2="ros"
 *     r o s
 *    0 1 2 3
 *  h 1 1 2 3
 *  o 2 2 1 2
 *  r 3 2 2 3
 *  s 4 3 3 2
 *  e 5 4 4 3
 *
 *  时间: 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]+1);
         }
      }
   }

   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 j = 0; j <= m; j++) {
      dp[j] = j;
   }

   int leftUp = dp[0];

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

   return dp[m];
}

回文

674. 回文子串(连续)

求回文子串个数

动态规划
/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i][j]: s[i:j]区间范围内是否为回文子串,如果是dp[i][j]为true,如果不是dp[i][j]=false
 *  2. 确定递推公式
 *     if(s[i] != s[j]) {
 *        dp[i][j] = false;
 *     } else {
 *        // s[i] == s[j]时
 *        // 分三种情况
 *        // ①i和j下标相同,即同一个字符,单个字符组成,例如a, 则为true
 *        // ②i和j相差为1,由2个字符组成,且字符要相同,比如aa, 则为true(不需要用到递推公式)
 *        // ③i和j相差大于1的时候,例如cabac,此时s[i]和s[j]首尾字符已经相同, 需要进一步判断s[i+1:j-1]区间是否是回文子串,即判断dp[i+1][j-1]是否是回文子串(状态转移方程)
 *        if(j-i <= 1) {
 *           dp[i][j] = true;
 *        } else {
 *           dp[i][j] = dp[i+1][j-1];
 *        }
 *     }
 *
 *  3. 初始化
 *     一开始都为false
 *  4. 遍历顺序
 *     由递推公式知
 *       |            |dp[i][j]|
 *       |dp[i+1][j-1]|      |
 *    所以矩阵应该是从下往上遍历, 从左向右遍历(保证每个dp[i+1][j-1]都是经过计算的)
 *
 *  5. 举例推导dp数组
 *     "aaa"
 *       0  1  2
 *       a  a  a
 *  0 a  T  T  T
 *  1 a  F  T  T
 *  2 a  F  F  T
 *  统计有6个T即为答案
 *
 *  时间: O(n^2)
 *  空间: O(n^2)
 */
/*public int countSubstrings(String s) {
   if(s == null || s.length() == 0) {
      return 0;
   }
   int len = s.length();
   int res = 0;
   boolean[][] dp = new boolean[len][len];
   for (int i = len-1; i >= 0; i--) {
      for (int j = i; j < len; j++) {
         if(s.charAt(i) != s.charAt(j)) {
            continue;
         } else {
            if(j - i <= 1) {
               dp[i][j] = true;
               res++;
            } else {
               if(dp[i+1][j-1]) {
                  dp[i][j] = true;
                  res++;
               }
            }
         }
      }
   }

   return res;
}*/

// 动态规划 + 空间优化
// 时间: O(n^2) 空间:O(n)
public int countSubstrings(String s) {
   if(s == null || s.length() == 0) {
      return 0;
   }
   int len = s.length();
   int res = 0;
   boolean[] dp = new boolean[len];
   boolean leftBottom = dp[len-1];
   for (int i = len-1; i >= 0; i--) {
      for (int j = i; j < len; j++) {
         boolean temp = dp[j];
         if(s.charAt(i) != s.charAt(j)) {
            dp[j] = false;
         } else {
            if(j - i <= 1) {
               dp[j] = true;
               res++;
            } else {
               if(leftBottom) {
                  dp[j] = true;
                  res++;
               } else {
                  dp[j] = false;
               }
            }
         }
         leftBottom = temp;
      }
      leftBottom = dp[i];
   }

   return res;
}
双指针法(中心扩展)
/**
 *  思路: 双指针法(中心扩展)
 *  1. 在遍历中心节点的时候,往两边扩散看是不是对称即可
 *  2. 中心节点有两种情况:
 *     - 一个元素作为中心节点
 *     - 两个元素作为中心节点
 *     比如ababa
 *     a可以作为中心节点进行扩展,但是这样就无法得到abab,所以同样也要有两个元素作为中心节点
 *     ab作为中心节点,就可以得到abab了
 *
 *  3. 两种情况可以放在一起计算,或者分别计算,这里给出的是分别计算
 *
 *  时间: O(n^2)
 *  空间: O(1)
 *
 */
public int countSubstrings(String s) {
   if(s == null || s.length() == 0) {
      return 0;
   }
   int res = 0;
   for (int i = 0; i < s.length(); i++) {
      // 一个元素作为中心节点
      res += extend(s, i, i);
      // 两个元素作为中心节点
      res += extend(s, i, i+1);
   }
   return res;
}

private int extend(String s, int left, int right) {
   int sum = 0;
   while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
      sum++;
      left--;
      right++;
   }
   return sum;
}

516. 最长回文子串(不连续)

/**
 * 思路: 动态规划
 * 1. 确定dp数组以及下标含义
 *    dp[i][j] : 字符串s[i:j]范围内的最长回文子序列的长度为dp[i][j]
 *
 * 2. 确定递推公式
 *       // 首尾字符相同
 *       if(s[i] == s[j]) {
 *           dp[i][j] = dp[i+1][j-1] + 2;
 *       } else {
 *           //  分两种情况, 说明s[i]和s[j]不能同时加入, 则分别加入s[i]、s[j]看看哪一个可以组成最长回文子序列
 *           // ①加入s[i]的回文子序列长度为: dp[i][j-1]
 *           // ②加入s[j]的回文子序列长度为: dp[i+1][j]
 *           // 比如 cbbd, 计算dp[0][3]时, s[0] != s[3]
 *           // 不加s[0]时,加入s[3], dp[0][3] = dp[1][3] 即bbd
 *           // 不加s[3]时,加入s[0], dp[0][3] = dp[0][2] 即cbb
 *           dp[i][j] = Math.max(dp[i][j-1], dp[i+1][j])
 *       }
 *
 *     3. 初始化
 *        i与j相同时,即指向了同一个字符,此时dp[i][j] = 1, 防止被0覆盖,成为2, 因为1个字符长度只能为1
 *
 *     4. 确定遍历顺序
 *        由递推公式可知, 从下往上, 从左往右遍历
 *
 *  5. 举例推导dp数组
 *  比如 s = "cbbd"
 *
 *    c b b d
 *  c 1 1 2 2
 *  b 0 1 2 2
 *  b 0 0 1 1
 *  d 0 0 0 1
 *
 *  时间: O(n^2)
 *  空间: O(n^2)
 */
/*public int longestPalindromeSubseq(String s) {
   int res = 0;
   int len = s.length();
   int[][] dp = new int[len][len];

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

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

   return dp[0][len-1];
}*/

// 动态规划 + 空间优化
// 时间: O(n^2)
// 空间: O(n)
public int longestPalindromeSubseq(String s) {
   if(s.length() == 1) {
      return 1;
   }

   int res = 0;
   int len = s.length();
   int[] dp = new int[len];

   dp[len-1] = 1;

   int leftBottom = dp[len-2];

   for (int i = len-2; i >= 0; i--) {
      dp[i] = 1;
      for (int j = i+1; j < len; j++) {
         int temp = dp[j];
         if(s.charAt(i) == s.charAt(j)) {
            dp[j] = leftBottom + 2;
         } else {
            dp[j] = Math.max(dp[j-1], dp[j]);
         }
         leftBottom = temp;
      }
      if(i != 0) {
         leftBottom = dp[i-1];
      }
   }

   return dp[len-1];
}

5. 最长回文子串(连续)

求最长的连续回文子串

动态规划
/**
 * 思路: 动态规划
 * 1. dp[i][j]表示s[i:j]区间是否是回文子串
 * 2. 递推公式
 *        if(s[i] != s[j]) {
 *           dp[i][j] = false;
 *        } else {
 *           // 两个字符时,比如aa,不需要用到递推公式
 *           if(j - i <= 1) {
 *              dp[i][j] = true;
 *           } else {
 *               dp[i][j] = dp[i+1][j-1];
 *           }
 *        }
 *
 *     3. 初始化
 *        i == j时, dp[i][j] = false;
 *
 *  4. 遍历顺序
 *     从下往上,从左往右
 *
 *  5. 举例推导dp数组
 *     b a b a d
 *  b  1 0 3 0 0
 *  a  0 1 0 3 0
 *  b  0 0 1 0 0
 *  a  0 0 0 1 0
 *  d  0 0 0 0 1
 *
 *  时间: O(n^2)
 *  空间: O(n^2)
 */
public static String longestPalindrome(String s) {
   int len = s.length();

   if(len < 2) {
      return s;
   }

   boolean[][] dp = new boolean[len][len];

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

   int maxLen = 1;
   int start = 0;

   for (int i = len-2; i >= 0; i--) {
      for (int j = i+1; j < len; j++) {
         if(s.charAt(i) != s.charAt(j)) {
            dp[i][j] = false;
         } else {
            // 两个字符相等时,比如aa,不需要用到递推公式
            if(j - i <= 1) {
               dp[i][j] = true;
            } else {
               dp[i][j] = dp[i+1][j-1];
            }
         }

         if(dp[i][j] && j - i + 1 > maxLen) {
            maxLen = j - i + 1;
            start = i;
         }
      }
   }

   return s.substring(start, start + maxLen);
}
双指针法(中心扩展法)
/**
 *  思路: 双指针法(中心扩展)
 *
 *  1. 在遍历中心节点的时候,往两边扩散看看是不是对称
 *  2. 中心节点有两种情况:
 *     - 一个元素作为中心节点
 *     - 两个元素作为中心节点
 *
 *  3. 两种情况分别计算
 *
 *  时间: O(n^2)
 *  空间: O(1)
 */
public String longestPalindrome(String s) {
   if(s.length() < 2) {
      return s;
   }

   int start = 0;
   int maxLen = 1;

   for (int i = 0; i < s.length(); i++) {
      int len1 = extend(s, i, i);
      int len2 = extend(s, i, i+1);
      int len = Math.max(len1, len2);
      if(len > maxLen) {
         maxLen = len;
         // 如果是cccc, 两个元素作为中心节点开始扩展的话,需要 start - (len -1)/2 , 即1 - (4-1)/2 = 0
         // 如果是ccc, 一个元素作为中心节点开始扩展的话, 也需要 start - (len -1)/2, 即1 - (3-1)/2 = 0;
         start = i - (len -1) / 2;
      }
   }

   return s.substring(start, start + maxLen);
}

private int extend(String s, int left, int right) {
   // 比如 a b a
   // left = 1, right = 1
   // 从b开始扩展, 最后返回长度: left = -1, right = 3, right - left - 1 = 3
   // 再比如 a b b a
   // 从b b两个元素开始扩展, 最后返回长度: left = -1, right = 3, right - left - 1 = 3
   while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
      left--;
      right++;
   }
   return right - left - 1;
}

小结

image-20210617145204148

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值