【蓝桥杯】【2017】【包子问题】非常容易理解的欧几里得算法

题目来源:蓝桥杯-2017

题目

小明几乎每天早晨都会在一家包子铺吃早餐。这家包子铺有N种蒸笼,其中第i种蒸笼恰好能放Ai个包子
每种蒸笼都有非常多笼,可以认为是无限笼。
每当有顾客想买X个包子,卖包子的大叔就会选出若干笼包子来,使得这若干笼中恰好一共有X个包子。
比如一共有3种蒸笼,分别能放3、4和5个包子。当顾客想买11个包子时,大叔就会选2笼3个的再加1笼5个的(也可能选出1笼3个的再加2笼4个的)。
当然有时包子大叔无论如何也凑不出顾客想买的数量。
比如一共有3种蒸笼,分别能放4、5和6个包子。而顾客想买7个包子时,大叔就凑不出来了。
小明想知道一共有多少种数目是包子大叔凑不出来的。

输入
第一行包含一个整数N。(1 <= N <= 100)
以下N行每行包含一个整数Ai。(1 <= Ai <= 100)

输出
输出一行包含一个整数代表答案。如果凑不出的数目有无限多个,输出INF。

示例

输入:

2
4
5

输出:

6

输入:

2
4
6

输出:

INF

解题思路

​ 本题的难点在于如何判断不能够组成的数字是否有无限个。下面首先进行推导什么情况下是无限个,已经知道原因的读者可以下划到分割线处直接看做题思路。

​ 拿两个示例举例,4和5不能组成的数只有6个:1,2,3,6,7,11。4和6不能组成的数是2和全体奇数

​ 从4和6两个数入手,我们就能知道什么时候不能组成的数字有无限个了:6和4都是2的倍数,那么几乎所有2的倍数都能用4和6表示,此时所有奇数都无法表示。也就是说如果两个数的最大公因数不是1的情况下,他们能够组成的数字其实都是最大公因数的倍数,例如4和6能组成的数都是2的倍数。那么此时只要取任意一个非2的倍数的数字,用4和6都必然不能表示。

​ 再去反观4和5两个数字,他们的最大公因数是1,那么他们是否就能组成所有的数字了呢?我们发现除掉1、2、3、6、7、11之外,所有的偶数好像都能用4的倍数表示,倘若一个数不能把4整除,那么我们可以留出一部分数让5来处理。拿10举例,10是偶数但是10不能被4整除,2*4=8,3*4=12,他们之间的差值为4,此时我们可以用2*5=10弥补中间的插值2,这样一来可以用a*4+b*5构成几乎所有的偶数。同理,可以用a*5+b*4构成几乎所有的奇数。那么用x*4+y*5就能构成几乎所有的数,此时不能构成的数是有限个。

​ 也就是说,无论给我们什么样的数据,如果他们中的部分数字彼此存在一个不为1的最大公因数,那么这几个数字其实就是这个最大公因数的倍数,中间必定存在无法表示的数字。

​ 综上,如果给我们的数彼此之间的最大公因数是1,也就是这些数字彼此互质,那么不能构成的数字就是有限个,反之就是无限个。


​ 首先我们需要编写一个函数用来判断输入的数据彼此间是否互质。这里我们采用的是欧几里得算法,也就是辗转相除法

​ 不了解该算法的可以查看示例,了解该算法的运行原理,或者点击链接访问欧几里得算法-百度百科

假如需要求 1997 和 615 两个正整数的最大公约数,用欧几里得算法,是这样进行的:
1997 / 615 = 3 (余 152)
615 / 152 = 4(余7)
152 / 7 = 21(余5)
7 / 5 = 1 (余2)
5 / 2 = 2 (余1)
2 / 1 = 2 (余0)
至此,最大公约数为1
以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数,所以就得出了 1997 和 615 的最大公约数 1。

public static int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

​ 编写一个启动函数,将数据预处理

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    int[] input = new int[n];
    for (int i = 0; i < n; i++)
        input[i] = scanner.nextInt();
    get(input);
}

​ 下面是个人认为非常容易理解的代码,该代码的执行效率非常低(至少运行起来很慢,肉眼可见的慢),在理解之后可以大幅度优化。

​ 本代码解释会利用输入数据2 4 5进行阐述。首先定义一个gcd,用来表示输入的数据彼此是否互质,由于每次计算都会刷新gcd的值,所以只有所有数据彼此之间互质的时候,gcd才会一直为1。 利用gcd可以直接确定是否输出INF。

​ 如果不输出INF,也就是无法表示的个数有限,那么我们就先创建一个list用来存储所有的输入的数据。然后创建一个res,用来存放无法表示的数字。首先利用for循环把所有数据添加到list中,如4,5。然后我们设置一个阈值,遍历这个范围所有的数,检测能否用list中的数据表示。

​ 首先定义一个flag=false,用来表示当前处理的数字i能否用list中的数字组成。如果能组成,那就让flag=true,同时把这个数字添加到list中。处理能否组成的方法是判断数字自减能否组成,例如我们判断数字6时,先判断0和6是否都在list中,然后判断1和5,2和4,3和3。其中只要有一组判断成功,就代表数字可以组成,本题是遍历了这4种情况之后,仍然没有找到处理方案,所以6是不能构成的数,反之,如果能够构成的话,就由flag=true结束循环,同时将值添加到list中,便于后续的计算。循环结束,利用flag=false将6添加到res中。直到遍历完整个循环,由于所有不能构成的数字我们都add到了res中,所以直接输出res的size即可。详细流程请查看代码。

public static void get(int[] input) {
    //gcd初始值
    int gcd = input[0];
    //判断所有输入的数据彼此是否互质
    for (int i = 0; i < input.length - 1; i++)
        gcd = gcd(gcd, input[i + 1]);
    //如果不互质,输出INF,否则进入
    if (gcd == 1) {
        //用来存放所有能够组成的数字,后续可以利用
        //因为 list.contains(4) && list.contains(5) , 4+5=9
        //所以 list.add(9)
        //这种思想迅速的判断能够组成的数
        List<Integer> list = new ArrayList<>();
        //用来存放不能组成的数字
        List<Integer> res = new ArrayList<>();
        //输入的数据自然都是能够组成的,直接add即可
        for (int i = 0; i < input.length; i++)
            list.add(input[i]);
        //遍历到阈值10000
        for (int i = 1; i <= 10000; i++) {
            //标识符
            boolean flag = false;
            //flag只有在能够组成时才会改变,而能够组成的话,循环就可以停止了,所以这里添加了!flag的判断
            for (int j = 0; j <= i / 2 && !flag; j++)
                //这里我们不考虑特殊值0的问题,实际上例如判断4的时候,虽然没有0,但是4切实存在,所以直接添加了 ||list.contains 条件
                if ((list.contains(j) && list.contains(i - j)) || list.contains(i)) {
                    //如果list中存在i,就不用再添加i了
                    if (!list.contains(i))
                        list.add(i);
                    //标识符更改,表示循环结束,也表示该数字可以组成
                    flag = true;
                }
            //根据flag的值,选择将i添加到res中
            if (!flag)
                res.add(i);
        }
        //res的大小就是不能组成的数字个数
        System.out.println(res.size());
    } else
        System.out.println("INF");
}

​ 我们发现,之所以使用list,其实就是为了使用他的contains方法,但是这样大量的消耗了时间资源,每一次都要判断是否contains,无疑带来了很大的时间损失,而实际上我们可以写一个bool数组,利用下标当作每一个数字用下标对应的true/false来确定能否组成。这样一来,list.contains(4)就可以写成bool[4]了。同样的,题目并未让我们给出所有不可组成的值,所以我们只需要定义一个count计数器,每次遇见不能组成的数字就加1就可以了。这样一来代码就可以写成这个样子(改写的部分已附加注释)

public static void get(int[] input) {
    int gcd = input[0];
    for (int i = 0; i < input.length - 1; i++)
        gcd = gcd(gcd, input[i + 1]);
    //定义计数器,不再创建res对不可组成的数字进行收集
    int count = 0;
    if (gcd == 1) {
        //阈值是10000,为了利用下标,创建到10001,默认均为false
        boolean[] isGet = new boolean[10001];
        //输入的数据能组成,也就是true
        for (int i = 0; i < input.length; i++)
            isGet[input[i]] = true;
        //前边的代码没有考虑到0,这里考虑到0。既确保了数组每个值都有实际意义,也确保了例如 0和4 
        isGet[0] = true;
        for (int i = 1; i <= 10000; i++) {
            boolean flag = false;
            for (int j = 0; j <= i / 2 && !flag; j++)
                //已经考虑过0的情况,所以或条件删除掉,上文也提到了contains可以用数组下标代替
                if (isGet[j] && isGet[i - j])
                    //让下标对应值改为true的同时,也让flag发生变化
                    isGet[i] = flag = true;
            if (!flag)
                count++;
        }
    }
    //输出语句整合成一条,其实也可以不用这么写,根据if语句分开写的可读性更高
    System.out.println(gcd == 1 ? count : "INF");
}

代码实现

import java.util.Scanner;

public class bao_zi_cou_shu {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] input = new int[n];
        for (int i = 0; i < n; i++)
            input[i] = scanner.nextInt();
        get(input);
    }

    public static void get(int[] input) {
        int gcd = input[0];
        for (int i = 0; i < input.length - 1; i++)
            gcd = gcd(gcd, input[i + 1]);
        int count = 0;
        if (gcd == 1) {
            boolean[] isGet = new boolean[10001];
            for (int i = 0; i < input.length; i++)
                isGet[input[i]] = true;
            isGet[0] = true;
            for (int i = 1; i <= 10000; i++) {
                boolean flag = false;
                for (int j = 0; j <= i / 2 && !flag; j++)
                    if (isGet[j] && isGet[i - j])
                        isGet[i] = flag = true;
                if (!flag)
                    count++;
            }
        }
        System.out.println(gcd == 1 ? count : "INF");
    }

    public static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}

本题无执行用时和内存消耗统计

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值