最长回文子串:动态规划推导

最长回文子串:结合图形推导动态规划

题目介绍

本题可以在力扣找到,题号为5。

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

示例 1:

输入:s = "babad"

输出:"bab"

解释:"aba" 同样是符合题意的答案。
示例 2:

输入:s = "cbbd"

输出:"bb"

提示:

1 <= s.length <= 1000

s 仅由数字和英文字母组成

暴力解法

先从暴力解法入手,对于长度为n的字符串,总共有 (n+1) * n / 2 种组合,我们可以找出每种组合,然后判断其是否是回文串,最后与当前的最大值进行比较,若新串更大,就更新答案。

我们先写一个判断回文串的函数,用双指针判断即可:

java 复制代码
    public static boolean isPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

然后我们用双重循环,遍历到每一种可能,在进行进一步判断:

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        String resp = "";
        int ans = 0;
        int[][] dp = new int[s.length()][s.length()];
        int n = s.length();
        for (int i = 0;i < n;i++) {
            for (int j = i;j < n;j++) {
            	//获取子串
                String subString = s.substring(i,j+1);
                //判断是否要更新最长子串
                if (subString.length() > resp.length() && isPalindrome(subString)) {
                    resp = subString;
                }
            }
        }
        //返回答案
        return resp;
    }

    public boolean isPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

动态规划

接下来我们可以考虑动态规划的解法,所谓动态规划,就是用之前的状态递推后面的状态。首先我们需要确定一个状态转移条件,对于一个 字符串 来说,如果有 一个子串str 为回文串,且 str的前一个字符和后一个字符也相同(比如说 "aba" 和 "aabaa")那么就说明 str-1,str+1 也为回文串。我们可以定一个二维数组,将其定义为 从 i 到 j 的子串是否是回文串:

java 复制代码
//初始化一个状态数组--布尔类型
int[][] dp = new int[n][n];
for (int i = 0;i < n;i++) {
	//对于单个字符的情况,必定为回文串
	dp[i][i] = true;
}

根据我们之前的结论,可以推出状态转移方程,即

java 复制代码
if (s.charAt(i) == s.charAt(j) && dp[i+1][j-1] == true) {
	dp[i][j] = true;
} else {
	dp[i][j] = false;
}

可以看到,dpij的状态是基于 dpi+1j-1的,即若要得到 dpij,则要先得到dpi+1j-1的状态,光这么描述其实还是很抽象,我们借助矩阵图来理解:

我们把横轴作为j,纵轴作为i,首先我们可以填充 dpii,即只有一个字符的情况,都可以填充为true:

其次,对于j > i 的情况其实都可以不用考虑,因为起始位置是要始终小于结束位置的:

接着,我们推到一般情况的依赖关系,我们先任选一点,比如 dp02,根据状态转移方程,我们需要参考的就是 dp11 的状态值了:

即,对于每一个状态,在二位图像上总是需要依赖其左下角的具体状态,所以理论上来说,只要从对线向右上填充,我们就可以得到整个dp数组的状态了:

对于上面的示例来说,只要我们填充了 i = j 以及 i + 1 = j 这两条对角线,我们就可以成功推导出整个dp数组的状态,接下来我们开始编程实现:

java 复制代码
    public static String longestPalindrome(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        //填充对角线
        for (int i = 0;i < n;i++) {
            dp[i][i] = true;
        }
        //填充 j = i + 1
        for (int i = 0;i < n-1;i++) {
            int j = i + 1;
            if (s.charAt(i) == s.charAt(j)) {
                dp[i][j] = true;
            }
        }
        //填充其余情况,按对角线顺序填充
        for (int offset = 0;offset < n-2;offset++) {
            for (int i = 0;i < n-2-offset;i++) {
                int j = i + 2 + offset;
                if (s.charAt(i) == s.charAt(j) && dp[i+1][j-1]) {
                    dp[i][j] = true;
                }
            }
        }
        //遍历dp数组,求出最长值
        int longestLength = 0;
        String resp = "";
        for (int i = 0;i < n;i++) {
            for (int j = 0;j < n;j++) {
                if (dp[i][j] && j - i + 1 > longestLength) {
                    longestLength = j - i + 1;
                    resp = s.substring(i,j+1);
                }
            }
        }
        return resp;
    }

具体实现就如上,需要注意的是,由于 dp[i][j] 依赖于 dp[i+1][j-1],即依赖于左下角的状态,所以我们填充一般状态的时候也需要按照这个顺序填充,即需要按照对角线层层填充才能得到正确答案。

我们可以与暴力做法的耗时作比较,暴力解法耗时大概在 1000ms左右,而上面这种基于动态规划的解法耗时只有 200ms左右:

大幅优化了耗时,不过上述解法中还有可以改进的地方,比如我们可以在填充dp数组的时候就开始寻找答案,而不是分两步走:

java 复制代码
    public static String longestPalindrome(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        //填充对角线
        for (int i = 0;i < n;i++) {
            dp[i][i] = true;
        }
        int longestLength = 1;
        String resp = s.charAt(0) + "";
        //填充 j = i + 1
        for (int i = 0;i < n-1;i++) {
            int j = i + 1;
            if (s.charAt(i) == s.charAt(j)) {
                dp[i][j] = true;
                if (j - i + 1 > longestLength) {
                    longestLength = j - i + 1;
                    resp = s.substring(i,j+1);
                }
            }
        }
        //填充其余情况,按对角线顺序填充
        for (int offset = 0;offset < n-2;offset++) {
            for (int i = 0;i < n-2-offset;i++) {
                int j = i + 2 + offset;
                if (s.charAt(i) == s.charAt(j) && dp[i+1][j-1]) {
                    dp[i][j] = true;
                    //将判断步骤放入填充步骤中
                    if (j - i + 1 > longestLength) {
                        longestLength = j - i + 1;
                        resp = s.substring(i,j+1);
                    }
                }
            }
        }
        return resp;
    }

这样我们就可以省去一次遍历的开销,最终优化效果到 139ms 左右:

相关推荐
江畔柳前堤11 分钟前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore1 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考3 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX4 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面4 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号4 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽5 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI5 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者6 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者6 小时前
【征程6】校准量化中HistogramObserver解析
算法