LeetCode5

版权声明:有些文章写的很随意,那是仅作为个人记录的文章,建议直接关掉,多看一秒亏一秒 https://blog.csdn.net/qq_36523667/article/details/85236927

题目

最长回文子串

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

审题

分aba、abba两种形式,不能考虑不周全

暴力思路

确定任意2点,检查回文

n^3

动态规划

官方的思路是,如果a[i],a[j]相等,那么如果a[i+1]到a[j-1]是回文子串,那么a[i]到a[j]也是回文子串了。

如果a[i],a[j]不相等,那么a[i]到a[j]不论如何都不是回文子串。

用这个方法去辅助暴力法的回文子串的判断。

记录下底层的dp[i][j]是否回文帮助高层进行判断。

我的思路是在遍历到新的一位的时候,前面的回文有多种可能,遍历前面的所有可能,得出包含新的一位的所有新的可能。实际上还是和官方的思路一样。

找到回文的中间线

aba

abba

都是有中间线的。所以遍历每一位,以每一位为基点向两边延伸。

暴力代码

class Solution {
    public String longestPalindrome(String k) {
        if(k.equals("")) {
            return k;
        }
        
        char[] chars = k.toCharArray();
        int len = chars.length;
        int max = 0,s = 0,e = 0;
        for(int i = 0; i < len; i ++) {
            for(int j = i + 1; j < len; j ++) {
                if(checkPali(i, j, chars)) {
                    if(j - i + 1 > max) {
                        max = j - i + 1;
                        s = i;
                        e = j;
                    }
                }
            }
        }
        StringBuilder sb = new StringBuilder();
        for(int i = s; i <= e; i ++) {
            sb.append(chars[i]);
        }
        return sb.toString();
    }
    
    private boolean checkPali(int s, int e, char[] chars) {
        for(int i = 0; i < (e - s + 1)/2; i ++) {
            if(chars[s + i] != chars[e - i]) {
                return false;
            }
        }
        return true;
    }
}

跑出来的结果是1212ms,惊了

官方动态规划代码

class Solution {
    public String longestPalindrome(String k) {
        if(k.equals("")) {
            return k;
        }
        
        //下面的都是非空非""的,数组至少有1位
        
        char[] chars = k.toCharArray();
        int len = chars.length;
        int max = 0;
        int bestS = 0;
        int bestE = 0;
        boolean[][] dp = new boolean[len][len];//i:开始,j:结束
        for(int j = 0; j < len; j ++) {//定死右点
            for(int i = j - 1; i >= 0; i --) {//遍历左点 这里正反来皆可
                if(chars[i] == chars[j]) {
                    if(i + 1 == j || i + 2 == j) {
                        dp[i][j] = true;
                    } else {
                       if(dp[i+1][j-1]) {
                           dp[i][j] = true;
                       } else {
                           dp[i][j] = false;
                       }
                    }
                    if(dp[i][j] && (j-i+1) > max) {
                        max = j - i + 1;
                        bestS = i;
                        bestE = j;
                    }
                } else {
                    dp[i][j] = false;
                }
            }
        }
        
        StringBuilder sb = new StringBuilder();
        for(int i = bestS; i <= bestE; i ++) {
            sb.append(chars[i]);
        }
        
        return sb.toString();
    }
}

动态规划原理分析

官方的:i->j。只有a[i]==a[j]且dp[i+1][j-1] = true,才会回文。

我的:向右遍历到i,对于回文串的右端定在i-1的进行判断,看是否另一端相等,如果是,在这个级别上增加回文。

我和官方的区别在于,我的是找到上个级别的所有回文,以及当前级别的数,再去找到首端的数进行比较;官方的是先判断首尾端的数相等不相等,如果相等,再看内部是否回文。

中间线

分aba、abba两种形式处理

class Solution {
    public static String longestPalindrome(String k) {
        if(k.equals("")) {
            return k;
        }

        //下面的都是非空非""的,数组至少有1位

        char[] chars = k.toCharArray();
        int len = chars.length;
        int max = 0;
        int bestS = 0;
        int bestE = 0;
        for(int i = 1; i < len - 1; i ++) {//aba形式
            //只管遍历,到头了为止。中间线在i的位置。
            int s = i-1;
            int e = i+1;
            while(s >= 0 && e <= len - 1) {
                if(chars[s] != chars[e]) {
                    break;
                }
                s--;
                e++;
            }
            //从while出来,要么不相等,要么越界,需要校准
            s++;
            e--;
            int l = e-s+1;
            if(l > max) {
                max = l;
                bestS = s;
                bestE = e;
            }
        }
        for(int i = 0; i < len - 1; i ++) {//abba形式
            //i分割了i i+1
            int s = i;
            int e = i+1;
            while(s >= 0 && e <= len - 1) {
                if(chars[s] != chars[e]) {
                    break;
                }
                s--;
                e++;
            }
            //从while出来,要么不相等,要么越界,需要校准
            s++;
            e--;
            int l = e-s+1;
            if(l > max) {
                max = l;
                bestS = s;
                bestE = e;
            }
        }
        
        StringBuilder sb = new StringBuilder();
        for(int i = bestS; i <= bestE; i ++) {
            sb.append(chars[i]);
        }
        
        return sb.toString();
    }
}

写完之后才发现这两种处理可以merge成一种处理的。有点尴尬。是我一开始没有想完善。奇偶也通常都可以merge的。

时间复杂度比动态规划优化了30%,这是可以理解的。官方动态规划是从2点出发,每两个点之间是否回文都被验证了一遍,可以说官方动态规划是稳定的n^2。而中间线思路,从中间线开始延伸,遇到不相等的就不继续了,是不稳定的,但是肯定是不足n^2的。最坏是所有的数全相等,这才是n^2。

翻转思路

这是官方的思路。把A翻转,得到B,再计算他们的最长公共子串就可以了。

比如"caba"-->"abac",得出答案aba。

但是"abcdkxdcba"-->"abcdxkdcba",得出答案abcd,显然这不是回文。

为何会有这样的误区呢?对于回文的字符aba,翻转后必然也是aba。但是对于不能回文的,也有些字符可以浑水摸鱼。浑水摸鱼的原因是回文串翻转后还是和自己比,这里的情况是翻转后和实现准备好的翻转副本比较。

解决方法很简单,把得到的结果,最后再check一次是否回文即可。

思路是很好的,有时候数组翻转可以解决很多问题!!!

这样一来问题就变成了怎么求最长公共子串了,这又是一个难题,最朴素的实现是,遍历a数组,每一位都去b数组中找相同的,找到相同的时候继续比。然后a数组继续遍历。。。复杂度大概是n^3,用上hashmap存储index,可以降到n^2,a数组当前遍历的值,直接去存储b数组的hashmap里找这个数的index,然后继续遍历。

还有个思路是偏移某个数组,进行直接映射比较,也是n^2。

也可以用动态规划,这样空间复杂度会从1变到n^2。a[i],b[j]分别是a中的某个字符,b中的某个字符。dp[i][j]是在a中以i结尾,b中以j结尾的最长串,那么dp[i+1][j+1]就可以因此递推。时间上的优化是因为dp保存了之前比较的结果,因此快了,这也是动态规划的原理。

空间复杂度用滚动数组可以优化到n。

详细见https://www.cnblogs.com/ider/p/longest-common-substring-problem-optimization.html

Manacher

学习自https://blog.csdn.net/ggggiqnypgjg/article/details/6645824

这个算法挺容易理解的。算法基于中心算法。

mx之前求得的回文串的右端最远距离,id则是这个回文串的中心。

当前要求的是i的半径。i'是ii关于id的映射。可以看到i'的半径和i的半径息息相关。

假如i'的半径没有超出mx'的界限,那么毫无疑问,i的半径和i'的半径是一样的。

如果i'的半径超出了mx'的界限,那么,i的半径就是mx-i?

当然不是。i的半径暂时是mx-i了。这种越界的情况,需要继续到mx后面去正儿八经的匹配。

上面的这种做法就是避免了不必要的判断,充分地利用了回文的特性去剪枝。

所以得出了这行代码

全部代码如下:

噢,对了,为了同化奇数和偶数的处理,这个算法还给原字符加上了乱七八糟的字符。这里我就不这么做了,还是分开处理他们。

public static String longestPalindrome(String k) {
    if(k.equals("")) {
        return k;
    }

    int max = 0;
    int bestS = 0;
    int bestE = 0;

    char[] chars = k.toCharArray();
    int len = chars.length;
    int id = 0;
    int mx = 0;
    int[] cache = new int[len];
    //aba
    for(int i = 1; i < len - 1; i ++) {//两端就不遍历了,没有必要
        //剪枝
        if(mx > i && i > id) {//i处在mx和id中间
            int j = 2 * id - i;//j一定是合法的,不可能小于0
            cache[i] = Math.min(cache[j], mx - i);
        } else {
            cache[i] = 0;
        }
        //遍历
        int s = i-cache[i]-1;
        int e = i+cache[i]+1;
        while(s >= 0 && e < len && chars[s] == chars[e]) {
            s--;
            e++;
            cache[i]++;
        }
        s++;//越界或不等,复位
        e--;
        //记录mx、id
        if(e > mx) {
            mx = e;
            id = i;
        }
        if(cache[i] > max) {
            max = cache[i];
            bestS = s;
            bestE = e;
        }
    }

    //abba
    int[] cache2 = new int[len];
    int id2 = 0;
    int mx2 = 0;
    for(int i = 0; i < len - 1; i ++) {
        //i分割i i+1
        if(mx2 > i && i > id2) {
            int j = 2*id2-i;
            if(j >= 1) {
                cache2[i] = Math.min(cache2[j], mx2-i);
            }
        } else {
            cache2[i] = 0;
        }
        //遍历
        int s = i-cache2[i];
        int e = i+cache2[i]+1;
        while(s >= 0 && e < len && chars[s] == chars[e]) {
            s--;
            e++;
            cache2[i]++;
        }
        s++;//复位
        e--;
        //记录mx、id
        if(e > mx2) {
            mx2 = e;
            id2 = i;
        }
        if (cache2[i] > max) {
            max = cache2[i];
            bestS = s;
            bestE = e;
        }
    }

    StringBuilder sb = new StringBuilder();
    for (int i = bestS; i <= bestE; i ++) {
        sb.append(chars[i]);
    }

    return sb.toString();
}

代码很渣。

为什么是N

因为i如果存在mx里,就会被找到映射,映射的最优也就是i的最优。i继续向下遍历,同时也会刷新mx。所以时间复杂度是跟你这mx走的,显然是n。

收获

从官方的动态规划有收获

1.思路:和往常的往右遍历,分析出转移不一样。它是考虑了两端,没有被数组限制。

2.做法:由于和常规dp的思路不一样,所以代码也不一样。往常是正常遍历,显然在这里行不通,这里是高层需要借助底层的东西,可以用递归,这里是定死右界,产生底层的数,给高层用的。

马拉车算法也很厉害,充分利用回文的特性剪枝。

猜你喜欢

转载自blog.csdn.net/qq_36523667/article/details/85236927