DP_硬币问题

       动态规划算法通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。动态规划也是面试笔试题中的一个考查重点,当阅读一个题目并且开始尝试解决它时,首先看一下它的限制。 如果要求在多项式时间内解决,那么该问题就很可能要用DP来解。遇到这种情况, 最重要的就是找到问题的“状态”和“状态转移方程”。(状态不是随便定义的, 一般定义完状态,你要找到当前状态是如何从前面的状态得到的, 即找到状态转移方程)如果看起来是个DP问题,但你却无法定义出状态, 那么试着将问题规约到一个已知的DP问题。

这里先说明一个最简单的动态规划实例:硬币问题。后续还会给出更多的实例,例如:最长公共子序列,最长公共子串,最长递增子序列,字符串编辑距离等。动态规划的关键就是找出“状态”和“状态转移方程”。

硬币问题:给你一些面额的硬币,然后给你一个值N,要你求出构成N所需要的最少硬币的数量和方案。分析:这个问题可以尝试用贪心算法去解决,先从面额最大的硬币开始尝试,一直往下找,知道硬币总和为N。但是贪心算法不能保证能够找出解(例如,给,2,3,5,然后N=11)。我们可以换个思路,我们用d(i)表示求总和为i的最少硬币数量(其实就是动态规划中的“状态”),那么怎么从前面的状态(并不一定是d(i-1)这一个状态)到d(i)这个状态?假设硬币集合为coins[0~N],在求d(i)之前,我们假设d(1~i-1)全部都求出来了,那么d(i)=min{d(j)+1},if i-j 在coins中(其实这就是“状态转移方程”)。另 我们把每种面值看作一个点!表示“还需要凑足的面值”,初始状态为S,目标状态为0。那么若当前状态在i,每使用一个硬币j,状态便转移到i-Vj。

举例说明:coins={2,3,5},N=11。

d(0)=0;

d(1)=0;

d(2)=d(0)+1=1;

d(3)=d(0)+1=1;

d(4)=d(2)+1=2;

d(5)=min{d(3)+1,d(2)+1,d(0)+1}=1;

d(6)=min{d(4)+1,d(3)+1}=2;

.......................

同时为了求出最后的方案(不仅仅是硬币个数),需要记录求每个状态选择的“路径”,例如:求d(5)我们选择了d(0)+1,那么我们选择的路径就是5-0=5。我们必须记录这些路径,然后根据路径得出结果。对于d(6),我们开始选择了3,也就是说我们选择了从d(3)状态和硬币3跳转到d(6),接着对于d(3),我们选择了3,也就是说我们选择了从d(0)状态和硬币3跳转到了d(3),接着对于d(0),这个是初始状态。所以我们得方案是3,3。


递推(打印最小序): 

#include <iostream>
#include <cstring>

using namespace std;

const int MAXN = 10000;
const int INF = 1000000000;
int n, S, V[MAXN], minn[MAXN], maxn[MAXN];   
//minn[i]表示还需凑足价值为i的话,所需的最少的硬币数目!maxn[i]表示还需凑足价值为i的话,所需的最多的硬币数目!

void print_ans(int* d, int S) {
  for(int i = 1; i <= n; ++i) {
    if(S >= V[i] && d[S] == d[S - V[i]] + 1){	//从小到大寻找满足条件的那个点!
      cout << i << " ";
      print_ans(d, S-V[i]);
      break;   //这个不能忘记!
    }
  }
}

int main() {
  cin >> n >> S;
  for(int i = 1; i <= n; ++i) {
    cin >> V[i];
  }
  for(int i=1; i<=S; ++i){
  	minn[i]=INF;maxn[i]=-INF;
  }
   for(int i=1; i<=S; ++i)
  	printf("minn=%d\n",minn[i]);
  	
  for(int i = 1; i <= S; ++i) {   //递推!求出所有minn[1...S]与maxn[1...S]!
    for(int j = 1; j <= n; ++j) {
      if(i >= V[j]) {
        minn[i] = min(minn[i], minn[i - V[j]] + 1);
        maxn[i] = max(maxn[i], maxn[i - V[j]] + 1);
      }
    }
  }
  cout << minn[S] << endl;
  cout << maxn[S] << endl;
  print_ans(minn, S);
  cout << endl;
  print_ans(maxn, S);
  cout << endl;
  return 0;
}

另一种打印方式:

#include <iostream>
#include <string>
using namespace std;

const int MAXN = 10000;
const int INF = 1000000000;
int n, S, V[MAXN], minn[MAXN], maxn[MAXN];
//minn[i]表示还需凑足价值为i的话,所需的最少的硬币数目!maxn[i]表示还需凑足价值为i的话,所需的最多的硬币数目!
int min_coin[MAXN], max_coin[MAXN];  
//min_coin[S]记录的是满足minn[S] = minn[S-V[i]]+1的最小的i。

void print_ans(int* d, int S) {
  while(S) {
    cout << d[S] << " ";
    S -= V[d[S]];
  }
}

int main() {
  cin >> n >> S;
  for(int i = 1; i <= n; ++i) {
    cin >> V[i];
  }
  for(int i=1; i<=S; ++i){
  	minn[i]=INF;maxn[i]=-INF;
  }
  for(int i = 1; i <= S; ++i) {   //递推!求出所有minn[1...S]与maxn[1...S]!
    for(int j = 1; j <= n; ++j) {
      if(i >= V[j]) {
        if(minn[i] > minn[i - V[j]] + 1) {
          minn[i] = minn[i - V[j]] + 1;
          min_coin[i] = j;
        }
        if(maxn[i] < maxn[i - V[j]] + 1) {
          maxn[i] = maxn[i - V[j]] + 1;
          max_coin[i] = j;
        }
      }
    }
  }
  cout << minn[S] << endl;
  cout << maxn[S] << endl;
  print_ans(min_coin, S);
  cout << endl;
  print_ans(max_coin, S);
  cout << endl;
  return 0;
}


你可能感兴趣的:(DP_硬币问题)