Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[pull] main from itcharge:main #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
pull merged 3 commits into AlgorithmAndLeetCode:main from itcharge:main
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
View file Open in desktop
Original file line number Diff line number Diff line change
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)$。
Loading

AltStyle によって変換されたページ (->オリジナル) /