蓝桥杯赛前冲刺补习第四课——《数学知识的应用》

第一题

地产大亨Q先生临终的遗愿是:拿出100万元给X社区的居民抽奖,以稍慰藉心中愧疚。
麻烦的是,他有个很奇怪的要求:

1.100万元必须被正好分成若干份(不能剩余)。
每份必须是7的若干次方元。
比如:1元, 7元,49元,343元,…
2.相同金额的份数不能超过5份。
3.在满足上述要求的情况下,分成的份数越多越好!

请你帮忙计算一下,最多可以分为多少份?


解法:
  先上暴力法,观察发现题目中给出的情况皆是7的若干次方,每位不超过5,设置一个进制计数数组picks做7进制记录,直接枚举出100万以内的所有可能的数

public class Main {

    public static void main(String[] args) {
        int[] sevens = {1, 7, 49, 343, 2401, 16807, 117649, 823543};
        int[] picks  = new int[sevens.length];
        Arrays.fill(picks, 0);

        pick(sevens, picks, 0);
    }

    /**
     * 暴力法,组合判断
     * @param sevens
     * @param picks
     * @param k
     */
    public static void pick(int[] sevens, int[] picks, int k) {
        if (k == picks.length) {
            if (judge(sevens, picks)) {
                System.out.println(getParts(picks));
            }
            return;
        }

        if (picks[k] == 5) {
            pick(sevens, picks, k + 1);
        } else {
            picks[k] += 1;
            pick(sevens, picks, k);
            picks[k] -= 1;
            pick(sevens, picks, k + 1);
        }
    }

    public static boolean judge(int[] sevens, int[] picks) {
        int sum = 0;
        for (int i = 0; i < picks.length; i++) {
            sum += sevens[i] * picks[i];
        }
        if (sum == 1000000) {
            return true;
        } else {
            return false;
        }
    }

    public static int getParts(int[] picks) {
        int parts = 0;

        for (int i = picks.length - 1; i >= 0; i--) {
            parts += picks[i];
        }

        return parts;
    }

  再上课堂上老师教学的简便方法,将目标1000000转换城7进制表达式,直接输出。从那天起,笔者又重新感受到了被数学支配的恐惧 (摊手)。

/**
 * 7进制,每位不超过5
 */
public class Main {

    public static void main(String[] args) {
        char[] c = Integer.toString(1000000, 7).toCharArray();
        int count = 0;

        for (char ch : c){
            count += (ch-'0');
        }

        System.out.println(count);
    }
}

第二题

用天平称重时,我们希望用尽可能少的砝码组合称出尽可能多的重量。
如果只有5个砝码,重量分别是1,3,9,27,81
则它们可以组合称出1到121之间任意整数重量(砝码允许放在左右两个盘中)。
本题目要求编程实现:对用户给定的重量,给出砝码组合方案。
例如:

用户输入:
5
程序输出:
9-3-1
用户输入:
19
程序输出:
27-9+1

要求程序输出的组合总是大数在前小数在后。
可以假设用户的输入的数字符合范围1~121。


解法:
  先上暴力,数字组合+,-,不拿,得算式,验证结果。

public class Main {

    public static void main(String[] args) {
        int[] fama = {1, 3, 9, 27, 81};
        int[] status = {0,0,0,0,0};
        int n = 19;

        f1(fama, status, 0, n)
    }

/**
     * 暴力法,全组合
     *
     * @param fama
     * @param status
     * @param k
     * @param goal
     */
    public static void f1(int[] fama, int[] status, int k, int goal) {
        if (k == -1) {
            if (goal == 0) {
                boolean first = true;
                for (int i = status.length - 1; i >= 0; i--) {
                    int num = fama[i] * status[i];
                    if (num != 0) {
                        if (num > 0 && !first) {
                            System.out.print("+");
                        }
                        System.out.print(num);
                        first = false;
                    }
                }
            }
            return;
        }

        status[k] = 1;
        f1(fama, status, k - 1, goal - fama[k]);
        status[k] = -1;
        f1(fama, status, k - 1, goal + fama[k]);
        status[k] = 0;
        f1(fama, status, k - 1, goal);
    }
}

  非递归方法初稿,代码可能看起来比较多,但是思路清晰

public class Main {

    public static void main(String[] args) {
        int[] fama = {1, 3, 9, 27, 81};

        int n = 19;

        System.out.println(f2(fama, n));
    }

    /**
     * 将n化为3进制数,并且不允许当n = 2,当发生时继续取余得到-1
     *
     * @param fama
     * @param n
     */
    public static String f2(int[] fama, int n) {
        StringBuilder result = new StringBuilder();
        /*
            求每个砝码状态
         */
        while (n != 0) {
            int remainder = n % 3;

            if (remainder < 2) {
                /*
                余数为0,1的情况
                 */
                n /= 3;
            } else {
                /*
                余数为2的情况
                 */
                n = n / 3 + 1;
                remainder -= 3;
            }

            result.insert(0, ",").insert(0, remainder);
        }
        /*
            补零
         */
        int[]    status  = new int[5];
        String[] statusB = result.toString().split(",");
        for (int i = 0; i < status.length - statusB.length; i++) {
            result.insert(0, "0,");
        }
        /*
            转换,装填
         */
        statusB = result.toString().split(",");
        for (int i = 0; i < status.length; i++) {
            status[i] = Integer.valueOf(statusB[i]);
        }

        /*
            根据status,拼接出算式
         */
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = fama.length - 1; i >= 0; i--) {
            switch (status[fama.length - 1 - i]) {
                case -1:
                    stringBuilder.append("-").append(fama[i]);

                    break;
                case 0:

                    break;
                case 1:
                    stringBuilder.append("+").append(fama[i]);

                    break;
            }
        }
        stringBuilder.replace(0, 1, "");

        return stringBuilder.toString();
    }
}    

  再来最后一稿,先上思路伪代码

f(int[] famas, int n)
首先找到>=n的那个砝码k
如果X(k)=n
  返回 n
如果X(k)>n
  subSum=[X(0)+X(1)+…+X(k)]
  如果subSum小于n //小数字全加起来都不够表示n,说明要靠大的减
    返回 X(k) + [f(n-subSum)]     //这里需要注意,下层返回的算式是不带括号的,所以如果上层是减的话,需要进行一次转换
  如果subSum大于等于n //小数字全加起来够表示n
    返回 X(k-1) + “+” + [f(n-subSum)]

reverse(String s)
将加减互换得到s1
返回 “-” + s1

public class Main {
    static int[] famas = {1, 3, 9, 27, 81};

    public static void main(String[] args) {
        for (int i = 1; i <= 121; i++) {
            System.out.println(i + ":" + f(i));
        }
    }

    public static String f(int n) {
        int a     = 1;
        int index = 0;
        while (a < n) {
            a *= 3;
            index++;
        }

        /*
            a 只会 >= n
         */
        if (a == n) return String.valueOf(n);
        else {//a>n
            int subSum = 0;
            for (int i = 0; i < index; i++) {
                subSum += famas[i];
            }
            if (subSum >= n) {
                int thinnerOne = famas[index - 1];
                return thinnerOne + "+" + f(n - thinnerOne);
            } else {
                return a + reverse(f(a - n));
            }
        }
    }

    public static String reverse(String s) {
        s = s.replace("+", "#").
                replace("-", "+").
                replace("#", "-");

        return "-" + s;
    }
}

第三题

有3堆硬币,分别是3,4,5
二人轮流取硬币。
每人每次只能从某一堆上取任意数量。
不能弃权。
取到最后一枚硬币的为赢家。
求先取硬币一方有无必胜的招法。


解法:
  尼姆堆问题关键在于求当前状态的Nim-sum,如果在自己手上的Nim-sum为0,则一定输;如果非0,则一定可以将0的局面给对手,则一定赢。
  为什么得出这个结论呢?因为考虑到Nim-sum计算的特性(全堆异或),如果一开始给到对手的是0局面,则对手不管怎么动都会促成一个非0的局面,则你接下来反手一定可以给对方一个0的局面,到最后的情况就是0xor0xor0….即全0局面,胜。
  所以我们要创造一个必胜的局面,就需要首先己方的初始状态Nim-sum一定非0,且走出的第一步一定要促成0的局面

public class Main {
    public static void main(String[] args) {
        int   n     = 3;
        int[] heaps = new int[n];

        heaps[0] = 3;
        heaps[1] = 4;
        heaps[2] = 5;

        List<Integer[]> methods = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= heaps[i]; j++) {
                heaps[i] -= j;
                if (isNimSum0(heaps)) {
                    //这种下手方法必胜
                    Integer[] method = new Integer[2];
                    method[0] = i + 1;
                    method[1] = j;
                    //意为从i堆中拿走j个
                    methods.add(method);
                }
                heaps[i] += j;
            }
        }

        for (Integer[] method : methods) {
            System.out.println(Arrays.asList(method));
        }
    }

    public static boolean isNimSum0(int[] heaps) {
        int num = heaps[0];
        for (int i = 1; i < heaps.length; i++) {
            num = num ^ heaps[i];
        }
        return num == 0;
    }
}

第四题

如果两个数很大,怎样求最大公约数,最小公倍数?
如果是n个数呢?比如1000个数的最小公倍数


解法:
  使用BigInteger类的gcd()方法求最大公约数

public static String gcd1(int a, int b) {
    BigInteger bigInteger = new BigInteger(String.valueOf(a));
    return bigInteger.gcd(new BigInteger(String.valueOf(b))).toString();
}

  使用BigInteger类的multiply()方法求乘积,用divide()方法除以上面得出的最大公约数,得最小公倍数

第五题

从昏迷中醒来,小明发现自己被关在X星球的废矿车里。
矿车停在平直的废弃的轨道上。
他的面前是两个按钮,分别写着“F”和“B”。
小明突然记起来,这两个按钮可以控制矿车在轨道上前进和后退。
按F,会前进97米。按B会后退127米。
透过昏暗的灯光,小明看到自己前方1米远正好有个监控探头。
他必须设法使得矿车正好停在摄像头的下方,才有机会争取同伴的援助。
或许,通过多次操作F和B可以办到。
矿车上的动力已经不太足,黄色的警示灯在默默闪烁…
每次进行 F 或 B 操作都会消耗一定的能量。
小明飞快地计算,至少要多少次操作,才能把矿车准确地停在前方1米远的地方。
请填写为了达成目标,最少需要操作的次数。


解法:
  此题就是求不定方程97x+127y = 1的整数解
已知扩展欧几里德定理
Ax+By = gcd(A,B)一定有解

Bx’+(A%B)y’ = gcd(B, A%B)
由以上两式子,且根据欧几里德定理gcd(A,B) = gcd(B, A%B)
可得Ax+By = Bx’+(A%B)y’
所以
Ax+By = Bx’+(A%B)y’ = Bx’ +[A–(A/B)*B]y’ = Bx’ + Ay’ –(A/B)*By’ = Ay’+B[x’–(A/B)y’]

A [x] + B [y] = A [y’] + B {[x’–(A/B)y’]}
得出恒等关
x = y’ , y = [x’ – (A/B)y’]
即当下层求得一对x’, y’时,上层即可通过恒等式得出当前层的x, y继续返回上层
此时递归关系得出
由于gcd算法最终递归出口为b == 0
所以递归出口条件为b == 0
且当b == 0时, Ax = gcd(A,0) = A
得此时x = 1, y = 上层传入的b = 0

这道题代码倒是不怎么重要,重要的是要知道扩展欧几里德定理
从那天起,笔者又重新感受到了被数学按在地上摩擦的恐惧 (摊手)。
当然要是题目条件允许的话,可以使用暴力求解,比如从-1000,-1000试到1000,1000,不行就再大点(逃

public class Main {
    /**
     * 遇到求解不定方程,首先可以想到暴力法
     * 然后就是将方程化为Ax+By = gcd(A,B)*m
     *
     * @param args
     */
    public static void main(String[] args) {
        int[] xy = new int[2];

        e_gcd(97, 127, xy);

        System.out.println(xy[0] + " " + xy[1]);
    }

    /**
     * 扩展欧几里德算法求解不定方程 Ax + By = gcd(A,B)*m (m是任意整数,这里只给出1的情况)
     * 可以求解Ax + By = gcd(A,B),然后将得出的x,y分别乘m就好
     * 再扩展的Ax + By = n的一般情况笔者暂时未思考
     *
     * @param a  上层向下传入的A
     * @param b  上层向下传入的B
     * @param xy 接收下层返回的x,y
     * @return
     */
    private static void e_gcd(int a, int b, int[] xy) {
        if (b == 0) {
            xy[0] = 1;
            xy[1] = 0;
            return;
        }

        e_gcd(b, a % b, xy);
        int x = xy[0];
        int y = xy[1];
        xy[0] = y;
        xy[1] = x - a / b * y;
    }
}

第六题

如果求 1/2 + 1/3 + 1/4 + 1/5 + 1/6 + …. + 1/100 = ?
要求绝对精确,不能有误差。


解法:
  自己定义分数计算类,不要使用浮点

public class Main {
    public static void main(String[] args) {
        Fraction sum = new Fraction(new BigInteger(String.valueOf(1)), new BigInteger(String.valueOf(2)));
        for (int i = 3; i <= 100; i++) {
            sum.add(new Fraction(new BigInteger(String.valueOf(1)), new BigInteger(String.valueOf(i))));
        }
        System.out.println(sum);
    }
}

class Fraction {
    private BigInteger numerator;
    private BigInteger denominator;

    public Fraction(BigInteger numerator, BigInteger denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Fraction add(Fraction b) {
        this.numerator = this.numerator.multiply(b.denominator).add(this.denominator.multiply(b.numerator));
        this.denominator = this.denominator.multiply(b.denominator);

        reduction(this);
        return this;
    }

    public Fraction subtract(Fraction b) {
        this.numerator = this.numerator.multiply(b.denominator).subtract(this.denominator.multiply(b.numerator));
        this.denominator = this.denominator.multiply(b.denominator);

        reduction(this);
        return this;
    }

    public Fraction multiply(Fraction b) {
        this.numerator = this.numerator.multiply(b.numerator);
        this.denominator = this.denominator.multiply(b.denominator);

        reduction(this);
        return this;
    }

    public Fraction divide(Fraction b) {
        this.numerator = this.numerator.multiply(b.denominator);
        this.denominator = this.denominator.multiply(b.numerator);

        reduction(this);
        return this;
    }

    public Fraction reduction(Fraction r) {
        BigInteger greatestCommonDivisor = r.numerator.gcd(r.denominator);

        r.numerator = r.numerator.divide(greatestCommonDivisor);
        r.denominator = r.denominator.divide(greatestCommonDivisor);

        return r;
    }

    @Override
    public String toString() {
        return this.numerator + "/" + this.denominator;
    }
}

第七题

第1个素数是2,第2个素数是3,…
求第100002(十万零二)个素数


解法:
  使用埃拉托斯特尼筛法,在这个基础上,我们还需要注意一个点——数组长度设置,这里笔者引用了素数范围的AKS证明:

This is an algorithm to test whether or not a given integer is prime.
It’s called the AKS primality test https://en.m.wikipedia.org/wiki/AKS_primality_test
And can be done in polynomial time, which is usually considered a decent amount of time.
Now if you’re trying to compute the nth prime, it has been proven that the nth prime must be greater than
  nln(n)+n(ln(ln(n))−1)
and less than
  nln(n)+nln(ln(n))
When n≥6.
So if you’re searching for the nth prime, I’d look in this gap.

public class Main {
    public static void main(String[] args) {
        long start = Calendar.getInstance().getTimeInMillis();
        int  n     = 100002;
        System.out.println(getNumberNPrime(n));

        long end = Calendar.getInstance().getTimeInMillis();
        System.out.println((end - start) + "ms");
    }

    public static int getNumberNPrime(int n) {

        int   size = n < 6 ? 12 : (int) (Math.ceil(n * Math.log(n) + n * Math.log(Math.log(n))));
        int[] nums = new int[size];
        Arrays.fill(nums, 0);

        double len = Math.sqrt(nums.length);
        for (int i = 2; i < len; i++) {
            if (nums[i] == 1) continue;
            for (int j = i * i; j < nums.length; j += i) {
                nums[j] = 1;
            }
        }

        int count = 0;
        int index = -1;
        for (int i = 2; i < nums.length; i++) {
            if (nums[i] == 0) {
                count++;
            }
            if (count == n) {
                index = i;
                break;
            }
        }

        return index;
    }
}

该算法经过笔者的细(随)心(意)优化后,计算100002th素数在笔者电脑上只需要要34ms,10000000th只需要3s

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值