枚举的思想其实时日常生活中提取的一种智慧 ^——————^ 枚举的思想在生活中有着非常广泛的应用
在对事物进行归纳推理时,会逐一考察某个事物的所有可能的情况,并且逐一进行检验,这就是枚举。例如,当我们忘记密码,就会按照记忆中的密码逐一进行尝试;警察确定凶手也是列出所有的嫌疑人然后排除。
可见枚举就是对问题变量的可能解集合的每一个解进行判断。
枚举时十分直观的,易于理解,不像递归那么“只能意会”,是计算机来求解问题的最常用的方法之一,常用来解决那些通过公式推导、规矩演算的方法不能解决的问题。
采用枚举解决问题时,要注意三个方面:
(1):建立一个简介的模型,模型中的变量要尽可能的少,变量之间时相互独立的,这样就能使得搜索空间的维度变小(在代码中的体现就是循坏嵌套的层数)。
(2):减少搜索的空间,根据自己的知识,缩小各个变量的范围,避免无效计算。
(3):采用一种合适的搜索顺序,要符合自己写的数学模型。例如是用从小到大的遍历还是从大到小的遍历等等。
目录
百钱百鸡
公鸡5文钱一只,母鸡3文钱一只,小鸡3只一文钱,用100文钱买一百只鸡,其中公鸡,母鸡,小鸡都必须要有。 问:公鸡,母鸡,小鸡要买多少只刚好凑足100文钱。
(1)缩小问题的搜索空间、搜索顺序(本题未用到)
拿到题肯定是按照题意
公鸡for(1~100) 母鸡for(1~100) 小鸡for(1~100)
想想看,公鸡可能买100只嘛?明显不可能,所以公鸡最多买多少只呢?(100/5=20)所以公鸡for(1~20),同理推出母鸡for(1~33),小鸡for(1~99)
(2)简洁的数学模型
想想看这个是不是一定要三重循坏呢?我们是不是可以将小鸡=100-公鸡-母鸡。这样是不是就减少一重循坏。
(3)进一步减少搜索空间
我们知道小鸡是三只一文钱,那么小鸡可能存在2只或者1只么?所以我们可以将小鸡的范围锁定在3的倍数
package com.suanfa;
public class Bai
{
public static void main(String[] args)
{
int x, y;
for(x=0; x<=20; x++)
{
for(y=0; y<=33; y++)
{
int z = 100 - x - y;
if (z%3==0)
if(5*x+3*y+z/3==100)
{
System.out.printf("公鸡%d只,母鸡%d只,小鸡%d只。\n", x, y, z);
}
}
}
}
}
生理周期
人生来就有三个生理周期,分别为体力、感情和智力周期,它们的周期长度为 23 天、 28 天和33 天。 每一个周期中有一天是高峰。 因为三个周期的周长不同,所以 通常三个周期的高峰不会落在同一天。 对于每个人,我们想知道何时三个高峰落在同一天。 对于每个周期,我们会给出从当前年份的第一天开始,到出现高峰的天数(不一定是第一次 高峰出现的时间)。 你的任务是给定一个从当年第一天开始数的天数,输出从给定时间开始 (不包括给定时间)下一次三个高峰落在同一天的时间(距给定时间的天数)(最大天数不超过21252)
(1)减少搜索的空间
由题意知道,需要我们求三者什么时候发生在同一天。
那求体力、智力共同高峰时
for(i=d+1;i<21252;i++)
判断i是否是23的倍数
for(j:1~21252)
判断j是否是28的倍数
那要是假如34是第一次体力高峰,那32+23是不是下一次的体力高峰。
共同高峰那肯定要是先是体力高峰,在看是不是智力高峰,优化如下
for(i=d+1;i<21252;i++)
for(j=i;j<21252;j+=23)
同理可推出三者共同高峰
package com.suanfa;
import java.util.Scanner;
public class ZhouQi
{
public static void main(String[] args)
{
Scanner scanner = new Scanner(System.in);
int t=0;
while(true){
int a = scanner.nextInt();//体力高峰时间
int b = scanner.nextInt();//智力高峰时间
int c = scanner.nextInt();//情感高峰时间
int d = scanner.nextInt();//开始时间
int j=0;
t++;
if(a==-1)
break;
for( j=d+1;j<=21252;j++)
{
if((j-a)%23==0)
break;//+23确保是23的倍数 进一步考虑28的倍数
}
for(;j<=21252;j+=23)
{
if((j-b)%28==0)
break;//满足条件的执行下一步 跳出循环
}
for(;j<=21252;j+=23*28)
{
if((j-c)%33==0)
break;
}
System.out.printf("\nCase:%d:the next triple peak
occurs in %d days.",t,j-d);//最坏情况下输出21252
}
}
}
完美立方
形如a^3= b^3 + c^3 + d^3的等式被称为 完美立方 等式。
例如12^3= 6^3 + 8^3 + 10^3 。
编写一个程序,对任给的正整 数 N (N≤100),寻找所有的 四元组 (a, b, c, d),使得a3 = b3 +c3 + d3, 其中a,b,c,d 大于1, 小于等于N,且b<=c<=d。 输入一个正整 数 N (N≤100)。
(1):减少运算消耗
按照循坏来说,三重循坏,每遍历一次,就可以得出一个三元组,我们是不是就要计算一次a*3是否等于b*3+c*3+d*3,就要计算9次乘法,2次加法,1次比较。
怎么简化呢?我们可以把范围内的数的三次方直接求出来,存入数组,在循坏在就只要计算2次加法,1次比较。
(2):减少搜索空间
如果a=1时,是不是没有解
a=2时,是不是没有解
a=3时,是不是也没有解
…………
a=6时,才出现解,那我们a直接从6开始,就节约了很多的搜索空间,看起来才是节约了5次搜索,但是这是四层循坏哎,那就多了(文字可能不好理解这个意思,可以看代码理解一下)
package com.suanfa;
import java.util.Scanner;
public class Cubic_number//完美立方数
{
public static void main(String[] args)
{
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(), a, b, c, d;
int[] arr =new int[n+1];
//与其在后面用一次计算一次立方,还不如直接在前面用数组直接存放每个数的立方值,后面判断时就只要算加法
for (int i=2; i <= n; i++)
arr[i] = i*i*i;
for (a=6; a<=n; a++)//很容易得知从2~5没有完美立方数,则从6开始,减少计算的次数
for (b=2; b<a; b++)//若b==a时,b、c、d则要为零,又已知不存在为零的情况,故不能等于
for (c=b; c<a; c++)
for (d=c; d<a; d++)
if (arr[a]==arr[b]+arr[c]+arr[d])//判断方法
System.out.printf("%d^3=%d^3+%d^3+%d^3\n", a, b, c, d);
}
}
熄灯问题
有一个6X5的矩阵,1表示开灯,0表示关灯,点一次灯,就会影响这个灯的上下左右的灯,改变他们的状态。
(1)当按下一个按钮后,该按钮以及周围位置(上、下、左、右)的灯都会改变一次状态,即: 灯原来是点亮的,就会被熄灭 灯原来是熄灭的,则会被点亮。
(2)对矩阵中的每盏灯设置一个初始状态 ,写一个程序,判断哪些初始状态下,按下某些按钮,可以使得所有灯都熄灭。
(1)减少搜索空间
第2次按下同一个按钮时,将抵消掉第1次按下时所产生的结果
由此,我们推导出:每个按钮最多只需要按下一次(因为按两次等于没操作)
对于第1行中每盏点亮的灯,按下第二行对应的按钮,就可以熄灭第1行的全部灯
如此重复下去,就可以熄灭第1、2、3、4行的全部灯
按照上述思路,我们的第一想法是:枚举所有可能的按钮(开关)状态,对每个状态计算一下最后 灯的情况,看是否都熄灭,但是这种情况是2的30次方。
为了使第1行的灯全部熄灭,第2行的合理开关状态就是唯一的
第2行的开关起作用后 为了熄灭第2行的灯,
第3行的合理开关状态,就也是唯一的
以此类推,最后一行的开关状态也是唯一的
问题就变成:只要第1行的灯状态定下来,记作A,那么剩余行的情况就是唯一确定的了 这样的话,我们只需要推算出最后一行的开关状态,然后看看最后一行的开关起作用后,最 后一行的所有灯是否都熄灭,这样就只要枚举2的6次方。
如何枚举第一排的灯的改变状态呢?
有一个方法是利用二进制来改变
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1
这就枚举了每一个改变状态。
package com.suanfa;
import java.util.Scanner;
/**
* @aouther lungcen
*/
public class Douse_the_light
{
/* 灯的矩阵:6x8 */
static int[][] puzzle = new int[6][8];//将角和边的特殊一般化,就可不写特殊方法
/* 开关的矩阵:6x8 */
static int[][] press = new int[6][8];
public static void main(String[] args)
{
Scanner scanner = new Scanner(System.in);//一个一个的输入灯的数组
for (int i = 1; i < 6; i++)
for (int j = 1; j < 7; j++)
puzzle[i][j] = scanner.nextInt();
enumerate();//进行枚举第一层灯的每一个变化
for (int i = 1; i < 6; i++)//输出可以完成任务的开关状态
for (int j = 1; j < 7; j++)
System.out.print(press[i][j]+" ");
System.out.println();
}
/**
* 枚举第一行开关press[1]的64种状态(模拟二进制进位来实现)
* 1、先将第一个状态赋给第一行开关 press[1][1] ~ press[1][6] -> 0 0 0 0 0 0
* 2、循环处理某一个开关的状态,是否能够熄灭所有的灯
* a、如果可以,就跳出循环
* b、如果不能,则枚举下一状态
*/
private static void enumerate()
{
int c;
for(c = 1; c < 7; c++)//第一种情况:第一排的开关一个都没有按下
press[1][c] = 0;
while(!guess())// 如果第一行开关的状态不能熄灭所有的灯
{
press[1][1]++;//以二进制来表示开关的变化
c = 1;
while(press[1][c] > 1)//大于二则进一位,循坏判断
{
press[1][c] = 0;//这一位就返回0
c++;//往前走一位
press[1][c]++;//此位加一
if(c > 6)//如果走到最后就停止
break;
}
}
}
/**
* 判断第一行开关的某个状态是否能够熄灭所有的灯
* @return true:能熄灭所有灯;false:不能
*/
private static boolean guess()//看看是否满足条件
{
// 根据第一行开关的状态,来得出所有5行开关的状态
for(int r = 1; r < 5; r++)//不取第五层是因为要根据第五行来判断
{
for(int c = 1; c < 7; c++)
{//灯的变化==灯的开始状态+上一行开关的变化+左一位开关的变化+本身开关的变化+右一位开关的变化
press[r + 1][c] = (puzzle[r][c] + press[r - 1][c] + press[r][c - 1] + press[r][c] + press[r][c + 1]) % 2;
}
}
// 针对第5行灯,看看第5行开关操作后,是否能将第5行的灯都熄灭
for(int c = 1; c < 7; c++)
{ //如果灯的状态==按下的所有开关后的变化相同
// 一开始灯是0,开关也是0,或者一开始灯是1,开关也是1;则是关灯
if(puzzle[5][c] != (press[4][c] + press[5][c - 1] + press[5][c] + press[5][c + 1]) % 2)
return false;
}
return true;
}
}
讨厌的青蛙
有一种小青蛙,到了晚上就会跳跃稻田,从而踩踏稻子,农民早上看到被踩踏的稻子,希望找 到造成最大损害的那只青蛙经过的路径。长宽不大于5000
枚举什么?(不好的枚举)
枚举每个被踩的稻子作为行走路径起点(5000个) 对每个起点,枚举行走方向(5000种) 对每个方向枚举步长(5000种) 枚举步长后还要判断是否每步都踩到水稻
时间:5000 * 5000 * 5000,不可取!
枚举什么?(好的枚举)
枚举路径上的开始两点 每条青蛙行走路径中至少有3棵水稻。 假设一只青蛙进入稻田后踩踏的前两棵水稻分别是(X1,Y1)、(X2,Y2),那么: 青蛙每一跳在X方向上的步长dx = X2 - X1, 青蛙每一跳在X方向上的步长dy = Y2 - Y1; (X1 - dx,Y1 - dy )需要落在稻田之外
当青蛙踩在水稻(X,Y)上时,下一跳踩踏的水稻是(X + dx,Y + dy ) 将路径上的最后一棵水稻记作(XK,Yk ),则(XK + dx,Yk + dy )需要落在稻田之外
思路:猜测一条路径 猜测的办法需要保证,每条可能的路径都能够被猜测到 从输入的水稻中任取两颗 作为一只青蛙进入稻田后踩踏的前两棵水稻 看能否形成一条穿越稻田的行走路径
猜测的过程需要尽快排除错误的答案 猜测(X1,Y1)、(X2,Y2) 所要寻找的行走路径上的前两棵水稻 当下列条件之一满足时,这个猜测就不成立 青蛙不能经过一跳从稻田外跳到(X1,Y1)上 如果MaxSteps是当前已经找到的最好答案,按照(X1,Y1)、(X2,Y2) 确定的步 长,从(X1,Y1)出发,青蛙最多经过(MaxSteps - 1)步,就会跳到稻田之外。
package com.suanfa;
import java.util.Comparator;
import java.util.Scanner;
public class Plant implements Comparator<Plant>
{
private int x;//水稻的横坐标
private int y;//水稻的纵坐标
public Plant()
{
}
@Override
public int compare(Plant p1, Plant p2)
{
if(p1.getX()== p2.getX())//如果两个水稻的横坐标相等,那么就返回两个水稻的纵坐标之差,否则返回横坐标只差
return p1.getY()-p2.getY();
return p1.getX()-p2.getX();
}
public Plant(int x, int y)
{
this.x = x;
this.y = y;
}
public int getX()
{
return x;
}
public void setX(int x)
{
this.x = x;
}
public int getY()
{
return y;
}
public void setY(int y)
{
this.y = y;
}
@Override
public String toString()
{
return "Plant{" +
"x=" + x +
", y=" + y +
'}';
}
}
class NastFrog {
public static void main(String[] args) {
/*
i,j:循环变量
dX,dY:被踩踏的相邻的两个水稻的步长
pX,pY:被踩踏的第一颗水稻的上一颗水稻的x,y坐标
steps:青蛙在行走路径上踩了多少颗水稻
max:存储踩踏最多的那条路径上,青蛙跳的步数
*/
int i, j, dX, dY, pX, pY, steps, max=2 ;
Scanner scan = new Scanner(System.in);
System.out.println("请输入水稻的行数");
int r = scan.nextInt();
System.out.println("请输入水稻的列数");
int c = scan.nextInt();
System.out.println("请输入被踩踏的水稻的水稻数");
int n = scan.nextInt();
Plant[] plants = new Plant[n];
for (i = 0; i < plants.length; i++) {
plants[i] = new Plant();
System.out.println("请输入第" + (i + 1) + "颗水稻的x坐标");
plants[i].setX(scan.nextInt());
System.out.println("请输入第" + (i + 1) + "颗水稻的y坐标");
plants[i].setY(scan.nextInt());
}
//只是为了阶段测试完成后可被注释
//System.out.println("排序前:->"+ Arrays.toString(plants));
sort(plants);
//System.out.println("排序后:->"+Arrays.toString(plants));
for (i = 0; i < n - 1; i++) {
for (j = i+1; j < n; j++) {
//先获的被踩踏的第一颗和第二颗水稻之间的步长
dX = Math.abs(plants[j].getX() - plants[i].getX());//出现负数 绝对值计算
dY = Math.abs(plants[j].getY() - plants[i].getY());
//获的稻田里,被踩踏的第一颗水稻的上一颗水稻的x,y坐标
pX = plants[i].getX() - dX;
pY = plants[i].getY() - dY;
//如果青蛙的上一跳不在稻田外,则该条路径
// 被淘汰,换下一条路径
if (pX >= 1 && pX <= r && pY >= 1 && pY <= c)
continue;
pX = plants[i].getX() + (max - 1) * dX;//如果x方向青蛙过早的跳出稻田
if ( pX > r|| pX < 1)
continue;
pY = plants[i].getY() + (max - 1) * dY;//如果y方向青蛙过早的跳出稻田
if ( pY > c||pY<1)
continue;
steps = searchPath(plants[j], dX, dY, r, c, plants);//在这条路径上,青蛙跳了多少步数
if (steps > max)
max = steps;
}
if (max == 2)
max =0;
}
System.out.println("在踩踏最多的那条路径上,共有" + max + "颗水稻被踩踏");
}
/**
* 搜索某一条可能的路径上,青蛙踩踏了多少颗水稻
* @param secPlant 这条路径上,青蛙踩踏第二颗水稻
* @param dX x方向的步长
* @param dY y方向的步长
* @param r
* @param c
* @param plants 被踩踏的所有水稻
* @return
*/
private static int searchPath(Plant secPlant, int dX, int dY, int r, int c, Plant[] plants) {
int steps = 2;
boolean flag;//开关
//根据第二颗被踩水稻坐标、步长,得到第三颗水稻的坐标
Plant plant = new Plant();
plant.setX(secPlant.getX() + dX);
plant.setY(secPlant.getY() + dY);
//看看这两棵水稻是否在稻田里,且属于被踩踏的水稻
while (plant.getX() >= 0 && plant.getX() <= r && plant.getY() >= 0 && plant.getY() <= c) {
flag = false;//先将开关设置一种状态
//将这颗水稻与所有被踩踏的水稻进行对比
for (Plant p : plants) {
//如果我们推出的第三颗水稻属于被踩踏的水稻
if (plant.getX() == p.getX() && plant.getY() == p.getY()) {
flag = true;
break;
}
}
//如果此时,flag为true,表示当前查找的第三颗水稻是被踩踏水稻,则无需跳出,否则跳出循环
if (!flag)
break;
plant.setX(plant.getX() + dX);
plant.setY(plant.getY() + dY);
steps++;
}
return steps;
}
/**
* 将乱序的plants数组,按照Plant中的比较逻辑进行排序
* 排序规则:
* 先按照x坐标升序排列,如果x坐标相同,则按照y坐标进行升序1排列
*/
private static void sort(Plant[] plants) {
Plant swapPlant;
for (int i = 0;i<plants.length-1;i++ ){
for (int j = i+1; j <plants.length ; j++) {
if(plants[i].compare(plants[i],plants[j])>0){
swapPlant=plants[i];
plants[i]=plants[j];
plants[j] = swapPlant;
}
}
}
}
}
//时间复杂度O(N^3)