详解_动态规划DAG_硬币找零问题(完全背包)

写了好多结果一下卡住都没了。。。(csdn怕是把大部分服务器资源用在了广告投放上吧)

参考数目:算法竞赛入门经典(第二版)

NYOJ 995:

描述

在现实生活中,我们经常遇到硬币找零的问题,例如,在发工资时,财务人员就需要计算最少的找零硬币数,以便他们能从银行拿回最少的硬币数,并保证能用这些硬币发工资。

我们应该注意到,人民币的硬币系统是 100,50,20,10,5,2,1,0.5,0.2,0.1,0.05,

0.02,0.01 元,采用这些硬币我们可以对任何一个工资数用贪心算法求出其最少硬币数。 

但不幸的是: 我们可能没有这样一种好的硬币系统, 因此用贪心算法不能求出最少的硬币数,甚至有些金钱总数还不能用这些硬币找零。例如,如果硬币系统是 40,30,25 元,那么 37元就不能用这些硬币找零;95 元的最少找零硬币数是 3。又如,硬币系统是 10,7,5,1元,那么 12 元用贪心法得到的硬币数为 3,而最少硬币数是 2。 

你的任务就是:对于任意的硬币系统和一个金钱数,请你编程求出最少的找零硬币数;
输入:

4 12
10 7 5 1

输出

2

分析 :由描述可知贪心并不一定可以获得最优解,但是一定是近似最优解的。将一个硬币面额看作是当前需要凑出的面额(保证可以凑出),则本问题就是一个固定起点终点的DAG最短路径问题,起点为待凑面额value,终点为0,每使用一枚硬币 i ,将状态转移到 value-v[i](value表示待凑面额,v数组用于存储每个硬币的面额),使用硬币就是就是在缩小问题规模的过程,也就是当前状态沿(i,j)的转移过程,所以可以得到状态转移方程:dis[i] = min(dis[i],dis[value-v[i]]+1)

代码:

1.记忆化搜索,代码更为清晰,更容易理解。(本质就是缩小问题规模,直到缩小到状态为0,而此时我们已经将状态为0时需要的硬币数算出,即:dis[0] = 0.O(n2))

#include<bits/stdc++.h>

#define min(a,b) a<b?a:b
#define infinite 0x3f3f3f
#define MAX 500
using namespace std;

int n,value;
int dis[MAX];
int v[MAX];

int dp(int s)
{
    int & ans = dis[s];//引用ans来表示dis[s]
    if(ans!=-1)
        return ans;

      ans = infinite;//将其设置为一个大数(试想如果将ans设为一个极小的数则ans将一直很小)
      for(int i = 0 ;i<n ; i++)//对每一个硬币面额进行匹配
        if(s>=v[i])
        ans = min(ans,dp(s-v[i])+1);//不断缩小问题规模,找到最短路径(dp会一直递归到s=0,此时dis[0] = 0保证使得ans会等于后一个的值,从而ans将由infinite变为从0开始)
      return ans;

}
int main()
{
    cin>>n>>value;
    for(int i = 0 ; i<n ; i++)
        cin>>v[i];
     memset(dis,-1,sizeof(dis));
     dis[0] = 0;//一定要赋初值(动态规划的思想是由从小到大,因此必须有至少一个最小的问题的解已经得出
     cout<<dp(value)<<endl;

    return 0;
}

2.递推法(利用刷表法思想,对每一个硬币面额所影响的状态进行维护,d[t] : 表示以t结束的最短路径长度)

#include<bits/stdc++.h>

#define MAX 500
using namespace std;

int main()
{

    int n,value;
    int v[MAX];
    int d[MAX];
    cin>>n>>value;

    for(int i = 0 ; i<n ; i++)
     cin>>v[i];

     memset(d,0x3f3f3f,sizeof(d));//初始化为一个很大的数
     d[0] = 0;//注意递推的终点必须事先得到

     for(int i = 0 ; i<n ; i++)//对每个硬币影响到的状态进行维护(更新)
        for(int j = value; j>=0 ; j--)//从待凑面值到 0进行遍历(反过来没有影响),确保对每一个状态都进行更新
        d[j] = min(d[j],d[j-v[i]]+1);

         cout<<d[value];
    return 0;
}

5.总结;动态规划和函数递归在解决问题的思路上有异曲同工之妙,它们的聪明之处在于不直接对复杂的问题进行处理,而是在保持问题本质不发生变化的基础上将问题的规模不断缩小,到达一个临界点时,复杂的问题被化为简单问题解决,这个过程中,复杂问题所依赖的所有问题都已得解,从而自然而然得到最终问题的解。一般动态规划最小的解是容易得到的,而且一定要保证在将问题规模缩小之前获得该解。对比函数递归,临界点就是结束递归的一个条件,满足这个条件时,对最小问题求解,然后再回溯。前者先得最小解,后者遇到最小问题再求最小解。如本题所示,最小解为dis[0] = 0,已事先存储好。 

猜你喜欢

转载自blog.csdn.net/SWEENEY_HE/article/details/82155185