蓝桥杯算法训练印章 个人想法(java函数 递归实现)

蓝桥杯算法训练印章 个人想法(java函数 递归实现)


 

问题描述

共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的 概率。

 

输入格式

一行两个正整数 n 和 m

 

输出格式

一个实数P表示答案,保留4位小数。

 

样例输入

2 3

 

样例输出

0.7500

 

数据规模和约定

1≤n,m≤20

 

代码部分

 
话不多说,代码先奉上(如果觉得代码长,这边做了一点优化,所以长,可以直接看讲解,已 经拆分好了)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;


public class Main {
    static int n, m;
    
    public static void main(String[] args) throws IOException {
//      streamTokenizer 比 scanner 效率高
        StreamTokenizer streamTokenizer = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        streamTokenizer.nextToken();
        n = (int) streamTokenizer.nval;
        streamTokenizer.nextToken();
        m = (int) streamTokenizer.nval;
//        特殊情况,当 印章的种数 为1时, 必定抽到(可不写,这里为了提高效率)
        if(n == 1) {
            System.out.println(String.format("%.4f", 1.0));
        }
        else {
            System.out.println(String.format("%.4f", search(0.0, 0.0)));
        }
    }


/***
 * 
 * @param current 已抽取几次
 * @param currentGet 当前得到的印章种数(!!!注意这是种数,不是个数)
 * @return 概率
 */
    public static double search(double current, double currentGet) {
//      当 还没开始抽取时, 第一次抽取必定抽中一种 印章(可不写, 此处为了提高效率)
        if(currentGet == 0) {
            return search(1.0, 1.0);
        }
//        当 已经得到的 印章种数 = 全部的印章种数 时, 后面的抽取结果无论是什么, 都不会影响现在的概率了, 所以返回 1
        else if(currentGet == n) {
            return 1.0;
        }
//        m - current = 剩余抽取次数
//        n - currentGet = 还需抽取的 印章种数
//        当 剩余抽取次数 = 还需要抽取的印章种数, 本次必须抽中(可不写,此处为了提高效率)
        else if(m - current == n - currentGet){
            return ((n - currentGet)/ n) * search(current + 1, currentGet + 1);
        }
//        当 剩余抽取次数 <   还需要抽取的印章种数, 不管本次抽不抽中, 都不可能达成
        else if(m - current < n - currentGet) {
            return 0.0;
        }
//        其他情况, 即 剩余抽取次数 >   还需要抽取的印章种数, 分为 本次抽中 和 不抽中 的情况
        return ((n - currentGet)/ n) * search(current + 1, currentGet + 1) + (currentGet/ n) * search(current + 1, currentGet);
    }

    
}

 

讲解

以题目的样例, 来说明(仅个人思路, 尽可能拆解每一步,让大家了解)

 

抽取情况

抽取情况分为两种

  • 抽中了新的印章
  • 抽中了旧的印章(即 重复抽取到已有的印章)

 

首次过程

img

第一次抽取,因为之前没抽过,所以无论怎么抽,都是抽中新的印章。

具体代码实现,相当于第一次递归,必定抽中。

  • currentGet:表示当前已经抽中的印章种类
	if(currentGet == 0) {
            return search(1.0, 1.0);
        }

 

中间过程(以第二次抽取为例)

img

第二次抽取开始,就有了两种可能。

  • 抽中旧的可能
  • 抽中新的可能

 

那么要找到 我们所要 求的概率, 就需要将两种可能情况的概率相加

(这边为了方便看,把代码分行写了)

n: 印章的总种数

currentGet: 当前已获得的印章种数

current: 当前抽取的次数

double new = ((n - currentGet)/ n) * search(current + 1, currentGet + 1);
double old = (currentGet/ n) * search(current + 1, currentGet);
return new + old;

img
 

终止过程

中间过程,已经可以帮助我们不断进行递归了。

但有递归,就要有终止条件,不然会无休止下去。

 
 
终止就存在两种可能

  1. 所有的印章种数都已经抽完。
  2. 当剩余次数 < 剩余的印章种数 或者 当前次数 > 抽取次数

(个人推荐使用 剩余次数 < 剩余的印章种数, 可以减少递归次数,增加效率)

 

具体实现
 //     当前获取的印章种数 = 总的印章种数
        if(currentGet == n) {
            return 1.0;
        }
//      当前剩余次数 < 剩余的印章种数
        else if(m - current < n - currentGet) {
            return 0.0;
        }

current: 已经抽取的次数

m : 总共次数

n : 所需印章的种数

currentGet: 已获取的印章种数

 

这样一个完整的函数差不多就做好了

  public static double search(double current, double currentGet) {
        if(currentGet == 0) {
            return search(1.0, 1.0);
        }
        else if(currentGet == n) {
            return 1.0;
        }
        else if(m - current < n - currentGet) {
            return 0.0;
        }
        double new = ((n - currentGet)/ n) * search(current + 1, currentGet + 1);
        double old = (currentGet/ n) * search(current + 1, currentGet);
        return new + old;
    }

 

主函数调用的话(因为m和n 始终需要用到,于是我将他们作为全局静态变量放在最上方)

System.out.println(String.format("%.4f", search(0.0, 0.0)));
public class Main {
    static int n, m;
}

 

 

优化

​ 递归其实有一些风险,当递归次数过多时(本题不会,因为m的次数不超过20),可能会栈溢出,因此要做些优化,提高效率。

 

优化一:印章种数为1 的概率都为1

通过观察,我们发现当 印章总种数 为1时, 无论次数为多少,我们都必定抽中那一种印章,所以我们可以在主函数写这样一个式子(不需要进行调用,直接输出)

//        特殊情况,当 印章的种数 为1时, 必定抽到
        if(n == 1) {
            System.out.println(String.format("%.4f", 1.0));
        }

 

优化二: 当还需抽取的印章种数 = 剩余次数

通过观察,我们发现当还需抽取的印章种数= 剩余次数,我们要将剩余抽取的印章种数抽取完,意味着,我们后面每一次抽取都要是抽中的情况(这样可以减少递归次数)

   if(m - current == n - currentGet){
            return ((n - currentGet)/ n) * search(current + 1, currentGet + 1);
        }

 

注意点

本题虽然没说要求四舍五入,但实际上结果还是保留小数点后四位并四舍五入的。截取后四位,而不四舍五入,会导致部分用例无法通过。(本人已经吃过这个亏了)

 

总结

但这样优化其实还是不够,但次数极多的情况下,且印章种数不为1时, 仍可能有出栈的风险。但想要在当前代码的基础上,继续优化,很难,我并没有找到其他的规律。至于网上的二维数组方法,其实我不太明白为什么,始终搞不清它的理念,因为我感觉概率始终是在变化的,这次抽中与否是会影响到之后的概率,概率应该是不固定的。但说实话,他们的方法效果会比较好,效率会比较高,出栈的风险也小,但理念比较难理解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值