文章目录
一.暴力枚举相关概念
1.基本概念
暴力枚举(也称为穷举法)是一种简单直接的算法设计策略,在 C 语言编程中经常会用到。暴力枚举就是对问题的所有可能情况进行逐一尝试,直到找到满足条件的解或者遍历完所有可能情况。它不依赖于问题的特定结构或巧妙算法,而是通过纯粹的计算能力来解决问题。虽然在某些复杂问题上可能效率不高,但对于一些规模较小的问题或者作为理解问题和初步求解的手段,是非常有效的。
2.适用场景
- 简单问题求解:当问题的可能解空间相对较小,通过逐一列举所有可能情况能够在可接受的时间内得到答案时,适合使用暴力枚举。例如,找出 1 到 100 之间的所有素数,可以对每个数进行判断是否为素数,这种情况下逐一检查是可行的。
- 验证其他算法:在设计更高效的算法时,暴力枚举可以作为一种基准来验证新算法的正确性。先通过暴力枚举得到准确结果,再与新算法的结果进行对比,确保新算法的有效性。
3.实现步骤
- 确定枚举范围
明确问题中需要进行枚举的变量以及它们的取值范围。例如,要找出两个整数在一定范围内(如 1 到 100)满足某种条件的组合,那么这两个整数的取值范围就是 1 到 100,这就是需要枚举的范围。
- 设计循环结构
根据枚举范围,使用 C 语言中的循环语句(如for
循环、while
循环等)来遍历所有可能的情况。通常会使用多层循环来处理多个变量的枚举。
- 进行条件判断
在循环内部,对于每一种枚举出来的情况,需要根据问题的要求进行条件判断,看是否满足特定的条件。如果满足条件,则可以进行相应的处理,比如输出结果、记录满足条件的情况等。
4.注意事项
- 循环边界条件
在设计循环结构时,一定要准确设置循环的边界条件,确保能够完整地遍历所有需要枚举的情况,同时又不会超出范围导致错误。例如,在上面列举两个整数组合的例子中,如果把循环条件写成for (a = 1; a < 10; a++)
和for (b = 1; b < 10; b++)
,就会遗漏一些组合情况。
- 时间复杂度
暴力枚举的最大缺点就是可能会导致很高的时间复杂度,尤其是当枚举范围很大或者需要枚举的变量较多时。例如,要枚举三个整数在 1 到 100 之间的所有组合,就需要三层for
循环,总共会有100×100×100 = 1000000
种情况,这会耗费大量的计算时间。所以在实际应用中,要根据问题的规模和要求,考虑是否能够接受暴力枚举带来的时间成本,或者是否需要寻找更高效的算法来替代。
- 内存使用
虽然暴力枚举本身通常不会占用大量的内存,但在某些情况下,如果需要存储大量枚举出来的结果或者中间数据,也可能会导致内存不足的问题。例如,要枚举所有可能的字符串组合并存储起来,可能就需要考虑内存的使用情况。
总之,暴力枚举是 C 语言中一种基础且常用的算法策略,通过准确确定枚举范围、合理设计循环结构以及正确进行条件判断,可以解决许多简单的数学问题、组合问题等,但在使用时也要注意其时间复杂度和内存使用等方面的问题。
二.洛谷暴力枚举相关习题
1.P2241 统计方形(数据加强版)
- 题目描述
有一个 n×m 方格的棋盘,求其方格包含多少正方形、长方形(不包含正方形)。
- 输入格式
一行,两个正整数 n,m(𝑛≤5000,𝑚≤5000)。
- 输出格式
一行,两个正整数,分别表示方格包含多少正方形、长方形(不包含正方形)。
如果一开始没什么想法,可以尝试数学方法写一下,思路会很清楚,不要一直专注在代码如何写。
如图所示:对于一个 2x3 的方阵,1x1 的矩形有2x3 个,1x2 的矩形有2x2 个……,依此类推,可知正方形个数为6 + 2 = 8个,长方形的个数为矩形个数 – 正方形个数 ,即18 - 8 = 10个。
以此类推逐渐扩大n、m。
如图所示(找规律):一个nxm 的方阵,对于分别所有边长的矩形的计算可以写为(n - i + 1) * (n - j + 1)
,这里的i
和j
分别代表行和列,即他们的宽和长
对于这种方法其实就是暴力枚举出每一个可能的矩形进行统计,直至判断完全。
#include <stdio.h>
int main() {
long long int n, m;
scanf("%lld%lld", &n, &m);
long long int square_count = 0, rectangle_count = 0, all_count = 0;
//判断正方形个数,(n < m) ? n : m是选出较小的作为正方形边长最大值
for (int i = 1; i <= ((n < m) ? n : m); i++) {
square_count += (n - i + 1) * (m - i + 1);
}
//双层循环暴力遍历每一个n、m组成的矩形
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
all_count += (n - i + 1) * (m - j + 1);
}
}
rectangle_count = all_count - square_count;
printf("%lld %lld", square_count, rectangle_count);
return 0;
}
2.P2089 烤鸡
- 题目描述
猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 1010 种配料(芥末、孜然等),每种配料可以放 11 到 33 克,任意烤鸡的美味程度为所有配料质量之和。
现在, Hanke 想要知道,如果给你一个美味程度 𝑛n ,请输出这 1010 种配料的所有搭配方案。
- 输入格式
一个正整数 𝑛n,表示美味程度。
- 输出格式
第一行,方案总数。
第二行至结束,1010 个数,表示每种配料所放的质量,按字典序排列。
如果没有符合要求的方法,就只要在第一行输出一个 00。
这段代码的功能是根据输入的一个整数
n
,通过递归的方式生成所有可能的由10
种配料组成且每种配料用量在1
到3
之间的组合情况.
#include <stdio.h>
// 存储每种配料的用量
int a[10];
// 记录符合要求的方案总数
int count = 0;
// 递归函数用于生成所有配料组合
void generate_Combinations_count(int index, int remainingSum, int n) {
//注意数组索引是从 0 到 9,所以 index == 10 表示全部处理完
if (index == 10) {
//在处理完所有配料后,如果此时剩余总和为 0,这就说明当前这种配料组合刚好满足总量要求,所以count++
if (remainingSum == 0) {
count++;
}
return;
}
for (int i = 1; i <= 3 && i <= remainingSum; i++) {
a[index] = i;
generate_Combinations_count(index + 1, remainingSum - i, n);
}
}
//打印函数
void generate_Combinations_print(int index, int remainingSum, int n) {
if (index == 10) {
if (remainingSum == 0) {
count++;
for (int i = 0; i < 10; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
return;
}
for (int i = 1; i <= 3 && i <= remainingSum; i++) {
a[index] = i;
generate_Combinations_print(index + 1, remainingSum - i, n);
}
}
int main() {
int n;
scanf("%d", &n);
generate_Combinations_count(0, n, n);
printf("%d\n", count);
generate_Combinations_print(0, n, n);
return 0;
}
递归函数 generate_Combinations_count
-
int index
:表示当前正在处理的配料索引。函数会从索引0
开始,逐步处理到索引9
(因为数组a
的大小是10
,索引范围是0
到9
) -
int remainingSum
:表示在设置当前配料用量之前,剩余的总和。初始时,这个值等于从标准输入读取的n
,随着配料用量的设置,它会不断减少。 -
int n
:从main
函数传入的一个整数,代表某种总量要求,可能是所有配料用量之和的目标值。
本题作者不知道如何用一个函数写完先输出总数再输出方案,所以就把一个函数写了两遍(T–T)
3.P1618 三连击(升级版)
- 题目描述
将 1,2,…,91,2,…,9 共 99 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是 𝐴:𝐵:𝐶A:B:C,试求出所有满足条件的三个三位数,若无解,输出 No!!!
。
- 输入格式
三个数,𝐴,𝐵,𝐶A,B,C。
- 输出格式
若干行,每行 33 个数字。按照每行第一个数字升序排列。
#include <stdio.h>
//检查是否有九个数
int all_Different(int num1, int num2, int num3) {
int book[10] = { 0 };
while (num1 > 0) {
int temp = num1 % 10;
book[temp]++;
num1 /= 10;
}
while (num2 > 0) {
int temp = num2 % 10;
book[temp]++;
num2 /= 10;
}
while (num3 > 0) {
int temp = num3 % 10;
book[temp]++;
num3 /= 10;
}
for (int i = 1; i <= 9; i++) {
if (book[i] != 1) {
return 0;
}
}
return 1;
}
// 检查是否满足比例关系
int check_Connextion(int num1, int num2, int num3, int a, int b, int c) {
return (num1 * b == num2 * a) && (num1 * c == num3 * a);
}
// 生成所有由1到9组成的三位数组合
void generate_Search(int a, int b, int c) {
int found = 0;
for (int i = 123; i <= 987; i++) {
for (int j = i + 1; j <= 987; j++) {
for (int k = j + 1; k <= 987; k++) {
if (check_Connextion(i, j, k, a, b, c) && all_Different(i, j, k)){
printf("%d %d %d\n", i, j, k);
found = 1;
}
}
}
}
if (!found) {
printf("No!!!\n");
}
}
int main() {
int A, B, C;
scanf("%d%d%d", &A, &B, &C);
generate_Search(A, B, C);
return 0;
}
(1.)函数 all_Different
- 函数功能和目的
- 该函数的主要目的是检查由三个整数
num1
、num2
和num3
所组成的所有数字(即每个整数的每一位数字)是否恰好是1
到9
这九个不同的数字。
- 该函数的主要目的是检查由三个整数
- 函数内部逻辑
- 首先定义了一个整型数组
book
,大小为10
,并初始化为0
。这个数组用于记录数字1
到9
出现的次数,数组下标对应数字的值,例如book[3]
用于记录数字3
出现的次数。 - 然后通过三个
while
循环分别处理num1-num2-num3
.- 对于每个整数,在循环中通过取余操作
num1 % 10
(以num1
为例)获取该整数的个位数字temp
,然后将book[temp]
的值增加1
,表示该数字出现了一次。接着通过除法操作num1 /= 10
将整数缩小10
倍,以便处理下一位数字,如此循环直到该整数变为0
。
- 对于每个整数,在循环中通过取余操作
- 最后通过一个
for
循环遍历数组book
的下标从1
到9
。- 如果发现
book
数组中某个元素的值不等于1
,这就意味着1
到9
这九个数字中存在某个数字出现的次数不是恰好一次,那么就返回0
,表示不满足所有数字都不同的条件。 - 如果循环结束后没有发现这样的情况,就返回
1
,表示三个整数所组成的所有数字恰好是1
到9
这九个不同的数字。
- 如果发现
- 首先定义了一个整型数组
(2.)函数 check_Connextion
- 函数功能和目的
- 该函数用于检查三个整数
num1
、num2
和num3
是否满足特定的比例关系。
- 该函数用于检查三个整数
- 函数内部逻辑
- 函数通过判断两个等式
num1 * b == num2 * a
和num1 * c == num3 * a
是否同时成立来确定是否满足比例关系。如果这两个等式都成立,那么就返回1
,表示满足比例关系;否则返回0
,表示不满足比例关系。
- 函数通过判断两个等式
(3.)函数 generate_Search
- 函数功能和目的
- 该函数的主要目的是生成所有由
1
到9
组成的三位数i
、j
和k
的组合,并检查这些组合是否满足特定的比例关系(由a
、b
和c
定义)以及是否由九个不同的数字组成(通过调用all_Different
函数检查)。如果满足条件,就将这些三位数打印出来;如果遍历完所有可能的组合都没有找到满足条件的组合,就打印出 “No!!!”。
- 该函数的主要目的是生成所有由
- 函数内部逻辑
- 首先定义了一个整型变量
found
,并初始化为0
,用于标记是否找到了满足条件的组合。 - 然后通过三层
for
循环来枚举所有由1
到9
组成的三位数组合:- 最外层循环
for (int i = 123; i <= 987; i++)
:遍历所有可能的第一个三位数i
。 - 中间层循环
for (int j = i + 1; j <= 987; j++)
:遍历所有可能的第二个三位数j
,这样可以保证j
大于i
,避免重复的组合。 - 最内层循环
for (int k = j + 1; k <= 987; k++)
:遍历所有可能的第三个三位数k
,这样可以保证k
大于j
,进一步避免重复的组合。
- 最外层循环
- 对于每一组生成的三位数组合
i
、j
和k
,通过if
语句进行条件判断:- 如果
check_Connextion(i, j, k, a, b, c)
(即满足特定的比例关系)且all_Different(i, j, k)
(即由九个不同的数字组成)这两个条件同时成立,那么就将这三个三位数打印出来,并将found
变量设置为1
,表示已经找到了满足条件的组合。
- 如果
- 最后,如果循环结束后
found
变量仍然为0
,这就意味着没有找到满足条件的组合,此时就打印出 “No!!!”。
- 首先定义了一个整型变量
三.总结
暴力枚举其实本质上就是对所有可能情况已以判断,但这样当所判断的数量过于庞大时很耗时耗力,对于简单,少量运行次数的数据来说是很简单方便的,但对需要大量处理的数据不适用。