第7章暴力求解法【教学内容相关章节】7.1简单枚举 7.2枚举排列 7.3子集生成7.4回溯法 7.5隐式图搜索【教学目标】(1)掌握整数、子串等简单对象的枚举方法;(2)熟练掌握排列生成的递归方法;(3)熟练掌握用“下一个排列”枚举全排列的方法;(4)理解解答树,并能估算典型解答树的结点数;(5)熟练掌握子集生成的增量法、位向量法和二进制法;(6)熟练掌握回溯法框架,并能理解为什么它往往比生成-测试法高效;(7)熟练掌握解答树的宽度优先搜索和迭代加深搜索;(8)理解倒水问题的状态图与八皇后问题的解答树的本质区别;(9)熟练掌握八数码问题的BFS实现;(10)熟练掌握集合的两种典型实现——hash表和STL集合。
【教学要求】掌握整数、子串等简单对象的枚举方法;熟练掌握排列生成的递归方法;熟练掌握用“下一个排列”枚举全排列的方法;理解子集树和排列树;熟练掌握回溯法框架;熟练掌握解答树的宽度优先搜索和迭代搜索;熟练掌握集合的两种典型实现——hash表和STL集合。
【教学内容提要】本章主要讨论暴力法(也叫穷举法、蛮力法),它要求调设计者找出所有可能的方法,然后选择其中的一种方法,若该方法不可行则试探下一种可能的方法。
介绍了排列生成的递归方法;在求解的过程中,提出了解答树的概念(如子集树和排列树);介绍了回溯法的基本框架;介绍了集合的两种典型实现——hash表和STL集合。
【教学重点、难点】教学重点:(1)熟练掌握排列生成的递归方法;(2)理解解答树,并能估算典型解答树的结点数;(3)熟练掌握子集生成的增量法、位向量法和二进制法;(4)熟练掌握回溯法框架,并能理解为什么它往往比生成-测试法高效;(5)熟练掌握解答树的宽度优先搜索和迭代搜索;(6)熟练掌握集合的两种典型实现——hash表和STL集合。
教学难点:(1)熟练掌握子集生成的增量法、位向量法和二进制法;(2)熟练掌握回溯法框架,并能理解为什么它往往比生成-测试法高效;(3)熟练掌握解答树的宽度优先搜索和迭代搜索;(4)熟练掌握集合的两种典型实现——hash表和STL集合。
【课时安排】7.1简单枚举 7.2枚举排列 7.3子集生成7.4回溯法 7.5隐式图搜索引言暴力法也称为穷举法、蛮力法,它要求调设计者找出所有可能的方法,然后选择其中的一种方法,若该方法不可行则试探下一种可能的方法。
暴力法也是一种直接解决问题的方法,常常直接基于问题的描述和所涉及的概念定义。
暴力法不是一个最好的算法,但当我们想不出更好的办法时,它也是一种有效的解决问题的方法。
暴力法的优点是逻辑清晰,编写程序简洁。
在程序设计竞赛时,时间紧张,相对于高效的、巧妙的算法,暴力法编写的程序简单,能更快地解决问题。
同时蛮力法也是很多算法的基础,可以在蛮力法的基础上加以优化,得到更高效的算法。
而且,某些情况下,算法规模不大,使用优化的算法没有必要,而且某些优化算法本身较复杂,在规模不在时可能因为复杂的算法浪费时间,反而不如简单的暴力搜索。
使用暴力法常用如下几种情况:(1)搜索所有的解空间;(2)搜索所有的路径;(3)直接计算;(4)模拟和仿真。
7.1 简单枚举在枚举复杂对象之前,先尝试着枚举一些相对简单的东西,如整数、子串等。
暴力枚举对问题进行一定的分析往往会让算法更加简洁、高效。
7.1.1 除法输入正整数n,按从小到大的顺序输出所有形如abcde/fghij=n的表达式,其中a~j 恰好为数字0~9的一个排列,2≤n≤79。
样例输入:62样例输出:79546/01283=6294736/01528=62【分析】只需要枚举fghij就可以计算出abcde,然后判断是否所有数字都不相同即可。
不仅程序简单,而枚举量也从10!=3628800降低至不到1万。
由此可见,即使采用暴力枚举,也是需要认真分析问题。
完整的程序如下:#include <iostream>using namespace std;bool test(int i,int j){ //用数组t存放i,j的各位数字int t[10]={0}; //初始化数组t,使得各位数字为0,好处是使得fghij<10000时f位置为0int ia = 0;while(i) { //取i中各位数字存放在数组t中t[ia++] = i % 10;i = i / 10;}while(j) { //取j中各位数字存放在数组t中t[ia++] = j % 10;j = j / 10;}//判断a~j是否恰好为数字的0~9的一个排列for(int m = 0; m < 10; ++m)for(int n = m+1; n < 10; ++n)if(t[n] == t[m]) return false;return true;}int main(){int n;int k;while(cin >> n && n >=2 && n <= 79) {k = 1234;while(k <= 98765) {int j = k * n;if(j < 100000) { //若fghij<10000,满足题目的条件,f位置输出0if(test(j,k)) {cout << j << "/" ;if(k < 10000) cout <<"0";cout << k << "=" << n <<endl;}}++k;}}return 0;}7.1.2 最大乘积输入n个元素组成的序列S,你需要找出一个乘积最大的连续子序列。
如果这个最大的乘积不是正整,应输出-1(表示无解)。
1≤n≤18,-10≤S i≤10。
样例输入:32 4 -352 5 -1 2 -1样例输出:820【分析】连续子序列有两个要素:起点和终点,因此只需要枚举起点和终点即可。
由于每个元素的绝对值不超过10,一共又不超过18个元素,最大可能的乘积示会超过1018,可以用long long存下。
完整的程序如下:#include <stdio.h>int main(){int a[20]; //存放输入序列的每一个元素__int64 ans = 0; //存放最大的乘积int n; //输入元素的个数__int64 tmp; //存放乘积的中间结果while(scanf("%d",&n)!=EOF){ //输入序列中元素的个数nfor(int i = 0;i < n; i++) //输入序列中的元素scanf("%d",&a[i]);for(i = 0; i < n; i++) {//从序列中的第0个元素开始,枚举最大连续乘积,并用ans存储 tmp = 1;for(int j = i; j < n; j++) {tmp *= a[j];if(tmp > ans) ans = tmp;}}if(ans>0)printf("%I64d\n",ans);elseprintf("%d\n",-1);}return 0;}7.1.3 分数拆分输入正整数k,找到所有的正整数x≥y,使得111k x y =+。
样例输入:212样例输出:21/2=1/6+1/31/2=1/4+1/481/12=1/156+1/131/12=1/84+1/141/12=1/60+1/151/12=1/48+1/161/12=1/36+1/181/12=1/30+1/201/12=1/28+1/211/12=1/24+1/24【分析】找出所有有x,y,枚举完成了就行了。
但是从1/12=1/156+1/13可以看出,x可以比y大很多。
由于x≥y,有11x y≤,因此111k y y-≤,即y≤2k。
这样,只需要在2k范围内枚举y,然后根据y尝试计算出x即可。
完整的程序如下:#include <stdio.h>int main(){int k;int x, y, count = 0;//变量count统计等式的个数while(scanf("%d", &k) != EOF) {for(y = k+1;y <= 2 * k; y++){ //判断1/k=1/x+1/y等式的个数, x=(k * y) / (y - k);if(x * (y-k) == k * y){count++;}}printf("%d\n",count); //输出满足条件的等式的个数for(y = k+1; y <= 2 * k; y++){ //输出满足条件的等式x=(k * y) / (y - k);if(x * (y - k) == k * y){printf("1/%d=1/%d+1/%d\n",k,x,y);}}}return 0;}7.1.4 双基回文数如果一个正整数n至少有两个不同的进位制b1和b2下都是回文数(2≤b1,b2≤10),则称n是双基回文数(注意,回文数不以包含前导零)。
输入正整数S<106,输出比S大的最小双基回文数。
样例输入:1600000样例输出:1632995【分析】最自然的想法是:从n+1开始依次判断每个数是否为双基回文数,而在判断时要枚举所有可能的基数(2~10)。
意外的是:对于S<106的“小规模数据”来说是足够快的——双基回文数太多太密。
完整的程序如下:#include <iostream>using namespace std;bool huiwen(int n){int i,j,a[30],s;int total = 0; //存放数s是回文数的基数个数for(int base = 2; base <= 10; base++) {i = 1;s = n;while(s) {a[i] = s % base;s = s / base;i++;}i--;for(j = 1; j <= i/2; j++) //判断数s在基base下是否是回文数if(a[j] != a[i-j+1]) break;if(j > i/2) total++; //数s在基base下是回文数,则total++ if(total >= 2) return true;}return false;}int main(){int s;while(scanf("%d",&s) == 1) {for(s = s+1; ; s++) {if(huiwen(s)) {cout << s << endl; break;}}}return 0;}7.2 枚举排列输入整数n,按字典序从大到小的顺序输出前n个数的所有排列。