Day22 力扣回溯 : 39. 组合总和 | 40.组合总和II |131.分割回文串

Day22 力扣回溯 : 39. 组合总和 | 40.组合总和II |131.分割回文串

39. 组合总和

本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制

题目链接/文章讲解:https://programmercarl.com/0039.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C.html
视频讲解:https://www.bilibili.com/video/BV1KT4y1M7HJ

第一印象:

这个集合可以用无数次,我觉得startIndex每次应该都是从集合的头开始遍历吧,那就不需要startIndex了啊。 我先试试。

不对 仍然需要startIndex。比如235,第一个分支选2,第二个分支选3.

2下面的集合应该是235,3下面的集合应该是35,如果3下面的集合还是235,就会出现重复的组合,没有意义了。但是因为没有结果集数量上的限制,所以终止条件应该是判断这个路径上sum的事情。

返回值和参数:

返回值回溯算法void,参数int[] candidates, int target, int startIndex
还需要路径的和sum

终止条件:

如果sum > target 就停了, == 就收获, < target说明还能继续加,树的深度就继续加深,往下搜索,直到 > 或者 ==。

sum > target 也算剪枝吗?
答: 不算,是终止条件🌶

单层递归逻辑:

这个比较常规,就是path添添删删,sum加加减减,startIndex和之前的不一样,因为可以元素重复,所以下一次递归的起点应该是startIndex本身开始。

传参应该是

backtracking(candidates, target, startIndex, sum);

但是运行结果我发现,还是会把重复的情况输出,好奇怪,我看看怎么事r。

可不重复么,递归传的参数还是startIndex,应该传 i 才对。 这次就对了

backtracking(candidates, target, i, sum);

我自己做出来了!!!!!!!

看完题解的思路:

看看题解吧

  1. 一个集合搜索需要startIndex, 多个集合搜索不需要startIndex。 这件事情只是对于组合来说,排列又是另一种情况, 我还没学。‘
  2. 剪枝的操作我没想到, 如下!

在这里插入图片描述

对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。

那么可以在for循环的搜索范围上做做文章了。

对总集合排序之后,如果下一层的sum(就是本层的 sum +
candidates[i])已经大于target,就可以结束本轮for循环的遍历。

就会变成这样的

在这里插入图片描述

那么for循环的终止条件就是

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)

这种判断,是在for循环的头还是尾,我就会纠结一下。我想想啊

一个for循环就是一个节点

当在2的分支取2的时候,进入for循环,path添,sum加(是4 == target),再去做递归…… 会在终止条件里收获结果。

在2的分支取3,进入for循环,但其实这个时候sum加(是5 > target),再进入递归的话就会在终止条件里返回。而改正之后的代码,在进入for循环的判断的时候,先sum加和试一试发现是5 ,那么就没必要再去递归它了,相当于树里少了一个节点(少了一个for循环)。

这个剪枝emmmm彳亍。

为什么求和问题剪枝,要先排序数组?

其实答案很简单,比如251,target是3,已经取了2,这个时候取5,发现已经是7,没必要再去搜索了,于是break,但这个break不仅仅是break了2-5这个节点,还把2-1这个也break掉了(因为2-2 2-5 2-1是for循环的形式去搜索的)。所以要先排序,125,如果1-2就超过target,那么1-5肯定超过,所以就可以break了!!

实现时的困难:

除了递归的时候传参把 i 写成了 startIndex,都不难。

我加入剪枝才做之后发现,这个数组必须先排序,卡哥说在求和问题中,排序之后加剪枝是常见的套路! 为啥啊 我要去看视频了。

ok懂了,答案写到上面剪枝的那里吧。

感悟:

自己做出来真爽啊, 画树形图真有用

代码:

class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates); // 先进行排序
        backtracking(candidates, target, 0, 0);
        return result;
    }

    private void backtracking(int[] candidates, int target, int startIndex, int sum) {
        //终止条件 
        if (sum > target) return;
        
        if(sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }
        //单层递归逻辑
        for (int i = startIndex; i < candidates.length; i++) {
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, i, sum);
            //回溯
            path.removeLast();
            sum -= candidates[i];
        }
    }
}

40.组合总和II

本题开始涉及到一个问题了:去重。

注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。

题目链接/文章讲解:https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html
视频讲解:https://www.bilibili.com/video/BV12V4y1V73A

第一印象:

去重啊 去重的问题我在哈希那就不太懂。比如 1 1 7,taget 是8,那么17 17 就是重复的组合。

我悟了,肯定要先排序,否则杂乱无章没法去重。比如1 1 4 5. 第一个1正常处理,遍历到第二个1的时候,发现和前面一个一样的数字,就可以跳过了。 想到这就会产生疑问:不会落下一些情况吗?

这个时候画出树形图:
在这里插入图片描述

我们细看,两个圈内的内容是不是一样的,也就是说,这就是重复的产生!!!其实第一个1已经把第二个1搜索到的组合包含了。

如果心里没底,画个 22246, target=6 的图。发现第一个2 也给第二三个2的组合都包含了。所以我坚定了我的想法。其实这个检查和前一个数字是否一样的,一样就跳过的操作在之前做题遇到过,所以我才有印象。

参数和返回值:

返回值回溯void,参数因为在一个集合内 要有startIndex

void backtracking(int[] candidates, int target, int startIndex, int sum)

终止条件:

和之前的题一样。

 //终止条件
        if (sum > target) return;
        if (sum == target) {
            result.add(new ArrayList<>(path));
        }

单层递归逻辑:

最开始整个数组排序,原始集合 比如1 1 2 2 4 8. 在 1 那里,startIndex是 0。startIndex之后如果有相同的数字就要跳过,所以1的分支下面是2 2 4 8.

同样的,这个集合startIndex是 2(数组下标),也是startIndex之后如果有相同的数字就要跳过,2的分支下面是4 8.

这时候就会考虑到如果集合末尾的元素一样怎么办,1 6 6这样呢。

1的分支下面是6 6。 6 6集合startIndex是 1, 也是同样的startIndex之后如果有相同的数字就要跳过。这个时候for循环的里的 i 就会变成3,下标就越界了。所以要再 跳过相同startIndex元素 的过程后判断下标是否越界,需要break的,不然就会去搜索越界的地方了,看一眼代码就知道。

本来之前的题使用for循环的终止条件去控制不越界,但是因为在for循环内我们要操作这个 i ,就可能越界了,所以要多一层判断。

剪枝也是和上一道题一样的剪枝。

反正我自己是做出来通过了。

//单层地柜逻辑
        for (int i = startIndex; i < candidates.length; i++) {
            //如果这个数字和上一个一样,往后找. 当前集合的第一个元素不用找
            while (i != startIndex && i < candidates.length && candidates[i] == candidates[i - 1]) {
                i++;
            }
            //越界的话就要手动break,不然添添删删的时候就处理越界下标了。
            if (i == candidates.length) break;
            //剪枝
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            sum += candidates[i];
            //递归
            backtracking(candidates, target, i + 1, sum);
            //回溯
            path.removeLast();
            sum -= candidates[i];
        }

看完题解的思路:

卡哥提出的概念是,在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。强调一下,树层去重的话,需要对数组排序!

这道题画出来之后就知道是同意树层上不能重复,但是同一树枝上可以重复,因为是两个元素嘛。 确实是要排序噢。

哎卧槽,他处理的方式使用used数组,和我不一样啊。我怎么觉得我更牛逼

但是这里跳过重复元素用continue才是最好的,直接i++ 并且不会越界啊

 //单层地柜逻辑
     for (int i = startIndex; i < candidates.length; i++) {
         //用continue啊
         if (i != startIndex && candidates[i] == candidates[i - 1]) {
             continue;
         }
         // if (i == candidates.length) break;
         //剪枝
         if (sum + candidates[i] > target) break;
         path.add(candidates[i]);
         sum += candidates[i];
         //递归
         backtracking(candidates, target, i + 1, sum);
         //回溯
         path.removeLast();
         sum -= candidates[i];
        }

我觉得used数组方式太麻烦了逻辑也混乱,而且他也给出了直接用startIndex去重的方式,就是我这个,我还是我牛逼。

实现时的困难:

//用continue啊
       if (i != startIndex && candidates[i] == candidates[i - 1]) {
           continue;
       }

我把这里的 i - 1写成了i-- 于是当时死循环了。

感悟:

我又做出来一道题!!

代码:

class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracking(candidates, target, 0, 0);
        return result;

    }

    private void backtracking(int[] candidates, int target, int startIndex, int sum) {
        //终止条件
        if (sum > target) return;
        if (sum == target) {
            result.add(new ArrayList<>(path));
        }

        //单层地柜逻辑
        for (int i = startIndex; i < candidates.length; i++) {
            //用continue啊
            if (i != startIndex && candidates[i] == candidates[i - 1]) {
                continue;
            }
            //越界的话就要手动break,不然添添删删的时候就处理越界下标了。
            // if (i == candidates.length) break;
            //剪枝
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            sum += candidates[i];
            //递归
            backtracking(candidates, target, i + 1, sum);
            //回溯
            path.removeLast();
            sum -= candidates[i];
        }

    }
}

131.分割回文串

本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。

https://programmercarl.com/0131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.html
视频讲解:https://www.bilibili.com/video/BV1c54y1e7k6

第一印象:

这个感觉不是组合问题了,是回溯应用的分割问题吧。直接看视频学习一下吧。他还说本题较难,我好想回家下棋啊。

看完题解的思路:

切割问题其实类似于组合问题:

组合问题: 选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题: 切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。

也可以抽象为一棵树:
在这里插入图片描述

这个过程比较熟悉,但是我觉得对字符串的操作,也就是图里红线的问题,对我来说是最难的。

返回值和参数:

回溯返回值void,参数除了字符串,因为在一个集合里切割,要有startIndex,和组合问题是一样的。

private void backtracking(String s, int startIndex) {

终止条件:

在这个树里面可以看出来,当能切到字符串的最末端,就是一个结果,要收集。也就是红线能到最后,因为不是回文的情况会return。

我想到这就会觉得很难,因为要操作字符串,我不知道怎么搞,但其实题解里很简单,就是startIndex就是这个红线,它代表下一次从哪切。

合理的

if (startIndex >= s.length()) {
            result.add(new ArrayList(path));
            return;
   }

单层递归逻辑:

在for (int i = startIndex; i < s.size(); i++)循环中,我们定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。

首先判断这个子串是不是回文,如果是回文,就加入在 path中,path用来记录切割过的回文子串。

如何判断回文串?是字符串那部分的题,用双指针就可以了。

这道题的优化提到了动态规划,超过我的能力了,先不看了。

for (int i = startIndex; i < s.length(); i++) {
      //如果是回文子串,则记录
      if (isPalindrome(s, startIndex, i)) {
          String str = s.substring(startIndex, i + 1);
          deque.addLast(str);
      } else {
          continue;
      }
      //起始位置后移,保证不重复
      backTracking(s, i + 1);
      deque.removeLast();
 }

比如aab,传入的startIndex是0,那么第一次判断回文串返回true。
substring [0,1), 取的是a 啊,这也对不上啊。而且第一刀为什么切在 |aab, 不应该是a|ab吗。

悟了,判断回文的函数是左闭右闭,传入startIndex是0之后,判断[0,0]这个区间是不是回文的,这里就是数组[0] 的意思。

我也不知道我刚才是怎么想的……

再有一个疑难的地方就是,像 a | ab | 这样的情况,切到ab之后发现不是回问,所以continue,i++ 之后,for循环不再进入了,到不了递归的那一行。

所以这个节点就不存在。

不然的话收集结果集只看startIndex是否到达末尾, a | ab | 这样的情况也是到达末尾了的。

而 a | a | b |这种情况,判断出是回文, 进入递归,backTracking(s, i + 1);, 传入的 i = size(), 会在终止条件那里收获结果。

实现时的困难:

主要还是逻辑混乱

对字符串操作的不熟悉。

还有 i 出现的位置变多了,我也开始乱了,什么时候左闭右闭,什么时候左闭右开。

感悟:

直接粘贴代码随想录里的感悟:

这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。

那么难究竟难在什么地方呢?

我列出如下几个难点:

切割问题可以抽象为组合问题
如何模拟那些切割线
切割问题中递归如何终止
在递归循环中如何截取子串
如何判断回文

我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力。

一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。

本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割。

如果意识到这一点,算是重大突破了。接下来就可以对着模板照葫芦画瓢。

但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了。

关于模拟切割线,其实就是index是上一层已经确定了的分割线,i是这一层试图寻找的新分割线

除了这些难点,本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1。

所以本题应该是一道hard题目了。

可能刷过这道题目的录友都没感受到自己原来克服了这么多难点,就把这道题目AC了,这应该叫做无招胜有招,人码合一。

代码:

class Solution {
    List<List<String>> lists = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();

    public List<List<String>> partition(String s) {
        backTracking(s, 0);
        return lists;
    }

    private void backTracking(String s, int startIndex) {
        //如果起始位置大于s的大小,说明找到了一组分割方案
        if (startIndex >= s.length()) {
            lists.add(new ArrayList(deque));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            //如果是回文子串,则记录
            if (isPalindrome(s, startIndex, i)) {
                String str = s.substring(startIndex, i + 1);
                deque.addLast(str);
            } else {
                continue;
            }
            //起始位置后移,保证不重复
            backTracking(s, i + 1);
            deque.removeLast();
        }
    }
    //判断是否是回文串
    private boolean isPalindrome(String s, int startIndex, int end) {
        for (int i = startIndex, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值