类似背包的dp问题-(牛客34)能天使的愿望题解(从01背包到多重背包)

一、本题的暴力解法dfs(只能过很少的点)

虽然剪枝了,但是对的点不多,大多数点都超时了。但是dfs的优点是很好写,如果没有思路的话,上手就能写,还可以拿来和自己的更优解法进行对拍(对拍可以检验你认为的更优解法的正确性)
这里贴上暴力代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define ll long long
const int maxn=1500;
ll a[maxn][maxn];
ll fei[maxn];
ll danjia[maxn][maxn];
ll vis[maxn];
using namespace std;
int n,m,k,y;
ll min_sum=1e9;
void dfs(ll cur,ll sum,ll kk){
    if(sum>min_sum) return;                             //最优化剪枝 
    if(kk==k){
        if(sum<min_sum) min_sum=sum;
        return;
    }
    if(cur>n) return;                                   //可行性剪枝 
    ll minj;
    if(m<k-kk) minj=m;                                  //可行性剪枝 
    else minj=k-kk;
    for(ll i=cur;i<=n;i++){
        for(ll j=1;j<=minj;j++){
            if(!vis[i] && j<=(k-kk)){
                vis[i]=1;
                dfs(cur+1,sum+a[i][j],kk+j);
                vis[i]=0;                               //这里回溯一下 
            }
        }
    }
}
int main(){
    //freopen("in.txt","r",stdin);      调试时候习惯用文件 
    //freopen("out.txt","w",stdout);
     
     
    cin>>n>>m>>k>>y;
    for(int i=1;i<=n;i++) cin>>fei[i];
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++) cin>>a[i][j];
    for(int i=1;i<=n;i++)
      for(int j=1;j<y;j++) a[i][j]+=fei[i]*j;
    /*for(int i=1;i<=n;i++){                             //这里是一个检测点,用来输出一下更新之后的矩阵 
        for(int j=1;j<=m;j++) cout<<a[i][j]<<' ';
        cout<<endl;
    }*/
    dfs(1,0,0);
    cout<<min_sum;
    return 0;
}

二、01背包问题

例题:信息学奥赛一本通评测网站 P1267 01背包问题
(感觉这个评测网站不错,对新手的话比较友好,前面有简单的语言题,也有一些基础算法的经典例题,还有各年noip的例题,做acm刷着也不错)。
题目描述很简单,01的意思就是要么装物品,要么不装物品,这其中每个物品只有一件。

下面的代码中的f[x]代表重量不超过x公斤的最大的价值,w代表每件物品的重量,c[i]代表每件物品的价值。那么在每次做出决定的时候,无非就涉及装或不装的选择。
装当前物品就是f[v-w[i]]+c[i],不装当前物品就是f[v];状态转移方程就是f[v]=max(f[v-w[i]]+c[i],f[v])
自己的理解,可以认为f[v-w[i]]就相当于是目前这个选择下,不装当前物品,也不装其它物品的情况(当然此时背包里面已经装了重量为v-w[i]的物品了),那再此基础上加上c[i]就相当于是在当前选择下装上当前物品的情况(这种情况下执行f[v]=f[v-w[i]]+c[i])。而f[v]就代表不选,也就是当前选择与之前的不发生变化(背包的东西没有任何变动,这种情况下执行f[v]=f[v])。显然我们要每次做出使得f[v]尽可能大的选择。

代码如下:

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

const int maxn=35,maxm=20000;

int main(){
  
  
  
  int f[maxm]={0},w[maxn],c[maxn];
  int m,n;
  
  cin>>m>>n;
  for(int i=1;i<=n;++i){
    cin>>w[i]>>c[i];
  }
  
  for(int i=1;i<=n;i++)           //从第一个开始往后选 
    for(int v=m;v>=w[i];v--){     //注意v在这里表示的是背包的剩余容量,所以一定是以从大到小的顺序 进行的 
      if(f[v-w[i]]+c[i]>f[v]){    //状态转移方程 
        f[v]=f[v-w[i]]+c[i];
      }
    }
  
  cout<<f[m];
  
  return 0;


}

三、完全背包问题

例题:信息学奥赛一本通评测网站 P1268 完全背包问题

完全背包不同于01背包的只是完全背包中,每种物品都有无限件可以用。那么问题就不是选不选,而是每种物品选几件的问题了。
题解代码:

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

const int maxn=35,maxm=20000;

int main(){
  
  

  
  int f[maxm]={0},w[maxn],c[maxn];
  int m,n;
  
  cin>>m>>n;
  for(int i=1;i<=n;++i){
    cin>>w[i]>>c[i];
  }
  
  for(int i=1;i<=n;i++)
    for(int v=w[i];v<=m;v++){
      if(f[v-w[i]]+c[i]>f[v]){
        f[v]=f[v-w[i]]+c[i];
      }
    }
  
  cout<<"max=";
  cout<<f[m];
  
  return 0;


}

我们发现,完全背包问题和多重背包问题,只有一行代码的差异。这个时候,我们就要找01背包问题和完全背包问题的关联。01背包问题中,背包的剩余重量是从大到小推的,因为我们要保证每次的f[v]是要从之前的f[v]或者f[v-w[i]]+c[i]得到的,这也正保证了每种物品最多只装一件。(保证了每次选第i件物品时,绝不会依据一个已经选入过一次i物品的子结果)而对于完全背包,每种物品有无限件,因此在选第i件物品时,可以依据已经选入过任意次i物品的子结果)。因而需要倒过来进行枚举。

四、多重背包问题

例题:信息学奥赛一本通评测网站 P1269 庆功会

多重背包问题有两个解法,第一个是在完全背包的基础上,规定每个物品最多可选几个。
第二个解法是将其化为01背包问题(需要用到二进制的思想)

第一个解法(朴素解法)代码:

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn=6005;	
int n,m,x,y;
int n1=0,t=1;
int v[maxn],w[maxn],s[maxn],f[maxn];
int main(){
	

	cin>>n>>m;
  
  for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
  for(int i=1;i<=n;i++)
    for(int j=m;j>=0;j--)                        //钱从多到少进行枚举 
      for(int k=0;k<=s[i];k++){                  //这里就不同于完全背包了,而是有一个购买数量的限制s[i] 
        if(j-k*v[i]<0) break;                    //买不起                         
	      f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);       //状态转移方程增加了k 
  }
	cout<<f[m];
	return 0;

  
}

第二种解法(二进制优化解法):

```cpp
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;

int main(){
	
  const int maxn=6005;	
  int n,m,x,y,s;
  int n1=0,t=1;
  int v[maxn],w[maxn],f[maxn]={0};

  
  cin>>n>>m;
  
  for(int i=1;i<=n;i++){
    cin>>x>>y>>s;                       //针对每一种商品,我们将其进行拆分 
    t=1;
    while(s>=t){                        //代表还能买,也就是数量大于1,就还要进行拆分 
      n1++;                             //把n个拆成n1个 
      v[n1]=x*t;
      w[n1]=y*t;
      s-=t;
      t*=2;                             //每件物品最多取的件数s就可以用二进制表示 
    }
    n1++;
    v[n1]=x*s;
    w[n1]=y*s;
  }
//下面就化成了01背包问题 
  for(int i=1;i<=n1;i++)
    for(int v1=m;v1>=v[i];v1--){
      f[v1]=max(f[v1],f[v1-v[i]]+w[i]);
    }

  cout<<f[m];
  
  return 0;

  
}

五、例题——能天使的愿望(dp正解)

原题是牛客挑战赛34的第一题

题解代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define ll long long 
using namespace std;
const int maxn=1500;
const int inf=0x3f3f3f3f;
ll a[maxn][maxn];
ll fei[maxn];
ll vis[maxn];
ll f[maxn];
int n,m,k,y;

int main(){
	
	cin>>n>>m>>k>>y;                            //这里的预处理是把邮费直接加在了价格里 
	for(int i=1;i<=n;i++) cin>>fei[i];
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=m;j++) cin>>a[i][j];
	for(int i=1;i<=n;i++)
	  for(int j=1;j<y;j++) a[i][j]+=fei[i]*j;
  for(int i=1;i<=n;i++){
  	for(int j=m+1;j<=k;j++) a[i][j]=inf;       //为了防止选择不存在的商品 
	}
	f[0]=0; 
//f[x]代表购买x把铳的最小花费 ,可以把这个问题看做是多重背包问题。 
  for(int i=1;i<=k;i++)f[i]=inf;               //首先假设都为无穷大,这样就可以挑选价格比无穷大小的商品 
  for(int i=1;i<=n;i++)                        //在每个商店进行挑选 
	  for(int j=k;j>=1;j--)                      //还可以购买的数量从大到小(就像上一道题里面的拨款金额从大到小一样) 
		  for(int kk=1;kk<=j;kk++)                 //每次购买的数量从小到大枚举,并且不能超过对应商店的最大购买量 
		       f[j]=min(f[j],f[j-kk]+a[i][kk]);    //这里选择的最小花费是不选择kk产品的最小花费和选择kk产品的最小花费 
	cout<<f[k];
	return 0;
}

这个问题是一个典型的dp问题,在思路上很像背包问题,但是背包问题一般是求解最大收益,这个则是求解最小花费,涉及求最小的问题,就一定要预处理,将f数组从1到m的值置为无穷大(实际上是一个很大很大的数)。

六、作者

Bowen
作者水平有限,如有纰漏之处,敬请斧正。
欢迎大家一起学习交流。

发布了50 篇原创文章 · 获赞 7 · 访问量 1150

猜你喜欢

转载自blog.csdn.net/numb_ac/article/details/103227577
今日推荐