- 炮兵问题的优化,设立逻辑数组
蛮力法设计思想
有策略地穷举 + 验证
- 制定穷举策略
- 避免重复
简单来说,就是列举问题所有可能的解,然后去看看是否满足题目要求,是一种逆向解题方式。(我也不知道答案是什么,但是我知道答案肯定在这些范围里面,我一个个试就完了。)
所以我们应该做的是
- 知道可能的答案都有哪些
- 使用方法将其一一列举
- 将每个解依次与条件比照
蛮力法使用范围
- 所有解空间:例如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只鸡,问公鸡、母鸡、小鸡各多少只?
- 知道解空间
- 公鸡:x 0~20
- 母鸡:y 0~33
- 小鸡:z 0~100
- 如何列举
- 最简单思路:三重循环
- 如何验证
- x + y + z = 100
- 5x + 3y + z/3 = 100
- z % 3 == 0
- 这样看来,可以将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所得的余数等于它的三个数字的平方和.
分析:
- 解空间:三位数
x
,100~999 - 如何枚举:循环
- 如何验证(约束条件):
x % 11 == 三个数字平方和
- 约束条件可否去限制解空间? 否
#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。
0 <= A % 11 <= 10
- x2 + y2 + z2 <= 10,
x*100 + y*10 + z = A
- 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]
- 穷举方式:三重循环
备注:验证方式需要具体问题具体分析。
充分体会不同算法的组合,因为一道题目,可能背后涉及到多个不同的算法的组合。