Day29 算法学习|回溯算法05

题目 491.递增子序列

问题描述

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:

给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。

解题思路

  • 首先本体求的是递增的子集,是不能先把集合sort的,因为这样后子集就会变化。
  • 画出本题的树形结构,
  • 这道题中我们关心的是不能使用重复数值,只要使用过了就不行,所以按照元素的值设置used=[0]*201, 而不是组合总和2中,因为sort过后就要消除相邻位置的相同元素,当nums[i]=nums[i-1]而且nums[i]==0,就跳过。

这里是引用

  • 从图中可以看到,去重有点类似上一题。对层去重,对树枝不去重。

一些代码问题

  • continue
    遇到某些条件时,加上continue就是跳过的意思,本体用来跳过递减等情况。
  • hashset的索引
    本题第二种方法采用hashmap去重
    • hash[0]*201,因为本题nums中的i取值范围为-100,100,一共201个数。
    • 如果nums[i]=1,我们希望的是hash[101]=1。1在这个set当中的第101个,,就直接hash[nums[i]+100]=1。 同理nums[i]=44,那就hash[nums[i]+100]=1.
  • 为什么回溯的时候不对set进行操作? 首先set的设置就是为了过滤同层相同元素,但是回溯相当于还在一个树枝,从下往上。
  • 为什么终止条件那里不加return
    if len(path) > 1: result.append(path[:]) 这段代码的目的是将长度大于1的递增子序列添加到 result 中。如果在这段代码后面加上 return,那么当找到一个长度大于1的递增子序列时,就会立即结束当前的函数调用,返回到上一层的函数调用。
  • 回溯以后不对used对应值进行重设,因为只排出同层中的相同元素,不同层间used会被重设一次,本题的used定义在backtracking函数中。但是组合总和的used定义在主函数中,每一次回溯都是总体改变。

代码

  • 回溯 利用set去重

class Solution:
    def findSubsequences(self, nums):
        result = []
        path = []
        self.backtracking(nums, 0, path, result)
        return result
    
    def backtracking(self, nums, startIndex, path, result):
        if len(path) > 1:
            result.append(path[:])  # 注意要使用切片将当前路径的副本加入结果集
            # 注意这里不要加return,要取树上的节点
        
        uset = set()  # 使用集合对本层元素进行去重
        for i in range(startIndex, len(nums)):
            if (path and nums[i] < path[-1]) or nums[i] in uset:
                continue
            
            uset.add(nums[i])  # 记录这个元素在本层用过了,本层后面不能再用了
            path.append(nums[i])
            self.backtracking(nums, i + 1, path, result)
            path.pop()

  • 回溯 利用哈希表去重

class Solution:
    def findSubsequences(self, nums):
        result = []
        path = []
        self.backtracking(nums, 0, path, result)
        return result

    def backtracking(self, nums, startIndex, path, result):
        if len(path) > 1:
            result.append(path[:])  # 注意要使用切片将当前路径的副本加入结果集
        
        used = [0] * 201  # 使用数组来进行去重操作,题目说数值范围[-100, 100]
        for i in range(startIndex, len(nums)):
            if (path and nums[i] < path[-1]) or used[nums[i] + 100] == 1:
                continue  # 如果当前元素小于上一个元素,或者已经使用过当前元素,则跳过当前元素
            
            used[nums[i] + 100] = 1  # 标记当前元素已经使用过
            path.append(nums[i])  # 将当前元素加入当前递增子序列
            self.backtracking(nums, i + 1, path, result)
            path.pop()

复杂度分析

  • 时间复杂度 O(n^n)

      最坏情况下每层n个不同元素,深度为n
      但是实际情况都会比这个小
    
  • 空间复杂度 O(2^n)

      长度为n的nums,最坏情况path数组最多会有2的n次方个nums的子序列。
    

题目 46.全排列 (回溯部分有点不懂)

问题描述

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

解题思路

  • 对于这个问题,我们需要找到数组中所有的排列,也就是长度等于原数组的子序列。
  • 这些子序列必须包含原数组中的所有元素,且每个元素在子序列中只出现一次。
  • 我们在每个节点选择一个还没有被选择过的数,然后进入下一层递归。在回溯的过程中,我们使用一个 used 数组来记录已经被选择的数。
  • 和题目491的区别在:
    • 每层都是从0开始搜索而不是startIndex
      需要used数组记录path里都放了哪些元素了
  • 树形结构:

这里是引用## 代码

  • 用等于限制,在特定条件下要把回溯全部放在if里面。
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        used=[0]*len(nums)
        result=[]
        self.backtracking(nums,[],result,used)
        return result
    def backtracking(self,nums,path,result,used):
    #设计一下,用used全为0的初始list且长度等于nums,将用过的数字对应的index加1
        #end logic
        if len(path)==len(nums):
            result.append(path[:])
            return
        #single logic
        for i in range(len(nums)):
            if used[i]==1:
                continue
            path.append(nums[i])
            used[i]=1
            self.backtracking(nums,path,result,used)
            #backtracking
            path.pop()
            used[i]=0

  • 用不等于限制
		if used[i]=1:
            path.append(nums[i])
            used[i]=1
            self.backtracking(nums,path,result,used)
            #backtracking
            path.pop()
            used[i]=0

复杂度分析

  • 时间复杂度O(n!)

    • 每个元素在结果中都有 n!(n的阶乘)种可能的排列方式,所以总的时间复杂度为O(n!)。
  • 空间复杂度O(n)

    • 对于每一次递归调用,我们都会创建一个新的 path 变量(这个 path 是旧的 path 的复制并添加了一个元素)。当递归深度最大为n时,空间复杂度为O(n)。

题目 47.全排列 II

问题描述

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:

1 <= nums.length <= 8
-10 <= nums[i] <= 10

解题思路

  • 本题和全排列46相似,差别在于
    • 本题nums中包含重复的数字
  • 在去重部分先对nums进行排序,让一会儿了去同数字更加简便
  • 注意在去重部分,nums【i】==nums【i-1】还要and两个条件:
    • i是大于0的
    • 前一个相同的数字而且还没被使用过·,即在同一层,如果是同一列被用过那就不用去重。
      在这里插入图片描述

代码


注意是前一个元素为0的意思是前一个元素刚被回溯上来所以used等于0!!因为前一个元素等于1的话是在同一支。

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        nums.sort()  # 排序
        result=[]
        hashset=[0]*len(nums)
        self.backtracking(nums,[],result,hashset)
        return result

    def backtracking(self,nums,path,result,hashset):
        #end logic
        if len(path)==len(nums):
            result.append(path[:])
            return
        #single logic
        for i in range(len(nums)):
            if hashset[i]!=0:
                continue
            if i>0 and nums[i]==nums[i-1] and hashset[i-1]==0:
                continue
            path.append(nums[i])
            hashset[i]=1
            self.backtracking(nums,path,result,hashset)
        #backtracking
            path.pop()
            hashset[i]=0


复杂度分析

  • 时间复杂度 O(n!)

  • 空间复杂度O(n)

  • 如果考虑到结果空间,空间复杂度会是O(n*n!),因为最坏的情况下,我们需要存储n!个长度为n的排列。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值