【leetcode-DP】最长回文子串/有效括号/最短回文串/分割回文串

最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。

示例 2:
输入:s = “cbbd”
输出:“bb”

示例 3:
输入:s = “a”
输出:“a”

示例 4:
输入:s = “ac”
输出:“a”

暴力匹配

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length();
        int max = 0;
        int start = 0;
        for (int i = 0; i < length; ++i) {
            for (int j = 0; j < length - i; ++j) {
                if (s.charAt(i) == s.charAt(length - j - 1)) {
                    String sub = s.substring(i, length - j);
                    int num = sub.length();
                    if (num <= max) {
                        break;
                    }
					if (isPalindrome(sub)) {
						if (num > max) {
							start = i;
                        	max = num;
						}
						break;
					}
                }
            }
        }
        return s.substring(start, start + max);
    }

    private boolean isPalindrome(String s) {
        String reverse = new StringBuffer(s).reverse().toString();
        if (s.equals(reverse)) {
            return true;
        }
        return false;
    }
}

扩展中心法

找一个或两个相同字符作为中心,向两边检测是否为相同字符
在这里插入图片描述

class Solution {
    public String longestPalindrome(String s) {
        int start = 0, max = 0;
        int length = s.length();
        for (int i = 0; i < length; i++) {
            int len1 = expandAroundCenter(s, i, i, length);
            int len2 = expandAroundCenter(s, i, i + 1, length);
            int len = Math.max(len1, len2);
            if (len > max) {
                start = i - (len - 1) / 2;
                max = len;
            }
        }
        return s.substring(start, start + max);
    }

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

动态规范法

在这里插入图片描述

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length();
        int start = 0, max = 0;
        boolean[][] dp = new boolean[length][length];
        for (int i = length - 1; i >= 0; i--) {
            for (int j = i; j < length; j++) {
                dp[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || dp[i + 1][j - 1]);
                if (dp[i][j] &&  j - i + 1 > max) {
                    start = i;
                    max = j + 1 - i;
                }
            }
        }
        return s.substring(start, start + max);
    }
}

最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:
输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”

示例 2:
输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”

示例 3:
输入:s = “”
输出:0

贪心法

从左到右和从右到左两次遍历

class Solution {
    public int longestValidParentheses(String s) {
        int len = s.length();
        int max = 0;
        int left = 0, right = 0;
        for (int i = 0; i < len; i++) {
            if (s.charAt(i) == '(')
                left++;
            if (s.charAt(i) == ')')
                right++;

            if (left == right)
                max = Math.max(max, left + right);
            if (left < right) {
                left = right = 0;
            }
        }

        left = right = 0;
        for (int i = len - 1; i >= 0; i--) {
            if (s.charAt(i) == '(')
                left++;
            if (s.charAt(i) == ')')
                right++;

            if (left == right)
                max = Math.max(max, left + right);
            if (left > right) {
                left = right = 0;
            }
        }

        return max;
    }
}

中心扩展法

找到一组“()”作为中心,向两侧扩展窗口,碰到上一个窗口则两窗口合并
在这里插入图片描述

class Solution {
    public int longestValidParentheses(String s) {
        int len = s.length();
        int max = 0;
        int left = 0, right = 0;
        Map<Integer, Integer> map = new HashMap<>();
        while (left >= 0 && right < len) {
            left = s.indexOf("()", right);
            right = left + 1;
            if (left < 0 || right >= len)
                break;

            int[] li = expand(s, left, right);
            left = li[0];
            right = li[1];

            while (map.containsKey(left - 1)) {
                li = expand(s, map.get(left - 1), right);
                left = li[0];
                right = li[1];
            }
            map.put(right, left);
            max = Math.max(max, right - left + 1);
        }

        return max;
    }

    private int[] expand(String s, int left, int right) {
        while (left > 0 && right < s.length() - 1 && s.charAt(left - 1) == '(' && s.charAt(right + 1) == ')') {
            left--;
            right++;
        }
        return new int[]{left, right};
    }
}

动态规划

定义 d p [ i ] dp[i] dp[i] 表示以下标 ii 字符结尾的最长有效括号的长度。

  1. s[i]=‘)’ 且 s[i - 1] = ‘(’,也就是字符串形如 “……()”,我们可以推出:dp[i]=dp[i−2]+2。
    我们可以进行这样的转移,是因为结束部分的 “()” 是一个有效子字符串,并且将之前有效子字符串的长度增加了 2 。
  2. s[i]=‘)’ 且 s[i−1]=‘)’,也就是字符串形如 “……))”,我们可以推出:如果 s[ i−dp[i−1]−1 ]=‘(’,那么 dp[i] = dp[i−1] + dp[ i−dp[i−1]−2 ]+2
class Solution {
    public int longestValidParentheses(String s) {
        int len = s.length();
        int max = 0;
        int[] dp = new int[len];
        for (int i = 1; i < len; i++) {
            if (s.charAt(i) == ')') {
                if (s.charAt(i - 1) == '(') {
                    dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
                } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
                    dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
                max = Math.max(max, dp[i]);
            }
        }
        return max;
    }
}

辅助栈法

用一个数组 valid 来记录匹配括号对的位置,在 valid 中找到连续最长的 1 序列。

class Solution {
    public int longestValidParentheses(String s) {
        int len = s.length();
        boolean[] valid = new boolean[len];
        
        LinkedList<Integer> stack = new LinkedList<>();
        for (int i = 0; i < len; i++) {
            if (s.charAt(i) == '(') {
                stack.push(i);
            } else {
                if (!stack.isEmpty()) {
                    valid[i] = true;
                    valid[stack.pop()] = true;
                }
            }
        }

        int max = 0, count = 0;
        for (int i = 0; i < len; i++) {
            count = valid[i] ? count + 1 : 0;
            max = Math.max(count, max);
        }

        return max;
    }
}

最短回文串

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。

示例 1:
输入:s = “aacecaaa”
输出:“aaacecaaa”

示例 2:
输入:s = “abcd”
输出:“dcbabcd”

暴力匹配

class Solution {
    public String shortestPalindrome(String s) {
        String r = new StringBuilder(s).reverse().toString();
        int len = s.length();
        for (int i = 0; i < len; i++) {
            if (s.substring(0, len - i).equals(r.substring(i))) {
                StringBuilder result = new StringBuilder(s.substring(len - i)).reverse();
                return result.append(s).toString();
            }
        }
        return new StringBuilder(s).reverse().append(s).toString();
    }
}

中心扩展法

class Solution {
    public String shortestPalindrome(String s) {
        int len = s.length();
        int start = 0;
        for (int i = (len - 1) / 2; i >= 0; i--) {
            int right1 = expandAroundCenter(s, i, i, len);
            int right2 = expandAroundCenter(s, i, i + 1, len);
            if (right1 != -1 || right2 != -1) {
                start = Math.max(right1, right2);
                break;
            }
        }
        StringBuilder sb = new StringBuilder(s);
        for (int i = start; i < len; i++)
            sb.insert(0, s.charAt(i));
        return sb.toString();
    }

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

双指针递归法

用两个指针查询从头开始的最长回文串。只要 j 进入了最长回文子串,一定会使得 i 走出最长回文子串,因此用递归法不断在字符串开头加入前缀,直至找到从头开始的回文串。

class Solution {
    public String shortestPalindrome(String s) {
        int i = 0;
        char[] charArray = s.toCharArray();
        for (int j = s.length() - 1; j >= 0; j--) {
            if (charArray[i] == charArray[j])
                i++;
        }
        if (i == s.length())
            return s;
        
        String suffix = s.substring(i);
        String reverse = new StringBuilder(suffix).reverse().toString();
        return reverse + shortestPalindrome(s.substring(0, i)) + suffix;
    }
}

字符串哈希

一个字符串是回文串,当且仅当该字符串与它的反序相同。因此,我们仍然暴力地枚举 s 1 s_1 s1的结束位置,并计算 s 1 s_1 s1 s 1 s_1 s1反序的哈希值。如果这两个哈希值相等,说明我们找到了一个 s s s 的前缀回文串。在枚举 s 1 s_1 s1的结束位置时,我们可以从小到大地进行枚举,这样就可以很容易地维护 s 1 s_1 s1 s 1 s_1 s1反序的哈希值:
设当前枚举到的结束位置为 i,对应的 s 1 s_1 s1 记为 s 1 i s_1^i s1i,其反序记为 s ^ 1 i \hat{s}_1^i s^1i。我们可以通过递推的方式,在 O(1) 的时间通过 s 1 i − 1 s_1^{i-1} s1i1 s ^ 1 i − 1 \hat{s}_1^{i-1} s^1i1的哈希值得到 s 1 i s_1^i s1i
s ^ 1 i \hat{s}_1^i s^1i的哈希值:
hash ( s 1 i ) = hash ( s 1 i − 1 ) × base + ASCII ( s [ i ] ) \text{hash}(s_1^i) = \text{hash}(s_1^{i-1}) \times \textit{base} + \text{ASCII}(s[i]) hash(s1i)=hash(s1i1)×base+ASCII(s[i])
hash ( s ^ 1 i ) = hash ( s ^ 1 i − 1 ) + ASCII ( s [ i ] ) × base i \text{hash}(\hat{s}_1^i) = \text{hash}(\hat{s}_1^{i-1}) + \text{ASCII}(s[i]) \times \text{base}^i hash(s^1i)=hash(s^1i1)+ASCII(s[i])×basei
即, hash ( s 1 i ) = base 0 × ASCII ( s [ 0 ] ) + base 1 × ASCII ( s [ 1 ] ) + base 2 × ASCII ( s [ 2 ] ) + . . . \text{hash}(s_1^i) = \text{base}^0 \times \text{ASCII}(s[0]) + \text{base}^1 \times \text{ASCII}(s[1]) + \text{base}^2 \times \text{ASCII}(s[2]) +... hash(s1i)=base0×ASCII(s[0])+base1×ASCII(s[1])+base2×ASCII(s[2])+...
hash ( s ^ 1 i ) = base i × ASCII ( s [ 0 ] ) + base i − 1 × ASCII ( s [ 1 ] ) + base i − 2 × ASCII ( s [ 2 ] ) + . . . \text{hash}(\hat{s}_1^i) = \text{base}^i \times \text{ASCII}(s[0]) + \text{base}^{i-1} \times \text{ASCII}(s[1]) + \text{base}^{i-2} \times \text{ASCII}(s[2]) +... hash(s^1i)=basei×ASCII(s[0])+basei1×ASCII(s[1])+basei2×ASCII(s[2])+...

class Solution {
    public String shortestPalindrome(String s) {
        int len = s.length();
        int base = 131, mod = 1000000007;
        int left = 0, right = 0, mul = 1;
        int best = -1;
        char[] ascii = s.toCharArray();
        for (int i = 0; i < len; ++i) {
            left = (int) (((long) left * base + ascii[i]) % mod);
            right = (int) ((right + (long) mul * ascii[i]) % mod);
            if (left == right)
                best = i;
            mul = (int) ((long) mul * base % mod);
        }
        String add = best == len - 1 ? "" : s.substring(best + 1);
        StringBuilder prefix = new StringBuilder(add).reverse();
        return prefix.append(s).toString();
    }
}

KMP

构造的字符串后缀就是原字符串的倒置,前缀后缀相等时,也就意味着当前前缀是一个回文串,而 next 数组是寻求最长的前缀,我们也就找到了开头开始的最长回文串。

class Solution {
    public String shortestPalindrome(String s) {
        String str = s + '#' + new StringBuilder(s).reverse();
        int max = getLastNext(str);
        return new StringBuilder(s.substring(max)).reverse() + s;
    }

    public int getLastNext(String s) {
        int len = s.length();
        char[] charArray = s.toCharArray();
        int[] next = new int[len + 1];
        next[0] = -1;
        next[1] = 0;
        int k = 0, i = 2;
        while (i <= len) {
            if (k == -1 || charArray[i - 1] == charArray[k]) {
                next[i] = k + 1;
                k++;
                i++;
            } else {
                k = next[k];
            }
        }
        return next[len];
    }
}

马拉车算法

class Solution {
    public String preProcess(String s) {
        int len = s.length();
        if (len == 0)
            return "^$";
        StringBuilder str = new StringBuilder("^");
        for (int i = 0; i < len; i++)
            str.append("#").append(s.charAt(i));
        str.append("#$");
        return str.toString();
    }

    public String shortestPalindrome(String s) {
        String str = preProcess(s);

        int len = str.length();
        int[] p = new int[len];
        int center = 0, max_right = 0;
        for (int i = 1; i < len - 1; i++) {
            int i_mirror = 2 * center - i;
            if (max_right > i)
                p[i] = Math.min(max_right - i, p[i_mirror]);
            else 
                p[i] = 0;

            while (str.charAt(i + 1 + p[i]) == str.charAt(i - 1 - p[i]))
                p[i]++;

            if (i + p[i] > max_right) {
                center = i;
                max_right = i + p[i];
            }
        }

        int max_length = 0;
        int centerIndex = 0;
        for (int i = 1; i < len - 1; i++) {
            int start = (i - p[i]) / 2;
            if (start == 0)
                max_length = Math.max(p[i], max_length);
        }
        StringBuilder prefix = new StringBuilder(s.substring(max_length)).reverse();
        String result = prefix.append(s).toString();
        return result;
    }
}

分割回文串1

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。

示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

示例 2:
输入:s = “a”
输出:[[“a”]]

回溯

class Solution {
    public List<List<String>> partition(String s) {
        int len = s.length();
        List<List<String>> result = new ArrayList<>();
        if (len != 0) {
            char[] charArray = s.toCharArray();
            backtrack(charArray, len, 0, new ArrayList<String>(), result);
        }
        return result;
    }

    private void backtrack(char[] charArray, int len, int start, List<String> path, List<List<String>> res) {
        if (len == start) {
            res.add(new ArrayList<String>(path));
            return;
        }

        for (int i = start; i < len; i++) {
            if (isPalindrome(charArray, start, i)) {
                path.add(new String(charArray, start, i + 1 - start));
                backtrack(charArray, len, i + 1, path, res);
                path.remove(path.size() - 1);
            }
        }
    }

    private boolean isPalindrome(char[] charArray, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (charArray[i] != charArray[j]) 
                return false;
        }
        return true;
    }
}

动态规划+回溯

class Solution {
    private List<List<String>> result = new ArrayList<>();
    private boolean[][] dp;
    public List<List<String>> partition(String s) {
        int len = s.length();
        if (len == 0) 
            return result;

        dp = new boolean[len][len];
        char[] charArray = s.toCharArray();
        for (int right = 0; right < len; right++) {
            for (int left = 0; left <= right; left++) {
                if (charArray[left] == charArray[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
                    dp[left][right] = true;
                }
            }
        }
        backtrack(charArray, len, 0, new ArrayList<String>());
        return result;
    }

    private void backtrack(char[] charArray, int len, int start, List<String> path) {
        if (len == start) {
            result.add(new ArrayList<String>(path));
            return;
        }

        for (int i = start; i < len; i++) {
            if (dp[start][i]) {
                path.add(new String(charArray, start, i + 1 - start));
                backtrack(charArray, len, i + 1, path);
                path.remove(path.size() - 1);
            }
        }
    }
}

分割回文串2

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。

示例 1:
输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。

示例 2:
输入:s = “a”
输出:0

示例 3:
输入:s = “ab”
输出:1

动态规划

class Solution {
    public int minCut(String s) {
        int len = s.length();
        char[] charArray = s.toCharArray();
        boolean[][] dp = new boolean[len][len];
        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                if (charArray[i] == charArray[j] && (j - i <= 1 || dp[i + 1][j - 1]))
                    dp[i][j] = true;
            }
        }

        int[] split_num = new int[len];
        Arrays.fill(split_num, Integer.MAX_VALUE);
        for (int i = 0; i < len; ++i) {
            if (dp[0][i]) {
                split_num[i] = 0;
            } else {
                for (int j = 0; j < i; ++j) {
                    if (dp[j + 1][i])
                        split_num[i] = Math.min(split_num[i], split_num[j] + 1);
                }
            }
        }

        return split_num[len - 1];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值