DP初步:状态转移与递推
eg:
最少硬币问题有多个不同面值的硬币(任意面值);数量不限; 输入金额S,输出最少硬币组合。思路:
int type[ ] = {1, 5, 10, 25, 50}; //5种面值硬币 定义数组 int Min[ ] 记录最少硬币数量:对输入的某个金额i,Min[i]是最少的硬币数量。第一步:
只考虑1元面值的硬币(把Min[]的变化叫做“状态转移”)
i=1元时,等价于:i = i-1 = 0元需要的硬币数量,加上1个1元硬币。
补:
把Min[]的变化叫做“状态转移”
继续
所有金额仍然都只用1元硬币
i=2元时,等价于:i = i-1 = 1元需要的硬币数量,加上1个1元硬币。i=3元时...i=4元时…
在1元硬币的计算结果基础上,再考虑加上5元硬币的情况。从i=5开始就行了:
i=5元时,等价于:
(1)i = i-5 = 0元需要的硬币数量,加上1个5元硬币。Min[5]=1。 (2)原来的Min[5]=5。 取(1)(2)的最小值,所以Min[5]=1用1元和5元硬币,结果:
递推关系: Min[i] = min(Min[i], Min[i - 5] + 1);
代码:
//ECUST luoyongjun #include<bits/stdc++.h> using namespace std; const int MONEY = 1001; //定义最大金额 const int VALUE = 5; //5种硬币 int type[VALUE] = {1, 5, 10, 25, 50}; //5种面值 int Min[MONEY]; //每个金额对应最少的硬币数量 void solve(){ for(int k = 0; k< MONEY; k++) //初始值为无穷大 Min[k] = INT_MAX; Min[0] = 0; for(int j = 0; j < VALUE; j++) for(int i = type[j]; i < MONEY; i++) Min[i] = min(Min[i], Min[i - type[j]] + 1); //递推式 } int main(){ int s; solve(); //提前计算出所有金额对应的最少硬币数量。打表 while(cin >> s) cout << Min[s] << endl; return 0; }扩展1:
打印最少硬币的组合
增加一个记录表 Min_path[i],记录金额i需要的最后一个硬币。利用Min_path[]逐步倒推,就能得到所有的硬币。 Min_Path[6]=5:最后一个硬币是5元的;Min_Path[6-5]=Min_Path[1]=1,1元硬币;Min_Path[1-1]=Min_Path[0]=0,结束; 输出:硬币组合是“5元 + 1元”代码:
//ECUST luoyongjun #include<bits/stdc++.h> using namespace std; const int MONEY = 1001; //定义最大金额 const int VALUE = 5; //5种硬币 int type[VALUE] = {1,5,10,25,50}; //5种面值 int Min[MONEY]; //每个金额对应最少的硬币数量 int Min_path[MONEY]={0}; //记录最小硬币的路径 void solve(){ for(int k=0; k< MONEY;k++) Min[k] = INT_MAX; Min[0]=0; for(int j = 0;j < VALUE;j++) for(int i = type[j]; i < MONEY; i++) if(Min[i] > Min[i - type[j]]+1){ Min_path[i] = type[j]; //在每个金额上记录路径,即某个硬币的面值 Min[i] = Min[i - type[j]] + 1; //递推式 } } void print_ans(int *Min_path, int s) { //打印硬币组合 while(s){ cout << Min_path[s] << " "; s = s - Min_path[s]; } } int main() { int s; solve(); while(cin >> s){ cout << Min[s] << endl; //输出最少硬币个数 print_ans(Min_path,s); //打印硬币组合 } return 0; }扩展2:
硬币组合方案有多少?
hdu 2069题 有5种面值的硬币:1分、5分、10分、25分、50分。输入一个钱数S,输出组合方案的数量。例如11元,有4种组合方案:11个1分、2个5分 + 1个1分、1个5分 + 6个1分、1个10分 + 1个1分。S ≤ 250,硬币数量NUM ≤ 100。思路:
定义状态为dp[i][j],建立一个“转移矩阵”,如下表。
横向是金额(题目中i ≤ 250),纵向是硬币数(题目中最多用100个硬币,j ≤ 100)。
矩阵元素dp[i][j]的含义是:用j个硬币实现金额i的方案数量。
矩阵元素dp[i][j] 例如表中dp[6][2] = 1,表示用2个硬币凑出6分钱,只有1种方案:5分+1分。表中的空格为0,即没有方案,例如dp[6][1]=0,用1个硬币凑6分钱,不存在这样的方案。表中列出了dp[10][7]以内的方案数。第一步:
只用1分硬币实现。 初始化:dp[0][0] = 1 dp[1][1] = dp[1][1] + dp[1-1][1-1] = dp[1][1] + dp[0][0] = 1。dp[1-1][1-1]的意思是:在1分金额中减去1分硬币的金额;原来1个硬币也减少1个。在这次状态转移中,dp[1][1]与dp[1-1][1-1]等价。第二步:
加上5分硬币,继续进行组合。 dp[i][j],i < 5时,组合中不可能有5分硬币。当i≧5时,金额为i,硬币为j个的组合数量,等价于在i中减去5分钱,而且硬币数量也减去1个(即这个面值5的硬币)的情况。dp[i][j] = dp[i][j] + dp[i-5][j-1] 。第三步:
陆续加上10分、25分、50分硬币,同理有dp[i][j] = dp[i][j] + dp[i-type[k]][j-1],k=2, 3, 4。代码:
//ECUST luoyongjun #include<bits/stdc++.h> using namespace std; const int COIN = 101; //题目要求不超过100个硬币 const int MONEY = 251; //题目给定的钱数不超过250 int dp[MONEY][COIN] = {0}; // DP转移矩阵 int type[5] = {1, 5, 10, 25, 50}; //5种面值 void solve() { // DP dp[0][0] = 1; for(int i=0; i<5; i++) for(int j=1; j<COIN; j++) for(int k = type[i]; k < MONEY; k++) dp[k][j] += dp[k-type[i]][j-1]; } int main() { int s; int ans[MONEY] = {0}; solve(); //用DP计算完整的转移矩阵 for(int i=0; i< MONEY; i++) //对每个金额,计算有多少种组合方案。打表 for(int j=0; j<COIN; j++) //从0开始,注意 dp[0][0]=1 ans[i] += dp[i][j]; while(cin >> s) cout << ans[s] << endl; return 0; }