有关动态规划的问题是一个重点,也是一个难点。一开始不知道如何插手的我在学长的建议和相关资料的帮助下,对于动态规划的学习我选择了从较为简单的背包开始。
01背包
给定一个容积为c的背包,去尝试装n个重量为wi、价值为vi的物体,求能装下的物体的最大价值。例如,当n=3,c=30,W[]={16,15,15},v={45,25,25}时,可以装下的是后两个物体,最大价值为50。对于给定的3个物体而言,有可能出现在最后的背包中,也有可能不出现在背包中。可以用一个取值为0和1的变量,来表示物体是否出现在最后的背包里。将个物体看成一个整体来表示,所有物体在背包中的出现可以用一个长度为n、由0和1构成的串来表示。显然,共存在00...00到11..11的2^n种情况。如果将00...00到11..11这个串看成是十进制数的二进制表示,则所表示的十进制数恰好是0到2^n-1。通过这样的转换,可以利用一个从0循环到2^n-1的变量来表示所有物体在背包中的出现形式,将它转换成一个长度为n的二进制,相应的位的数值(0和1)表示所对应的物体是否现出在背包中。基于该思想,设计的程序如下:
#include
#include
#define n 3
#define c 30
int w[n]={16,15,15};
int v[n]={45,25,25};
int main()
{
int num,temp;
int i;
int tempvalue, tempweight;
int maxvalue=0;
for(num=0;nummaxvalue)
maxvalue=tempvalue;
}
printf("%d\n",maxvalue);
return 0;
}
相信大家看了这段代码后大体知道什么是01背包了,但是这种方法在现实情况中并不实用,因为在当物体的个数很大时,将物体的每种放置情况一一列举,它的计算量将变得相当庞大,在这里只是让大家理解什么是01背包。
然后,我们来讲动态规划处理01背包,在动态规划下的01背包是逐层取最优的过程,由于每种物品只有放与不放两种状态,所以它的状态转移方程是: dp[i][v]=max{dp[i-1][v],dp[i-1][v-cc[i]]+ww[i]};
该怎么来理解状态转移方程呢,其中dp[i][v]内存放的值表示背包内物体的价值,dp[i][v]中的i,v分别表示第i个物体和背包的容量为v;所以方程就变的简单了dp[i-1][v]表示第i个物体不放入背包种,dp[i-1][v-cc[i]]+ww[i]表示第i个物体放入背包中(物体放入背包牺牲掉背包容量cc[i],加上物体价值ww[i]);并选择二者中较大的情况。前面说过这个过程是逐层取最优,也就是说在放第i个物体时前i-1个物体已经是最优的情况,所以在放第i个物体的时候是在前i-1个物体的基础之上选择是否放置。
明白了状态转移方程我们来看几个题
HDU2602 http://acm.hdu.edu.cn/showproblem.php?pid=2602
题目大意: 许多年前,在泰迪的故乡,有一个叫“骨收藏者”的男人。这个男人喜欢收集各种各样的骨头,比如狗,牛,他也去了坟墓...骨收集者有一个大袋子V,在他的收集之旅有很多骨头,显然,不同的骨骼具有不同的价值和不同的体积,现在给出他的旅行中的每个骨骼的价值,你可以计算骨收集器可以获得的总值的最大值?
这显然是一个01背包问题
AC代码:
#include
#include
#include
using namespace std;
int dp[1100][1100];
int main (){
int T;
scanf ("%d",&T);
while(T--){
int N,V;
int WW[1100];
int VV[1100];
scanf ("%d%d",&N,&V);
memset(dp,0,sizeof(dp));
for (int i=1;i<=N;i++){
scanf ("%d",&VV[i]);
}
for (int i=1;i<=N;i++){
scanf ("%d",&WW[i]);
}
// 这里是核心代码 第一层循环表示物体的个数 第二层循环表示背包的容积
for (int i=1;i<=N;i++){
for (int j=0;j<=V;j++){
if(WW[i]<=j){// 当背包的容积可以放入第i个物体是 判断放还是不放
dp[i][j]=max(dp[i-1][j],dp[i-1][j-WW[i]]+VV[i]);
}
else{// 如果背包容积不能放入第i个物体 这时的最优解为在此容积下前i-1个物体的最大价值
dp[i][j]=dp[i-1][j];
}
}
}
// 背包容积的变化过程
/*for (int i=0;i<=N;i++){
for (int j=0;j<=V;j++){
printf ("%5d",dp[i][j]);
}
putchar('\n');
} */
printf ("%d\n",dp[N][V]);
}
return 0;
}
这里用到一点点的数学技巧 求收到至少一份offer的最大概率 也可以说成 求一份offer也收不到的最小概率
AC代码:
#include
#include
#include
using namespace std;
float dp[100010];
int mm[100010];
float ff[100010];
int main (){
int n,m;
while (scanf ("%d%d",&n,&m)&&(n||m)){
for (int i=0;i=mm[i];j--){// 一维时内层循环为逆序
dp[j]=min(dp[j],dp[j-mm[i]]*ff[i]);
}
}
printf ("%.1f%%\n",(1-dp[n])*100);
}
return 0;
}
HDU1864 http://acm.hdu.edu.cn/showproblem.php?pid=1864
审题很重要 数据处理很重要 题目中给出小数位数 所以我们可以将 小数化为整数 然后在进行01背包求解
AC代码:
#include
#include
#include
using namespace std;
int dp[3000001];
int cost[33];
int num[3];
int main (){
int N;
float Q;
int Q2;
while (scanf ("%f%d",&Q,&N)&&N){
Q2=(int)(Q*100);
int len=0;
memset(dp,0,sizeof(dp));
memset(cost,0,sizeof(cost));
while (N--){
int m;
scanf ("%d",&m);
float x;
memset(num,0,sizeof(num));
int flag=1;
while (m--){
char o;
scanf (" %c:%f",&o,&x);
// 只用A B C类能报销 并且单件价格不超过600
if (o=='A'&&x<=600){
num[0]+=(int)(x*100);
}
else if (o=='B'&&x<=600){
num[1]+=(int)(x*100);
}
else if (o=='C'&&x<=600){
num[2]+=(int)(x*100);
}
else {// 存在其他类物品 便都不能报销
flag=0;
}
}
if (num[0]+num[1]+num[2]<=1000*100&&num[0]<=600*100&&num[1]<=600*100&&num[2]<=600*100&&flag){
cost[len++]=num[0]+num[1]+num[2];
}
}
for (int i=0;i=cost[i];j--){
dp[j]=max(dp[j],dp[j-cost[i]]+cost[i]);
}
}
printf ("%.2f\n",dp[Q2]/100.0);
}
return 0;
}
HDU2955 http://acm.hdu.edu.cn/showproblem.php?pid=2955
题目大意:有一个强盗要去几个银行偷盗,他既想多投点钱,又想尽量不被抓到。已知各个银行的金钱数和被抓的概率,以及强盗能容忍的最大被抓概率。求他最多能偷到多少钱?
乍一看是一个01背包的题目 但是里面却存在着问题 背包的容量是一个小数 这就难办了 因为数组的下标不能是小数而且我们也并不知道小数的位数所以就不能向上个题一样把它化为整数再去求解了。那我们怎么办呢。其实这是一个01背包的变形题 我们可以将抢到的总钱数作为背包的容量 然后求出其不被抓的最大概率 对就是不被抓的最大概率 由于题目给出被抓的概率 所以要用 1-被抓概率 求完以后再遍历一遍数组 找到第一个大于等与不被抓的概率 输出钱数即可
AC代码:
#include
#include
#include
using namespace std;
float dp[10110];
int ww[110];
float vv[110];
int main (){
int t;
scanf ("%d",&t);
while (t--){
float v;
int n;
int sum=0;
scanf ("%f%d",&v,&n);
v=1-v;
for (int i=0;i=ww[i];j--){
dp[j]=max(dp[j],dp[j-ww[i]]*vv[i]);
}
}
for (int i=sum;i>=0;i--){// 找到概率
if(dp[i]>=v){
printf ("%d\n",i);
break;
}
}
}
return 0;
}
完全背包
完全背包与01背包的区别是没种物体都有无限个
状态转移方程为:dp[i][v]=max{dp[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]};
这里与01背包的区别在于内层循环为顺序
现在关键的是考虑:为何完全背包可以这么写?
在次我们先来回忆下,01背包逆序的原因?是为了是max中的两项是前一状态值。那么这里,我们顺序写,这里的max中的两项当然就是当前状态的值了,为何?因为每种背包都是无限的。当我们把i从1到N循环时,f[v]表示容量为v在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我们要考虑的当然是当前状态。
HDU1114 http://acm.hdu.edu.cn/showproblem.php?pid=1114
题意:某人有个存钱罐(存硬币),空着的时候重E,存满钱是重F。现在他把罐子装满钱了,给出硬币种类和重量,计算存钱罐最少能存多少钱。
完全背包求最小 所以要将dp初始为无穷大
AC代码:
#include
#include
#include
using namespace std;
int dp[10100];
int vv[510];
int ww[510];
int main (){
const int maxinf=0x3f3f3f3f;
int t;
scanf ("%d",&t);
while (t--){
int low,hight;
scanf("%d%d",&low,&hight);
int weight=hight-low;
int n;
scanf ("%d",&n);
for (int i=0;i
HDU1248 http://acm.hdu.edu.cn/showproblem.php?pid=1248
#include
#include
#include
using namespace std;
int dp[10010];
int vv[3]={150,200,350};
int main (){
int T;
scanf ("%d",&T);
while (T--){
int m;
scanf ("%d",&m);
memset(dp,0,sizeof(dp));
for (int i=0;i<3;i++){
for (int j=vv[i];j<=m;j++){
dp[j]=max(dp[j],dp[j-vv[i]]+vv[i]);
}
}
printf ("%d\n",m-dp[m]);
}
return 0;
}
多重背包
多重背包与01背包和完全背包的区别在于每种物体的个数都是有限个
我们可以将其看成01背包来做 其思想是将n个物体X 看成n个相同的独立物体
HDU2191 http://acm.hdu.edu.cn/showproblem.php?pid=2191
AC代码:
#include
#include
#include
using namespace std;
int dp[110];
int vv[20020];
int ww[20020];
int main (){
int t;
scanf ("%d",&t);
while (t--){
int n,m;
scanf ("%d%d",&n,&m);
int len=0;
while (m--){
int p,h,c;
scanf ("%d%d%d",&p,&h,&c);
while (c--){
vv[len]=p;
ww[len]=h;
len++;
}
}
memset(dp,0,sizeof(dp));
for (int i=0;i=vv[i];j--){
dp[j]=max(dp[j],dp[j-vv[i]]+ww[i]);
}
}
printf ("%d\n",dp[n]);
}
return 0;
}