多重背包,二进制优化,单调队列优化

                                                               采药
【问题描述】
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。” 如果你是辰辰,你能完成这个任务吗?
【输入文件】

输入文件medic.in的第一行包含两个正整数N,M。M表示总共能够用来采药的时间,N代表山洞里的草药的数目。接下来的N行每行包括两个的整数,分别表示采摘某株草药的时间Ti和这株草药的价值Vi。

【输出文件】
  输出文件为medic.out,仅包含一个整数表示规定时间内可以采到的草药的最大总价值。
【输入样例】
  3 9
 10 10
  8 1
 1 2
【输出样例】
 3
【数据规模和约定】
50%的数据中 N,M ≤ 1000;
100%的数据中 N,M ≤ 100000,Ti,Vi ≤10。


若有n种物品,背包容量为m,物品体积、价值、最大使用次数为v,w,c,则朴素的动规方程为:f[i]=max{f[i-v*k]+w*k} (1<=k<=c)。我们把所有可能达到的体积按照除以当前物品体积v的余数划分为0~v-1,则当余数为k(k∈[0,v-1])时又可以划分为k,k+v,k+2*v…k+j*v…(1<=j<=(m-k)div v)这几种具体的体积,由于对于余数为k时的转移只会发生在以上列举出的几个体积上,所以可以建立关于以上几个体积的单调队列,以便于快速地找到最优决策。但是这里要注意一点,由于这几个决策的体积和价值都不相同,直接没有可比性,所以我们把这些决策的体积统一到为k时比较,统一方法只需要利用体积为k+j*v这一特点,把需要插入队中的f[k+j*v]的价值减去j*w,就是当体积为k时的一个可以用于比较的“参考”价值。可以很容易想到:由于转移时,使用当前物品贡献的那一部分是二者之差,所以这与减掉j*w之前是等效的。

这样,每次求f[k+j*v]时,只需要在队列中找到一个最优的决策f[k+j’*v],使得j-j’<=c即可,剩下的工作就只有维护单调队列了。

核心代码和注释:

procedure insert(x,y:longint);//插入到一个价值单调递减,使用次数单调递增的队列中
 begin
  while (l<=r)and(b[r]<=y) do dec(r);
  inc(r);a[r]:=x;b[r]:=y;
 end;

begin
 readln(n,m);  //n为物品个数、m为背包容量
 for i:=1 to n do
  begin
    read(v,w,c);  //读入当前物品:v为物品体积、w为物品价值、c为物品可用次数

    if m div v<c then c:=m div v;  //最多可使用次数
    for k:=0 to v-1 do  //把所有的体积按除以v的余数划分为0~v-1
     begin
       l:=1;r:=0;  //清空队列
       for j:=0 to (m-k) div v do  //余数为k的又分为k,v+k,2*v+kj*v+k
         begin
          insert(j,f[j*v+k]-j*w);  //等效于把体积统一到k,价值减去j*w,这样比较优劣才有意义
         while a[l]<j-c do inc(l);  //删除次数超过c
          f[j*v+k]:=b[l]+j*w;      //用队列头的值更新f[j*v+k]
         end;
     end;
  end;
  writeln(f[m]);

end.


/*
多重背包问题:第一种为二进制优化 时间复杂度 O(NMlog2S)
             第二种使用单调队列 时间复杂度 O(NM) 
*/ 
//方法一 
#include <cstdio>
#define max(a,b) (a)>(b)?(a):(b)

int dp[100001];
int w[11][11];

int main()
{
  freopen("medic.in","r",stdin);
  freopen("medic.out","w",stdout);
  int n,m;
  scanf("%d%d",&n,&m);
  int a,b;
  for(int i = 0; i < n; ++i)
  {
    scanf("%d%d",&a,&b);
    w[a][b]++;
  }
  for(int i = 1; i < 11; ++i)//多重背包 
  {
    for(int j = 1; j < 11; ++j)
    {
      if(!w[i][j]) continue;
      
      if(w[i][j]*i < m)//01背包 
      {
        int k = 1;
        int count = w[i][j];
        while(k < count)//2进制思想,每次处理一堆
        {
         for(int v = m; v >= k*i; --v)
              dp[v] = max(dp[v],dp[v-k*i]+k*j);
               
         count -= k;
         k *= 2;
        }
        for(int v = m; v >= count*i; --v)
            dp[v] = max(dp[v],dp[v-count*i]+count*j);
      }
     else//完全背包 
     {
       for(int v = 1; v <= m; ++v)
       if(v-i>=0) dp[v] = max(dp[v],dp[v-i]+j);
     }
    }
  }

  int ans = 0;
  for(int i = 0; i <= m; ++i)
   if( ans < dp[i]) ans = dp[i];
   
  printf("%d\n",ans);
  
  return 0;
}

/* 方法2 
#include <cstdio>
#define max(a,b) (a)>(b)?(a):(b)

int f[100001];
int ww[11][11];
int l,r;
int a[100001];
int b[100001];

inline void insert(int x,int y)
{
  while(l<=r && b[r]<=y) r--;
  ++r;
  a[r] = x;
  b[r] = y;
}

int main()
{
  freopen("medic.in","r",stdin);
  freopen("medic.out","w",stdout);
  int N,m;
  scanf("%d%d",&N,&m);//m为背包容量 
  int cc,dd;
  int sum = 0;
  for(int i = 0; i < N; ++i)
  {
    scanf("%d%d",&cc,&dd);
    ww[cc][dd]++;
  }
  for(int i = 1; i < 11; ++i)
   for(int jj = 1; jj < 11; ++jj)
    {
      if(!ww[i][jj]) continue;//处理每件物品 
      int c = i;//物品的耗费 
      int w = jj; //物品价值 
      int n = ww[i][jj];//物品个数 
      if(m/c < n) n = m/c; //最多可使用次数
      for(int k = 0; k <= c-1; ++k)//把所有的体积按除以v的余数划分为0~v-1
      {
        l = 1;r = 0; //清空队列
        for(int j = 0; j <= (m-k)/c; ++j)//余数为k的又分为k,v+k,2*v+k…j*v+k…
        {
          //insert(j,f[j*c+k]-j*w);//等效于把体积统一到k,价值减去j*w,这样比较优劣才有意义
          int x = j;
          int y = f[j*c+k]-j*w;
          while(l<=r && b[r]<=y) r--;
          ++r;
          a[r] = x;
          b[r] = y;
          while(a[l]<(j-n)) ++l; //删除次数超过n的
          f[j*c+k] = b[l]+j*w; //用队列头的值更新f[j*c+k]
        }
      }
    }
  printf("%d\n",f[m]);
  return 0;
}
*/ 



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值