代码随想录算法训练营第二十八天|39.组合总和 40.组合总和II 131.分割回文串

39.组合总和

思路:

由于元素互不重复,而且都是正整数,说明不需要考虑负数和零的情况(难度大大减低),首先将数组排序成递增数组,由于可以重复使用元素,所以每次递归都从上一次遍历的元素开始遍历递归,不从数组头部的元素开始而从上一次遍历的元素开始的原因是,在第一次遍历的时候已经遍历完了需要取用【最开始的元素】的所有情况,所以如果从最开始的元素开始,只会重复,之后遍历过的元素也是同理。每次遍历都把该元素进行加和判断是否小于target,如果符合则继续遍历,如果等于target说明找到符合的结果,压入结果数组。大于target则直接返回。

由于是排序过的递增数组,可以进行剪枝:如果加和已经大于target,后面的元素加和肯定也会大于target,此时可以直接返回。

class Solution:

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:

        self.target = target

        self.result = []

        candidates.sort() # 有序数组可便于剪枝

        self.backtracking(candidates, 0, [], 0)

        return self.result



    def backtracking(self, arr, start, path, cur) -> None:

        for i in range (start, len(arr)):

            if cur+arr[i] < self.target:

                path.append(arr[i])

                self.backtracking(arr, i, path[:], cur+arr[i])

                path.pop()

            elif cur+arr[i] == self.target:

                path.append(arr[i])

                self.result.append(path[:]) # 记得加[:]

                path.pop()

                return

            else:       # 有序数组剪枝:大于target直接break

                return

规范代码:

回溯剪枝(版本二)

class Solution:

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:

        result =[]

        candidates.sort()

        self.backtracking(candidates, target, 0, [], result)

        return result

    def backtracking(self, candidates, target, startIndex, path, result):

        if target == 0:

            result.append(path[:])

            return



        for i in range(startIndex, len(candidates)):

            if target - candidates[i]  < 0:

                break

            path.append(candidates[i])

            self.backtracking(candidates, target - candidates[i], i, path, result)

            path.pop()

40.组合总和II

思路:

自己一开始没有想出什么思路,想要套用上一题的代码,然后进行去重复元素的操作来进行修改,但后面发现始终改不对,对照解析发现是去重的条件错误。

for i in range (start, len(arr)):
           if i>start and arr[i]==arr[i-1]:    # start是本轮递归遍历起始点(结果路径的该位置的第一种情况),
               # 如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue
               continue


这部分代码的条件自己写成了

if i>0 and arr[i]==arr[i-1]:

究其原因是没完全理解去重的过程和核心:start是本轮递归遍历起始点(结果路径的该位置的第一种情况),如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue。

如果按照自己的写法,那只是将重复的元素去除了。

以下是学习的总结:

如果arr[i]==arr[i-1]且arr[i-1]的used为0,也就是本次遍历中,与本元素相同的前一个元素并未在当前的path中使用,这说明本次遍历中【选用本元素】的情况,已经在上一次【选用上一个相同元素】的遍历情况中重复了。所以当与本元素相同的前一个元素】并未在当前的path中使用时,该情况不可以再重复,应该跳过,直接continue。

而used[i - 1] == true,说明是进入下一层递归,去下一个数。

更详细可见:

https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html#%E6%80%9D%E8%B7%AF

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

此时for循环里就应该做continue的操作。

这块比较抽象,如图:

40.组合总和II1

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

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

可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。

而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:

另外一种思路去重:

与上一题类似,但start是本轮递归遍历起始点(结果路径的该位置的第一种情况),如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue

代码实现如下:

class Solution:
   def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
       self.target = target
       self.result = []
       candidates.sort()
       self.backtracking(candidates, 0, [], 0)
       return self.result

   def backtracking(self, arr, start, path, cur) -> None:
       for i in range (start, len(arr)):
           if i>start and arr[i]==arr[i-1]:    # start是本轮递归遍历起始点(结果路径的该位置的第一种情况),
               # 如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue
               continue
           if cur+arr[i] < self.target:
               path.append(arr[i])
               self.backtracking(arr, i+1, path[:], cur+arr[i])
               path.pop()
           elif cur+arr[i] == self.target:
               path.append(arr[i])
               self.result.append(path[:]) # 记得加[:]
               path.pop()
               return
           else:       # 有序数组剪枝:大于target直接break
               return

规范代码:使用used

class Solution:

    def backtracking(self, candidates, target, total, startIndex, used, path, result):

        if total == target:

            result.append(path[:])

            return



        for i in range(startIndex, len(candidates)):

            # 对于相同的数字,只选择第一个未被使用的数字,跳过其他相同数字

            if i > startIndex and candidates[i] == candidates[i - 1] and not used[i - 1]:

                continue



            if total + candidates[i] > target:

                break



            total += candidates[i]

            path.append(candidates[i])

            used[i] = True

            self.backtracking(candidates, target, total, i + 1, used, path, result)

            used[i] = False

            total -= candidates[i]

            path.pop()



    def combinationSum2(self, candidates, target):

        used = [False] * len(candidates)

        result = []

        candidates.sort()

        self.backtracking(candidates, target, 0, 0, used, [], result)

        return result

回溯优化

class Solution:

    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:

        candidates.sort()

        results = []

        self.combinationSumHelper(candidates, target, 0, [], results)

        return results



    def combinationSumHelper(self, candidates, target, index, path, results):

        if target == 0:

            results.append(path[:])

            return

        for i in range(index, len(candidates)):

            if i > index and candidates[i] == candidates[i - 1]:

                continue  

            if candidates[i] > target:

                break  

            path.append(candidates[i])

            self.combinationSumHelper(candidates, target - candidates[i], i + 1, path, results)

            path.pop()

131.分割回文串

思路:

自己的大致递归思路是有的,但如何分割,分割后的具体操作自己其实想的不够透彻,还是看了文字解析之后才动手做的。

判断回文字符串很简单,尤其python只需要倒序判断是否相等即可。但重点在于如何递归切割呢?其实开始下一次递归回溯的函数调用就是一次切割,只有已遍历的字符串符合回文串的条件才能进行下一次切割,当所有切割都满足条件的时候,就是一种我们要的结果。当有一个不满足,说明切割的位置不正确,继续往后切就可以了。选取切割的位置是由for来挨个尝试的,而实际进行切割的操作是由调用函数实现的,当切割的位置是字符串的末尾,说明切割的所有都正确,此时压入结果数组。

代码实现如下:

class Solution:

    def partition(self, s: str) -> List[List[str]]:

        self.result = []

        self.backtracking(s, 0, [])

        return self.result



    def backtracking(self, s:str, start:int, path:List) -> None:

        if start >= len(s):

            self.result.append(path[:])

            return



        for i in range(start, len(s)):

            if s[start:i+1] == s[start:i+1][::-1]:

                path.append(s[start:i+1])

                self.backtracking(s, i+1, path)

                path.pop()

            else:

                continue

规范代码:

回溯+优化判定回文函数

class Solution:



    def partition(self, s: str) -> List[List[str]]:

        result = []

        self.backtracking(s, 0, [], result)

        return result



    def backtracking(self, s, start_index, path, result ):

        # Base Case

        if start_index == len(s):

            result.append(path[:])

            return

        

        # 单层递归逻辑

        for i in range(start_index, len(s)):

            # 若反序和正序相同,意味着这是回文串

            if s[start_index: i + 1] == s[start_index: i + 1][::-1]:

                path.append(s[start_index:i+1])

                self.backtracking(s, i+1, path, result)   # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串

                path.pop()             # 回溯

  

回溯+高效判断回文子串

class Solution:

    def partition(self, s: str) -> List[List[str]]:

        result = []

        isPalindrome = [[False] * len(s) for _ in range(len(s))]  # 初始化isPalindrome矩阵

        self.computePalindrome(s, isPalindrome)

        self.backtracking(s, 0, [], result, isPalindrome)

        return result



    def backtracking(self, s, startIndex, path, result, isPalindrome):

        if startIndex >= len(s):

            result.append(path[:])

            return



        for i in range(startIndex, len(s)):

            if isPalindrome[startIndex][i]:   # 是回文子串

                substring = s[startIndex:i + 1]

                path.append(substring)

                self.backtracking(s, i + 1, path, result, isPalindrome)  # 寻找i+1为起始位置的子串

                path.pop()           # 回溯过程,弹出本次已经添加的子串



    def computePalindrome(self, s, isPalindrome):

        for i in range(len(s) - 1, -1, -1):  # 需要倒序计算,保证在i行时,i+1行已经计算好了

            for j in range(i, len(s)):

                if j == i:

                    isPalindrome[i][j] = True

                elif j - i == 1:

                    isPalindrome[i][j] = (s[i] == s[j])

                else:

                    isPalindrome[i][j] = (s[i] == s[j] and isPalindrome[i+1][j-1])

回溯+使用all函数判断回文子串

class Solution:

    def partition(self, s: str) -> List[List[str]]:

        result = []

        self.partition_helper(s, 0, [], result)

        return result



    def partition_helper(self, s, start_index, path, result):

        if start_index == len(s):

            result.append(path[:])

            return



        for i in range(start_index + 1, len(s) + 1):

            sub = s[start_index:i]

            if self.isPalindrome(sub):

                path.append(sub)

                self.partition_helper(s, i, path, result)

                path.pop()



    def isPalindrome(self, s):

        return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))

回溯+使用all函数判断回文子串
关于all函数:

Python的内置函数all,它接受一个可迭代对象作为参数。如果可迭代对象中的所有元素都为True,则all函数返回True;如果有任何元素为False,则返回False

class Solution:

    def partition(self, s: str) -> List[List[str]]:

        result = []

        self.partition_helper(s, 0, [], result)

        return result



    def partition_helper(self, s, start_index, path, result):

        if start_index == len(s):

            result.append(path[:])

            return



        for i in range(start_index + 1, len(s) + 1):

            sub = s[start_index:i]

            if self.isPalindrome(sub):

                path.append(sub)

                self.partition_helper(s, i, path, result)

                path.pop()



    def isPalindrome(self, s):

        return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))

第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float(&#39;inf&#39;) total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float(&#39;inf&#39;) else 0 ``` 以上就是第二十二算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值