两个动态规划的经典问题

硬币问题

问题描述:设有n种不同面值的硬币,各硬币的面值存于数组T[1:n]中。现要用这些面值的硬币来找钱。可以使用的各种面值的硬币个数存于数组Coins[1:n]中。

对任意钱数0≦m≦20001,设计一个用最少硬币找钱m的方法。

算法设计:对于给定的1≦n≦10,硬币面值数组T和可用使用的各种面值的硬币个数数组Coins,以及钱数m,0≦m≦20001,计算找钱m的最少硬币数。

我们再回想一下实现动态规划的前提:

  • 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
  • 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关;
  • 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。

在做这道题之前,我们先来看一个样例:

问题描述

假设有 1 元,3 元,5 元的硬币若干(无限),现在需要凑出 11 元,问如何组合才能使硬币的数量最少?

问题分析

乍看之下,我们简单的运用一下心算就能解出需要 2 个 5 元和 1 个 1 元的解。当然这里只是列出了这个问题比较简单的情况。当硬币的币制或者种类变化,并且需要凑出的总价值变大时,就很难靠简单的计算得出结论了。贪心算法可以在一定的程度上得出较优解,但不是每次都能得出最优解。

这里运用动态规划的思路解决该问题。按照一般思路,我们先从最基本的情况来一步一步地推导。

我们先假设一个函数 d(i) 来表示需要凑出 i 的总价值需要的最少硬币数量。

1.当 i = 0 时,很显然我们可以知道 d(0) = 0。因为不要凑钱了嘛,当然也不需要任何硬币了。注意这是很重要的一步,其后所有的结果都从这一步延伸开来。
2.当 i = 1 时,因为我们有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = 1。
3.当 i = 2 时,因为我们并没有 2 元的硬币,所以只能拿 1 元的硬币来凑。在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = 2。
4.当 i = 3 时,我们可以在第 3 步的基础上加上 1 个 1 元硬币,得到 3 这个结果。但其实我们有 3 元硬币,所以这一步的最优结果不是建立在第 3 步的结果上得来的,而是应该建立在第 1 步上,加上 1 个 3 元硬币,得到 d(3) = 1。

接着就不再举例了,我们来分析一下。可以看出,除了第 1 步这个看似基本的公理外,

其他往后的结果都是建立在它之前得到的某一步的最优解上,加上 1 个硬币得到。

得出:d(i) = d(j) + 1

这里 j < i。通俗地讲,我们需要凑出 i 元,就在凑出 j 的结果上再加上某一个硬币就行了。

那这里我们加上的是哪个硬币呢。嗯,其实很简单,把每个硬币试一下就行了:

假设最后加上的是 1 元硬币,那 d(i) = d(j) + 1 = d(i – 1) + 1。
假设最后加上的是 3 元硬币,那 d(i) = d(j) + 1 = d(i – 3) + 1。
假设最后加上的是 5 元硬币,那 d(i) = d(j) + 1 = d(i – 5) + 1。

我们分别计算出 d(i – 1) + 1d(i – 3) + 1d(i – 5) + 1 的值,取其中的最小值,即为最优解,也就是 d(i)

最后公式:

img

int main() {
    int n, m, dp[20002];
    cin >> n;//输入硬币种类
    int value[n + 1], coins[n + 1];
    for (int i = 1; i <= n; i++)
        cin >> value[i] >> coins[i];//分别输入面值与数量
    cin >> m;//最后找钱m
    for (int i = 1; i < 20002; i++)
        dp[i] = 1000000;//一维数组存储结果初定义
    dp[0] = 0;
    
    
    for (int i = 1; i <= n; i++)//面值种类
        for (int j = 1; j <= coins[i]; j++)//面值数量
            for (int k = m; k >= value[i]; k--)
                dp[k] = min(dp[k - value[i]] + 1, dp[k]);//核心(先找面值大的硬币,)

    
    cout << (dp[m] < 1000000 ? dp[m] : -1) << endl;//问题无解输出-1
    return 0;
}

背包问题

5个物体,重量分别为3,5,7,8,9,价值分别为4,6,7,9,10,背包容量为22,物体不能分割,求装入背包物体最大价值,写出求解过程。

//题目:
//求解下面的背包问题。有5个体积是3,5,7,8和9,价值为4,6,7,9和10的物品,背包的容量是22。
#include<iostream>
using namespace std;
#define NUM 5 //物体的个数
#define CAPCITY 22 //背包的容量
int main(){
	//初始化每个物体的体积以及价值,注意数组第一个元素是0
    int volume[CAPCITY+1]={0,3,5,7,8,9};
	int value[NUM+1]={0,4,6,7,9};
	//新建一个二维表用于存储每一步的结果
	int result[NUM+1][CAPCITY+1];
 
	//初始化二维结果表,这里的初始化是指记录每次选择(每一步)的最大价值,注意这里的选择不是穷举啊,算法的逻辑不清楚的话可以看
    //算法图解
	for(int i=0;i<=NUM;i++){result[i][0]=0;}
	for(int j=0;j<=CAPCITY;j++){result[0][j]=0;}
	for(int i=1;i<=NUM;i++){
		for(int j=1;j<=CAPCITY;j++){
			result[i][j]=result[i-1][j];
			if(volume[i]<=j){
				result[i][j]=max(result[i][j],result[i-1][j-volume[i]]+value[i]);
			}
		}
	}
    //将包含选择结果的表打印下来
	for(int k=0;k<=NUM;k++){
		for(int l=0;l<=CAPCITY;l++){
			cout<<result[k][l];
			if(result[k][l]>9)
				cout<<' ';
			else
				cout<<"  ";
		}
		cout<<endl;
	}
	cout<<"由上表可知,背包内可装下的物品的最大价值是:"<<result[NUM][CAPCITY]<<endl;
    system("pause");
	return 0;
}

image