1. 蓝桥杯印章
问题描述
共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的概率。
输入格式
一行两个正整数n和m
输出格式
一个实数P表示答案,保留4位小数。
样例输入
2 3
样例输出
0.7500
数据要求
m,n <=30
题解
一看数据量就猜测是dp的问题,数学问题的数据量一般很大。如果想到了dp,那么就好办了。
- 确定维度和含义,题目就两个数字,直接两维(i, j);含义就按照题目 or 子问题的思路去思考。
- (i, j)表示买了i枚印章,有j种的概率。
- 确定子问题,也就是递推式,也就是划分。这里根据最后一次买的印章种类进行划分。
- 如果最后一次买的是新种的印章, f(i, j) = f(i - 1, j - 1) * (n - (j - 1) / n)
- 如果最后一次买的是旧种类的印章, f(i, j - 1) = f(i - 1, j - 1) * ((j - 1) / n)
- 上面的式子2需要进行变形,因为我需要求解的是f(i, j), 那么f(i, j) = (i - 1, j) * (j / n)。
- 可以看到上面的划分是不重不漏的,最后的f(i, j)就是式1和2的和。相比于直接想到,f(i - 1, j)这里使用背包dp的思维更加容易。
- 最后确定边界。f(1, 1) = 1, 其他等于0。表示买一张有一种的概率是1。
代码
- 使用滚动数组优化
package lanqiao.imporve;
import java.util.Arrays;
import java.util.Scanner;
/**
* @author: Zekun Fu
* @date: 2022/11/21 10:38
* @Description: 印章
*
* 1. dp[n][m]:表示买n枚,集齐m种印章的概率
*
* 当前这一种是一种新印章的概率 (n - j) / n
* 当前这一种是一种旧印章的概率 (j) / n
*
* 1. dp[i][j - 1] = dp[i - 1][j - 1] * ((j - 1) / n)
* -> dp[i][j] = dp[i - 1][j] * (j / n)
*
* 2. dp[i][j] = dp[i - 1][j - 1] * ((n - j + 1) / n)
*
* 综上
* dp[i][j] = dp[i - 1][j] * (j / n) + dp[i - 1][j - 1] * ((n - j + 1) / n)
*
* 每一种印章有两种选择
* 1. 抽到新的
* 2. 抽到旧的
*
* 可以大体看作是背包模型。
*
* 2 3的时候
*
* i == 1的时候
* 0 : 0
* 1 : 1
*
* i == 2的时候
* 1 : 1 * 0.5
* 2 : 0.5
*
* i 等于3的时候
* 1 = 1
* 2 = 0.5 + 0.5 * 0.5 = 0.75
*
*
*/
public class YinZhang {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
double[] dp = new double[n + 1];
Arrays.fill(dp, 0);
dp[1] = 1;
double d = (double)n;
for (int i = 2; i <= m; i++) {
double[] pre = Arrays.copyOf(dp, n + 1);
for (int j = 1; j <= Math.min(n, i); j++) {
dp[j] = pre[j - 1] * ((double) (n - j + 1) / d) + pre[j] * ((double)j / d);
}
}
System.out.printf("%.4f", dp[n]);
}
}
cpp代码
- 没有优化的代码
#include <iostream>
#include <cstdio>
using namespace std;
// m种印章种
/*
f[i][j]:表示从i种印章中恰好选择j种印章的概率
f[m][n] = f[m - 1][n - 1]
1. i种印章中含有j种印章的第一种划分
当前选择的这个是已经选择过的印章
f[i - 1][j] * j/n
2. 当前选择的是一种新的印章
f[ - 1][j - 1] * (n - j + 1) / n;
3. 边界条件
i = 1的时候,f[1][j] = 1 / n;
i < j的时候,等于0
*/
const int N =1005;
double f[N][N];
int main() {
int m, n;
cin >> n >> m;
f[1][1] = 1.0;
for (int i = 2; i <= m; i++) {
for (int j = 1; j <= i; j++) {
f[i][j] = f[i - 1][j - 1] * (double)(n - j + 1) / n + f[i - 1][j] * (double) j / n;
//cout << f[i - 1][j] * (double) j / n << endl;
}
}
printf("%.4llf\n", f[m][n]);
}
- 使用滚动数组优化
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
// m种印章种
/*
f[i][j]:表示从i种印章中恰好选择j种印章的概率
f[m][n] = f[m - 1][n - 1]
1. i种印章中含有j种印章的第一种划分
当前选择的这个是已经选择过的印章
f[i - 1][j] * j/n
2. 当前选择的是一种新的印章
f[ - 1][j - 1] * (n - j + 1) / n;
3. 边界条件
i = 1的时候,f[1][j] = 1;
i < j的时候,等于0
*/
const int N = 25;
double f[2][N];
int main() {
int m, n;
cin >> n >> m;
f[1][1] = 1.0;
for (int i = 2; i <= m; i++) {
for (int j = 1; j <= i; j++) {
f[i & 1][j] = f[(i - 1) & 1][j - 1] * (double)(n - j + 1) / n + f[(i - 1) & 1][j] * (double) j / n;
//cout << f[i - 1][j] * (double) j / n << endl;
}
}
printf("%.4llf\n", f[m & 1][n]);
}