Expand Up
@@ -2,17 +2,22 @@
> **枚举算法(Enumeration Algorithm)**:也称为穷举算法,指的是按照问题本身的性质,一一列举出该问题所有可能的解,并在逐一列举的过程中,将它们逐一与目标状态进行比较以得出满足问题要求的解。在列举的过程中,既不能遗漏也不能重复。
枚举算法是设计最简单、最基本的搜索算法,其核心思想是通过列举问题的所有状态 ,将它们逐一与目标状态进行比较,从而得到满足条件的解。
枚举算法的核心思想是:通过列举问题的所有状态 ,将它们逐一与目标状态进行比较,从而得到满足条件的解。
- **枚举算法的优点**:
- 容易编程实现,也容易调试。
- 建立在考察大量状态、甚至是穷举所有状态的基础上,所以算法的正确性比较容易证明。
- **枚举算法的缺点**:
- 效率比较低,不适合求解规模较大的问题。
由于枚举算法要通过列举问题的所有状态来得到满足条件的解,因此,在问题规模变大时,其效率一般是比较低的。但是枚举算法也有自己特有的优点:
1. 多数情况下容易编程实现,也容易调试。
2. 建立在考察大量状态、甚至是穷举所有状态的基础上,所以算法的正确性比较容易证明。
所以,枚举算法通常用于求解问题规模比较小的问题,或者作为求解问题的一个子算法出现,通过枚举一些信息并进行保存,而这些消息的有无对主算法效率的高低有着较大影响。
## 2. 枚举算法解题思路
## 2. 枚举算法的解题思路
### 2.1 枚举算法的解题思路
枚举算法是设计最简单、最基本的搜索算法。是我们在遇到问题时,最应该优先考虑的算法。
因为其实现足够简单,所以在遇到问题时,我们往往可以先通过枚举算法尝试解决问题,然后在此基础上,再去考虑其他优化方法和解题思路。
采用枚举算法解题的一般思路如下:
Expand All
@@ -26,6 +31,52 @@
2. 加强约束条件,缩小枚举范围。
3. 根据某些问题特有的性质,例如对称性等,避免对本质相同的状态重复求解。
### 2.2 枚举算法的简单应用
下面举个著名的例子:「百钱买百鸡问题」。这个问题是我国古代数学家张丘在「算经」一书中提出的。该问题叙述如下:
> **百钱买百鸡问题**:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一;百钱买百鸡,则鸡翁、鸡母、鸡雏各几何?
翻译一下,意思就是:公鸡一只五块钱,母鸡一只三块钱,小鸡三只一块钱。现在我们用 100ドル$ 块钱买了 100ドル$ 只鸡,问公鸡、母鸡、小鸡各买了多少只?
下面我们根据算法的一般思路来解决一下这道题。
1. 确定枚举对象、枚举范围和判断条件,并判断条件设立的正确性。
1. 确定枚举对象:枚举对象为公鸡、母鸡、小鸡的只数,那么我们可以用变量 $x$、$y$、$z$ 分别来代表公鸡、母鸡、小鸡的只数。
2. 确定枚举范围:因为总共买了 100ドル$ 只鸡,所以 0ドル \le x, y, z \le 100,ドル则 $x$、$y$、$z$ 的枚举范围为 $[0, 100]$。
3. 确定判断条件:根据题意,我们可以列出两个方程式:5ドル \times x + 3 \times y + \frac{z}{3} = 100,ドル$x + y + z = 100$。在枚举 $x$、$y$、$z$ 的过程中,我们可以根据这两个方程式来判断是否当前状态是否满足题意。
2. 一一枚举可能的情况,并验证是否是问题的解。
1. 根据枚举对象、枚举范围和判断条件,我们可以顺利写出对应的代码。
```Python
class Solution:
def buyChicken(self):
for x in range(101):
for y in range(101):
for z in range(101):
if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100 and x + y + z == 100:
print("公鸡 %s 只,母鸡 %s 只,小鸡 %s 只" % (x, y, z))
```
3. 考虑提高枚举算法的效率。
1. 在上面的代码中,我们枚举了 $x$、$y$、$z,ドル但其实根据方程式 $x + y + z = 100,ドル得知:$z$ 可以通过 $z = 100 - x - y$ 而得到,这样我们就不用再枚举 $z$ 了。
2. 在上面的代码中,对 $x$、$y$ 的枚举范围是 $[0, 100],ドル但其实如果所有钱用来买公鸡,最多只能买 20ドル$ 只,同理,全用来买母鸡,最多只能买 33ドル$ 只。所以对 $x$ 的枚举范围可改为 $[0, 20],ドル$y$ 的枚举范围可改为 $[0, 33]$。
```Python
class Solution:
def buyChicken(self):
for x in range(21):
for y in range(34):
z = 100 - x - y
if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
print("公鸡 %s 只,母鸡 %s 只,小鸡 %s 只" % (x, y, z))
```
## 3. 枚举算法的应用
### 3.1 两数之和
Expand All
@@ -36,20 +87,44 @@
#### 3.1.2 题目大意
- **描述**:给定一个整数数组 `nums` 和一个整数目标值 `target`。
- **要求**:在该数组中找出和为 `target` 的两个整数,并输出这两个整数的下标。
**描述**:给定一个整数数组 `nums` 和一个整数目标值 `target`。
**要求**:在该数组中找出和为 `target` 的两个整数,并输出这两个整数的下标。可以按任意顺序返回答案。
**说明**:
- 2ドル \le nums.length \le 10^4$。
- $-10^9 \le nums[i] \le 10^9$。
- $-10^9 \le target \le 10^9$。
- 只会存在一个有效答案。
**示例**:
- 示例 1:
```Python
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
```
- 示例 2:
```Python
输入:nums = [3,2,4], target = 6
输出:[1,2]
```
#### 3.1.3 解题思路
这里说下枚举算法的解题思路。
使用两重循环枚举数组中每一个数 `nums[i]`、`nums[j]`。判断所有的 `nums[i] + nums[j]` 是否等于 `target`。
- 如果出现 `nums[i] + nums[j] == target`,则说明数组中存在和为 `target` 的两个整数,将两个整数的下标 `i`、`j` 输出即可。
##### 思路 1:枚举算法
利用两重循环进行枚举的时间复杂度为 $O(n^2)$。
1. 使用两重循环枚举数组中每一个数 `nums[i]`、`nums[j]`,判断所有的 `nums[i] + nums[j]` 是否等于 `target`。
2. 如果出现 `nums[i] + nums[j] == target`,则说明数组中存在和为 `target` 的两个整数,将两个整数的下标 `i`、`j` 输出即可。
#### 3.1.4 代码
##### 思路 1: 代码
```Python
class Solution:
Expand All
@@ -61,6 +136,11 @@ class Solution:
return []
```
##### 思路 1:复杂度分析
- **时间复杂度**:$O(n^2)$
- **空间复杂度**:$O(1)$。
### 3.2 计数质数
#### 3.2.1 题目链接
Expand All
@@ -69,25 +149,48 @@ class Solution:
#### 3.2.2 题目大意
**描述**:给定 一个非负整数 `n`。
**描述**:给定 一个非负整数 $n$。
**要求**:统计小于 $n$ 的质数数量。
**说明**:
- 0ドル \le n \le 5 * 10^6$。
**示例**:
**要求**:统计小于 `n` 的质数数量。
- 示例 1:
```Python
输入 n = 10
输出 4
解释 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7。
```
- 示例 2:
```Python
输入:n = 1
输出:0
```
#### 3.2.3 解题思路
这里说下枚举算法的解题思路(注意:提交会超时,只是讲解一下枚举算法的思路)。
对于小于 `n` 的每一个数 `x`,我们可以枚举区间 `[2, x - 1]` 上的数是否是 `x` 的因数,即是否存在能被 `x` 整数的数。如果存在,则该数 `x` 不是质数。如果不存在,则该数 `x` 是质数。
##### 思路 1:枚举算法(超时)
对于小于 $n$ 的每一个数 $x,ドル我们可以枚举区间 $[2, x - 1]$ 上的数是否是 $x$ 的因数,即是否存在能被 $x$ 整数的数。如果存在,则该数 $x$ 不是质数。如果不存在,则该数 $x$ 是质数。
这样我们就可以通过枚举 ` [2, n - 1]` 上的所有数 `x` ,并判断 `x` 是否为质数。
这样我们就可以通过枚举 $ [2, n - 1]$ 上的所有数 $x$ ,并判断 $x$ 是否为质数。
在遍历枚举的同时,我们维护一个用于统计小于 `n` 的质数数量的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 `1` 。最终返回该数目作为答案。
在遍历枚举的同时,我们维护一个用于统计小于 $n$ 的质数数量的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 1ドル$ 。最终返回该数目作为答案。
考虑到如果 `i` 是 `x` 的因数,则 $\frac{x}{i}$ 也必然是 `x` 的因数,则我们只需要检验这两个因数中的较小数即可。而较小数一定会落在 $[2, \sqrt x]$ 上。因此我们在检验 `x` 是否为质数时,只需要枚举 $[2, \sqrt x]$ 中的所有数即可。
考虑到如果 $i$ 是 $x$ 的因数,则 $\frac{x}{i}$ 也必然是 $x$ 的因数,则我们只需要检验这两个因数中的较小数即可。而较小数一定会落在 $[2, \sqrt x]$ 上。因此我们在检验 $x$ 是否为质数时,只需要枚举 $[2, \sqrt x]$ 中的所有数即可。
利用枚举算法单次检查单个数的时间复杂度为 $O(\sqrt{n}),ドル检查 `n` 个数的整体时间复杂度为 $O(n \sqrt{n})$。
利用枚举算法单次检查单个数的时间复杂度为 $O(\sqrt{n}),ドル检查 $n$ 个数的整体时间复杂度为 $O(n \sqrt{n})$。
#### 3.2.4 代码
##### 思路 1: 代码
```Python
class Solution:
Expand All
@@ -105,6 +208,11 @@ class Solution:
return cnt
```
##### 思路 1:复杂度分析
- **时间复杂度**:$O(n \times \sqrt{n})$。
- **空间复杂度**:$O(1)$。
### 3.3 统计平方和三元组的数目
#### 3.3.1 题目链接
Expand All
@@ -113,27 +221,46 @@ class Solution:
#### 3.3.2 题目大意
**描述**:给你一个整数 `n` 。
**描述**:给你一个整数 $n$ 。
**要求**:请你返回满足 1ドル \le a, b, c \le n$ 的平方和三元组的数目。
**说明**:
- **平方和三元组**:指的是满足 $a^2 + b^2 = c^2$ 的整数三元组 `(a, b, c)` 。
- **平方和三元组**:指的是满足 $a^2 + b^2 = c^2$ 的整数三元组 $(a, b, c)$。
- 1ドル \le n \le 250$。
**示例**:
- 示例 1:
```Python
输入 n = 5
输出 2
解释 平方和三元组为 (3,4,5) 和 (4,3,5)。
```
- 示例 2:
```Python
输入:n = 10
输出:4
解释:平方和三元组为 (3,4,5),(4,3,5),(6,8,10) 和 (8,6,10)。
```
#### 3.3.3 解题思路
枚举算法。
##### 思路 1: 枚举算法
我们可以在 ` [1, n]` 区间中枚举整数三元组 ` (a, b, c)` 中的 `a` 和 `b` 。然后判断 $a^2 + b^2$ 是否小于等于 `n` ,并且是完全平方数。
我们可以在 $ [1, n]$ 区间中枚举整数三元组 $ (a, b, c)$ 中的 $a$ 和 $b$ 。然后判断 $a^2 + b^2$ 是否小于等于 $n$ ,并且是完全平方数。
在遍历枚举的同时,我们维护一个用于统计平方和三元组数目的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 `1` 。最终,我们返回该数目作为答案。
在遍历枚举的同时,我们维护一个用于统计平方和三元组数目的变量 `cnt`。如果符合要求,则将计数 `cnt` 加 1ドル$ 。最终,我们返回该数目作为答案。
利用枚举算法统计平方和三元组数目的时间复杂度为 $O(n^2)$。
- 注意:在计算中,为了防止浮点数造成的误差,并且两个相邻的完全平方正数之间的距离一定大于 `1` ,所以我们可以用 $\sqrt{a^2 + b^2 + 1}$ 来代替 $\sqrt{a^2 + b^2}$。
- 注意:在计算中,为了防止浮点数造成的误差,并且两个相邻的完全平方正数之间的距离一定大于 1ドル$ ,所以我们可以用 $\sqrt{a^2 + b^2 + 1}$ 来代替 $\sqrt{a^2 + b^2}$。
#### 3.3.4 代码
##### 思路 1: 代码
```Python
class Solution:
Expand All
@@ -147,69 +274,7 @@ class Solution:
return cnt
```
## 4. 二进制枚举子集
### 4.1 二进制枚举子集简介
先来介绍一下「子集」的概念。
- **子集**:如果集合 `A` 的任意一个元素都是集合 `S` 的元素,则称集合 `A` 是集合 `S` 的子集。可以记为 $A \in S$。
有时候我们会遇到这样的问题:给定一个集合 `S`,枚举其所有可能的子集。
枚举子集的方法有很多,这里介绍一种简单有效的枚举方法:「二进制枚举子集算法」。
对于一个元素个数为 `n` 的集合 `S` 来说,每一个位置上的元素都有选取和未选取两种状态。我们可以用数字 `1` 来表示选取该元素,用数字 `0` 来表示不选取该元素。
那么我们就可以用一个长度为 `n` 的二进制数来表示集合 `S` 或者表示 `S` 的子集。其中二进制的每一位数都对应了集合中某一个元素的选取状态。对于集合中第 `i` 个元素(`i` 从 `0` 开始编号)来说,二进制对应位置上的 `1` 代表该元素被选取,`0` 代表该元素未被选取。
举个例子来说明一下,比如长度为 `5` 的集合 `S = {5, 4, 3, 2, 1}`,我们可以用一个长度为 `5` 的二进制数来表示该集合。
比如二进制数 `11111` 就表示选取集合的第 `0` 位、第 `1` 位、第 `2` 位、第 `3` 位、第 `4` 位元素,也就是集合 `{5, 4, 3, 2, 1}` ,即集合 `S` 本身。如下表所示:
| 集合 S 对应位置(下标) | 4 | 3 | 2 | 1 | 0 |
| :---------------------- | :--: | :--: | :--: | :--: | :--: |
| 二进制数对应位数 | 1 | 1 | 1 | 1 | 1 |
| 对应选取状态 | 选取 | 选取 | 选取 | 选取 | 选取 |
再比如二进制数 `10101` 就表示选取集合的第 `0` 位、第 `2` 位、第 `5` 位元素,也就是集合 `{5, 3, 1}`。如下表所示:
| 集合 S 对应位置(下标) | 4 | 3 | 2 | 1 | 0 |
| :---------------------- | :--: | :----: | :--: | :----: | :--: |
| 二进制数对应位数 | 1 | 0 | 1 | 0 | 1 |
| 对应选取状态 | 选取 | 未选取 | 选取 | 未选取 | 选取 |
再比如二进制数 `01001` 就表示选取集合的第 `0` 位、第 `3` 位元素,也就是集合 `{5, 2}`。如下标所示:
| 集合 S 对应位置(下标) | 4 | 3 | 2 | 1 | 0 |
| :---------------------- | :----: | :--: | :----: | :----: | :--: |
| 二进制数对应位数 | 0 | 1 | 0 | 0 | 1 |
| 对应选取状态 | 未选取 | 选取 | 未选取 | 未选取 | 选取 |
通过上面的例子我们可以得到启发:对于长度为 `5` 的集合 `S` 来说,我们只需要从 `00000` ~ `11111` 枚举一次(对应十进制为 0ドル \sim 2^5 - 1$)即可得到长度为 `5` 的集合 `S` 的所有子集。
我们将上面的例子拓展到长度为 `n` 的集合 `S`。可以总结为:
- 对于长度为 `5` 的集合 `S` 来说,只需要枚举 0ドル \sim 2^n - 1$(共 2ドル^n$ 种情况),即可得到所有的子集。
### 4.2 二进制枚举子集代码
```Python
class Solution:
def subsets(self, S): # 返回集合 S 的所有子集
n = len(S) # n 为集合 S 的元素个数
sub_sets = [] # sub_sets 用于保存所有子集
for i in range(1 << n): # 枚举 0 ~ 2^n - 1
sub_set = [] # sub_set 用于保存当前子集
for j in range(n): # 枚举第 i 位元素
if i >> j & 1: # 如果第 i 为元素对应二进制位删改为 1,则表示选取该元素
sub_set.append(S[j]) # 将选取的元素加入到子集 sub_set 中
sub_sets.append(sub_set) # 将子集 sub_set 加入到所有子集数组 sub_sets 中
return sub_sets # 返回所有子集
```
## 参考资料
##### 思路 1:复杂度分析
- 【书籍】算法竞赛入门经典:训练指南 - 刘汝佳,陈锋 著
- 【书籍】ACM-ICPC 程序设计系列 - 算法设计与实现 - 陈宇 吴昊 主编
- 【博文】[枚举排列和枚举子集 - CUC ACM-Wiki](https://cuccs.github.io/acm-wiki/search/enumeration/)
- **时间复杂度**:$O(n^2)$。
- **空间复杂度**:$O(1)$。