我们经常会接触到一些算法的题目,而某一类的题目都是用Dynamic Programming(动态规划)来解决。
动态规划的概念
动态规划通常基于一个递推方程和一个(或者多个)初始状态。它将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。使用DP通常可以得到多项式的时间复杂度。
动态规划的两个特征:
1. 最优子结构性质
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。
2. 子问题重叠性质
子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。
我们通过例子来学习动态规划。
题目:给定面值1,3和5的硬币,求使用最少硬币组成和为11的方案?
分析:这个题目的状态是‘和’i (i <= 11)。对于每一个硬币j, Vj<=i (Vj 表示面值),查询一下构成i-Vj硬币总和所需要的最少硬币数目(我们之前已经计算过了)。不妨假设这个数字是m,如果m+1小于所求得硬币总和为i的最少硬币数量,我们更新它的状态。
由此可得状态转移公式:number[Si] =min (number[Si - V[j]]) (1<Si<=sum, 0<j<coinNumber)
式中,number[Si]表示和为Si的最少硬币数。V[j]表示硬币的面值。
代码如下:
void find_min_coin_sum1(int coinValue[], int coinNum, int sum)
{
const int MAX = 100;
int pmin[MAX] = {0};
//Initialize to the max...
for (int index = 1; index <= sum; ++index)
pmin[index] = MAX;
for (int sub_sum = 1; sub_sum < sum + 1; ++sub_sum)
for (int coin_index = 0; coin_index < coinNum; ++coin_index)
if (coinValue[coin_index] <= sub_sum && (pmin[sub_sum - coinValue[coin_index]] + 1 < pmin[sub_sum]))
pmin[sub_sum] = pmin[sub_sum - coinValue[coin_index]] + 1;
cout << "Dear, the sum combine is " << pmin[sum] << endl;
}
此外,如果如果我们记录下从前一个状态得到当前状态所需要的硬币,我们可以找出组成该硬币总和所需要的所有硬币。例如: 要组成硬币总和11,我们可以通过面值为1的硬币加上面值为10的硬币。为了得到10,我们需要得到5,而5需要从0,因此我们找到了所使用到的硬币:1,5和5。
代码如下:
void find_min_coin_sum2(int coinValue[], int coinNum, int sum)
{
const int MAX = 100;
vector<map<int, int> > vec(sum+1);
int pmin[MAX] = {0};
//Initialize to the max...
for (int index = 1; index <= sum; ++index)
pmin[index] = MAX;
for (int sub_sum = 1; sub_sum < sum + 1; ++sub_sum)
for (int coin_index = 0; coin_index < coinNum; ++coin_index)
if (coinValue[coin_index] <= sub_sum && (pmin[sub_sum - coinValue[coin_index]] + 1 < pmin[sub_sum]))
{
pmin[sub_sum] = pmin[sub_sum - coinValue[coin_index]] + 1;
map<int, int> coin_sum_map;
coin_sum_map.insert( pair<int, int>(coinValue[coin_index], sub_sum - coinValue[coin_index]) );
vec[sub_sum] = coin_sum_map;
}
//output the coin value
cout << "the min coin sum to " << sum << " is " ;
do {
map<int, int> coin_map = vec[sum];
if (!coin_map.size())
break;
cout << coin_map.begin()->first << " ";
sum = coin_map.begin()->second;
} while( sum > 0);
}
现在让我们从一个不同的角度来看待这个问题。已知了硬币总和为0需要0枚硬币。现在我们试图在所有已经知道的结果中加上第一个,硬币(面值为1)。如果硬币总和t比起它的前一个状态需要的硬币数量更少,我们将更新它。对于第二个硬币,第三个硬币,和剩下的都是这样处理。例如,我们首先在硬币总和0上加上1得到硬币总和1. 标记S[1]=1。通过在硬币总和1上加上1,我们得到了硬币总和2,标记S[2]=2。然后对于第一个硬币依次下去。其余的硬币亦如此。这种方法类似素数的埃拉托斯特尼筛选法。
代码如下:
void find_min_coin_sum3(int coinValue[], int coinNum, int sum)
{
const int MAX = 100;
int pmin[MAX] = {0};
//Initialize to the max...
for (int index = 1; index <= sum; ++index)
pmin[index] = MAX;
for (int index = 0; index < coinNum; ++index)
{
for (int sub_sum = 0; sub_sum <= sum - coinValue[index]; ++sub_sum)
{
if ( coinValue[index] + sub_sum <= sum &&
(pmin[sub_sum] + 1 <= pmin[coinValue[index] + sub_sum]) )
pmin[coinValue[index] + sub_sum] = pmin[sub_sum] + 1;
}
}
cout << "Dear, the sum combine is " << pmin[sum] << endl;
}
参考文献:
http://zh.wikipedia.org/wiki/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92
http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=dynProg