算法思想(枚举)——百钱百鸡+生理周期+完美立方+熄灯问题+讨厌的青蛙

枚举的思想其实时日常生活中提取的一种智慧 ^——————^ 枚举的思想在生活中有着非常广泛的应用

       在对事物进行归纳推理时,会逐一考察某个事物的所有可能的情况,并且逐一进行检验,这就是枚举。例如,当我们忘记密码,就会按照记忆中的密码逐一进行尝试;警察确定凶手也是列出所有的嫌疑人然后排除。 

        可见枚举就是对问题变量的可能解集合的每一个解进行判断。

        枚举时十分直观的,易于理解,不像递归那么“只能意会”,是计算机来求解问题的最常用的方法之一,常用来解决那些通过公式推导、规矩演算的方法不能解决的问题。

采用枚举解决问题时,要注意三个方面:

(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)

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lungcen

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值