lee动态规划 lee5 最长回文子串( 5.22修改 ) lee516 最长回文子序列(可不连续)

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

示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”

暴力解法

在记录最长回文子串的时候,可以只记录“当前子串的起始位置”和“子串长度”,不必做截取。这一步我们放在后面的方法中实现。
说明:暴力解法时间复杂度高,但是思路清晰、编写简单。由于编写正确性的可能性很大,可以使用暴力匹配算法检验我们编写的其它算法是否正确。优化的解法在很多时候,是基于“暴力解法”,以空间换时间得到的,因此思考清楚暴力解法,分析其缺点,很多时候能为我们打开思路。

public class longestPalindrome5 {
    public static void main(String[] args) {
        String s = "babaadadadfcda";
        String res = longestPalindrome(s);
        System.out.println(res);
    }

    private static String longestPalindrome(String s){
        int n = s.length();
        if(n<2) return s;
        int max = 1;
        int start = 0;
        // s.charAt(i) 每次都会检查数组下标越界,因此先转换成字符数组
        char[] num = s.toCharArray();
  // 枚举所有长度大于 1 的子串 charArray[i..j]
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){
                if(j-i+1>max && validHelper(num,i,j)){
                    max = j-i+1;
                    start = i;
                }
            }
        }
        return s.substring(start,start+max);
    }

 //验证子串 s[left..right] 是否为回文串
    private static boolean validHelper(char[] num, int left, int right) {
        while(left<right){
            if(num[left]!=num[right]){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

时间复杂度:O(N^3)

动态规划 思考

第 1 步:定义状态
dp[i][j] 表示子串 s[i, j] 是否为回文子串。

第 2 步:思考状态转移方程
这一步在做分类讨论(根据头尾字符是否相等),根据上面的分析得到:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
分析这个状态转移方程:
(1)“动态规划”事实上是在填一张二维表格,i 和 j 的关系是 i <= j ,因此,只需要填这张表的上半部分;
(2)看到 dp[i + 1][j - 1] 就得考虑边界情况。
边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。
这个结论很显然:当子串 s[i, j] 的长度等于 2 或者等于 3 的时候,我其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。

如果子串 s[i + 1, j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,当然是回文;
如果子串 s[i + 1, j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。

(这一段看晕的朋友,直接看代码吧。我写晕了,车轱辘话来回说。)

第 3 步:考虑初始化
初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 1,即 dp[i][i] = 1 。

事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i] 根本不会被其它状态值所参考。

第 4 步:考虑输出
只要一得到 dp[i][j] = true,就记录子串的长度和起始位置,没有必要截取,因为截取字符串也要消耗性能,记录此时的回文子串的“起始位置”和“回文长度”即可。

第 5 步:考虑状态是否可以压缩
因为在填表的过程中,只参考了左下方的数值。事实上可以压缩,但会增加一些判断语句,增加代码编写和理解的难度,丢失可读性。在这里不做状态压缩。

下面是编码的时候要注意的事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果。

思路是:

1、在子串右边界 j 逐渐扩大的过程中,枚举左边界可能出现的位置;

2、左边界枚举的时候可以从小到大,也可以从大到小。

这两版代码的差别仅在内层循环,希望大家能够自己动手,画一下表格,思考为什么这两种代码都是可行的,相信会对“动态规划”作为一种“表格法”有一个更好的理解。

class Solution {
    public String longestPalindrome(String s) {
        int n=s.length();
        if(s==null) return null;
        if(n<2) return s;//预先判断

        boolean[][] dp=new boolean[n][n];
        for(int i=0;i<n;i++){// base case
            dp[i][i]=true;
        }
        int max=1; //记录最长长度 
        int start=0;//记录最长长度开始位置
        for(int j=1;j<n;j++){ // 注意循环开始情况 为j 里套着 i
            for(int i=0;i<j;i++){
                if(s.charAt(i)==s.charAt(j)){
                    if(j-i<3) {//不用再判断了 剩一个或者不剩都算回文
                        dp[i][j]=true;
                        
                    }
                    else{
                        dp[i][j]=dp[i+1][j-1];
                    }
                }else{
                    dp[i][j]=false;
                }

                if(dp[i][j]){//不substring  只是记录就可
                    int cur=j-i+1;
                    if(cur>max){
                        max=cur;
                        start=i;
                    }
                }

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

中心扩散法

暴力法采用双指针两边夹,验证是否是回文子串。

除了枚举字符串的左右边界以外,比较容易想到的是枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。

因此中心扩散法的思路是:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。

枚举“中心位置”时间复杂度为 O(N)O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为 O(N)O(N),因此时间复杂度可以降到 O(N^2)O(N
2
)。

在这里要注意一个细节:回文串在长度为奇数和偶数的时候,“回文中心”的形式是不一样的。

奇数回文串的“中心”是一个具体的字符,例如:回文串 “aba” 的中心是字符 “b”;
偶数回文串的“中心”是位于中间的两个字符的“空隙”,例如:回文串串 “abba” 的中心是两个 “b” 中间的那个“空隙”。

们可以设计一个方法,兼容以上两种情况:

1、如果传入重合的索引编码,进行中心扩散,此时得到的回文子串的长度是奇数;

2、如果传入相邻的索引编码,进行中心扩散,此时得到的回文子串的长度是偶数。

具体编码细节在以下的代码的注释中体现。

class Solution {
    public String longestPalindrome(String s) {
        int n=s.length();
        if(n<2) return s;
        int max=1;//最少一个字母
        String res=s.substring(0,1);//先设第一个字母为res
        for(int i=0;i<n;i++){// 遍历一遍数
            String oddstr=centerSpread(s, i, i);
            String evenstr=centerSpread(s,i,i+1);
            String maxstr=(oddstr.length()>evenstr.length()) ?oddstr:evenstr;
            if(maxstr.length()>max){
                max=maxstr.length();
                res=maxstr;
            }
        }
        return res;
    }
    public String centerSpread(String s, int i,int j){
        while(i>=0 && j<s.length()){
            if(s.charAt(i)== s.charAt(j)){
                i--;
                j++;
            }
            else{
                break;
            }
        }
        return s.substring(i+1,j);
    }
}

5.22 重写三种方法

package dailyLeetcode;

public class longestPalindrome5 {
    public static void main(String[] args) {
        String s = "babaadadadfcda";
   //     String res = longestPalindrome(s);// 暴力
   //     String res = longestPalindrome_2(s);// 动态规划
        String res = longestPalindrome_3(s);// 中心扩散
        System.out.println(res);
    }

    private static String longestPalindrome_3(String s) {
        int n = s.length();
        if(n<2) return s;
        int max = 1;
        String res = s.substring(0,1);
        for(int i=0;i<n;i++){
            String oddStr = centerHelper(s,i,i);
            String evenStr = centerHelper(s,i,i+1);
            String maxStr = (oddStr.length()>evenStr.length())?oddStr:evenStr;

            if(maxStr.length()>max){
                max = maxStr.length();
                res = maxStr;
            }
        }
        return res;
    }

    private static String centerHelper(String s, int i, int j) {
        while(i>=0 && j<s.length()){
            if(s.charAt(i) == s.charAt(j)){
                i--;
                j++;
            }
            else{
                break;
            }
        }
        return s.substring(i+1,j);
    }

    private static String longestPalindrome_2(String s) {
        int n = s.length();
        if(n<2) return s;
        int max = 1;
        int start = 0;
        boolean[][] dp = new boolean[n][n];
        char[] str = s.toCharArray();
        for(int i=0;i<n;i++){
            dp[i][i] = true;
        }
        for(int j = 1;j<n;j++){
            for(int i=0;i<j;i++){// 注意了 这里取得事i到j的范围
                if(str[i]!=str[j]){
                    dp[i][j] = false;
                }
                else {// 如果相等的时候 不能写两个else if
                    if(j-i<3)
                    {
                        dp[i][j] = true;
                    }else{
                        dp[i][j] = dp[i+1][j-1];
                    }
                }
                if(dp[i][j] && j-i+1>max ){
                    max = j-i+1;
                    start = i;
                }
            }
        }
        return s.substring(start,start+max);
    }

    private static String longestPalindrome(String s){
        int n = s.length();
        if(n<2) return s;
        int max = 1;
        int start = 0;
        char[] num = s.toCharArray();
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){
                if(j-i+1>max && validHelper(num,i,j)){
                    max = j-i+1;
                    start = i;
                }
            }
        }
        return s.substring(start,start+max);
    }

    private static boolean validHelper(char[] num, int left, int right) {
        while(left<right){
            if(num[left]!=num[right]){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

lee516 最长回文子序列

可不连续,输出长度

class Solution {
    public int longestPalindromeSubseq(String s) {
        int n=s.length();
        if(n<2) return n;
        int[][] dp=new int[n][n];
        for(int i=0;i<n;i++){
            dp[i][i]=1;
        }
        for(int i=n-1;i>=0;i--){//注意方向
            for(int j=i+1;j<n;j++){//注意方向
                if(s.charAt(i)==s.charAt(j)){
                    dp[i][j]=dp[i+1][j-1]+2;//注意这里要+2  求长度
                }
                else{
                    dp[i][j]=Math.max(dp[i+1][j], dp[i][j-1]);
                }
            }
            
        }
        return dp[0][n-1];
    }
}

有个大神方法,二分查找发

添加链接描述

public class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = nums.length;
        if (len <= 1) {
            return len;
        }

        // tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
        int[] tail = new int[len];
        // 遍历第 1 个数,直接放在有序数组 tail 的开头
        tail[0] = nums[0];
        // end 表示有序数组 tail 的最后一个已经赋值元素的索引
        int end = 0;

        for (int i = 1; i < len; i++) {
            // 【逻辑 1】比 tail 数组实际有效的末尾的那个元素还大
            if (nums[i] > tail[end]) {
                // 直接添加在那个元素的后面,所以 end 先加 1
                end++;
                tail[end] = nums[i];
            } else {
                // 使用二分查找法,在有序数组 tail 中
                // 找到第 1 个大于等于 nums[i] 的元素,尝试让那个元素更小
                int left = 0;
                int right = end;
                while (left < right) {
                    // 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
                    // int mid = left + (right - left) / 2;
                    int mid = left + ((right - left) >>> 1);
                    if (tail[mid] < nums[i]) {
                        // 中位数肯定不是要找的数,把它写在分支的前面
                        left = mid + 1;
                    } else {
                        right = mid;
                    }
                }
                // 走到这里是因为 【逻辑 1】 的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素
                // 因此,无需再单独判断
                tail[left] = nums[i];
            }
            // 调试方法
            // printArray(nums[i], tail);
        }
        // 此时 end 是有序数组 tail 最后一个元素的索引
        // 题目要求返回的是长度,因此 +1 后返回
        end++;
        return end;
    }

    // 调试方法,以观察是否运行正确
    private void printArray(int num, int[] tail) {
        System.out.print("当前数字:" + num);
        System.out.print("\t当前 tail 数组:");
        int len = tail.length;
        for (int i = 0; i < len; i++) {
            if (tail[i] == 0) {
                break;
            }
            System.out.print(tail[i] + ", ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] nums = new int[]{3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12};
        Solution solution = new Solution8();
        int lengthOfLIS = solution8.lengthOfLIS(nums);
        System.out.println("最长上升子序列的长度:" + lengthOfLIS);
    }
}


作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/dong-tai-gui-hua-er-fen-cha-zhao-tan-xin-suan-fa-p/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值