【精选】JAVA算法题(十七)

一、镜面反射

题目:

/**
 * 有一个特殊的正方形房间,每面墙上都有一面镜子。除西南角以外,每个角落都放有一个接受器,编号为 0, 1,以及 2。
 * 正方形房间的墙壁长度为 p,一束激光从西南角射出,首先会与东墙相遇,入射点到接收器 0 的距离为 q 。
 * 返回光线最先遇到的接收器的编号(保证光线最终会遇到一个接收器)。
 */ 

示例:

输入: p = 2, q = 1
输出: 2
解释: 这条光线在第一次被反射回左边的墙时就遇到了接收器 2 。

可以想象没有背面的墙,东西两个方向的墙无限长,光线会被不断反射,我们会发现光线被接收一定是走过的竖直路径长度是p的整数倍,最终落在西墙那么肯定就是被2接收,如果落在东墙就会被0和1接收,他俩的接收又有一定规律,如果走过的路径长度是p的偶数倍那么就会被0接收,奇数倍就会被1接收。我们根据此写出程序。

    public int method1(int p, int q) {
        int pq=0;
        for (int i=(p>q?p:q);i<=p*q;i++){
            if (i%p==0&&i%q==0){
                pq=i;
                break;
            }
        }
        if ((pq/p)%2==0){
            if ((pq/q)%2==0){
                return 2;
            }else {
                return 0;
            }
        }else {
            if ((pq/q)%2==0){
                return 2;
            }else {
                return 1;
            }
        }
    }

 这个算法还有可以优化的地方,在上面这个算法中我们是先求出p,q的最小公倍数,然后再进行判断,我们可以把它的效率提高一些,因为最终都是执行了最小公倍数除p除q再对二取余的判断,所以我们可以直接分别对p,q除2运算,直到一方对二取余不等于0,然后再对2取余结果进行判断返回

    public int method2(int p, int q) {
        while(p%2==0&&q%2==0) {
            p /= 2;
            q /= 2;
        }
        if(p%2 == 0)
            return 2;
        if(q%2 == 0)
            return 0;
        return 1;
    }

二、吃巧克力

题目:

/**
 * 小Q的父母要出差N天,走之前给小Q留下了M块巧克力。小Q决定每天吃的巧克力数量不少于前一天吃的一半,
 * 但是他又不想在父母回来之前的某一天没有巧克力吃,请问他第一天最多能吃多少块巧克力
 * 输入描述:
 * 每个输入包含一个测试用例。
 * 每个测试用例的第一行包含两个正整数,表示父母出差的天数N(N<=50000)和巧克力的数量M(N<=M<=100000)。
 * 输出描述:
 * 输出一个数表示小Q第一天最多能吃多少块巧克力。
 * 输入例子1:
 * 3 7
 * 输出例子1:
 * 4
 */

这题和猴子吃桃有相似之处,猴子吃桃是每天吃昨天的一半多一个,给天数和最后的数量求开始的数量,这题是吃不少于昨天数量的一半,问第一天最多能吃多少。

重要的条件有两个:一个是每天吃的巧克力数量不少于前一天的一半 也就是说可以吃前一天的一半 二就是不能在父母回来之前的某一天没巧克力吃,可以刚好在最后一天吃完。

那么要想在第一天吃的更多就要想办法让后面吃的少一些,尽量压着最低数量(前一天数量的一半)去吃

那昨天吃的数量是偶数,今天就吃昨天的一半,如果昨天吃的数量是奇数,那就吃前一天数量一半向上取整

然后再对天数进行判别:如果给的天数是0或者1,那么就可以直接吃完

如果多于一天,那第一天最多吃三分之二的,因为最低天数为2的话,第一天吃三分之二,第二天吃三分之一是最佳分配

然后用一个for循环取检测第一天吃这个数量是否符合要求

    public static int getMaxNum(int day, int chocolates) {
        if (day < 2) return chocolates;
        double maxNum=(chocolates/3.0)*2;
        for (; maxNum > 1; maxNum--) {
            double currentNum = maxNum, currentChocolates = 0;
            for (int i = 0; i < day; i++) {
                currentChocolates += currentNum;
                currentNum = Math.ceil(currentNum / 2.0);
                if (currentChocolates > chocolates) {
                    break;
                }
            }
            if (currentChocolates <= chocolates) {
                return (int) maxNum;
            }
        }
        return 1;
    }

但这种方法很低效,要判断很多无用的数字,在学习基础查找算法时我们学过二分查找,在这里可以派上用场,不断去缩小查找范围。

    public static void method2() {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();//天数
        int m = sc.nextInt();//巧克力个数
        double min = 0;//设置二分初始低位为0
        double max = (double) m;//设置二分初始高位为初始巧克力个数m
        double stillHas = (double) m;//剩余巧克力个数
        boolean flag = true;
        double temp;
        while (min < max)//当低位<高位时,开始二分查找
        {
            temp = Math.ceil((min + max) / 2);//取低位和高位中间的一位向上取整,即为试探的第一天吃的巧克力数
            for (int i = 1; i < n + 1; i++)//循环n天
            {
                if (temp <= stillHas)//当前天需要吃的巧克力个数<=剩余巧克力个数时,减少巧克力,同时第二天巧克力个数变为第一天的一半
                {
                    stillHas = stillHas - temp;
                    temp = Math.ceil(temp / 2);
                } else//当前天需要吃的巧克力个数>剩余巧克力个数时,说明没有撑到n天巧克力吃完,置flag=false;跳出循环
                {
                    flag = false;
                    break;
                }
            }
            if (flag)//flag==true,说明上面的for循环正常循环结束跳出,说明当前的第一天吃的Math.ceil((min+max)/2)个巧克力满足要求
            {
                //判断一下,如果比Math.ceil((min+max)/2)个巧克力大1个巧克力时
                //isTrue返回false说明再大1个巧克力就不满足要求了,那么当前的Math.ceil((min+max)/2)就是最大的第一天吃的巧克力数,输出即可
                if (!isTrue(n, m, Math.ceil((min + max) / 2) + 1)) {
                    System.out.println((int) Math.ceil((min + max) / 2));
                    return;
                }
                //如果大1个巧克力仍然满足要求那么说明当前的第一天吃的Math.ceil((min+max)/2)取小了应取大一点的巧克力数,需要继续二分查找
                else {
                    min = Math.ceil((min + max) / 2);//取低位为当前的试探的第一天吃的巧克力数
                    stillHas = (double) m;//重置剩余巧克力数为总数
                }
            }
            //flag==false,说明上面的for循环遇到break跳出,说明当前的第一天吃的Math.ceil((min+max)/2)取大了应取小一点的巧克力数,需要继续二分查找
            else {
                max = Math.ceil((min + max) / 2);//取高位为当前的试探的第一天吃的巧克力数
                stillHas = (double) m;//重置剩余巧克力数为总数
                flag = true;//重置标志位
            }

        }
    }

    //用于判断当每天吃X个巧克力时是否能撑到父母回来的方法
    public static boolean isTrue(int n, double m, double x) {
        for (int i = 1; i < n + 1; i++) {
            if (x <= m) {
                m = m - x;
                x = Math.ceil(x / 2);
            } else {
                return false;
            }
        }
        return true;
    }

三、打家劫舍

题目:

/**
 *你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
 * 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
 *给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
 *
 * 示例 1:
 * 输入: [1,2,3,1]
 * 输出: 4
 * 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
 *      偷窃到的最高金额 = 1 + 3 = 4 。
 *
 * 示例 2:
 * 输入: [2,7,9,3,1]
 * 输出: 12
 * 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
 *      偷窃到的最高金额 = 2 + 9 + 1 = 12 。
 */

这道题是一个很经典的动态规划问题,这道题的本质相当于在一列数组中取出一个或多个不相邻数,使其和最大。那么我们对于这类求极值的问题首先考虑动态规划Dynamic Programming来解, 我们维护一个一位数组dp,其中dp[i]表示到i位置时不相邻数能形成的最大和,那么状态转移方程怎么写呢, 我们先拿一个简单的例子来分析一下,比如说nums为{3, 2, 1, 5},那么我们来看我们的dp数组应该是什么样的, 首先dp[0]=3没啥疑问,再看dp[1]是多少呢,由于3比2大,所以我们抢第一个房子的3,当前房子的2不抢,所以dp[1]=3,那么再来看dp[2], 由于不能抢相邻的,所以我们可以用再前面的一个的dp值加上当前的房间值,和当前房间的前面一个dp值比较,取较大值当做当前dp值, 所以我们可以得到状态转移方程dp[i] = max(num[i] + dp[i - 2], dp[i - 1]), 由此看出我们需要初始化dp[0]和dp[1], 其中dp[0]即为num[0],dp[1]此时应该为max(num[0], num[1])

    //dp 方程 dp[i] = max(dp[i-2]+nums[i], dp[i-1])
    public static int method2(int[] nums){
        int n = nums.length;
        if (n <= 1) return n == 0 ? 0 : nums[0];
        int[] memo = new int[n];
        memo[0] = nums[0];
        memo[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < n; i++)
            memo[i] = Math.max(memo[i - 1], nums[i] + memo[i - 2]);
        return memo[n - 1];
    }

由此我们可以扩展讲解一下动态规划的基础问题,硬币问题

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够n元;

我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0,表示凑够0元最小需要0个硬币。

当i=1时,只有面值为1元的硬币可用,因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,

而这个是已经知道答案的,即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时,

仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币,接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),

而这个答案也已经知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你都可能会觉得,好无聊,感觉像做小学生的题目似的。

因为我们一直都只能操作面值为1的硬币!耐心点,让我们看看i=3时的情况。当i=3时,我们能用的硬币就有两种了:

1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。既然能用的硬币有两种,我就有两种方案。

如果我拿了一个1元的硬币,我的目标就变为了:凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。

这个方案说的是,我拿3个1元的硬币;第二种方案是我拿起一个3元的硬币,我的目标就变成:凑够3-3=0元需要的最少硬币数量。

即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币。好了,这两种方案哪种更优呢?记得我们可是要用最少的硬币数量来凑够3元的。

所以,选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。 OK,码了这么多字讲具体的东西,

让我们来点抽象的。从以上的文字中,我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。 上文中d(i)表示凑够i元需要的最少硬币数量,

我们将它定义为该问题的"状态",这个状态是怎么找出来的呢?我在另一篇文章 动态规划之揹包问题(一)中写过:根据子问题定义状态。

你找到子问题,状态也就浮出水面了。最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。

那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i),上文中包含状态d(i)的方程是:

d(3)=min{d(3-1)+1, d(3-3)+1}。没错,它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值; 有了状态和状态转移方程,这个问题基本上也就解决了

四、快乐数

题目:

/**
 * 编写一个算法来判断一个数是不是“快乐数”。
 * 一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,
 * 也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
 *
 * 示例:
 * 输入: 19
 * 输出: true
 * 解释:
 * 12 + 92 = 82
 * 82 + 22 = 68
 * 62 + 82 = 100
 * 12 + 02 + 02 = 1
 */

百度一下:

不是快乐数的数称为不快乐数(unhappy number),所有不快乐数的数位平方和计算,最后都会进入 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 的循环中。

但是我们要如何检测一个数是不是一个快乐数呢,一个重点:不快乐数会陷入到循环之中,我们就要用这点来解题,我们把所有计算过的结果用一个容器记住,然后一旦新的结果在容器中出现过,那它就会陷入循环,它就不快乐,反之快乐数最终会收敛到1.

    public static boolean isHappy(int n) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        while (getPowSum(n)!=1) {
            if (list.contains(n)) {
                return false ;
            }else {
                list.add(n);
                n = getPowSum(n);
            }
        }
        return true ;
    }

    public static int getPowSum(int n) {
        int temp = 0 ;
        while (n>0) {
            temp+=Math.pow(n%10, 2);
            n/=10;
        }
        return temp;
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章