题目来源:蓝桥杯算法训练
知识点:动态规划(DP)
动态规划介绍
动态规划常用于求解最优问题。在我看来,动态规划的求解过程是走一步看一步,每走一步都确保当前的状态下是最优的。也可以说,动态规划将大问题划分为多个阶段的子问题,每个子问题都求得最优解,那么组合成的大问题就得到了最优解。
我将动态规划的实现写成如下四个步骤:
- 定义DP数组,一般是二维数组。这个数组描述了问题的状态和对应状态下的值。这个所谓的值一般就是我们要求的东西(最大数、最小数)。数组的行、列分别代表什么含义也是需要想清楚的。
- 确定状态转移方程。如在
dp[i][j]
位置的值可以由dp[i-1][j]
或者dp[i][j-1]
位置经过变换得到,就可以写出类似这样的状态转移方程:dp[i][j]=max(dp[i-1][j],dp[i][j-1])
。 - 设置边界条件。边界条件可以限制问题的边界,还可以为动态规划提供初始值。一般常见的边界条件是DP数组的行、列取 0、1 这种值时,DP数组对应的值。这个视具体问题定。
- 动态规划开始,填充DP数组。按照定义的状态转移方程,填写DP数组。
印章
问题描述
共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的概率。
输入格式
一行两个正整数n和m。
输出格式
一个实数P表示答案,保留4位小数。
样例输入
2 3
样例输出
0.7500
数据规模和约定
1≤n,m≤20
问题分析
按照上述动态规划的步骤,逐步分析问题:
-
定义二维数组
dp
,dp[i][j]
表示买了i
张印章,集齐了j
张,数组的值为概率。 -
确定状态转移方程。
dp[i][j]
是在dp[i-1][j]
或者dp[i-1][j-1]
的基础上又买了一张,并集齐了j
张。买了i-1
张时可能集齐了j-1
张或j
张。在
dp[i-1][j]
的状态下,变换到dp[i][j]
的公式为:dp[i][j] = dp[i-1][j] * (j/n)
。因为在购买i-1
张时已经集齐了j
张,所以再买一张概率为j / n
的印章(意为与前j
张的其中一张相同)即可满足“买了i
张印章,集齐了j
张”的情况。在
dp[i-1][j-1]
的状态下,变换到dp[i][j]
的公式为:dp[i][j] = dp[i-1][j-1] * (n-(j-1)/n)
。因为在购买i-1
张时已经集齐了j-1
张,所以新的一张不能与之前的重复才能集齐j
张,概率就是n-(j-1)/n
。两种情况都可能存在,因此变换到
dp[i][j]
的公式,即状态转移方程为:dp[i][j] = dp[i-1][j] * (j/n) + dp[i-1][j-1] * (n-(j-1)/n)
。 -
设置边界条件。若买的印章数量小于集齐印章的数量,即
i<j
,这种情况显然是不可能的,概率就是0,dp[i][j] = 0
。若j = 1
,只需要买一张就能集齐,后面再买只是图案变化,就会得到不同的印章组合,所以此时的概率为:dp[i][j] = pow(1/n, i-1)
。 -
填充数组。使用循环,从
dp[1][1]
开始遍历数组,根据状态转移方程填写数组。
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
double p = 1.0 / n;
double dp[25][25] = {0};
for(int i=1; i<=m; i++) {
for(int j=1; j<=n; j++) {
if(j > i) break;
if(j == 1) dp[i][j] = pow(p, i - 1);
else dp[i][j] = dp[i-1][j] * (j * 1.0 / n) + dp[i-1][j-1] * (1.0 * (n - j + 1) / n);
}
}
cout << fixed << setprecision(4) << dp[m][n] << endl;
return 0;
}
拿金币
问题描述
有一个N x N的方格,每一个格子都有一些金币,只要站在格子里就能拿到里面的金币。你站在最左上角的格子里,每次可以从一个格子走到它右边或下边的格子里。请问如何走才能拿到最多的金币。
输入格式
第一行输入一个正整数n。
以下n行描述该方格。金币数保证是不超过1000的正整数。
输出格式
最多能拿金币数量。
样例输入
3
1 3 3
2 2 2
3 1 2
样例输出
11
数据规模和约定
n<=1000
问题分析
本题是最优解问题,显然可以使用DP。有了上面的基础,按照动态规划的四个步骤就可以快速解题:
- 定义二维数组
dp
,数组的值为在该状态下的最大金币数量。 - 确定状态转移方程。定义保存每个位置的金币数量的数组为
coin
,那么dp[i][j]
可以由其左边的格子dp[i][j-1]
或其上面的格子dp[i-1][j]
变换过来,两个值取其中的最大值即可。状态转移方程可以写为:dp[i][j] = coin[i][j] + max(dp[i][j-1], dp[i-1][j])
。 - 设置边界条件。在数组第一行的格子只能由其左边的格子变化而来,写为:
dp[i][j] = coin[i][j] + dp[i][j-1]
。第一列的格子只能有其上面的格子变化而来,写为:dp[i][j] = coin[i][j] + dp[i-1][j]
。 - 填充DP数组。
提示
本题 n 的范围较大,最好动态给数组分配空间。
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int** coin = new int*[n + 1];
for(int i=1; i<=n; i++) {
coin[i] = new int[n + 1]();
for(int j=1; j<=n; j++) {
cin >> coin[i][j];
}
}
int** dp = new int*[n + 1];
for(int i=1; i<=n; i++) {
dp[i] = new int[n + 1]();
for(int j=1; j<=n; j++) {
if(i == 1) dp[i][j] = coin[i][j] + dp[i][j-1];
else if(j == 1) dp[i][j] = coin[i][j] + dp[i-1][j];
else dp[i][j] = coin[i][j] + max(dp[i-1][j], dp[i][j-1]);
}
}
cout << dp[n][n] << endl;
return 0;
}