题意
有五种硬币不限数量,面值分别是1,5,10,25,50。问你用这些硬币组成n元有多少种方式,硬币的总数要不超过100个。
思路
状态表示
如果类比01背包,那么至少我们可以认为应该用一个二维数组来记录状态,在背包问题中,我们用dp[ i ][ j ]来表示“前 i 件物品装入容量为 j 的背包所能得到的最大总价值”;在这里,我们可以尝试(事实是我也不知道为什么)用dp[ i ][ j ] 来表示“用 j 个硬币组成 k 元的方法数”。
对于dp [ 11 ]而言,它一定程度上可以由dp[ 10 ], dp[ 6 ], dp[ 1 ]…得到。于是开始我想设dp[ i ] 为第 i 个数的组成方式,再企图用之前的结果推出它,但是失败了。
二维这个定义很巧妙,我认为这也是一个对问题的拆解,牺牲了空间,存储了更多的信息,将所需求解的问题转化为求“用1、2、3…j个硬币组成k元的方法数”
状态转移方程
dp[k][j] += dp[k - type[]][j - 1]
其中 k 表示第 k 元, j 表示使用的硬币数量, type[]表示硬币种类
首先答案和硬币的种类密切相关,不同的硬币种类答案也不尽相同,因此方程中一定会出现和种类相关的量,这也意味着可能还要枚举一下不同种类的硬币。
然后我们可以想到,硬币数量为 j 的状态应该与数量为 j - 1 的状态密切相关,因此我们可能需要从“上一行”的状态来推出当前状态。
有了一个坐标,还要锁定另外一个坐标,那第 k 元的状态是和第 k - 1 元的状态密切相关吗?
这时可以举个栗子,比如dp(3,2)能否由dp(2, 1)推出?好像真可以,前者等于后者。
但是dp(10, 2)和dp(9, 1)呢?
这时我们发现,10的出现,允许我们使用新的硬币——10元,因此前者会比后者多一种方法,不仅如此,它还可以使用两个5元硬币,整整比后者多了两种方法。这印证了我们之前的想法——答案和硬币的种类密切相关。
于是我们可以猜测,dp(k, j)与dp(k - 种类,j)有关系。
为什么是“ += ”呢?这我也没什么依据(扯不下去),我感觉,dp(k,j)不能简单地由另外一项“ + 1 ”得到,但总得有个加法之类的把,于是可以考虑dp(k,j)不断累加其他项。
老实说,上面这些想法都是看了题解倒推得到的,做题的时候想法肯定会更多,希望能给下次写动态规划的我一点启示吧。
代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int maxcoin = 101;
const int maxval = 251;
int dp[maxval][maxcoin] = { 0 };
//dp[k][j]表示用j个硬币组成k元的方法数
int ans[maxval];
int type[] = { 1, 5, 10, 25, 50 };
int main(void) {
dp[0][0] = 1;
//并不是输入一个数再计算,而是先打表再回答
for (int i = 0; i < 5; ++i) {
for (int j = 1; j < maxcoin; ++j) {
for (int k = type[i]; k < maxval; ++k) {
dp[k][j] += dp[k - type[i]][j - 1];
}
}
}
//把各个子问题的答案加起来就是最后答案了
for (int i = 0; i < maxval; ++i) {
for (int j = 0; j < maxcoin; ++j) {
ans[i] += dp[i][j];
}
}
int n;
while (scanf("%d", &n) != EOF) {
printf("%d\n", ans[n]);
}
return 0;
}
模拟
这题顺带提供我研究流程时的代码,也可以试着手动模拟一下。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
const int maxcoin = 101;
const int maxval = 251;
int dp[maxval][maxcoin] = { 0 };
//dp[k][j]表示用j个硬币组成k元的方法数
int ans[maxval];
int type[] = { 1, 5, 10, 25, 50 };
int times = 1;
void show(int x) {//x是想要观察的 rmb 值
printf("-----the %d times-----\n", times++);
for (int j = 0; j <= x; ++j) {
for (int i = 0; i <= x; ++i) {
printf("%d ", dp[i][j]);
}
printf("\n");
}
printf("\n\n");
return;
}
int main(void) {
dp[0][0] = 1;
//并不是输入一个数再计算,而是先打表再回答
int x = 11; //x是想要观察的 rmb 值
for (int i = 0; i < 5; ++i) {
for (int j = 1; j <= x; ++j) {
for (int k = type[i]; k <= x; ++k) {
dp[k][j] += dp[k - type[i]][j - 1];
}
}
show(x);
}
for (int i = 0; i < maxval; ++i) {
for (int j = 0; j < maxcoin; ++j) {
ans[i] += dp[i][j];
}
}
int n;
/*
while (scanf("%d", &n) != EOF) {
printf("%d\n", ans[n]);
}
*/
return 0;
}
最后
刚刚接触动态规划,我的理解还十分有限,还有很多细节无法尽善尽美的解释(比如代码中的三层循环)。谨以本题作为起步,希望以后能慢慢领会其中的奥秘。另外最后还要感谢两篇优质的题解指点。
HDU - 2069 Coin Change(换硬币)解题报告(dp入门)
HDU - 2069 Coin Change