1. 穷举法概述
1.1 什么是穷举法?
穷举法(Exhaustive Method),又称暴力法(Brute Force)或枚举法,是一种最直接、最简单的解决问题的方法。其核心思想是:遍历所有可能的情况,从中找出符合条件的结果。
哲学思考:穷举法体现了计算机科学的本质------用计算能力换取思考的简化。人类面对复杂问题需要巧妙的数学推导,而计算机可以凭借强大的计算能力,逐一检查所有可能性。
1.2 穷举法的适用条件
条件 说明
解空间有限 所有可能情况的数量在可接受范围内
可枚举性 能够系统地列出所有可能解
验证简单 判断某个候选解是否满足条件很容易
1.3 穷举法的优缺点
优点:
· ✅ 逻辑简单,易于实现
· ✅ 只要解空间有限,一定能找到解
· ✅ 常用于验证其他算法的正确性
缺点:· ❌ 效率低下,时间复杂度高
· ❌ 可能产生"组合爆炸"(状态数量指数级增长)
· ❌ 不适合大规模问题
2. 穷举法的核心思想
2.1 三大要素
┌─────────────────────────────────────────┐
│ 穷举法三要素 │
├─────────────────────────────────────────┤
│ 1. 确定解空间 → 所有可能解的集合 │
│ 2. 确定枚举方式 → 如何遍历解空间 │
│ 3. 确定约束条件 → 判断是否为有效解 │
└─────────────────────────────────────────┘
2.2 穷举法的基本框架
// 穷举法基本代码框架
for (枚举所有可能的解) {
if (满足约束条件) {
// 找到一个有效解
记录或输出结果;
}
}
3. 经典案例详解
3.1 案例1:百钱买百鸡
问题描述:公鸡5元一只,母鸡3元一只,小鸡1元三只。用100元买100只鸡,问公鸡、母鸡、小鸡各多少只?
#include <stdio.h>
/*
* 问题分析:
* 设公鸡x只,母鸡y只,小鸡z只
* 约束条件:
* 1. x + y + z = 100 (总数)
* 2. 5x + 3y + z/3 = 100 (总价)
* 3. z 必须是3的倍数
* 4. x, y, z >= 0
*
* 解空间分析:
* x: 0~20 (因为5*20=100)
* y: 0~33 (因为3*33≈100)
* z: 由x,y决定,无需单独枚举
*/
int main() {
int x, y, z;
int solutionCount = 0;
printf("========== 百钱买百鸡问题 ==========\n\n");
printf("解法枚举:\n");
printf("----------------------------------------\n");
// 穷举所有可能的公鸡数量
for (x = 0; x <= 20; x++) {
// 穷举所有可能的母鸡数量
for (y = 0; y <= 33; y++) {
// 小鸡数量由总数决定
z = 100 - x - y;
// 检查约束条件
if (z >= 0 && z % 3 == 0 && 5*x + 3*y + z/3 == 100) {
solutionCount++;
printf("解法%d: 公鸡 %2d 只, 母鸡 %2d 只, 小鸡 %2d 只\n",
solutionCount, x, y, z);
printf(" 总价: %d + %d + %d = 100元\n",
5*x, 3*y, z/3);
}
}
}
printf("----------------------------------------\n");
printf("共找到 %d 种解法\n", solutionCount);
return 0;
}
运行结果:
```
========== 百钱买百鸡问题 ==========
解法枚举:
解法1: 公鸡 0 只, 母鸡 25 只, 小鸡 75 只
总价: 0 + 75 + 25 = 100元
解法2: 公鸡 4 只, 母鸡 18 只, 小鸡 78 只
总价: 20 + 54 + 26 = 100元
解法3: 公鸡 8 只, 母鸡 11 只, 小鸡 81 只
总价: 40 + 33 + 27 = 100元
解法4: 公鸡 12 只, 母鸡 4 只, 小鸡 84 只
总价: 60 + 12 + 28 = 100元
共找到 4 种解法
3.2 案例2:水仙花数
问题描述:一个三位数,其各位数字的立方和等于该数本身,称为水仙花数。
#include <stdio.h>
#include <math.h>
/*
* 问题分析:
* 解空间:100 ~ 999
* 约束条件:a³ + b³ + c³ = 100a + 10b + c
* 其中a为百位,b为十位,c为个位
*/
int main() {
int num, a, b, c;
int count = 0;
printf("========== 水仙花数 ==========\n\n");
printf("三位数中,满足 a³+b³+c³ = 100a+10b+c 的数有:\n\n");
// 穷举所有三位数
for (num = 100; num <= 999; num++) {
a = num / 100; // 百位
b = (num / 10) % 10; // 十位
c = num % 10; // 个位
// 检查是否满足水仙花数条件
if (a*a*a + b*b*b + c*c*c == num) {
count++;
printf("%d = %d³ + %d³ + %d³\n", num, a, b, c);
}
}
printf("\n共找到 %d 个水仙花数\n", count);
return 0;
}
3.3 案例3:完全数
问题描述:一个数恰好等于它的所有真因子(除了自身以外的因子)之和,称为完全数。
#include <stdio.h>
/*
* 问题分析:
* 解空间:1 ~ N
* 约束条件:所有真因子之和 = 该数本身
*/
void findPerfectNumbers(int limit) {
int count = 0;
printf("1 到 %d 之间的完全数:\n\n", limit);
// 穷举每个数
for (int num = 2; num <= limit; num++) {
int sum = 0;
// 找出所有真因子
for (int i = 1; i <= num / 2; i++) {
if (num % i == 0) {
sum += i;
}
}
// 检查是否为完全数
if (sum == num) {
count++;
printf("%d = ", num);
// 输出因子序列
int first = 1;
for (int i = 1; i <= num / 2; i++) {
if (num % i == 0) {
if (first) {
printf("%d", i);
first = 0;
} else {
printf(" + %d", i);
}
}
}
printf("\n");
}
}
printf("\n共找到 %d 个完全数\n", count);
}
int main() {
printf("========== 完全数求解 ==========\n\n");
findPerfectNumbers(10000);
return 0;
}
3.4 案例4:密码破解(字典攻击)
问题描述:假设密码由4位数字组成,通过穷举法破解密码。
#include <stdio.h>
#include <string.h>
#include <time.h>
/*
* 问题分析:
* 解空间:0000 ~ 9999,共10000种可能
* 验证:检查是否与正确密码匹配
*/
int crackPassword(const char* target) {
char guess[5];
int attempts = 0;
for (int i = 0; i <= 9999; i++) {
attempts++;
sprintf(guess, "%04d", i);
if (strcmp(guess, target) == 0) {
return i;
}
}
return -1; // 未找到(理论上不会发生)
}
int main() {
char password[5];
clock_t start, end;
printf("========== 穷举法破解密码 ==========\n\n");
printf("请输入一个4位数字密码(0000-9999):");
scanf("%s", password);
start = clock();
int result = crackPassword(password);
end = clock();
if (result != -1) {
printf("\n破解成功!密码是:%04d\n", result);
printf("尝试次数:%d 次\n", result + 1);
printf("耗时:%.3f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}
return 0;
}
4. 穷举法的优化技巧
4.1 减少枚举范围
// ❌ 未优化的穷举
for (int i = 0; i <= 100; i++) {
for (int j = 0; j <= 100; j++) {
// 检查条件...
}
}
// ✅ 优化:利用约束条件缩小范围
for (int i = 0; i <= 20; i++) { // 利用上限约束
for (int j = 0; j <= 100 - i; j++) { // 利用和约束
// 检查条件...
}
}
4.2 对称性剪枝
// 问题:从1-10中选3个数,不考虑顺序
// ❌ 枚举所有排列(720种)
for (int i = 1; i <= 10; i++)
for (int j = 1; j <= 10; j++)
for (int k = 1; k <= 10; k++)
// ✅ 利用对称性,只枚举组合(120种)
for (int i = 1; i <= 8; i++)
for (int j = i+1; j <= 9; j++)
for (int k = j+1; k <= 10; k++)
4.3 提前终止
// 在验证过程中,一旦确定不可能满足条件,立即跳出
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += arr[i];
if (sum > target) {
break; // 已经超过目标值,无需继续
}
}
5. 穷举法的局限性与优化
5.1 组合爆炸问题
#include <stdio.h>
#include <math.h>
// 演示组合爆炸:计算不同规模下的状态数量
void demonstrateExplosion() {
printf("========== 组合爆炸演示 ==========\n\n");
printf("问题类型\t\t\t状态数量\n");
printf("----------------------------------------\n");
// n 个物品的排列数
for (int n = 1; n <= 10; n++) {
long long permutations = 1;
for (int i = 1; i <= n; i++) {
permutations *= i;
}
printf("%d个物品的排列\t\t%lld\n", n, permutations);
}
printf("\n");
// 2^n 子集问题
for (int n = 5; n <= 30; n += 5) {
printf("%d个元素的子集\t\t%lld\n", n, (long long)pow(2, n));
}
}
int main() {
demonstrateExplosion();
printf("\n结论:当问题规模增大时,穷举法很快变得不可行!\n");
printf("这就是为什么需要更高效的算法(如回溯、动态规划等)\n");
return 0;
}
5.2 剪枝优化
#include <stdio.h>
#include <stdbool.h>
// 八皇后问题的剪枝优化演示
#define N 8
int solutions = 0;
// 检查在(row, col)放置皇后是否安全
bool isSafe(int board[N][N], int row, int col) {
// 检查同一列
for (int i = 0; i < row; i++) {
if (board[i][col]) return false;
}
// 检查左上对角线
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) {
if (board[i][j]) return false;
}
// 检查右上对角线
for (int i = row, j = col; i >= 0 && j < N; i--, j++) {
if (board[i][j]) return false;
}
return true;
}
// 递归求解(带剪枝)
void solveNQueens(int board[N][N], int row) {
if (row == N) {
solutions++;
return;
}
for (int col = 0; col < N; col++) {
// 剪枝:如果当前位置不安全,直接跳过
if (isSafe(board, row, col)) {
board[row][col] = 1; // 放置皇后
solveNQueens(board, row + 1);
board[row][col] = 0; // 回溯
}
}
}
int main() {
int board[N][N] = {0};
solveNQueens(board, 0);
printf("%d皇后问题共有 %d 种解法\n", N, solutions);
printf("如果不使用剪枝,需要检查 %d 种可能\n", (int)pow(N, N));
return 0;
}
6. 穷举法 vs 其他算法
算法 适用场景 时间复杂度 优点 缺点
穷举法 小规模问题 通常指数级 简单直观,一定能找到解 效率低,组合爆炸
回溯法 约束满足问题 平均优于穷举 带剪枝,减少搜索 实现较复杂
分治法可分解的问题 O(n log n) 效率高 问题必须可分解
动态规划 最优子结构问题 多项式时间 避免重复计算 需要状态设计
贪心法 特定优化问题 O(n) 非常高效 不一定得到最优解
7. 实战案例:旅行商问题(TSP)
#include <stdio.h>
#include <limits.h>
#define CITIES 4
// 距离矩阵
int distance[CITIES][CITIES] = {
{0, 10, 15, 20},
{10, 0, 35, 25},
{15, 35, 0, 30},
{20, 25, 30, 0}
};
int visited[CITIES] = {0};
int bestPath[CITIES + 1];
int bestDistance = INT_MAX;
// 计算路径总距离
int calculateDistance(int path[], int n) {
int total = 0;
for (int i = 0; i < n - 1; i++) {
total += distance[path[i]][path[i+1]];
}
total += distance[path[n-1]][path[0]]; // 返回起点
return total;
}
// 穷举所有排列
void tsp(int current[], int pos, int n) {
if (pos == n) {
int dist = calculateDistance(current, n);
if (dist < bestDistance) {
bestDistance = dist;
for (int i = 0; i < n; i++) {
bestPath[i] = current[i];
}
bestPath[n] = current[0]; // 形成回路
}
return;
}
for (int i = 0; i < n; i++) {
if (!visited[i]) {
visited[i] = 1;
current[pos] = i;
tsp(current, pos + 1, n);
visited[i] = 0;
}
}
}
int main() {
int path[CITIES];
printf("========== 旅行商问题(TSP)==========\n\n");
printf("城市距离矩阵:\n");
for (int i = 0; i < CITIES; i++) {
for (int j = 0; j < CITIES; j++) {
printf("%3d ", distance[i][j]);
}
printf("\n");
}
visited[0] = 1;
path[0] = 0;
tsp(path, 1, CITIES);
printf("\n最优路径:");
for (int i = 0; i <= CITIES; i++) {
printf("%d", bestPath[i]);
if (i < CITIES) printf(" → ");
}
printf("\n最短距离:%d\n", bestDistance);
printf("\n注意:当城市数量增加时,排列数呈阶乘增长!\n");
printf("10个城市就需要检查 3,628,800 种可能,\n");
printf("20个城市则高达 2.4×10^18 种,穷举法完全不可行!\n");
return 0;
}
8. 思维启发:从穷举到智能搜索
8.1 穷举法的哲学意义
穷举法告诉我们:
┌────────────────────────────────────────────┐│ 人类擅长抽象思维,但不擅长大量重复计算 │
│ 计算机擅长重复计算,但不擅长抽象思维 │
│ 优秀的程序员懂得如何结合两者优势 │
└────────────────────────────────────────────┘
8.2 学习路径建议
穷举法(基础)
↓
回溯法(带剪枝的穷举)
↓
分支限界法(带上下界的剪枝)
↓
动态规划(利用重叠子问题)
↓
启发式搜索(近似求解大规模问题)
9. 练习题
基础练习
找出所有三位数中,百位数字等于十位和个位数字之和的数
找出 1-1000 之间的所有质数(使用穷举法)
求解:鸡兔同笼,头35个,脚94只,求鸡兔各多少只
进阶练习
四色定理验证:用穷举法验证地图四色染色的可行性
数独求解器:使用穷举法(回溯)求解数独
找零问题:有1元、2元、5元硬币,找出组成100元的所有组合
挑战练习
八皇后问题:输出所有解,并统计解的数量
骑士巡游:在国际象棋棋盘上,骑士能否走遍所有格子且不重复
最短路径:用穷举法找出图中两点间的所有路径和最短路径
10. 总结
核心要点
要点 说明
理解本质 穷举法是用计算能力换取思维简化
掌握框架 确定解空间 → 枚举方式 → 约束条件
学会优化 减少枚举范围、对称剪枝、提前终止
认识局限 组合爆炸问题无法用穷举解决
启发思考 穷举是理解其他算法的基础
学习感悟:
穷举法看似"笨拙",却是算法学习的基石。
只有深刻理解了穷举法的"笨",才能真正领悟其他算法的"巧"。
当你遇到一个复杂问题时,不妨先问自己:如果我用穷举法,怎么做?
这个问题本身就包含了从问题到算法的第一步转化。
记住:优秀的程序员不是不用穷举法,而是知道什么时候该用,什么时候该放弃,以及如何优化它!