目录
1. 比如需要收集的结果中需要 每一个部分都必须符合某个条件
一、什么是枚举
枚举算法是一种简单但有效的解决问题的方法,它通过列举所有可能的情况来寻找问题的解决方案。枚举算法通常用于解决小规模的问题,因为它的时间复杂度通常较高,随着问题规模的增大,性能可能会急剧下降。
枚举算法主要解决的问题:当问题的解规模相对较小且可以穷尽时,枚举算法是一种简单有效的解决方案。
枚举代码的优点:枚举算法的代码通常简单清晰,易于理解。
枚举算法缺点:时间复杂度高,解决的问题相对单一。根据问题的变化不同,往往要手动修改很多问题。
二、枚举的模板
一般情况下,枚举的解决需要将问题的全部方案测试一遍,最终找到问题的解。所以一般情况下使用for循环,根据问题规模不同需要使用多层for循环的嵌套。
ans = []
for i in range(x1):
for j in range(x2):
for k in range(x3):
if <进行相关的处理>:
ans.append(<符合条件的解>)
三、枚举解决问题
(一)与7相关的数
问题描述:
如果一个正整数,它能被7整除或者它的十进制表示法中某个位数上的数字为7,则称之为与7相关的数。
现在我们给定一个正整数n(n<1000),求所有小于等于n的与7无关的正整数的平方和。
输入格式:
共一行,为一个正整数。
输出格式:
共一行,为一个正整数。
输入输出示例
输入
输出
50
31469
单层循环枚举即可解决问题。
枚举对象1:n以内的数字。枚举范围:[1, n]
n = int(input())
ans = []
for i in range(1,n + 1):
new_i = str(i)
if i % 7 != 0 and '7' not in new_i:#与7无关的数,进行判断,注意这里的并列关系
ans.append(i ** 2)
print(sum(ans))
(二)百钱白鸡
问题描述:
"百钱买百鸡"是一个古老的数学谜题,它涉及到在一定的限制条件下找出满足特定条件的解。
假设有一个人要用一百块钱买一百只鸡,其中公鸡(每只5块钱)、母鸡(每只3块钱)和小鸡(每只1块钱)都有,问有多少种买法?
这是一个典型的应用问题,可以用数学方法解决。解决这个问题的一种常见方法是通过枚举所有可能的买法,并验证它们是否符合题目要求。
输入格式:
无
输出格式:
全部可能得方案。
输入输出示例
输入
输出
公鸡:0 只,母鸡:25 只,小鸡:75 只
公鸡:4 只,母鸡:18 只,小鸡:78 只
公鸡:8 只,母鸡:11 只,小鸡:81 只
公鸡:12 只,母鸡:4 只,小鸡:84 只
解题需要明确枚举的对象以及他们的取值范围。
枚举对象1:公鸡的数量。枚举范围:[0, 20]
枚举对象2:母鸡的数量。枚举范围:[0, 33]
def buy_chickens():
solutions = []
for roosters in range(21): # 公鸡最多买20只,因为20*5=100
for hens in range(34): # 母鸡最多买33只,因为33*3=99
chicks = 100 - roosters - hens
if chicks >= 0 and 5 * roosters + 3 * hens + chicks / 3 == 100:
solutions.append((roosters, hens, chicks))
return solutions
solutions = buy_chickens()
for solution in solutions:
print("公鸡:%d 只,母鸡:%d 只,小鸡:%d 只" % solution)
(三)不重复的三位数
问题描述:
用 1、3、5、7 这 4 个数字,能组成的互不相同且无重复数字的三位数有哪些?共有多少个?这些数的和为多少?输入:
无
输出:
多行数字,每行一个三位数
组成的三位数的总个数
这些三位数的总和
输入
输出
这些互不相同且无重复数字的三位数有: [135, 137, 153, 157, 173, 175, 315, 317, 351, 357, 371, 375, 513, 517, 531, 537, 571, 573, 713, 715, 731, 735, 751, 753]
共有 24 个满足条件的三位数。
这些数的和为: 10656
解题需要明确枚举的对象以及他们的取值范围。
枚举对象1:个位的可能。枚举范围:[1,3,5,7]
枚举对象2:十位的可能。枚举范围:[1,3,5,7]
枚举对象3:百位的可能。枚举范围:[1,3,5,7]
def find_unique_three_digit_numbers():
numbers = []
total_sum = 0
count = 0
for i in range(1, 8, 2): # 百位数只能是 1、3、5、7 中的一个
for j in range(1, 8, 2): # 十位数只能是 1、3、5、7 中的一个
for k in range(1, 8, 2): # 个位数只能是 1、3、5、7 中的一个
if i != j and i != k and j != k: # 确保三个数字互不相同
number = i * 100 + j * 10 + k
numbers.append(number)
total_sum += number
count += 1
return numbers, count, total_sum
numbers, count, total_sum = find_unique_three_digit_numbers()
print("这些互不相同且无重复数字的三位数有:", numbers)
print("共有 %d 个满足条件的三位数。" % count)
print("这些数的和为:", total_sum)
(四)货币兑换
现有M元,需要把这些钱换成 2元、5元、10元、20元、50元、100元。请问有多少种不同的兑换方案?
输入格式:
一个整数M。
输出格式:
全部的兑换方案和方案总数。
示例
输入:
33
输出:
2 2 2 2 2 2 2 2 2 2 2 2 2 2 5
2 2 2 2 2 2 2 2 2 5 5 5
2 2 2 2 2 2 2 2 2 5 10
2 2 2 2 5 5 5 5 5
2 2 2 2 5 5 5 10
2 2 2 2 5 10 10
2 2 2 2 5 20
7
解题需要明确枚举的对象以及他们的取值范围。
枚举对象1:2元的可能。枚举范围:[0, target // 2]
枚举对象2:5元的可能。枚举范围:[0, target // 5]
枚举对象3:10元的可能。枚举范围:[0, target // 10]
枚举对象4:20元的可能。枚举范围:[0, target // 20]
枚举对象5:50元的可能。枚举范围:[0, target // 50]
枚举对象6:100元的可能。枚举范围:[0, target // 100]
nums = [2, 5, 10, 20, 50, 100]
target = 33
count = 0
for a in range(target // 2 +1):
for b in range(target // 5 +1):
for c in range(target // 10 +1):
for d in range(target // 20 +1):
for e in range(target // 50 +1):
for f in range(target // 100 + 1):
if a * 2 + b * 5 + c * 10 + d * 20 + e * 50 + f * 100 > target:
break
if a * 2 + b * 5 + c * 10 + d * 20 + e * 50 + f * 100 == target:
count += 1
print(" ".join(a * ['2']), " ".join(b * ['5']), " ".join(c * ['10']), " ".join(d * ['20']), " ".join(e * ['50']), " ".join(f * ['100']))
print(count)
(五)组合(LeetCode77)
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合。你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]示例 2:
输入:n = 1, k = 1 输出:[[1]]提示:
1 <= n <= 20
1 <= k <= n
枚举对象1:第一位的可能。枚举范围:[1,n]
枚举对象2:第二位的可能。枚举范围:[1,n]
n = 4
k = 2
ans = []
for i in range(1, n +1):
for j in range(i +1, n +1):
ans.append([i, j])
print(ans)
四、什么是回溯
回溯是深度优先搜索的一种,它是深搜中带回退的哪一类型。回溯主要使用的工具就是全局列表和递归。回溯算法本质上是一种穷举搜索的方法,因此在处理大规模问题时可能会遇到效率问题。尤其是在问题的解空间非常庞大时,回溯算法可能会变得非常耗时。但是很多的问题必须使用穷举。
回溯解决的问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等
回溯算法核心思想是在搜索过程中,不断尝试可能的解,并在达到某种条件的情况下进行回溯,回到之前的状态继续搜索。
五、回溯的模板
回溯解决的问题包含了多层for循环解决的题目可以解决的题目,可以将多层嵌套for循环的写法类比回溯的工作原理。
比如 求nums = [1,2,3,4,5] 中长度为 k = 2的全部组合有哪些?通常使用如下暴力循环嵌套:
temp = []
nums = [1,2,3,4,5]
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
temp.append([i,j])
很明显,k的大小(枚举对象的数量)决定了嵌套循环需要写多少层。但是假如k很大,或者k是从控制台输入的,不确定大小,那么继续使用嵌套循环就非常麻烦。
使用回溯算法,就是是用递归来取代多层的嵌套循环。也就是根据k的大小随着可以随意控制嵌套循环的层数,并且代码很简短。
当然回溯还可以解决嵌套循环不方便解决问题的,比如全排列和全排列ii,如果使用嵌套循环 必须要在收集结果之后进行去重操作,没有办法在嵌套循环中完成去重的操作。
如何理解回溯?
回溯都可以抽象为一个树形结构,一般来讲都是一个N叉树。第一层就是集合nums=[1,2,3,4,5]。第二层就是原始nums分别取出一个元素之后剩下的部分,也就是对第一层嵌套循环的一个分解。
枚举对象的控制:最终的叶子节点取决于 k的值。比如k== 1 只有第一层就够了,k==2,那么需要两层树结构,k == 3需要三层树结构。回溯中的第一个for循环对应的就是嵌套循环的第一层,分别生成第二层的树枝(取出一个节点之后剩余的集合)。回溯中的递归解决的就是将第二层的树节点分别拿去遍历,直至走到终止条件,然后收集结果。
枚举对象的范围:start_index 控制。在for循环中使用一个start_index来模拟循环嵌套中的下一层循环比上一层的循环起点快一个。
基于以上分析,回溯可以使用一个for循环和一个递归函数来对N叉树的进行遍历,并根据题目要求确定什么时间递归终止,并收集叶子节点。
回溯的模板:
path = []
result = []
start_index = 0
def backtracking(start_index)
if <终止条件>:
result.append(path[:])
return#收集节点并且不再继续
for i in range(start_index, len(nums)):
path.append(nums[i])
backtracking(i + 1)#给下一层递归中遍历的循环一个起点,实现下一层嵌套循环的起点比上一层快一个。
path.pop()#上面一层递归完成之后需要回退一个。
六、回溯解决的问题
(一)组合总和III(LeetCode216)
找出所有相加之和为
n
的k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。示例 3:
输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。提示:
2 <= k <= 9
1 <= n <= 60
本题求解的是组合,并且集合中的每个元素都是不重复的,求解和等于n并且元素个数为k的全部组合。
回溯的终止条件:当len(path) == k and sum(path) == n 时可以终止并收集path中的结果并return结束递归。但是如果只有sum(path) == n,那么递归会无休止的递归下去,因为当前path中的元素进行求和的时候如果正好跳过了n,尽管已经不再可能会等于n,但是还是会继续往path里面加入元素,因为该终止条件无法终止这种情况。因此,需要补充另外一个回溯的终止条件:当sum(path) > n 时也要return 结束递归。
if sum(path) > n:
return
if sum(path) == n and len(path) == k:#n和k是确定终止条件用的,每一层的递归用的元素是1-9
result.append(path[:])
return
回溯的单层递归:
start_index的设置:首次的值是0,后面每次传入回溯的值是当前for循环中i + 1本身,确保下一层递归中遍历的集合是从i之后开始的,因为这里是求解组合。
for 循环范围:从start_index 开始到len(nums)结束。
for 循环内部:往path中加入元素nums[i];传入i +1作为backtracking的参数;当前层递归结束之后将当前层加入到path中的nums[i]取出,准备进行for循环中nums[i +1]的加入。
for i in range(start_index, len(nums)):
path.append(nums[i])
backtracking(i + 1)
path.pop()
回溯的参数:result 和 path 都可以使用全局变量,不一定要传入回溯的参数里面去。唯一需要传入回溯中递归参数就是start_index 因为需要他来控制单层递归中遍历的集合大小。start_index和 nums控制for循环的遍历范围。本题中求的还是组合,并且组合不能重复,因此每往下递归一层,循环的遍历对象要用start_index 控制多往前走一个。第一层nums = [1,2,3,4,5,6,7,8,9] 、第二层节点就是将1-9挨个取出了一个 之后剩余的集合2- 9 ,3- 9,4-9 ,5-9 ... 9 。
是否涉及去重操作:集合中元素都是不重复的,因此不涉及到树层去重。另外,因为本题是求解组合,也不是求解排列,所以不涉及的树枝去重。
剪枝:
1. 在任何一个树的子集合中 start_index 的最大可以用的值是 9 - k + len( path) + 1 再往后取,即使全部加入也是没有意义的,因为全部加入也不够k个。
2. 如果中间计算到的sum(path)大于n了 也就不必要继续往深了递归了,直接返回。
本题中集合中的元素是可以无限重复使用,求解换开M的方案和方案总数,如果只是求解方案总数,那么本题就是一个动态规划的完全背包题目。但是如果要求解兑换的具体方案使用回溯会更加方便并且代码也会更加的简洁。
完整代码
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
result = []
path = []
start_index = 1
def backtracking(start_index,path):
if sum(path) > n:
return
if sum(path) =&