枚举
一.枚举算法
1.对问题可能解集合的每一项
2.根据问题给定的检验条件判定哪些是成立的
3.使条件成立的即是问题的解
二.枚举中三个关键问题
1.问题一:给出解空间,建立简洁的数学模型,可能的情况是什么,模型中变量数尽可能少,它们之间相互独立
(“求小于N的最大素数” 中的条件是 “n不能被[2,n)中任意一个素数整除”而不是 “n不能被[2,n)中任意一个整数整除”)
2.问题二:减少搜索的空间,利用知识缩小模型中各变量的取值范围, 避免不必要的计算,减少代码中循环体执行次数
(除2之外, 只有奇数才可能是素数, {2,2*i+1|1<=i, 2*i+1<n})
3.问题三:采用合适的搜索顺序,搜索空间的遍历顺序要与模型中条件表达式一致
(对{2,2*i+1|1<=i, 2*i+1<n}按照从小到大的顺序)
三.百钱百鸡问题
鸡翁一值钱五,鸡母一值钱三,鸡雏三值钱一,百钱买百鸡,问鸡翁,鸡母,鸡雏各几何
求解方法:
1.先构造可能的解的集合 S={(X,Y,Z)|0<=X<=20,0<=Y<=33,Z=100-X-Y}(X, Y, Z分别代表买公鸡, 母鸡和小鸡的只数)
2.然后验证条件X+Y+Z=100, 5X+3Y+Z/3=100
1 #include<iostream> 2 using namespace std; 3 4 int main() 5 { 6 for (int x = 0; x <= 20; x++) 7 { 8 for (int y = 0; y <= 33; y++) 9 { 10 int z = 100 - x - y; 11 if (z % 3 == 0) 12 { 13 if (5 * x + 3 * y + z / 3 == 100) 14 { 15 cout << x << " " << y << " " << z << endl; 16 } 17 } 18 } 19 } 20 }
四.熄灯问题
1.问题描述:
有一个由按钮组成的矩阵, 其中每行有6个按钮, 共5行。每个按钮的位置上有一盏灯
当按下一个按钮后, 该按钮以及周围位置(上边, 下边, 左边, 右边)的灯都会改变一次,如果灯原来是点亮的, 就会被熄灭,如果灯原来是熄灭的, 则会被点亮。
2.解题分析:
(第2次按下同一个按钮时, 将抵消第1次按下时所产生的结果,每个按钮最多只需要按下一次,各个按钮被按下的顺序对最终的结果没有影响
对第1行中每盏点亮的灯, 按下第2行对应的按钮, 就可以熄灭第1行的全部灯,如此重复下去, 可以熄灭第1, 2, 3, 4行的全部灯)
枚举所有可能的按钮(开关)状态, 对每个状态计算一下最后灯的情况, 看是否都熄灭。每个按钮有两种状态(按下或不按下)一共有30个开关, 那么状态数是230, 太多, 会超时如何减少枚举的状态数目呢?
基本思路: 如果存在某个局部, 一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种,或者不多的n种,那么就只需枚举这个局部的状态即可。
本题是否存在这样的 “局部” 呢?经过观察,发现第1行就是这样的一个 “局部”
因为第1行的各开关状态确定的情况下,这些开关作用过后,将导致第1行某些灯是亮的,某些灯是灭的要熄灭第1行某个亮着的灯(假设位于第i列), 那么唯一的办法就是按下第2行第i列的开关(因为第1行的开关已经用过了, 而第3行及其后的开关不会影响到第1行)为了使第1行的灯全部熄灭, 第2行的合理开关状态就是唯一的。第2行的开关起作用后,为了熄灭第2行的灯, 第3行的合理开关状态就也是唯一的。以此类推, 最后一行的开关状态也是唯一的只要第1行的状态定下来, 记作A, 那么剩余行的情况就是确定唯一的了。
用一个6*8的数组来表示按钮矩阵:
简化计算数组下一行的值的计算公式,第0行, 第0列和第7列不属于PRESS矩阵范围, 可全置0
1 #include <stdio.h> 2 int puzzle[6][8];//灯的初始状态 3 int press[6][8];//要计算的结果,1表示按下按钮 4 bool guess(){ 5 int c, r; 6 for(r=1; r<5; r++) 7 for(c=1; c<7; c++) 8 press[r+1][c] = (puzzle[r][c]+press[r][c]+ 9 press[r-1][c]+ press[r][c-1]+press[r][c+1]) %2; 10 for(c=1; c<7; c++) 11 if ((press[5][c-1] + press[5][c] + press[5][c+1] + 12 press[4][c]) %2 != puzzle[5][c] ) 13 return(false); 14 return(true); 15 } 16 void enumerate (){ 17 int c; 18 bool success; 19 for ( c=1; c<7; c++) 20 press[1][c] = 0; 21 while(guess()==false){ 22 press[1][1]++; 23 c = 1; 24 while (press[1][c] > 1) { 25 press[1][c] = 0; 26 c++; 27 press[1][c]++; 28 } 29 } 30 return; 31 } 32 int main() { 33 int cases, i, r, c; 34 scanf("%d", &cases); 35 for ( r=0; r<6; r++ ) 36 press[r][0] = press[r][7] = 0; 37 for ( c=1; r<7; r++ ) 38 press[0][c] = 0; 39 for (i=0; i<cases; i++){ 40 for(r=1; r<6; r++) 41 for(c=1; c<7; c++) 42 scanf("%d", &puzzle[r][c]); 43 enumerate(); 44 printf("PUZZLE #%d\n", i+1); 45 for(r=1; r<6; r++){ 46 for(c=1; c<7; c++) 47 printf("%d ", press[r][c]); 48 printf("\n"); 49 } 50 } 51 return 0; 52 }
五.讨厌的青蛙
1.问题描述:有一种小青蛙,每到晚上,这种青蛙会跳跃稻田,从而踩踏稻子。农民早上看到被踩踏的稻子,希望找到造成最大损害的那只青蛙经过的路径每只青蛙总是沿着一条直线跳跃稻田且每次跳跃的距离都相同。
稻田里的稻子组成一个栅格, 每棵稻子位于一个格点上,而青蛙总是从稻田的一侧跳进稻田, 然后沿着某条直线穿越稻田, 从另一侧跳出去
可能会有多只青蛙从稻田穿越,青蛙每一跳都恰好踩在一棵水稻上, 将这棵水稻拍倒。有些水稻可能被多只青蛙踩踏,农民所见到的是右图中的情形, 并看不到左图中的直线,也见不到别人家田里被踩踏的水稻。
2.解题思路 — 枚举
枚举每个被踩的稻子作为行走路径起点 (5000个),对每个起点, 枚举行走方向 (5000种),对每个方向枚举步长 (5000种),枚举步长后还要判断是否每步都踩到水稻,时间: 5000*5000*5000 * , 不行!
每条青蛙行走路径中至少有3棵水稻,假设一只青蛙进入稻田后踩踏的前两棵水稻分别是(X1,Y1),(X2,Y2)。那么青蛙每一跳在 X 方向上的步长 dX = X2 - X1,在 Y 方向上的步长 dY = Y2 - Y1;
(X1 – dX, Y1 - dY) 需要落在稻田之外;当青蛙踩在水稻(X, Y)上时, 下一跳踩踏的水稻是(X + dX, Y + dY);将路径上的最后一棵水稻记作(XK, YK), (XK + dX, YK + dY)需要落在稻田之外; 11
猜测(X1, Y1), (X2, Y2) -- 所要寻找的行走路径上的前两棵水稻当下列条件之一满足时, 这个猜测就不成立:
• 青蛙不能经过一跳从稻田外跳到(X1, Y1)上
• 按照(X1, Y1), (X2, Y2)确定的步长, 从(X1, Y1)出发, 青蛙最多经过(MAXSTEPS - 1)步, 就会跳到稻田之外MAXSTEPS是当前已经找到的最好答案
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <algorithm> 4 using namespace std; 5 int r, c, n; 6 struct PLANT { 7 int x, y; 8 }; 9 PLANT plants[5001]; 10 PLANT plant; 11 int searchPath(PLANT secPlant, int dX, int dY) ; 12 13 int main() 14 { 15 int i, j, dX, dY, pX, pY, steps, max = 2; 16 scanf("%d %d", &r, &c); 17 //行数和列数, x方向是上下, y方向是左右 18 scanf("%d", &n); 19 for (i = 0; i < n; i++) 20 scanf("%d %d", &plants[i].x, &plants[i].y); 21 //将水稻按x坐标从小到大排序, x坐标相同按y从小到大排序 22 sort(plants, plants + n); 23 for (i = 0; i < n - 2; i++) //plants[i]是第一个点 24 for ( j = i + 1; j < n -1 ; j++) { //plants[j]是第二个点 25 dX = plants[ j ].x - plants[i].x; 26 dY = plants[ j ].y - plants[i].y; 27 pX = plants[ i ].x - dX; 28 pY = plants[ i ].y - dY; 29 if (pX <= r && pX >= 1 && pY <= c && pY >= 1) 30 continue; 31 //第一点的前一点在稻田里, 32 //说明本次选的第二点导致的x方向步长不合理(太小) 33 // 取下一个点作为第二点 34 if (plants[ i ].x + (max - 1) * dX > r) 35 break; 36 //x方向过早越界了. 说明本次选的第二点不成立 37 //如果换下一个点作为第二点, x方向步长只会更大, 更不成立, 所以应该 38 //认为本次选的第一点必然是不成立的, 那么取下一个点作为第一点再试 39 pY = plants[ i ].y + (max - 1) * dY; 40 if ( pY > c || pY < 1) 41 continue; //y方向过早越界了, 应换一个点作为第二点再试 42 steps = searchPath(plants[ j ], dX, dY); 43 //看看从这两点出发, 一共能走几步 44 if (steps > max) max = steps; 45 } 46 if ( max == 2 ) max = 0; 47 printf("%d\n", max); 48 } 49 bool operator < ( const PLANT & p1, const PLANT & p2) 50 { 51 if ( p1.x == p2.x ) 52 return p1.y < p2.y; 53 return p1.x < p2.x ; 54 } 55 //判断从 secPlant点开始, 步长为dx, dy, 那么最多能走几步 56 int searchPath(PLANT secPlant, int dX, int dY){ 57 PLANT plant; 58 int steps; 59 plant.x = secPlant.x + dX; 60 plant.y = secPlant.y + dY; 61 steps = 2; 62 while (plant.x <= r && plant.x >= 1 && plant.y <= c && plant.y >= 1) { 63 if (! binary_search(plants, plants + n, plant)) { 64 //每一步都必须踩倒水稻才算合理, 否则这就不是一条行走路径 65 steps = 0; 66 break; 67 } 68 plant.x += dX; 69 plant.y += dY; 70 steps++; 71 } 72 return(steps); 73 }
13