leetcode 46 和47 . 全排列,两种生成树的方案来解决全排列问题

题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems
https://www.nowcoder.com/activity/oj
特别鸣谢:来自夸夸群的 醉笑陪公看落花@知乎王不懂不懂@知乎QFIUNE@csdn
感谢醉笑陪公看落花@知乎 倾囊相授,感谢小伙伴们督促学习,一起进步


tips

  • python中传参是形参,如int,tuple等,但是遇到动态数组或集合就是传递的实参,如set和list,dict

  • 动态数组无法被hash化,所以无法加入集合,转为tuple形式即可

  • 深度优先搜索(DFS) 的一般框架,参考博文

    
    void dfs()   //参数的个数根据实际情况确定
    {  
        if(终止条件)  {  
            //根据题意添加  
            return;  
        }  
     
        for(扩展方式)  
        {  
            if(扩展方式所达到状态合法)  
            {  
                操作;  
                dfs();  
                回溯;   # 主要是让系统状态回到 “操作” 步骤之前
            }  
     
        }  
    

leetcode 46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations

把寻找全排列的过程,看作一棵树的生成过程。
【方法1】空集作为根节点,每次加入一个之前(父节点,祖先节点等)未使用的元素,直到元素被使用完。 时间复杂度O(n!n^2) 空间O(n+n!)
【方法2】每一层对原数组进行交换,i=0时,第一层为原数组,此时交换i号位置和i号及以后位置的数据(j= 0,1,2),i= 1时,第二层是由第一层交换之后得到的,继续进行交换,交换i号位置和i号及以后位置的数据(j= 1,2),当i= len(nums)-2的时候,把交换后的集合保存下来。 时间复杂度O(n
n!),空间O(n!)

【方法1】是对队列进行pop和append,与【方法1】相比,【方法2】的每一个节点,都是来自原数组的修改,没有后再额外申请变量,因此占用时间更少。

在这里插入图片描述
扩展: 方法2 只看交换之前的内容 nums[:i] ,可以 视作求子集过程
实现见 leetcode 446. 等差数列划分 II - 子序列 - 困难题目- 官方题解 - 动态规划 - DFS

红色框中表示求得的子集,求得的子集有重复的,可以剪枝避免重复计算

在这里插入图片描述

用方法1 构建树

广度优先

逐层遍历

from copy import deepcopy
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        self.nums = nums
        self.all_subs = []
        self.BFS()
        return self.all_subs
    def BFS(self):
        from queue import Queue
        q = Queue()
        q.put([])
        while(not q.empty()):
            node = q.get()
            if len(node)==len(self.nums):
                self.all_subs.append(node[:])
                continue
            for i in range(len(self.nums)):
                if self.nums[i] in node:continue
                node.append(self.nums[i])
                q.put(node[:])
                node.pop()

在这里插入图片描述

循环体内判断某个元素是否在node集合中比较费时

self.nums[i] in node

深度优先+候选者数组

每使用一个元素,候选者数组丢弃一个元素,从而保证self.nums[i] not in node
尝试用深度优先来做

from copy import deepcopy
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:

        self.nums = nums
        self.all_subs = []
        self.DFS(nums[:],[])
        return self.all_subs
    def DFS(self,candidates,subset):
        if len(subset)==len(self.nums):
            self.all_subs.append(subset[:])
            return
        n = len(candidates)
        for i in range(n):
            elem = candidates.pop(0)
            subset.append(elem)
            self.DFS(deepcopy(candidates),subset)
            candidates.append(elem)
            subset.pop()


候选者数组涉及频繁的删除和插入操作,会导致低效率,经观察发现,候选者数组,用set实现的时候,会有一点问题见后文【候选者数组用set实现】
由于set集合每次pop出来的元素是随机的,不可控因素太大,因此弃用

尤其是删除数组的第一个元素,会导致后面的元素全部向前移动一位,解决这个问题可以用队列来优化,但是队列也频繁地插入和删除,依然会浪费很多时间。

下文用访问数组来标记元素是否被使用过,每次对访问数组的值进行修改即可

在这里插入图片描述

深度优先+访问数组

from copy import deepcopy
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        self.nums = nums
        self.all_subs = []
        self.DFS([0 for i in range(len(nums))],[])
        return self.all_subs
    def DFS(self,visited,subset):
        if len(subset)==len(self.nums):
            self.all_subs.append(subset[:])
            return
        for i in range(len(self.nums)):
            if visited[i] == 1:continue
            visited[i] = 1
            elem = self.nums[i]
            subset.append(elem)
            self.DFS(visited,subset)
            subset.pop()
            visited[i] = 0

在这里插入图片描述

用方法2构建树

深度优先搜索

from copy import deepcopy
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        self.nums = nums
        self.all_subs = []
        self.DFS(0)
        return self.all_subs

    def DFS(self,i):
        if i == len(self.nums)-1:
            self.all_subs.append(self.nums[:])
            return
        for j in range(i,len(self.nums)):
            self.nums[i],self.nums[j] = self.nums[j],self.nums[i]
            self.DFS(i+1)
            self.nums[i],self.nums[j] = self.nums[j],self.nums[i]

效率
在这里插入图片描述

广度优先搜索

需要确定所在的层数,从而决定i的值,一层是否遍历结束不太好确定

'''
交换 DFS

长度为3
i=0 n_ndoe = 1 j = 0,1,2
i=1 n_node = n_ndoe * len(j) = 1*3 = 3, j= 1,2
i=2 n_node = n_node * len(j) = 3 * 2 = 6 , j = 2
'''
class Solution:
    def permute(self,nums):
        self.nums = nums
        self.all_subs = []
        self.BFS()
        return self.all_subs
    
    def BFS(self):
        from queue import Queue
        q = Queue()
        q.put(self.nums)
        i = 0
        n_node = len(self.nums) # 第i+1层结点数目
        if n_node ==1:
            self.all_subs.append(self.nums[0])
            return
        while not q.empty():
            subset = q.get()
            for j in range(i,len(subset)):
                subset[i],subset[j] = subset[j],subset[i]
                if i == len(self.nums)-2:self.all_subs.append(subset[:])
                else:
                    q.put(subset[:])
                subset[i],subset[j] = subset[j],subset[i]
            if q.qsize() == n_node:
                i+=1
                n_node = n_node * (len(self.nums) - i)

在这里插入图片描述

候选者数组用set实现的问题

当 nums = [0,-1,1] 时,pycharm结果和提交的结果不一致

'''
pycharm

# DFS  {0, 1, -1}, DFS  {1, -1}, DFS  {-1}, DFS  set(),[0, 1, -1]
# DFS  {1}, DFS  set(),[0, -1, 1]
# DFS  {0, -1}, DFS  {-1}, DFS  set(),[1, 0, -1]
# DFS  {0}, DFS  set(),[1, -1, 0]  ##### 此处pop的是-1  
# DFS  {0, 1}, DFS  {1}, DFS  set(),[-1, 0, 1]
# DFS  {0}, DFS  set(),[-1, 1, 0]

leetcode

# DFS  {0, 1, -1},DFS  {1, -1},DFS  {-1},DFS  set(),[0, 1, -1]
# DFS  {1},DFS  set(),[0, -1, 1]
# DFS  {0, -1},DFS  {-1},DFS  set(),[1, 0, -1]
# DFS  {-1},DFS  set(),[1, 0, -1]  ##### 此处pop的是0                                
# DFS  {0, 1},DFS  {1},DFS  set(),[-1, 0, 1]
# DFS  {0},DFS  set(),[-1, 1, 0]
'''
from copy import deepcopy


class Solution:
    def permute(self,nums):
        self.nums = nums
        self.all_subs = []
        self.DFS(set(nums),[])
        return self.all_subs
    def DFS(self,candidates,subset):
        if len(subset)==len(self.nums):
            self.all_subs.append(subset[:])
            return
        n = len(candidates)
        for i in range(n):
            elem = candidates.pop()
            subset.append(elem)
            self.DFS(deepcopy(candidates),subset)
            candidates.add(elem)
            subset.pop()

pycharm上的结果

在这里插入图片描述
leetcode 上 测试用例结果
在这里插入图片描述
猜想原因是 set是一个无序的集合,每次pop的内容是随机的。

在pycharm和leetcode中,当键都是数值的话,都是按照键值从小到大来pop的,首先pop的是非负数从小到大,然后是负数从小到大。

   nums = [0,-1,1,2,-5]

   myset = set(nums)
   while(myset):
       print(myset.pop(),end=',')
   print('\n###############')
   myset = set(nums)
   for x in nums:
       myset.add(x)
   while(myset):
       print(myset.pop(),end=',')
0,1,2,-5,-1,
###############
0,1,2,-5,-1,

而在实际调试中发现,pycharm在遇到{-1,0} 的时候,第一次pop的是0,第二次pop的是-1

在这里插入图片描述

说明前面按照键大小pop的想法是有问题的,事实上set每次pop的内容是随机的,会导致一些元素反复被pop出来,一些元素始终没有被pop。

leetcode 47. 全排列 II 包含重复数字序列,返回不重复全排列

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

示例 1:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations-ii

在方法1构建树(元素加入结点集中的方式构建树)的基础上,加入备忘录进行剪枝即可,将树的每一个结点加入备忘录中,相同结点集合直接返回,不再遍历

在这里插入图片描述
关键代码

if tuple(subset) in self.memor:return
self.memor.add(tuple(subset[:]))
class Solution:
    def permuteUnique(self,nums):
        self.nums = nums
        self.all_subs = []
        self.memor = set()
        self.DFS([0 for i in range(len(nums))],[])
        return self.all_subs

    def DFS(self,visited,subset):
        if tuple(subset) in self.memor:return
        self.memor.add(tuple(subset[:]))
        if len(subset)==len(self.nums):
            self.all_subs.append(subset[:])
            return
        for i in range(len(self.nums)):
            if visited[i] == 1:continue
            visited[i] = 1
            elem = self.nums[i]
            subset.append(elem)
            self.DFS(visited,subset)
            subset.pop()
            visited[i] = 0

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值