题目来源:力扣(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(nn!),空间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