蓝桥杯算法训练印章 个人想法(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);
}
}
讲解
以题目的样例, 来说明(仅个人思路, 尽可能拆解每一步,让大家了解)
抽取情况
抽取情况分为两种
- 抽中了新的印章
- 抽中了旧的印章(即 重复抽取到已有的印章)
首次过程
第一次抽取,因为之前没抽过,所以无论怎么抽,都是抽中新的印章。
具体代码实现,相当于第一次递归,必定抽中。
- currentGet:表示当前已经抽中的印章种类
if(currentGet == 0) {
return search(1.0, 1.0);
}
中间过程(以第二次抽取为例)
第二次抽取开始,就有了两种可能。
- 抽中旧的可能
- 抽中新的可能
那么要找到 我们所要 求的概率, 就需要将两种可能情况的概率相加
(这边为了方便看,把代码分行写了)
n: 印章的总种数
currentGet: 当前已获得的印章种数
current: 当前抽取的次数
double new = ((n - currentGet)/ n) * search(current + 1, currentGet + 1);
double old = (currentGet/ n) * search(current + 1, currentGet);
return new + old;
终止过程
中间过程,已经可以帮助我们不断进行递归了。
但有递归,就要有终止条件,不然会无休止下去。
终止就存在两种可能
- 所有的印章种数都已经抽完。
- 当剩余次数 < 剩余的印章种数 或者 当前次数 > 抽取次数
(个人推荐使用 剩余次数 < 剩余的印章种数, 可以减少递归次数,增加效率)
具体实现
// 当前获取的印章种数 = 总的印章种数
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时, 仍可能有出栈的风险。但想要在当前代码的基础上,继续优化,很难,我并没有找到其他的规律。至于网上的二维数组方法,其实我不太明白为什么,始终搞不清它的理念,因为我感觉概率始终是在变化的,这次抽中与否是会影响到之后的概率,概率应该是不固定的。但说实话,他们的方法效果会比较好,效率会比较高,出栈的风险也小,但理念比较难理解。