【算法】蛮力法/穷举法/枚举法 的基本问题分析

  • 炮兵问题的优化,设立逻辑数组

蛮力法设计思想

有策略地穷举 + 验证

  • 制定穷举策略
  • 避免重复

简单来说,就是列举问题所有可能的解,然后去看看是否满足题目要求,是一种逆向解题方式。(我也不知道答案是什么,但是我知道答案肯定在这些范围里面,我一个个试就完了。)

所以我们应该做的是

  • 知道可能的答案都有哪些
  • 使用方法将其一一列举
  • 将每个解依次与条件比照

蛮力法使用范围

在这里插入图片描述

  • 所有解空间:例如a,b,c取值范围都是 0~9,问abc组成的三位数的所有情况
  • 所有路径:遍历图、遍历树

蛮力法的格式

蛮力法由循环和选择语句构成

  • 使用循环,穷举所有情况
  • 使用选择,判断当前情况是否成立
    • 若成立,则得到问题的解
    • 若不成立,则继续循环

循环 + 分支

for(){
	for(){
		// 获取X所有可能的情况
		if(X满足条件){
			printf("X");
		}
	}
}
蛮力法思想:穷举所有情况,找到符合条件的。

这也意味着,得能够穷举,因此问题规模不能太大

示例1:求完全数

在这里插入图片描述
思想:穷举所有数字,将符合条件的找出来。

关键点:求数字的所有因子。
对于数x,因子y,需要x % y = 0,此处y也需要尝试1 ~ x-1的所有数。

判断因子和是否等于x。

因此程序为

// 求完全数
#include <iostream>
using namespace std;

int main() {
	for (int i = 2; i <= 1000; i++) {
		int sum = 0;
		for (int j = 1; j <= i - 1; j++) {
			if (i % j == 0) { // 若是因子
				sum += j;
			}
		}
		if (sum == i) {
			cout << "完全数:" << i << endl;
		}
	}

	return 0;
}

这里还有点bug,是数学知识不完备,因子在1 ~ m/2的范围,因子不可能比原数字的一半还要大(除了它本身),我们循环地太多了。内循环应该是j <= i/2

结果为:
在这里插入图片描述
我们需要非常清晰地体会到构建程序框架的思维过程

int main() {
	// 穷举所有情况
	for (int i = 2; i <= 1000; i++) {
		// 求因子之和


		// 比较因子与该数是否相等

	}

	return 0;
}

然后,我们再实现其细节。

充分体会:穷举情况,再依次判断的计算过程。

示例2:求水仙花数

在数字100~999中,求水仙花数,例如:13 + 53+ 33 = 153。

同样地,穷举 + 验证

关键点:求一个三位数的每一位的数。

先建立思维框架

// 穷举100~999的所有数字
for(){
	// 求三位数的每一位数,将其立方和相加
	
	// 判断数字与其立方和是否相等
	
}

代码为:

// 求水仙花数
#include <iostream>
using namespace std;

int main() {
	for (int i = 100; i <= 999; i++) {
		int sum = 0;
		int temp = i;
		while (temp){
			int x = temp % 10;
			sum += (x*x*x);
			temp /= 10;
		}

		if (i == sum) {
			cout << "水仙花数:" << i << endl;
		}
	}
	return 0;
}

答案为:
在这里插入图片描述
再强调一遍

  • 循环 + 选择
  • 先构建思维框架,再填充细节

示例3:象棋算式

在这里插入图片描述
我们先将问题的输入找到:也就是5个变量,且满足

  • 每个变量范围是0~9
  • 5个变量各不相同

然后穷举所有的可能性,看是否能够满足表达式,这是恐怖的5层循环,但是问题规模还在可接受范围内。

我们试试看。

  • 兵:a
  • 炮:b
  • 马:c
  • 卒:d
  • 车:e
#include <iostream>
using namespace std;

int main() {
	int a, b, c, d, e;
	for (a = 0; a <= 9; a++) {
		for (b = 0; b <= 9; b++) {
			for (c = 0; c <= 9; c++) {
				for (d = 0; d <= 9; d++) {
					for (e = 0; e <= 9; e++) {
						// 若5个数都不同,再看是否满足表达式
						if (!(a == b || a == c || a == d || a == e
							|| b == c || b == d || b == e
							|| c == d || c == e
							|| d == e)) {
							int add1 = a * 1000 + b * 100 + c * 10 + d;
							int add2 = a * 1000 + b * 100 + e * 10 + d;
							int sum = e * 10000 + d * 1000 + c * 100 + a * 10 + d;
							if (sum == add1 + add2) {
								cout << "兵 = " << a << " 炮 = " << b 
									<< " 马 = " << c << " 卒 = " << d 
									<< " 车 = " << e << endl;
							}
						}
					}
				}
			}
		}
	}

	return 0;
}

结果是
在这里插入图片描述
不过显然,太低效率了,如果有n的话,这个算法的时间复杂度是O(n5),这几乎是不可接受的,太暴力了,问题规模显得有点大,是105了。

所以,我们虽然遵循了 循环 + 选择,但是,穷举地过于直接,看看有没有其他办法呢?

思想:即便是穷举,也要“聪明地”穷举

同样是穷举,也有不同的方法,最粗暴的就是循环,一层循环不行就嵌套多重循环……好蠢但是很简单好想。

我们来看看,同样是穷举,不同做法的差异。

示例

对于一个含有6个元素的一维数组,该数组每个数只能是0或1,将所有情况穷举出来。
例如(0,0,0,0,0,0)、(0,1,0,1,1,0)

最愚蠢的做法,直接6重循环

#include <iostream>
using namespace std;

int main() {
	int a, b, c, d, e, f;
	int number[6];
	for (a = 0; a <= 1; a++) {
		for (b = 0; b <= 1; b++) {
			for (c = 0; c <= 1; c++) {
				for (d = 0; d <= 1; d++) {
					for (e = 0; e <= 1; e++) {
						for (f = 0; f <= 1; f++) {
							number[0] = a;
							number[1] = b;
							number[2] = c;
							number[3] = d;
							number[4] = e;
							number[5] = f;
							for (int i = 0; i < 6; i++) {
								cout << number[i] << "  ";
							}
							cout << endl;
						}
					}
				}
			}
		}
	}
	return 0;
}

结果:在这里插入图片描述
这的确可以完成任务,但是这样的算法真的很糟糕,不是吗?

我们来看看优化后的穷举的方法

我们将其与二进制结合,因为对于这样的0、1序列,与二进制有直接联系,我们直接将十进制是0~63,转换为二进制,存入数组中,就可以了,这样大大降低了时间复杂度!

#include <iostream>
using namespace std;

int main() {
	int number[6];
	for (int i = 0; i <= 63; i++) {
		// 转换为二进制,除二求余再倒置
		int temp = i;
		for (int j = 5; j >= 0; j--) {
			number[j] = temp % 2;
			temp /= 2;
		}
		// 输出结果
		for (int k = 0; k < 6; k++) {
			cout << number[k] << "  ";
		}
		cout << endl;
	}
	return 0;
}

穷举加验证,循环加选择,蛮力解难题

穷举有方法,验证有策略,循环尽量浅,选择看题目

我们之前谈过,穷举法,需要能够穷举,数据规模不太大,现在,我们在此基础上进行了优化,同样能够穷举的,穷举方式的选择也很重要。

对于循环,我们希望尽可能减少嵌套层数

当然了,简单的属于通用普适技法,稍难的就需要动用你的观察力,但是这些东西你熟悉了,也就变成了简单的了。

接下来我们尝试优化示例3。

示例3优化

我们只关注,如何进行穷举,并且尽可能减小穷举规模空间,关注一个条件:5个数各不相同

这样一来,我们的问题规模就由105 = 10,000 变成了 10 x 9 x 8 x 7 x 6 = 30,240 ,变成了原来的30%左右。

可以用多重循环来列举出它们各种不同的取值情况,逐一地判断它们是否满足上述等式;为了避免同一数字被重复使用,可设立逻辑数组x,x[i](0≤i≤9)值为1时表示数i没有被使用,为0时表示数i已被使用。

int main() {
	int x[10];
	int a, b, c, d, e, i, m, n, s;
	for (i = 0; i <= 9; i++) 
		x[i] = 1;   /*x数组置初值*/
	
	for (a = 1; a <= 9; a++)
	{
		x[a] = 0; /*表示不能再让其他变量取与a相同的值*/
		for (b = 0; b <= 9; b++)
			if (x[b])  /*如果b取的当前值未被其他的变量重复*/
			{
				x[b] = 0; /*表示不能再让其他变量取与b相同的值*/
				for (c = 0; c <= 9; c++)
					if (x[c])   /*如果c取的当前值未被其他的变量重复*/
					{
						x[c] = 0;  /*表示不能再让其他变量取与c相同的值*/
						for (d = 0; d <= 9; d++)
							if (x[d])    /*如果d取的当前值未被其他的变量重复*/
							{
								x[d] = 0;   /*表示不能再让其他变量取与d相同的值*/
								for (e = 0; e <= 9; e++)
									if (x[e])
									{
										m = a * 1000 + b * 100 + c * 10 + d;
										n = a * 1000 + b * 100 + e * 10 + d;
										s = e * 10000 + d * 1000 + c * 100 + a * 10 + d;
										if (m + n == s)
											printf("兵:%d 炮:%d 马:%d 卒:%d车:%d\n",
												a, b, c, d, e);
									}
								x[d] = 1;  /*本次循环未找到解,让d取其他值*/
							}
						x[c] = 1;  /*本次循环未找到解,让c取其他值*/
					}
				x[b] = 1;     /*本次循环未找到解,让b取其他值*/
			}
		x[a] = 1;  /*本次循环未找到解,让a取其他值*/
	}
	return 0;
}

重要的收获:穷举有方法,不能上来题都不看就开始举,有条件地穷举,减少解空间,对于本题,5个数不同,可以当作题目条件来验证,也可以当成穷举的约束条件

同样的条件,不同的看法,就会产生不同的解决方案。

还记得离散数学中的附加条件证明法吗,将结论中的前提当条件用,再推出最终结论,极大简化了运算。

百元买百鸡问题

已知公鸡5元一只,母鸡3元一只,小鸡1元三只,用100元买100只鸡,问公鸡、母鸡、小鸡各多少只?

  1. 知道解空间
    • 公鸡:x 0~20
    • 母鸡:y 0~33
    • 小鸡:z 0~100
  2. 如何列举
    • 最简单思路:三重循环
  3. 如何验证
    1. x + y + z = 100
    2. 5x + 3y + z/3 = 100
    3. z % 3 == 0
    4. 这样看来,可以将z变量去掉了,解空间也减少了100倍,三重循环也变成了二重循环。也就是我们将验证条件当成解空间的约束条件,减少了解空间的规模,这与我们上面的示例3优化是一个思路。

注意:小鸡是1元3只,需要注意 z/3 时,int会去掉小数点,需要验证z是3的倍数。

#include <iostream>
using namespace std;

int main() {
	int x, y, z;
	for (x = 0; x <= 20; x++) {
		for (y = 0; y <= 33; y++) {
			if ((100 - x - y) % 3 == 0) {
				if ((5 * x + 3 * y + (100 - x - y) / 3) == 100) {
					cout << "公鸡:" << x << endl;
					cout << "母鸡:" << y << endl;
					cout << "小鸡:" << 100 - x - y << endl;
					cout << "**********" << endl;
				}
			}
		}
	}

	return 0;
}

在这里插入图片描述

示例

求所有的三位数,它除以11所得的余数等于它的三个数字的平方和.

分析:

  1. 解空间:三位数x,100~999
  2. 如何枚举:循环
  3. 如何验证(约束条件):x % 11 == 三个数字平方和
  4. 约束条件可否去限制解空间? 否
#include <iostream>
using namespace std;
// 求所有的三位数,它除以11所得的余数等于它的三个数字的平方和.
int main() {
	for (int i = 100; i <= 999; i++) {
		// 求三个数平方和
		int sum = 0;
		int temp = i;
		while (temp)
		{
			int x = temp % 10;
			sum += (x * x);
			temp /= 10;
		}

		// 验证
		if (sum == i % 11) {
			cout << "数字:" << i << endl;
		}
	}
	return 0;
}

结果:
在这里插入图片描述

对问题进行建模

我们进一步分析上一题

求所有的三位数,它除以11所得的余数等于它的三个数字的平方和.

假设3位数A的三个数是x,y,z。

  1. 0 <= A % 11 <= 10
  2. x2 + y2 + z2 <= 10,x*100 + y*10 + z = A
  3. 1 <= x <= 3, 0 <= y <= 3,0 <= z <= 3,且都是整数

这样一来,经过简单的人工处理,解空间减少了很多,然后再进行 穷举 + 验证 就可以了,显然比之前的更加高效,因为进行了人工分析

int x, y, z;
for (x = 1; x <= 3; x++) {
	for (y = 0; y <= 3; y++) {
		for (z = 0; z <= 3; z++) {
			int num = x * 100 + y * 10 + z;
			if ((x*x + y * y + z * z) == num % 11) {
				cout << "数字:" << num << endl;
			}
		}
	}
}

这里强调的其实也是,将验证条件进一步分析,将其转换为解空间的约束条件,以降低解空间规模。

穷举 + 验证:穷举范围、穷举方式、验证条件、约束条件

我们再回顾之前的例子,充分体会穷举法的分析思路

穷举依赖的技术是遍历,也就是解空间的每个解,都要找一遍,注意尽量避免充分。

示例1

在这里插入图片描述

  • 解空间:2~1000
  • 验证条件:各因子之和等于本身
  • 约束条件:没有可以转化的验证条件
  • 穷举方式:解空间 + 约束条件–>循环

示例2

在数字100~999中,求水仙花数,例如:13 + 53+ 33 = 153。

  • 解空间:100~999
  • 验证条件:水仙花数
  • 约束条件:无
  • 穷举方式:解空间+约束条件–>循环

示例3

在这里插入图片描述

  • 解空间:5个变量,每个变量范围0~9
  • 验证条件:满足题目表达式
  • 约束条件:5个变量各不相同
  • 穷举方式:解空间+约束方式–>减小解空间–>5重循环,如果有值重复,则跳出

示例4

对于一个含有6个元素的一维数组,该数组每个数只能是0或1,将所有情况穷举出来。
例如(0,0,0,0,0,0)、(0,1,0,1,1,0)

注意:本题本身就是求解空间

  • 解空间:6个数,每个数是0或1,求全部的0、1序列
  • 验证条件:情况不重复
  • 约束条件:无
  • 穷举方式:解空间+约束条件–>6重循环

问题建模 & 转化:类二进制数

很明显这题的0、1序列,与二进制数类似,可以转换问题,改为求0~63十进制的二进制,间接解题。

属于特殊解法,需要惊人的洞察力。

示例5

已知公鸡5元一只,母鸡3元一只,小鸡1元三只,用100元买100只鸡,问公鸡、母鸡、小鸡各多少只?

  • 原始解空间:3个变量,x∈[0,20],y∈[0,33],z∈[0,100]
  • 验证条件
    • x + y + z = 100(看做约束条件)
    • 5x + 3y + z/3 = 100
    • z / 3为整数(z % 3 = 0)
  • 约束条件:将验证条件中第一条,转换为约束条件,也就是z = 100 - x -y这与就去掉了一个变量,这个变量范围最大,能够充分减少解空间。
  • 约束后解空间:2个变量,x∈[0,20],y∈[0,33]
  • 穷举方式:双重循环

注意:谁是验证条件,谁是约束条件,是跟你的看法有关的。

示例6

求所有的三位数,它除以11所得的余数等于它的三个数字的平方和.

  • 原始解空间:数字A,三个位是x,y,z,A范围100~999
  • 验证条件
    • A % 11 = x2 + y2 + z2
  • 约束条件:由验证条件进一步分析得出,需要有经验和洞察力
    • A % 11 ∈[0,10]
    • x∈[1,3],y∈[0,3],z∈[0,3]
  • 约束后解空间:x∈[1,3],y∈[0,3],z∈[0,3]
  • 穷举方式:三重循环

备注:验证方式需要具体问题具体分析

充分体会不同算法的组合,因为一道题目,可能背后涉及到多个不同的算法的组合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XV_

感谢您的认可,我会继续努力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值