一、硬币问题:
最少硬币问题
有n种硬币,面值分别为v1,v2,v3···,vn,数量无限。输入非负整数s。要求输出最少的硬币组合。
定义一个数组int Min[],其中MIn[i]是金额i对应的最少硬币数量。
以五种面值(1、5、10、25、50)的硬币为例演示递推过程。
1. 只使用最小面值的1分硬币:Min[0]=0 -> Min[1] = Min[1-1] + 1 = 1 -> Min[2] = Min[2-1] + 1 = 2,持续这个过程,得到递推关系MIn[i] = min(Min[i], Min[i-1] + 1) 。
2. 在使用1分硬币的基础上增加使用第二小面值5分硬币:Min[5] = Min[5-5] + 1 =1 -> Min[6] = Min[6-1] + 1 = 6。持续这个过程,得到递推关系Min[i] = min(Min[i], MIn[i-5] + 1)。
继续处理其他面值的硬币,从Min[i-1]或Min[i-5]得到Min[i]的递推关系成为 “状态转移” 。而根据之前递推的规律,我们可以总结得出Min[i] = min(Min[i], Min[i-x] + 1(x为硬币的面值)。
代码如下:
#include<iostream>
using namespace std;
const int inf=0x3f3f3f3f;
int main()
{
int i,j,n,Min[250],money[5]={1,5,10,20,50};
Min[0]=0;
for(i=1;i<250;i++){
Min[i]=inf;
}
for(i=0;i<5;i++){
for(j=money[i];j<250;j++){
Min[j]=min(Min[j],Min[j-money[i]]+1);
}
}
cin>>n;
cout<<Min[n]<<endl;
return 0;
}
打印最少硬币的组合
这种题目状态表需要适当的扩展,以包含更多信息。可以通过增加一个记录表Min_path[i]来记录金额i需要的最后一个硬币。利用记录表逐步倒推来获得所有的硬币。
#include<iostream>
using namespace std;
const int inf = 0x3f3f3f3f;
int n;
int Min[251];
int Min_path[251];
int type[5] = {1, 5, 10, 25, 50};
int main(){
for (int k = 1; k < 251; k++)
Min[k] = inf;
for (int i = 0; i < 5; i++)
for (int j = type[i]; j < 251; j++)
if(Min[j] > Min[j-type[i]]+1){
Min_path[j] = type[i];
Min[j] = Min[j - type[i]] + 1;
}
while(cin>>n){
cout << Min[n] << endl;
while(n){
cout << Min_path[n] << endl;
n = n - Min_path[n];
}
}
return 0;
}
所有硬币组合
问题:有5中面值的硬币,即1分、5分、10分、25分、50分。输入一个钱数s,输出组合方案的数量。例如11分有四种组合方案,即11个1分、2个5分+1个1分、1个5分+6个1分、1个10分+1个1分。s≤250,硬币数量num≤100。
不完全解决方案
假设硬币数量不限,模仿前面最少硬币问题的状态转移也能容易地递推出这题的状态转移:dp[i] = dp[i] + dp[i-x](x为硬币面值)。
代码如下:
#include<iostream>
using namespace std;
int n;
int typ[5] = {1, 5, 10, 25, 50};
int dp[251] = {1};
int main(){
for (int i = 0; i < 5; i++)
for (int j = typ[i]; j < 251; j++)
dp[j] = dp[j] + dp[j - typ[i]];
while(~scanf("%d", &n))
printf("%d\n", dp[n]);
return 0;
}
完全解决方案
定义一个二维数组int dp[][]其中dp[i][j]的含义是用j个硬币实现金额i的方案数量。递推得到状态转移:dp[i][j] = dp[i][j] + dp[i-x][j-1]。
代码如下:
#include<iostream>
using namespace std;
int main(){
int n,i,j,k,dp[251][251]={1},w[251];
int type[5]={1,5,10,25,50};
scanf("%d",&n);
for(i=0;i<5;i++)
for(j=1;j<101;j++)
for(k=type[i];k<251;k++)
dp[k][j]=dp[k][j]+dp[k-type[i]][j-1];
for(i=0;i<251;i++)
for(j=0;j<101;j++)
w[i]=w[i]+dp[i][j];
printf("%d\n",w[n]);
return 0;
}
二、01背包问题:
1.问题描述
给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
如果物品不能被分割,即物品i要么整个地选取,要么不选取; 不能将物品i装入背包多次,也不能只装入部分物品i,则该问题称为0—1背包问题,适合使用DP。
如果物品可以拆分,则问题称为背包问题,适合使用贪心算法。
2.分析问题
假设xi表示物品i装入背包的情况
当xi=0时,表示物品没有装入背包;
当xi=1时,表示把物品装入背包。
约束方程: ≤W
目标函数:
因此问题就归结为找到一个满足上述约束方程, 并使目标函数达到最大的解向量: X={x1,x2,…,xn}
0-1背包问题的子问题: i≤k≤n的最优值为p(i,j)
p(i,j)是背包容量为j,可选物品为i,i+1,…,n时0-1背包问题的最优值!
此时一定要理解公式p(i,j)的意思。
3.过程推理
W=5
1
2
3
4
重量w
2
1
3
2
价值v
12
10
20
15
p[2][1]=max(p[3][1],p[3][0]+10)=10;
p[2][2]=max(p[3][2],p[3][1]+10)=15;
p[2][3]=max(p[3][3],p[3][2]+10)=25;
p[2][4]=max(p[3][4],p[3][3]+10)=30;
p[2][5]=max(p[3][5],p[3][4]+10)=35;
p[1][5]=max(p[2][5],p[2][3]+12)=37;
从下向上,从左到右填表
第4行,只考虑物品4,物品4的重量是2,价值是15
背包容量为0和1的时候,放不进去4,因此p[4][0]=p[4][1]=0
背包容量为2,3,4,5的时候,放进一个4,因此p[4][2]=p[4][3]=p[4][4]=p[4][5]=15
第3行,只考虑物品4和3,物品4的重量2,价值15;物品3的重量3,价值20
以p[3][5]为例,代入上述方程可得:
p[3][5]=max(p[4][5],p[4][5-3]+20)=35
5-3的3是物品3的重量,20是物品3的价值
意思是j(5)>wi(3),此时物品3可以放进背包
如果物品3性价比不高,就选择不放进来,p[3][5]=p[4][5]
如果物品3性价比高,就选择放进背包,p[3][5]=p[4][2]+20,意思是物品3放入背包,背包容量变为2,而价值增加20
p[3][5]=p[4][5]=15
意思是如果j<wi,此时物品i不能放进背包
我们最终需要的结果放在了p[1][5]中
求p[1][5]需要用到第二行的数据,而p[1][0~4]的数据都没有用,可以置0
p[1][5]=max(p[2][5],p[2][5-3]+20)=35
p[1][5]=p[4][5]=15
代码如下:
#include <iostream>
using namespace std;
#define NUM 50//物品数量的上限
#define CAP 1500//背包容量的上限
int w[NUM];//物品重量
int v[NUM];//物品价值
int p[NUM][CAP];//存放结果的数组
//求最优值
//c是背包容量,n是物品数量
void dg(int c,int n){
int jmax=min(w[n]-1,c);//分界点
//整型,使得分界点是比物品n的重量刚好小一点的容量
//初始化
for(int j=0;j<=jmax;j++)
p[n][j]=0;
for(int j=w[n];j<=c;j++)
p[n][j]=v[n];
for(int i=n-1;i>1;i--){//行,从下往上
jmax=min(w[i]-1,c);
for(int j=0;j<=jmax;j++)//列,放不开物品i
p[i][j]=p[i+1][j];
for(int j=w[i];j<=c;j++)//列,放得开物品i
p[i][j]=max(p[i+1][j],p[i+1][j-w[i]]+v[i]);
}
//第一行单独求
p[1][c]=p[2][c];
if(c>=w[1])
p[1][c]=max(p[1][c],p[2][c-w[1]]+v[1]);
}
//求最优解
void trace(int c,int n,int x[]){
//判断p[i][j]和p[i+1][j]是否相等
for(int i=1;i<n;i++){
if(p[i][c]==p[i+1][c])//相等,说明物品i没有放进去
x[i]=0;
else{
x[i]=1;//不等,说明物品i放进背包
c=c-w[i];//背包现有容量=背包原容量-减去物品i重量
}
x[n]=p[n][c]?1:0;//p[n][c]为真则返回1,否则返回0
}
}
int main()
{
int n,c;
cin>>c>>n;
int x[n];
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
dg(c,n);
cout<<p[1][5]<<endl;
trace(c,n,x);
for(int i=1;i<=n;i++)
if(x[i])
cout<<i<<" ";
return 0;
}