基础DP
内容参考书籍《算法竞赛入门到进阶》
DP与分治法的区别:
(1)分治法是把问题分成独立的子问题,各个子问题能独立解决,一个子问题内部的计算不需要其他子问题的数据,例如归并排序。
(2)DP的子问题之间是相关的,前面子问题的解决结果被后面的子问题使用。
(DP比分治复杂得多)
求解DP问题有3步,即定义状态、状态转移、算法实现。DP的核心是状态、状态转移方程。用状态转移方程求解状态,状态往往就是问题的解。
下面从简单的硬币问题开始引出动态规划:
最少硬币问题:有n种硬币,面值分别为v1 v2 v3 ...vn ,数量无限。输入非负整数s,选用硬币使其和为s,要求输出最少的硬币组合。
定义一个数组Min[MONEY],其中Min[i]是金额i对应的最少硬币数量。对于输入的某个金额i,只要查Min[i]即可。
那么如何计算Min[i]呢?下面我们以面值(1、5、10、25、50)为例讲解递推过程。
首先我们类比一下斐波那契数列,要输出斐波那契的某一项时,我们可以将斐波那契数列前n项全部算出来,对于第i项直接查数组即可。
我们知道,斐波那契数列的第一项是1,第二项是1,往后的所有项都是前两项的和即F[n]=F[n-1]+F[n-2],于是我们可以通过一个循环一项一项地计算到第n项。
同样,对于这道题我们知道Min[0]=0,假设现在我只考虑面值为1的硬币,那么Min[1]=1,Min[2]=2,即:Min[i]=Min[i-1]+1
那么如果我们只有面值为1的硬币,对于金额为i的解我们便可以通过上述方程通过一个循环解出来。
接下来我们增加面值为5的硬币,我们知道Min[5]=1,因为金额为5只需要一枚面值为5的硬币即可,而Min[6]=2,因为只需要一枚5和一枚1,Min[7]=3
我们发现金额大于5的答案在加入了金额为5之后将更新为Min[i]=min(Min[i],Min[i-5]+1)。接下来处理其他面值即可。
值得注意的是,在DP中,不仅要求最优解数量,往往还要输出最优解本身。
在上诉问题中,需要增加一个记录表Min_path[i],记录金额i需要的最后一枚硬币。例如Min_path[6]=5,表示最后一个硬币是5,而Min_path[6-5]=1,表示接下来一枚是1,最后Min_path[0]=0,输出即可。
那如果我们要输出所有方案数呢?可以通过类似上述思路求解,首先定义一个数组dp[num],dp[i]表示金额i对应的组合方案数。
假设我们现在也只考虑面值为1的硬币,显然dp[0]=1,一个硬币也不用也是一种方案,这方便我们接下来的处理,dp[1]=1,dp[2]=1...由于只有一个硬币所以方案数都是1。
现在增加面值为5的硬币,那么显然dp[5]=2,dp[6]=2,dp[10]=3,由此我们推导出方程为dp[i]=dp[i]+dp[i-5],同理增加剩下的面值即可。
然而我们又遇到了一个新的问题,假如题目限制了硬币的数量,这时我们就需要重新定义状态为dp[i][j],其中i表示金额,j表示硬币数,dp[i][j]表示用j个硬币实现金额i的方案数。
我们定义type[5]={1,5,10,25,50},初始化dp[0][0]=1,其余为0.
先考虑只有面值为1的硬币,dp[1][1]=1,因为硬币数量+1,方案数为前一个状态方案数,即:dp[1][1]=dp[0][0]=1,但要考虑到原有方案数,故修正关系为dp[1][1]=dp[1][1]+dp[0][0].
把上述方程改写成dp[1][1]=dp[1][1]+dp[1-type[0]][1-1].
增加面值为5的硬币有dp[i][j]=dp[i][j]+dp[i-type[1]][j-1]
继续增加则有此关系:dp[i][j]=dp[i][j]+dp[i-type[k]][j-1],k=2,3,4
hdu2069:http://acm.hdu.edu.cn/showproblem.php?pid=2069
代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 const int COIN = 101;//题目要求不超过100个硬币 4 const int MONEY = 251;//题目给定的钱数不超过250 5 int dp[MONEY][COIN];//初始化dp转移矩阵 6 int type[5] = {1,5,10,25,50};//5种面值 7 void solve() //dp 8 { 9 dp[0][0] = 1; 10 for (int i = 0; i < 5; ++i)//依次增加5种面值,更新dp 11 for (int j = 1; j < COIN; ++j)//遍历硬币数 12 for (int k = type[i]; k < MONEY; ++k)//从该面值开始往后更新 13 dp[k][j] += dp[k-type[i]][j-1];//状态转移 14 } 15 16 int main(int argc, char const *argv[]) 17 { 18 int s; 19 int ans[MONEY] = {0}; 20 solve();//用dp计算完整的转移矩阵 21 for (int i = 0; i < MONEY; ++i)//对每个金额计算有多少种组合方案,打表 22 for (int j = 0; j < COIN; ++j)//从0开始,注意dp[0][0]=1 23 ans[i] += dp[i][j]; 24 while(cin>>s) 25 cout<<ans[s]<<endl; 26 return 0; 27 }