算法训练Day27 | LeetCode39. 组合总和 (元素可重复,数组长度不限);40. 组合总和III(去重);131.分割回文串

news2024/11/23 12:06:12

目录

LeetCode39. 组合总和

1. 思路

2. 代码实现

3. 剪枝优化

4. 复杂度分析

5. 思考与收获

LeetCode40. 组合总和III

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode131.分割回文串 

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


LeetCode39. 组合总和

 链接:39. 组合总和 - 力扣(LeetCode)

1. 思路

本题和 组合,组合III的两个不同点是:

  • 组合没有数量要求
  • 元素可以无限制的被重复选取

关于题目条件的思考

  1. 题目中的无限制重复被选取,吓得我赶紧想想 出现0 可咋办,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了;
  2. 不用考虑去重问题,因为数组中的每个元素都互不相同

本题虽然说是组合没有数量要求,元素也可以无限重复,但是有总和的限制,所以间接地也是有元素个数的限制;

本题搜索的过程抽象成树形结构如下:

注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

而在77.组合216.组合总和III中都可以知道要递归K层,因为要取k个元素的组合

为什么取5的下面是在【5,3】中取,而不是【2,5,3】?

因为组合不强调元素顺序,如果这样写的话会出现重复的组合,比如【2,3】和【3,2】;

2. 代码实现

回溯三部曲

2.1 递归函数参数

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量也可以作为函数参数传入,但这里不这么做);

首先是题目中给出的参数,集合candidates, 和目标值target。

此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

  • 如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 ,216.组合总和III ;

  • 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合;

    注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍;

class Solution:
    def __init__(self):
        self.path = []
        self.paths = []

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        '''
		def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:

2.2 递归终止条件

在以上树形结构中,从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target;sum等于target的时候,需要收集结果,代码如下:

# Base Case
if sum_ == target:
		# 因为是shallow copy,所以不能直接传入self.path
    self.paths.append(self.path[:]) 
    return
if sum_ > target:
    return 

2.3 单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合。

注意本题和77.组合 ,216.组合总和III 的一个区别是:本题元素为可重复选取的;如何重复选取呢?在向下一层递归函数的时候,不写i+1了,直接写i,表示一个重复读取当前的数;

# 单层递归逻辑 
for i in range(start_index, len(candidates)):
    sum_ += candidates[i]
    self.path.append(candidates[i])
    # 因为无限制重复选取,所以不是i+1
    self.backtracking(candidates, target, sum_, i)  
    sum_ -= candidates[i]   # 回溯
    self.path.pop()        # 回溯

2.4 整体代码如下

class Solution(object):
    def __init__(self):
        self.path = []
        self.result = []

    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if candidates == []: return []
        self.traversal(candidates,target,0,0)
        return self.result
    
    def traversal(self,candidates,target,curSum,startIndex):
        # 终止条件
        if curSum == target:
            # 因为是shallow copy,所以不能直接传入self.path
            self.result.append(self.path[:])
            return 
        if curSum > target:
            return 
        # 单层回溯搜索
        for i in range(startIndex,len(candidates)):
            self.path.append(candidates[i])
            curSum += candidates[i]
            # 因为无限制重复选取,所以不是i+1
            self.traversal(candidates,target,curSum,i)
            self.path.pop()
            curSum -= candidates[i]

3. 剪枝优化

在这个树形结构中:

以及上面的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了;那么可以在for循环的搜索范围上做做文章了。对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历;

for循环剪枝代码如下:

# 别忘了在主函数里面先排序
for i in range(start_index, len(candidates)):
    if sum_ + candidates[i] > target: 
        return

剪枝之后整体代码如下:

class Solution(object):
    def __init__(self):
        self.path = []
        self.result = []

    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if candidates == []: return []
        **candidates.sort()**
        self.traversal(candidates,target,0,0)
        return self.result
    
    def traversal(self,candidates,target,curSum,startIndex):
        # 终止条件
        if curSum == target:
            # 因为是shallow copy,所以不能直接传入self.path
            self.result.append(self.path[:])
            return 
        if curSum > target:
            return 
        # 单层回溯搜索
        for i in range(startIndex,len(candidates)):
						**# 剪枝操作**
            **if curSum > target:
                return** 
            self.path.append(candidates[i])
            curSum += candidates[i]
            # 因为无限制重复选取,所以不是i+1
            self.traversal(candidates,target,curSum,i)
            self.path.pop()
            curSum -= candidates[i]

4. 复杂度分析

子集问题分析:

  • 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

5. 思考与收获

  1. 本题和我们之前讲过的7.组合 ,216.组合总和III 有两点不同:

    • 组合没有数量要求
    • 元素可无限重复选取

    针对这两个问题,以上做了详细的分析;

  2. 对于组合问题,什么时候用startIndex,什么时候不用,并用17.电话号码的字母组合 做了对比;

  3. 还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到;在求和问题中,排序之后加剪枝是常见的套路;

  4. 就是要不断和之前的题目作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。

Reference:代码随想录 (programmercarl.com)

本题学习时间:80分钟。


LeetCode40. 组合总和III

链接: 40. 组合总和 II - 力扣(LeetCode)

1. 思路

本题和LeetCode39. 组合 问题有如下的区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次;
  2. 本题数组candidates的元素是有重复的,而 LeetCode39. 组合 是无重复元素的数组candidates;

最后本题和 LeetCode39. 要求一样,解集中不可以包含重复的元素;

本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合;

直观的暴力思路

一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时!所以不考虑这种做法;所以要在搜索的过程中就去掉重复组合。

去重是使用过的元素不可以重复选取,但是“使用过”在本题中有两个维度,比较难理解;

很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了;

这个去重为什么很难理解呢,所谓去重,其实就是使用过的元素不能重复选取。 这么一说好像很简单!都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因;

首先定义两个概念,一个是“ 树枝” ,另一个是“ 树层”

那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?

回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

Example:[ 1a,1b,2,3 ] ; target = 4(为了区别第一个1和第二个1,在后面加上了ab)

组合 [ 1a, 1b, 2] 是OK的,是属于树枝重复,虽然有两个1 但是是属于集合中的不同元素;

组合[ 1a, 3] 和[1b,3] 是不能同时存在于result中的,他们都是[ 1,3 ] 是重复的,这种情况属于树层重复;

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重;理解了这一点就是这个题目最核心的思想了。

如何才能做到把树层重复去掉,保留树枝重复呢?

首先我们需要把数组candidates排个序,至于排序的具体原因在以下的说明中会理解,举个例子,candidates = [1, 1, 2], target = 3;

为了实现这一点,需要在回溯搜索的过程中,加上一个参数,称之为used,初始化的数值为 [ 0,0,0 ];其长度和candidates数组相同,记录同一个树枝上面的元素是否使用过,如果没使用过,值为0,如果使用过,值为1;

所以树层去重且保留树枝重复的原理是这样的,首先判断candidates[i]是否和candidates[i-1]的值相等;(这里就是前面需要排序的原因,把值相等的元素放在一起)如果相等的话,看看used[i-1]的值是0还是1,如果是1的话,说明是树枝重复的情况,不处理;如果used[i-1]的值为0的话,说明是树层重复的情况,应该直接跳过此次循环,continue去取下一个数,下一个数也要做同理的去重判断;

2. 代码实现

回溯三部曲

2.1 递归函数参数

与之前的组合问题类似,需要path记录单条路径,result记录符合条件的组合;

参数是输入的数组 candidates, 目标值 target,现有的和 curSum,控制for循环起始位置的 startIndex;除了这些之外,为了实现去重,还需要加一个数组 used;

class Solution:
    def __init__(self):
        self.paths = []
        self.path = []
        self.used = []
		def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:

		def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:

2.2 递归终止条件

终止条件仍然为sum > target和 sum == target

# Base Case
        if sum_ == target:
            self.paths.append(self.path[:])
            return
				if sum_ > target:
						return

注意:sum > target 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。

2.3 单层搜索的逻辑

前面我们提到:要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。此时for循环里就应该做continue的操作。

我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的;

那么单层搜索的逻辑代码如下:

for i in range(start_index, len(candidates)):
    # 剪枝,同39.组合总和
    if sum_ + candidates[i] > target:
        return
# 检查同一树层是否出现曾经使用过的相同元素
# 若数组中前后元素值相同,但前者却未被使用(used == False),说明是for loop中的同一树层的相同元素情况
    if i > 0 and candidates[i] == candidates[i-1] 
             and self.usage_list[i-1] == False: continue
    sum_ += candidates[i]
    self.path.append(candidates[i])
    self.usage_list[i] = True
    self.backtracking(candidates, target, sum_, i+1)
    self.usage_list[i] = False  # 回溯,为了下一轮for loop
    self.path.pop()             # 回溯,为了下一轮for loop
    sum_ -= candidates[i]       # 回溯,为了下一轮for loop

注意sum + candidates[i] <= target为剪枝操作,在LeetCode39.组合总和中有讲过;

2.4 整体代码实现如下

# 剪枝+去重
class Solution(object):
    def __init__(self):
        self.path = []
        self.result = []
        self.used = []
    def combinationSum2(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        self.used = [0]*len(candidates)
        if candidates == []: return []
        # 必须提前进行数组排序,避免重复
        candidates.sort()
        self.traversal(candidates,target,0,0)
        return self.result
        
    def traversal(self,candidates,target,curSum,startIndex):
        # 终止条件
        if curSum == target:
            self.result.append(self.path[:])
            return 
        #if curSum > target:
            #return 
        # 单层回溯搜索逻辑
        for i in range(startIndex,len(candidates)):
            # 剪枝
            if curSum>target:
                return 
            # 去重
            # 检查同一树层是否出现曾经使用过的相同元素
            # 若数组中前后元素值相同,但前者却未被使用(used == False)
            # 说明是for loop中的同一树层的相同元素情况
            if i>0 and candidates[i-1] == candidates[i] and self.used[i-1] == 0:
                continue
            self.path.append(candidates[i])
            curSum += candidates[i]
            self.used[i] = 1
            self.traversal(candidates,target,curSum,i+1)
            self.path.pop() # 回溯,为了下一轮for loop
            curSum -= candidates[i] # 回溯,为了下一轮for loop
            self.used[i] = 0 # 回溯,为了下一轮for loop

3. 复杂度分析

子集问题分析:

  • 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

4. 思考与收获

  1. (二刷再看)这里直接用startIndex来去重也是可以的, 就不用used数组了。

    # **回溯+巧妙去重(省去使用used**
    class Solution:
        def __init__(self):
            self.paths = []
            self.path = []
    
        def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
            '''
            类似于求三数之和,求四数之和,为了避免重复组合,需要提前进行数组排序
            '''
            self.paths.clear()
            self.path.clear()
            # 必须提前进行数组排序,避免重复
            candidates.sort()
            self.backtracking(candidates, target, 0, 0)
            return self.paths
    
        def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:
            # Base Case
            if sum_ == target:
                self.paths.append(self.path[:])
                return
            
            # 单层递归逻辑
            for i in range(start_index, len(candidates)):
                # 剪枝,同39.组合总和
                if sum_ + candidates[i] > target:
                    return
                
                # 跳过同一树层使用过的元素
                if i > start_index and candidates[i] == candidates[i-1]:
                    continue
                
                sum_ += candidates[i]
                self.path.append(candidates[i])
                self.backtracking(candidates, target, sum_, i+1)
                self.path.pop()             # 回溯,为了下一轮for loop
                sum_ -= candidates[i]       # 回溯,为了下一轮for loop
    
    1. 本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于39.组合总和 难度提升了不少。
    2. 关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可。所以有必要把去重的这块彻彻底底的给大家讲清楚,就连“树层去重”和“树枝去重”都是Carl自创的词汇。

    Reference:代码随想录 (programmercarl.com)

    本题学习时间: 120分钟。


LeetCode131.分割回文串 

链接:131. 分割回文串 - 力扣(LeetCode) 

1. 思路

本题这涉及到两个关键问题:

  1. 切割问题,有不同的切割方式
  2. 判断回文

1.1 切割问题其实就是一种组合问题!

这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。一些同学可能想不清楚 回溯究竟是如何切割字符串呢?我们来分析一下切割,其实切割问题类似组合问题。

例如对于字符串abcdef:

组合问题,选取的是某个元素,分割问题,分割的是某段区间;

比如组合问题中abcdef中选a,再选d的情况,用分割问题的角度理解,就相当于在a后面切了一刀,再在d后面切一刀,代表的是这种切割情况(a| bcd | ef);

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

所以切割问题,也可以抽象为一棵树形结构,如图:

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的;

1.2 判断回文

在单层处理逻辑中,如果到了叶子节点,需要写一个函数判断,是否为回文字符串,这个过程相对容易,可以使用双指针法,一个指针从前向后,一个指针从后先前,如果前后指针所指向的元素是相等的,则是回文字符串,如果在相遇之前,两个指针对应的内容不一样,则返回false;

2. 代码实现

回溯三部曲

2.1 递归函数参数

全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里,但这里没这样做)

本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。

class Solution:
    def __init__(self):
        self.paths = []
        self.path = []

    def partition(self, s: str) -> List[List[str]]:
		def backtracking(self, s: str, start_index: int) -> None:

2.2 递归函数终止条件

从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。

那么在代码里什么是切割线呢?

 

在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是左边的切割线,在for循环中,还有一个i的值,这个i实际上是右边的切割线,这两个切割线中间的[ startIndex , i ]区间,就是新的子串;

def backtracking(self, s: str, start_index: int) -> None:
    # Base Case
    if start_index >= len(s):
        self.paths.append(self.path[:])
        return

2.3 单层搜索的逻辑

来看看在递归循环,中如何截取子串呢?

在 for i in range(start_index, len(s)): 循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串;

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

注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i+1;

for i in range(start_index, len(s)):
  # 此次比其他组合题目多了一步判断:
  # 判断被截取的这一段子串([start_index, i])是否为回文串
  if self.is_palindrome(s, start_index, i):
      self.path.append(s[start_index:i+1])
      self.backtracking(s, i+1)   
      # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
      self.path.pop()             # 回溯
  else:
      continue

2.4 判断回文子串

可以使用双指针法,一个指针从前向后,一个指针从后先前,如果前后指针所指向的元素是相等的,就是回文字符串了。

def is_palindrome(self, s: str, start: int, end: int) -> bool:
        i: int = start
        j: int = end
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

2.5 整体代码实现

class Solution:
    def __init__(self):
        self.paths = []
        self.path = []

    def partition(self, s: str) -> List[List[str]]:
        '''
        递归用于纵向遍历
        for循环用于横向遍历
        当切割线迭代至字符串末尾,说明找到一种方法
        类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线)
        '''
        self.path.clear()
        self.paths.clear()
        self.backtracking(s, 0)
        return self.paths

    def backtracking(self, s: str, start_index: int) -> None:
        # Base Case
        if start_index >= len(s):
            self.paths.append(self.path[:])
            return

        # 单层递归逻辑
        for i in range(start_index, len(s)):
            # 此次比其他组合题目多了一步判断:
            # 判断被截取的这一段子串([start_index, i])是否为回文串
            if self.is_palindrome(s, start_index, i):
                self.path.append(s[start_index:i+1])
                self.backtracking(s, i+1)   
								# 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
                self.path.pop()             # 回溯
            else:
                continue

    def is_palindrome(self, s: str, start: int, end: int) -> bool:
        i: int = start
        j: int = end
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

3. 复杂度分析

  • 时间复杂度:O(2^n),因为每一个元素的状态无外乎切割与不切割,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

4. 思考与收获

  1. (二刷再看)上面的代码还存在一定的优化空间, 在于如何更高效的计算一个子字符串是否是回文字串。上述代码isPalindrome函数运用双指针的方法来判定对于一个字符串s, 给定起始下标和终止下标, 截取出的子字符串是否是回文字串。但是其中有一定的重复计算存在:

    例如给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"而可以直接判定它一定不是回文字串。

    具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]s[1:n-1]是回文字串。

    大家如果熟悉动态规划这种算法的话, 我们可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在我们的回溯函数中直接查询即可, 省去了双指针移动判定这一步骤.

    class Solution {
    private:
        vector<vector<string>> result;
        vector<string> path; // 放已经回文的子串
        vector<vector<bool>> isPalindrome; // 放事先计算好的是否回文子串的结果
        void backtracking (const string& s, int startIndex) {
            // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
            if (startIndex >= s.size()) {
                result.push_back(path);
                return;
            }
            for (int i = startIndex; i < s.size(); i++) {
                if (isPalindrome[startIndex][i]) {   // 是回文子串
                    // 获取[startIndex,i]在s中的子串
                    string str = s.substr(startIndex, i - startIndex + 1);
                    path.push_back(str);
                } else {                                // 不是回文,跳过
                    continue;
                }
                backtracking(s, i + 1); // 寻找i+1为起始位置的子串
                path.pop_back(); // 回溯过程,弹出本次已经填在的子串
            }
        }
        void computePalindrome(const string& s) {
            // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 
            isPalindrome.resize(s.size(), vector<bool>(s.size(), false)); // 根据字符串s, 刷新布尔矩阵的大小
            for (int i = s.size() - 1; i >= 0; i--) { 
                // 需要倒序计算, 保证在i行时, i+1行已经计算好了
                for (int j = i; j < s.size(); j++) {
                    if (j == i) {isPalindrome[i][j] = true;}
                    else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);}
                    else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
                }
            }
        }
    public:
        vector<vector<string>> partition(string s) {
            result.clear();
            path.clear();
            computePalindrome(s);
            backtracking(s, 0);
            return result;
        }
    };
    
  2. 除了用函数在判断回文串,还可以用正反序来判断回文串,代码如下;

    class Solution:
        def __init__(self):
            self.paths = []
            self.path = []
    
        def partition(self, s: str) -> List[List[str]]:
            '''
            递归用于纵向遍历
            for循环用于横向遍历
            当切割线迭代至字符串末尾,说明找到一种方法
            类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线)
            '''
            self.path.clear()
            self.paths.clear()
            self.backtracking(s, 0)
            return self.paths
    
        def backtracking(self, s: str, start_index: int) -> None:
            # Base Case
            if start_index >= len(s):
                self.paths.append(self.path[:])
                return
    
            # 单层递归逻辑
            for i in range(start_index, len(s)):
                # 此次比其他组合题目多了一步判断:
                # 判断被截取的这一段子串([start_index, i])是否为回文串
                temp = s[start_index:i+1]
                if temp == temp[::-1]:  # 若反序和正序相同,意味着这是回文串
                    self.path.append(temp)
                    self.backtracking(s, i+1)   # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
                    self.path.pop()
                else:
                    continue
    
    
  3. 这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。那么难究竟难在什么地方呢?

    列出如下几个难点:

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

    **我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力;**一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。

  4. 本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割。如果意识到这一点,算是重大突破了。接下来就可以对着模板照葫芦画瓢;

  5. 但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了。关于模拟切割线,其实就是index是上一层已经确定了的分割线,i是这一层试图寻找的新分割线;

  6. 除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1;**所以本题应该是一个道hard题目了。

本题学习时间:120分钟

Reference:代码随想录 (programmercarl.com)


本篇学习时间为5个多小时,总结字数14000+;讲了两个组合问题和一个切割问题,切割问题的本质其实也是组合问题。(求推荐!)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/28520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过DewarpNet解决图片扭曲问题

一、论文 DewarpNet:使用堆叠的三维和二维回归网络进行单幅图像文件纠正 论文地址: https://paperswithcode.com/paper/dewarpnet-single-image-document-unwarping 代码地址: https://github.com/cvlab-stonybrook/DewarpNet 二、效果展示 2.1 论文图片效果展示 从上往下…

Android Material Design之SwitchMaterial(三)

老规矩先上图 引入 implementation com.google.android.material:material:1.4.0说明 该控件就是Switch控件的增强版本,属性基本一致 属性 属性描述android:text文本android:textOn滑块打开时显示的文本android:textOff滑块关闭时显示的文本android:thumb滑块图片app:trac…

这款比奥维地图还方便的地图软件!轻便好用,功能很强大

写方案或报告的人往往会遇到需要在地图上标注的问题。最简单的方法莫过于在百度上截图&#xff0c;然后在ppt里标注。现场勘察需要定位&#xff0c;最基本的方法是利用手机读取和记录经纬度坐标。但使用百度底图标注给客户的感觉太普通&#xff0c;而勘察现场记录的经纬度数据事…

深度分页、唯一索引的坑、分库分表、查询分离、连接池、bufferpool优化等

文章目录表过大深度分页count(*) 与 count(列名)唯一索引分库分表只分库不分表不分库只分表分库也分表查询分离使用方法查询分离的适用场景冷热分离适用场景实现方案数据库连接池优化主键无序buffer pool 太小MySQL频繁抖动的性能优化原因解决表过大 历史数据进行归档 深度分页…

需求开发到一半需要改别的分支的bug该怎么办呢?(git stash 和 git commit)

在实际开发中&#xff0c;经常我们会遇到需求开发到一半&#xff0c;别的分支有bug急需解决的情况&#xff0c;这个时候我们改怎么办呢&#xff1f; 有的人会说可以先提交当前分支的代码再切换到别的分支改bug&#xff0c;当然这样是没问题的&#xff0c;但是呢&#xff0c;在项…

【计算机毕业设计】1.房屋租赁系统

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘要 当今社会房屋租赁、出售买卖是必不可少的&#xff0c;人们不管走到哪里都需要有一个温馨的家&#xff0c;有一个落脚之地&#xff0c;所以房屋租赁、出售市场也是非常火爆&#xff01;不管是房屋租赁、出售、中…

基于conda的OpenCV库安装

基于conda的OpenCV库安装 OpenCV库的调用名是cv2 所以会看到这样的import语句 import cv2这句话就是对openCV库的调用 openCV库的下载安装 First 好像不需要像网上的教程那样, 首先去官网下载exe执行文件, 然后在VS中进行配置 直接在teminal中, 进入要指定的conda环境, …

编译原理实验--实验二 递归下降法判断算术表达式的正确性--Python实现

目录 一、实验目的和要求 二、实验内容 三、实验环境 四、实验步骤 1、语法分析所依据的文法&#xff1b; 2、给出消除左递归及提取左公因子的文法&#xff1b; 五、测试要求 六、实验步骤 1、语法分析所依据的文法 2、给出消除左递归及提取左公因子的文法&#xff1…

iNFTnews|国内数藏平台大撤退,寒冬之下海外市场是否有出路?

腾讯旗下继腾讯新闻、幻核之后&#xff0c;仅存的数藏平台也关停了。 11月16日&#xff0c;据界面新闻报道&#xff0c;腾讯TME旗下QQ音乐已经叫停“TME数字藏品”业务&#xff0c;原团队部分成员已内部活水。 一接近腾讯集团的知情人士称&#xff0c;腾讯方面曾对数字藏品业…

【Linux】生产者消费者模型

文章目录1.生产者消费者模型1.1生产者消费者模型的特点1.2生产者消费者模型的原则1.3生产者消费者模型的优点2.基于阻塞队列的生产者消费者模型2.1如何理解生产者消费者模型的并发&#xff1f;3.信号量3.1信号量接口3.2基于环形队列的生产者消费者模型3.3信号量和条件变量的区别…

Git_GitHub——基本操作、创建远程库、远程库操作、团队协作、SSH免密登录

网址:GitHub: Let’s build from here GitHub 目录 一、创建远程仓库 二、远程库操作 2.1 查看远程库别名 2.2 创建远程仓库别名 2.3 推送本地分支到远程仓库 2.4 拉取远程库到本地库 2.5 克隆远程库到本地 三、 跨团队协作 3.1 团队内协作 3.2 跨团队协作 四、SSH免密码登…

Go : golang发布三方包流程简介

文章目录一、创建项目仓库二、拉去仓库&#xff0c;编辑代码三、推送与发布代码四、使用发布的第三方包小结一、创建项目仓库 1.输入仓库的名字&#xff0c;我这里输入simpleExample&#xff0c;用来做演示 2.选择public&#xff0c;公开。要不并不好拉 3.选择需要添加的文件(…

用PyPy加速Python程序

用PyPy加速Python程序 在《Python性能优化指南–让你的Python代码快x3倍的秘诀》中有提到&#xff0c;我们可以用更好的Python运行环境或运行时优化来提升Python的速度&#xff0c;其中最成熟、使用最简单的当属PyPy。用PyPy&#xff0c;可以在不改变源代码的情况下&#xff…

二叉树相关OJ - C++

文章目录&#xff1a;根据二叉树创建字符串二叉树的层序遍历二叉树的最近公共祖先二叉搜索树与双向链表从前序与中序遍历序列构造二叉树从中序与后序遍历序列构造二叉树二叉树的前序遍历&#xff08;非递归&#xff09;二叉树的中序遍历&#xff08;非递归&#xff09;二叉树的…

【LeetCode与《代码随想录》】数组篇:做题笔记与总结-Java版

代码随想录地址 是学习过程中的笔记&#xff01;图来自代码随想录。 文章目录理论题目704. 二分查找35. 搜索插入位置34. 在排序数组中查找元素的第一个和最后一个位置69. x 的平方根367.有效的完全平方数理论 数组是存放在连续内存空间上的相同类型数据的集合。 数组下标都是…

[附源码]java毕业设计新能源汽车租赁管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

基于armv8的kvm实现分析(一)虚拟化介绍

本文基于以下软硬件假定&#xff1a; 架构&#xff1a;AARCH64 内核版本&#xff1a;5.14.0-rc5 1 什么是虚拟化 虚拟化就是把一台物理计算机虚拟成多台逻辑计算机&#xff0c;每台逻辑计算机里面可以运行不同操作系统&#xff0c;而相互之间不受影响&#xff0c;其典型架构…

面试了个 985 毕业的同学,回答“性能调优”题时表情令我毕生难忘

又逢“金九银十”&#xff0c;年轻的毕业生们满怀希望与忐忑&#xff0c;去寻找、竞争一个工作机会。已经在职的开发同学&#xff0c;也想通过社会招聘或者内推的时机争取到更好的待遇、更大的平台。 然而&#xff0c;面试人群众多&#xff0c;技术市场却相对冷淡&#xff0c;面…

JavaIO流:概述

在接触 IO 流前&#xff0c;无论是 变量的声明、数组的创建&#xff0c;又或者是复杂的并发设计还是 Jvm 的性能调优&#xff0c;我们更多的还是和内存打交道。但我们知道计算机组成包括运算器&#xff0c;控制器&#xff0c;存储器&#xff0c;输入设备&#xff0c;输出设备。…

springcloud4:服务注册中心Eureka

直接调用即可&#xff0c;为什么用Eureka什么是服务治理&#xff1f; 多个服务调用&#xff0c;需要有依赖中心管理什么是服务注册&#xff1f; 有一个注册中心&#xff0c;当服务器启动时&#xff0c;会把自己的信息注册到注册中心上什么是服务发现&#xff1f; Client通过注册…