动态规划---最长子串/子序列问题序列1

动态规划---最长子串/子序列问题序列1

    • 5. 最长回文子串
    • 300. 最长上升子序列

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:
输入: "cbbd"
输出: "bb"

解法1:动态规划

  • 1、当子串只包含 1 个字符,它一定是回文子串;
  • 2、当子串包含 2 个以上字符的时候:如果 s[l, r] 是一个回文串,例如 “abccba”,那么这个回文串两边各往里面收缩一个字符(如果可以的话)的子串 s[l + 1, r - 1] 也一定是回文串,即:如果 dp[l][r] == true 成立,一定有 dp[l + 1][r - 1] = true 成立。
    根据这一点,我们可以知道,给出一个子串 s[l, r] ,如果 s[l] != s[r],那么这个子串就一定不是回文串。如果 s[l] == s[r] 成立,就接着判断 s[l + 1] 与 s[r - 1],这很像中心扩散法的逆方法。
  • 时间和空间都是o(n^2).
  • 强调:
    状态定义: dp[i][j] 表示数组子串 s[i, j] 是否为回文子串。
    状态转移方程: dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))
class Solution {
    public String longestPalindrome(String s) {
        if (s.length() <= 1) {
            return s;
        } 
        int len = s.length();
        int longestStrLength = 1;
        String longestStr = s.substring(0,1);
        //dp[left][right] 表示从left到right是否是回文子串
        boolean[][] dp = new boolean[len][len];
        for (int right=0; right<len; right++) {
            for (int left=0; left<right; left++) {
                //1 如果 dp[l, r] = true 那么 dp[l + 1, r - 1] 也一定为 true,所以反过来递推
                //2 剪枝:如果left到right间的数只有一个或者没有,比如left=1> right=2(中间没有数)或者1>3(中间只有一个数),则不用判断,中间的数肯定是回文的
                //3 left到right间的数:至少有2个才有判断:dp[left+1][right-1]的必要。
                if (s.charAt(left) == s.charAt(right) && (right-left <=2 || dp[left+1][right-1])) {
                    dp[left][right] = true;
                    if (right - left + 1 > longestStrLength) {
                        longestStrLength = right - left + 1;
                        // 因为substring右开的,所以别忘了+1.
                        longestStr = s.substring(left, right +1);
                    }
                }
            }
        }
        return longestStr;
    }
}

解法2:

  • 回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n+n-1 个中心。。这里的n指的是字符串的长度为n,而 n-1是字符之间的*。所以共有2n-1个中心。
  • 时间复杂度:O(n²)。
  • 空间复杂度:O(1)。
class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() == 0) {
            return "";
        }
        int start = 0,end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expandAroudCenter(s,i,i); // s长度是奇数
            int len2 = expandAroudCenter(s,i,i+1); // s长度是偶数
            int len = Math.max(len1,len2);
            if (len > end - start) {
                start = i - (len-1) / 2;
                end = i + len / 2;
            }
        }
         //substring(int beginIndex, int endIndex):左闭右开
//返回一个新字符串,它是此字符串的一个子字符串。该子字符串从指定的 beginIndex 处开始, endIndex:到指定的 endIndex-1处结束。所以下面end+1.
        return s.substring(start, end+1);  
    }

    private int expandAroudCenter(String s, int left, int right) {
        int L = left, R = right;
        while (L >=0 && R <s.length() && s.charAt(L) == s.charAt(R)) {
//注意这里是以中心往两边扩散,所以左--,右++。
            L--;
            R++;
        }
        return R-L-1;
    }
}

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4

解法1 : 动态规划

  • 默认每次会选定nums[i] ,这样通过j的枚举来确定在nums[i]之前最长的子序列,最后别忘了+1(把nums[i]带上),这样确定状态转移方程:dp[i] = Math.max(dp[i], dp[j] + 1); 同时状态定义的数组可以确定为是一个一维数组。
  • 时间复杂度0(n*n),空间为O(n);
  • dp[i] 表示以 nums[i] 结尾的「上升子序列」的长度。注意:这个定义中 nums[i] 必须被选取,且必须是这个子序列的最后一个元素。
class Solution {
    public int lengthOfLIS(int[] nums) {
        //1 边界值处理
        if (nums.length == 0 || nums == null) {
            return 0;
        }
        //2 当nums不为空是,返回的最长子序列长度最少为1
        int res = 1;
        		
		//3  初始化一个全是1的数组,这样这个最长子序列长度也是1.也是边界处理
        int[] dp = new int[nums.length];
        for (int i =0; i< nums.length; i++) {
            dp[i] = 1;
        }
       
		//4 默认每次会选定nums[i] ,这样通过j的枚举来确定在nums[i]之前最长的子序列,最后别忘了+1(把nums[i]带上),可以理解为每次求以nums[i]结尾的最长子序列。
        for (int i=1; i < nums.length; i++) {
            for (int j=0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);//这里dp[j]每次都为1
                }
            }
			// 在外围大范围内确定最终值
            res = Math.max(res,dp[i]);
        }
        return res;
    }
}

解法2:二分解法(推荐)

  • 时间复杂度:O(NlogN),遍历nums列表需: O(N),在每个nums[i]二分法需O(logN)。
  • 空间复杂度:O(N),tails 列表占用线性大小额外空间。
class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length <= 1) {
            return nums.length;
        }
        int len = nums.length;
        //维护一个tail数组,该数组是递增的,可以进行二分,长度是len+1
        int[] tail = new int[len];
        tail[0] = nums[0];
        //end表示有序数组tail最后一个已经赋值元素的索引
        int end = 0;

        for (int i=1; i<len; i++) {
            //比tail数组最大的元素还大,直接加入tail数组
            if (nums[i] > tail[end]) {
                end++;
                tail[end] = nums[i];
            } else { 
                // 否则在数组tail中二分查找第1个大于等于nums[i]的那个元素,尝试让那个元素更小
                int left = 0;
                int right = end;
                while (left < right) {
                    // 使用左中位数
                    int mid = left + ((right - left) >>> 1);
                    if (tail[mid] < nums[i]) {
                        //mid更小,则赋值mid+1
                        left = mid + 1;
                    } else {
                        right = mid;
                    }
                }
                //一定能找到第 1 个大于等于 nums[i] 的元素
                tail[left] = nums[i];
            }
        }
        //end 是有序数组 tail 最后一个元素的索引,所以加1返回
        end++;
        return end;
    }
}

在这里插入图片描述

你可能感兴趣的:(数据结构和算法)