代码随想录算法训练营Day27 | 39. 组合总和、40.组合总和II、131.分割回文串 | Python | 个人记录向

39. 组合总和

代码随想录:39. 组合总和
Leetcode:39. 组合总和

做题

无思路。

看文章

看了下思路,思路图如下:
在这里插入图片描述

自己动手实现了一下,AC代码如下:

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        self.res = []
        self.path = []
        candidates.sort()
        self.size = len(candidates)
        self.backtracking(candidates, target, 0, 0)
        return self.res
        
        
    def backtracking(self, candidates, target, cur, start):
        if cur > target:
            return
        if cur == target:
            self.res.append(self.path[:])
            return
        
        for i in range(start, self.size):
            cur += candidates[i]
            if cur > target:
                cur -= candidates[i]
                break
            self.path.append(candidates[i])
            self.backtracking(candidates, target, cur, i)
            cur -= candidates[i]
            self.path.pop()

时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
空间复杂度: O(target),大概理解为:假如target=100,那么最多就是100个1相加,即需要100个空间(bit?)

40.组合总和II

代码随想录:40.组合总和II
Leetcode:40.组合总和II

做题

不会去重。

看文章

所谓去重,其实就是使用过的元素不能重复选取。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
因此,需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。此时for循环里就应该做continue的操作。
在这里插入图片描述在candidates[i] == candidates[i - 1]相同的情况下:

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

使用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

其实,用 i 和 start 的关系就能去重,具体代码如下:

class Solution:
    def backtracking(self, candidates, target, total, startIndex, 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]:
                continue

            if total + candidates[i] > target:
                break

            total += candidates[i]
            path.append(candidates[i])
            self.backtracking(candidates, target, total, i + 1, path, result)
            total -= candidates[i]
            path.pop()

    def combinationSum2(self, candidates, target):
        result = []
        candidates.sort()
        self.backtracking(candidates, target, 0, 0, [], result)
        return result

理解起来,还是遵循同一思路:在同一树层内不选重复的。
时间复杂度: O(n * 2^n)
空间复杂度: O(n)

131.分割回文串

代码随想录:131.分割回文串
Leetcode:131.分割回文串

做题

无思路。

看文章

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

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

切割问题类似组合问题。例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。

基本解法

class Solution:

    def partition(self, s: str) -> List[List[str]]:
        '''
        递归用于纵向遍历
        for循环用于横向遍历
        当切割线迭代至字符串末尾,说明找到一种方法
        类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线)
        '''
        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)):
            # 此次比其他组合题目多了一步判断:
            # 判断被截取的这一段子串([start_index, i])是否为回文串
            if self.is_palindrome(s, start_index, i):
                path.append(s[start_index:i+1])
                self.backtracking(s, i+1, path, result)   # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
                path.pop()             # 回溯


    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 

时间复杂度: O(n * 2^n)
空间复杂度: O(n^2)

更简洁的回文串判断方法(python):

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

或用all函数:

def isPalindrome(self, s): # s为子串
	return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))

用动态规划优化回文串判断

例如给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"而可以直接判定它一定不是回文字串。具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]且s[1:n-1]是回文字串。
如果熟悉动态规划这种算法的话, 可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在回溯函数中直接查询即可, 省去了双指针移动判定这一步骤。
具体代码如下:

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])

以往忽略的知识点小结

  • 组合问题用回溯
  • 组合问题去重:同一树层不取重复值
  • 切割问题用回溯
  • 回文判断

个人体会

完成时间:3h。
心得:题比较难,还需要熟悉什么时候、如何使用回溯,如何去重、如何切割更是难题。另外,还要学一下动态规划的思路来提前计算。

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值