zoukankan      html  css  js  c++  java
  • 【LeetCode】647. 回文子串

    题目链接

    647. 回文子串

    题目描述

    示例 1:
    输入:"abc"
    输出:3
    解释:三个回文子串: "a", "b", "c"
    
    示例 2:
    输入:"aaa"
    输出:6
    解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
    
    提示:
    输入的字符串长度不会超过 1000 。
    

    解题思路

    1.暴力枚举

    • 字符串问题最土最有效的方法应该就是暴力枚举了,利用双重for循环可以得到字符串s的所有子串,然后对这些子串进行一一判断,如果是回文串,ans++;
    • 一般来说,暴力都会被卡超时,没想到这题竟然过了

    2.中心扩展法

    通过观察可以发现,回文字符串分为两种:

    • 偶数型回文字符串:如果回文字符串的长度为偶数,那么它中间一定是两个相同的元素,例如cbaabc,中间是两个相同的aa;
    • 奇数型回文字符串:如果回文字符串的长度为奇数,那么它中间一定是一个元素,例如cbabc,中间一个a元素;

    所以用for先找偶数型(aa)回文,找s[i]=s[i+1],找到后再往两边推,直到s[i-len]与s[i+1+len]不等为止。

    然后再找奇数型(aba)回文,找s[i]=s[i+2],找到后处理同上。

    因为这是从回文中间出发,所以也叫中心扩展法。

    3.动态规划

    动态规划一般也只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
    
    动态规划算法分以下4个步骤:
    
    (1)描述最优解的结构
    (2)递归定义最优解的值
    (3)按自底向上的方式计算最优解的值   //此3步构成动态规划解的基础。
    (4)由计算出的结果构造一个最优解。   //此步如果只要求计算最优解的值时,可省略。
    好,接下来,咱们讨论适合采用动态规划方法的最优化问题的俩个要素:最优子结构性质,和子问题重叠性质。
    
    (1)最优子结构
        如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。意思就是,总问题包含很多个子问题,而这些子问题的解也是最优的。
    
    (2)重叠子问题
        子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
    

    该问题满足最优子结构性质:一个最长回文串去掉两头以后,剩下的部分依然是最长回文串。

    第一步:定义状态

    dp[i][j]:表示子串s[i,j]是否为回文子串。

    当dp[i][j] = 1:表示子串s[i,j]为回文子串,反之。

    第二步:状态转移方程

    这一步在做分类讨论(根据头尾字符是否相等)。

    if(j-i >= 2){
        dp[i][j] = 1, if dp[i+1][j-1] == 1 && s[i] == s[j]
    	dp[i][j] = 0, if dp[i+1][j-1] == 0 || s[i] != s[j]
    }
    else{
        dp[i][j] = 1,if s[i] == s[j]
    	dp[i][j] = 0,if s[i] != s[j]
    }
    
    -Q:j-i >= 2这个条件是如何求解出来的?
    -A:因为dp[i][j]表示子串s[i,j]是否为回文子串,所以i<=j,同理dp[i+1][j-1]中,i+1 <= j-1,整理得j - i <= 2.
    

    第三步:考虑初始化

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

    第四步:考虑输出

    判断dp[i,j]是否等于 1,等于的话ans++即可。

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

    AC代码

    1.暴力枚举

    class Solution {
        
        //判断是否为回文子串建议都这样写for循环语句,这样能够减少循环次数。
        boolean charge(String s){
            for(int i = 0; i < s.length() / 2; i++){
                if(s.charAt(i) != s.charAt(s.length() - i - 1)) return false;
            }
            return true;
        }
    
        public int countSubstrings(String s) {
            //因为单个字母也算回文,所以ans的初始值就是字符串的长度。
            int ans = s.length();
            for(int i = 0; i < s.length() - 1; i++){
                StringBuilder sb = new StringBuilder();
                sb.append(s.charAt(i));
                for(int j = i + 1; j < s.length(); j++){
                    sb.append(s.charAt(j));
                    if(charge(sb.toString())){
                        ans++;
                    }
                }
            }
            return ans;
        }
    }
    

    2.中心扩展法

    class Solution {
        public int countSubstrings(String s) {
            int ans = s.length();
            //奇数型回文
            for(int i = 0; i < s.length() - 2; i++){
                if(s.charAt(i) == s.charAt(i+2)){
                    ans++;
                    int temp = 1;
                    while((i-temp)>=0 && (i+2+temp)<s.length() && s.charAt(i-temp)==s.charAt(i+2+temp)){
                        ans++;
                        temp++;
                    }
                }
            }
            //偶数型回文
            for(int i = 0; i < s.length() - 1; i++){
                if(s.charAt(i) == s.charAt(i+1)){
                    ans++;
                    int temp = 1;
                    while((i-temp)>=0&&(i+1+temp)<s.length() && s.charAt(i-temp)==s.charAt(i+1+temp)){
                        ans++;
                        temp++;
                    }
                }
            }
            return ans;
        }
    }
    

    3.动态规划法

    填表必须是从上往下如箭头所示,一开始我选择从左到右填表,卡了很久。

    dp[0][5]依赖与dp[1][4]以及s[0]s[5]是否相等,如果你是从左到右填表,那dp[1][4]的值是未知的。
    
    class Solution {
        public int countSubstrings(String s) {
            int[][] dp = new int[s.length()][s.length()];
            int ans = 0;
            for(int i = 0; i < s.length(); i++){
                dp[i][i] = 1;
                ans++;
            }
            //下面的两个for循环就是在填表格!但是要注意填表的顺序!!!!!!!!
            for(int j = 0; j < s.length(); j++){
                for(int i = 0; i < j; i++){
                    if(j - i <= 2){
                        if(s.charAt(j) == s.charAt(i)) dp[i][j] = 1;
                        else dp[i][j] = 0;
                    }
                    else{
                        if(s.charAt(j) == s.charAt(i) && dp[i+1][j-1]==1) dp[i][j] = 1;
                        else dp[i][j] = 0;
                    }
    
                    if(dp[i][j] == 1) ans++; 
                }
            }
            return ans;
        }
    }
    
  • 相关阅读:
    vue 设置全局变量、指定请求的 baseurl
    npm ERR! missing script: build
    npm install 报错,提示`gyp ERR! stack Error: EACCES: permission denied` 解决方法
    vue-cli 4 安装与 新建项目 路由
    PHP7安装redis扩展
    C#中的list的System.Predicate<in T>和System.Comparison<in T>的应用
    6个2教你认识递归的力量!
    C#中的预编译指令介绍[转]
    C#---数据库访问通用类、Access数据库操作类、mysql类 .[转]
    GOTO语句以及GOTO机制的模式实现
  • 原文地址:https://www.cnblogs.com/XDU-Lakers/p/13530731.html
Copyright © 2011-2022 走看看