深度优先搜索、回溯

题目

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

题解

方法一:递归回溯

思路与算法

组合:不计较排列顺序,也即[1,2]和[2,1]为同一组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution:
def combine(self,n,k):# n:int k:int 返回值:List[List[]]
res=list()
if k<=0 or n<k:
return res

# 从1开始是题目的设定
path =list()
self.dfs(n,k,1,path,res)
return res
def dfs(self,n,k,begin,path,res):

# 递归终止条件:path长度等于k
if len(path)==k:
res.append(path[:])# 由于path递归结束为空,这里res添加的是地址,故这里拷贝一下
return

# 遍历所有可能的搜索起点
for i in range(begin,n+1):
# 向路径变量添加一个数
path.append(i)
print("递归前-->:",path)
# 进行下一轮搜索,设置的搜索起点加1,因为组合数不允许出现重复的元素
self.dfs(n,k,i+1,path,res)
# 深度优先遍历有回去的过程,因此递归之前做了什么,递归之后需要形同的操作的逆向操作
path.pop()
print("递归后-->:",path)
# print("i3:",i)
#print("t3:",path)
1
2
3
4
a=Solution()
n=4
k=3
a.combine(n,k)
递归前-->: [1]
递归前-->: [1, 2]
递归前-->: [1, 2, 3]
递归后-->: [1, 2]
递归前-->: [1, 2, 4]
递归后-->: [1, 2]
递归后-->: [1]
递归前-->: [1, 3]
递归前-->: [1, 3, 4]
递归后-->: [1, 3]
递归后-->: [1]
递归前-->: [1, 4]
递归后-->: [1]
递归后-->: []
递归前-->: [2]
递归前-->: [2, 3]
递归前-->: [2, 3, 4]
递归后-->: [2, 3]
递归后-->: [2]
递归前-->: [2, 4]
递归后-->: [2]
递归后-->: []
递归前-->: [3]
递归前-->: [3, 4]
递归后-->: [3]
递归后-->: []
递归前-->: [4]
递归后-->: []





[[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]

递归优化,剪枝

例如:n = 6 ,k = 4。

path.size() == 1 的时候,接下来要选择 3个数,搜索起点最大是 4,最后一个被选的组合是 [4, 5, 6];
path.size() == 2 的时候,接下来要选择 2 个数,搜索起点最大是 5,最后一个被选的组合是 [5, 6];
path.size() == 3 的时候,接下来要选择 1个数,搜索起点最大是 6,最后一个被选的组合是 [6]

可以归纳出:

搜索起点的上界 + 接下来要选择的元素个数 - 1 = n

也即搜索上界:

搜索上界=n-(k-len(path))+1

我们将range(begin,n+1)改为range(begin,n-(k-len(path))+1+1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution:
def combine(self,n,k):# n:int k:int 返回值:List[List[]]
res=list()
if k<=0 or n<k:
return res

# 从1开始是题目的设定
path =list()
self.dfs(n,k,1,path,res)
return res
def dfs(self,n,k,begin,path,res):

# 递归终止条件:path长度等于k
if len(path)==k:
res.append(path[:])# 由于path递归结束为空,这里res添加的是地址,故这里拷贝一下
return

# 遍历所有可能的搜索起点
for i in range(begin,n-(k-len(path))+2):
# 向路径变量添加一个数
path.append(i)
print("递归前-->:",path)
# 进行下一轮搜索,设置的搜索起点加1,因为组合数不允许出现重复的元素
self.dfs(n,k,i+1,path,res)
# 深度优先遍历有回去的过程,因此递归之前做了什么,递归之后需要形同的操作的逆向操作
path.pop()
print("递归后-->:",path)
1
2
3
4
a=Solution()
n=4
k=3
a.combine(n,k)
递归前-->: [1]
递归前-->: [1, 2]
递归前-->: [1, 2, 3]
递归后-->: [1, 2]
递归前-->: [1, 2, 4]
递归后-->: [1, 2]
递归后-->: [1]
递归前-->: [1, 3]
递归前-->: [1, 3, 4]
递归后-->: [1, 3]
递归后-->: [1]
递归后-->: []
递归前-->: [2]
递归前-->: [2, 3]
递归前-->: [2, 3, 4]
递归后-->: [2, 3]
递归后-->: [2]
递归后-->: []





[[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]

方法二:使用itertools.combinations库

测试发现使用该库的时间效率比较高,故下列是对源码的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def combinations(iterable, r):
# combinations('ABCD', 2) --> AB AC AD BC BD CD
# combinations(range(4), 3) --> 012 013 023 123

pool = tuple(iterable) # tuple([1,2])=(1,2) tuple("abc")=('a', 'b', 'c')
n = len(pool)


if r > n:
return
indices = list(range(r)) # list(range(2))=[0,1]
yield tuple(pool[i] for i in indices) # yield生成器,下次从这里继续
while True:
for i in reversed(range(r)):
if indices[i] != i + n - r:
break
else:
return
indices[i] += 1
for j in range(i+1, r):
indices[j] = indices[j-1] + 1
yield tuple(pool[i] for i in indices)
1
2
nums=[1,2,3]
list(combinations(nums,2))
[(1, 2), (1, 3), (2, 3)]
1
import itertools
1
2
3
4
def combine(n,k):
return list(itertools.combinations(range(1,n+1),k))

combine(4,2)
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]

参考链接

https://leetcode-cn.com/problems/combinations/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/