java数据结构与算法刷题-----LeetCode131. 分割回文串

java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

在这里插入图片描述

题目想要实现的效果,很多人拿到这道题,根本不知道示例表达的是什么,下面这张图,是代码随想录的一张回溯切割模拟图:

文章链接:https://leetcode.cn/problems/palindrome-partitioning/solutions/640336/131-fen-ge-hui-wen-chuan-hui-su-sou-suo-yp2jq/

在这里插入图片描述

  1. 可见这道题的示例就是实现,每次根据不同的个数进行切割。
  2. 每次切割的结果,为一种切割方案
  3. 题目要所有的方案
  4. 简单来说,我们选择一个start变量,作为这次切割的起始位置。例如"aab"这个字符串,对应下标位置为[0,1,2]
  1. 初始我们start肯定在0位置
  2. 我们第一次先切割1个. 也就是获取[0,0]位置的长度为1的字符"a"
  3. 此时,下一个位置只能从1开始,也就是start = 1.依然我们先选择切割1个,[1,1]位置取得"a". 再下个位置是2开始,start = 2,依然我们先切割一个[2,2]位置取得"b"
  4. 此时字符串切割完成,我们得到一组切割方案[a,a,b]
  5. 然后,回头来看start = 2 ,我们现在选择切割2个[2,3],此时发现下标越界了,所以start = 2的情况枚举完成
  6. 回到start = 1的情况,选择切割2个[1,2],从而切割出"ab",发现不是回文串。然后发现字符串又枚举完了
  7. 回到start = 0的情况,选择切割2个[0,1]从而切割出"aa",发现是回文串
  8. 然后下次只能是start = 2.先切割1个[2,2]从而切割出"b",也是回文串。然后再次枚举完成
  9. 得到第二个组切割方案[aa,b]
  10. 然后回到start = 0,选择切割3个[0,2],切割出"aab",不是回文串,字符串枚举完成,没有任何回文串。
  11. 故,得到两组切割方案[a,a,b]和[aa,b]

很显然,如果只要其中任意一种切割方案,都有很多方法解决,例如动态规划,记忆化搜索,双指针判断是否回文串等

但是这道题是要所有方案,所以,在上面这些方法的基础上,还得加上回溯。

1. 回溯+双指针

解题思路:时间复杂度O( n 2 ∗ l o g 2 n n^2*log_2{n} n2log2n),空间复杂度O(n^2)
  1. 使用start变量,指定当前想要枚举回文串的起点
  2. 然后以1,2,3…这样递增的数字,来限定本次以start为起点的字符串长度。
  3. 然后通过双指针法,判断是否是回文串,如果是,就添加到答案中
代码:此算法,使用双指针法,会产生大量重复计算,但是因为都是对数组进行直接操作,所以做题情况下,这个算法的速度最快,但是时间复杂度并不优秀

在这里插入图片描述

class Solution {
    List<List<String>> res;
    String s;
    int len;
    public List<List<String>> partition(String s) {
        this.res = new ArrayList<List<String>>();//保存最终答案
        this.s = s;//字符串保存到公共变量中,增加可读性
        this.len = s.length();
        String[] records = new String[len];
        backtracking(0, records,0);//回溯算法
        return res;
    }

    /**
     *
     * @param start 当前回文串的起点
     * @param records 保存当前切割方案枚举的回文串
     * @param index records的下标
     */
    private void backtracking(int start, String[] records,int index) {
        if (start == len) {//如果没有下一个字符可以作为回文串的起点,就保存当前切割方案
            ArrayList<String> list = new ArrayList<>();
            for(int i = 0;i<index;i++) list.add(records[i]);
            res.add(list);
        } else {//否则继续构造以start为起始位置的回文串
            for (int i = start; i < len; i++) {//从start开始,第一次分割1个,第二次分割两个,依次类推
                if(isPalin(s, start, i)) {//是否是回文串,分割完成后,判断分割出来的是否是回文串
                    records[index] = s.substring(start,i+1);//是回文串就保存
                    //则目前已经分割到i了,接下来从i+1开始继续分割,试图找寻下一个回文串
                    backtracking( i + 1, records,index+1);
                }
            }
        }
    }
    //判断当前字符串是否是回文串
    private boolean isPalin(String s, int low, int high) {
        while (low < high) {
            if (s.charAt(low) != s.charAt(high)) {
                return false;
            }
            low++;
            high--;
        }
        return true;
    }
}

2. 动态规划+回溯

很多人觉得动态规划很难,但它就是固定套路而已。其实动态规划只不过是将多余的步骤,提前放到dp数组中(就是一个数组,只不过大家都叫它dp),达到空间换时间的效果。它仅仅只是一种优化思路,因此它目前的境地和线性代数一样----虚假的难。

  1. 想想线性代数,在国外留学的学生大多数不觉得线性代数难理解。但是中国的学生学习线性代数时,完全摸不着头脑,一上来就是行列式和矩阵,根本不知道这玩意是干嘛的。
  2. 线性代数从根本上是在空间上研究向量,抽象上研究线性关系的学科。人家国外的教科书都是第一讲就帮助大家理解研究向量和线性关系。
  3. 反观国内的教材,直接把行列式搞到第一章。搞的国内的学生在学习线性代数的时候,只会觉得一知半解,觉得麻烦,完全不知道这玩意学来干什么。当苦尽甘来终于理解线性代数时干什么的时候,发现人家国外的教材第一节就把这玩意讲清楚了。你只会大骂我们国内这些教材,什么狗东西(以上是自己学完线性代数后的吐槽,我们同学无一例外都这么觉得)。

而我想告诉你,动态规划和线性代数一样,我学完了才知道,它不过就是研究空间换时间,提前将固定的重复操作规划到dp数组中,而不用暴力求解,从而让效率极大提升。

  1. 但是网上教动态规划的兄弟们,你直接给一个动态方程是怎么回事?和线性代数,一上来就教行列式和矩阵一样,纯属恶心人。我差不多做了30多道动态规划题目,才理解,动态方程只是一个步骤而已,而这已经浪费我很长时间了,我每道题都一知半解不理解,过程及其痛苦。最后只能重新做。
  2. 动态规划,一定是优先考虑重复操作与dp数组之间的关系,搞清楚后,再提出动态方程。而你们前面步骤省略了不讲,一上来给个方程,不是纯属扯淡吗?
  3. 我推荐研究动态规划题目,按5个步骤,从上到下依次来分析
  1. DP数组及下标含义
  2. 递推公式
  3. dp数组初始化
  4. 数组遍历顺序(双重循环及以上时,才考虑)
  5. dp数组打印,分析思路是否正确(相当于做完题,检查一下)
思路
  1. 上面的方法,每次都要重复判断某个范围的子串是否是回文串
  2. 例如,[a,a,b]和[aa,b]这两组方案,都重复的判断了"b"是否是回文串
  3. 现在我们将某个范围的子串是否是回文串保存到dp数组中。只需要处理一次dp数组,之后直接取值就知道是否是回文串了。
  4. 所以我们创建二维数组,横坐标表示子串起始位置,纵坐标表示子串结束位置。对应的元素值为当前这个子串是否是回文子串。
动态规划5步曲
  1. DP数组及下标含义
  1. 我们要求出的是当前子串是否是回文串。显然dp数组中存储的是当前子串是否是回文串。要求出谁的?显然是求出,以横坐标为起点位置,纵坐标为终点位置的子串的。那么下标就是代表子串的左右边界,很显然,需要两个下标,也就是二维数组。
  1. 递推公式
  1. 对于aba这个子串,我们判断两边的a,也就是(a)b(a),括号中内容是否相等。
  2. 如果相等,除去两边的,剩下的中间内容a(b)a,也就是b是否是回文串,若b是回文串,那么加上两边相同的a,aba就是回文串
  3. 引入两个下标i和j,i表示左边界,j表示右边界
  4. 先判断i和j位置的字符是否一致,如果一致,再判断去掉左右两边后,也就是i+1到j-1位置的子串是否是回文串
  5. 故递推公式为:dp[i][j] = s[i] == s[j] && dp[i+1][j-1]
  1. dp数组初始化:自低向上初始化,因为递推公式中,总是需要下一行的数据,也就是第i行需要dp[i+1]行的数据

在这里插入图片描述

  1. 数组遍历顺序:先行后列,i表示起始位置,然后j表示终止位置,先规定起始位置,再规定终止位置
  2. 打印dp数组(自己生成dp数组后,将dp数组输出看看,是否和自己预想的一样。),下图是"aab"这个字符串的dp数组

在这里插入图片描述

代码:时间复杂度O( n ∗ 2 n n*2^n n2n),空间复杂度O(n^2)

在这里插入图片描述

class Solution {
    boolean[][] f;//dp数组,下标代表:[字符串起始位置][字符串结束位置] = 是否是回文串。true表示是回文串
    List<List<String>> ret = new ArrayList<List<String>>();//保存所有答案
    List<String> ans = new ArrayList<String>();//保存当前枚举出的方案
    int n;//字符串长度

    public List<List<String>> partition(String s) {
        n = s.length();
        f = new boolean[n][n];//初始化dp数组
        for (int i = 0; i < n; ++i) {//初始默认都是回文串
            Arrays.fill(f[i], true);
        }
        //将不是回文串的赋值为false
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                // acbca是否是回文串,取决于两边的字符是否相等a(cbc)a,并且中间的子串是否是回文串(cbc)
                // 如果当前子串,两边字符相等,并且除去两边的字符,剩下的中间部分是回文串,则当前子串就是回文串
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
            }
        }

        dfs(s, 0);//有了dp数组后,之后需要判断某区间是否是回文串,就可以直接访问dp数组
        return ret;
    }
    //回溯算法,枚举不同的切割方案
    public void dfs(String s, int i) {//其中 遍历 i 表示当前切割的子串的起始位置
        if (i == n) {//如果没有位置可以当作起始位置后,当前切割方案就枚举完成了
            ret.add(new ArrayList<String>(ans));//将当前切割方案放入最终结果
            return;
        }
        for (int j = i; j < n; ++j) {//第一次从i位置切割1个,第二次从i位置切割两个,依次类推
            if (f[i][j]) {//访问dp,如果字符串s的i到j位置的子串是回文串
                ans.add(s.substring(i, j + 1));//将当前子串添加到当前切割方案中
                dfs(s, j + 1);//进行下一个回文串的切割
                ans.remove(ans.size() - 1);//使用链表时,不要忘记最后要将当前切割方案的移除,以免影响下次切割方案的枚举结果
            }
        }
    }
}

3. 记忆化搜索+回溯

解题思路:就是动态规划方法,只不过是简化版动态规划。时间复杂度O( n ∗ 2 n n*2^n n2n),空间复杂度O(n^2)
  1. 不在需要抽象思考动态规划的dp数组初始化
  2. 而是改为,如果我们第一次遇到这个子串,就进行判断,如果它是回文串,就再对应dp[i][j]位置设置为true
  3. 之后如果再次遇到这个子串,直接取值,而不是再次判断这个子串是否是回文串
代码

在这里插入图片描述

class Solution {
    int[][] f;//dp数组
    List<List<String>> ret = new ArrayList<List<String>>();
    List<String> ans = new ArrayList<String>();
    int n;

    public List<List<String>> partition(String s) {
        n = s.length();
        f = new int[n][n];//不进行初始化

        dfs(s, 0);
        return ret;
    }
    //这里的逻辑不变
    public void dfs(String s, int i) {
        if (i == n) {
            ret.add(new ArrayList<String>(ans));
            return;
        }
        for (int j = i; j < n; ++j) {
            if (isPalindrome(s, i, j) == 1) {//这里不直接访问dp
                ans.add(s.substring(i, j + 1));
                dfs(s, j + 1);
                ans.remove(ans.size() - 1);
            }
        }
    }

    // 记忆化搜索中,f[i][j] = 0 表示未搜索,1 表示是回文串,-1 表示不是回文串
    public int isPalindrome(String s, int i, int j) {
        if (f[i][j] != 0) {
            return f[i][j];
        }
        if (i >= j) {
            f[i][j] = 1;
        } else if (s.charAt(i) == s.charAt(j)) {
            f[i][j] = isPalindrome(s, i + 1, j - 1);
        } else {
            f[i][j] = -1;
        }
        return f[i][j];
    }
}
  • 30
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值