01背包问题+完全背包问题+多重背包问题

一 01背包问题

1.1题目

有N件物品和一个容量为V 的背包。放入第i件物品耗费的空间是Ci,得到 的价值是Wi。

求解将哪些物品装入背包可使价值总和最大。

1.2 基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不 放。

用子问题定义状态:即F[i, v]表示前i件物品恰放入一个容量为v的背包可以 获得的最大价值。

则其状态转移方程便是:

F[i, v] = max { F [i − 1, v], F [i − 1, v − Ci ] + Wi }

对于“将前i件物品放入容量为v的背包 2 中”这个子问题,若只考虑第i件物品的策略(放或不放),

那么就可以转化 为一个只和前i − 1件物品相关的问题。

如果不放第i件物品,那么问题就转化 为“前i − 1件物品放入容量为v的背包中”,价值为F[i − 1, v];

如果放第i件物 品,那么问题就转化为“前i − 1件物品放入剩下的容量为v − Ci的背包中”,

此时能获得的最大价值就是F[i − 1, v − Ci ]再加上通过放入第i件物品获得的价 值Wi。

#include
#include
using namespace std;
const int maxn = 105;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
  int v;//背包容量
  int n;//N件物品
  while(scanf("%d %d",&v,&n) != EOF){
    for(int i = 1;i <= n; i++){
      scanf("%d",&c[i]);
    }
    for(int i = 1;i <= n; i++){
      scanf("%d",&w[i]);
    }
    //动态规划边界问题
    for(int i = 0;i <= n; i++){
      dp[i][0] = dp[0][i] = 0;
    }
    for(int i = 1;i <= n; i++){//枚举物品
      for(int j = 1;j <= v; j++){//枚举背包容量
        if(j < c[i]){   //当前背包容量放不下这件物品
          dp[i][j] = dp[i - 1][j];
        }else{      //当前背包容量可以放下这件物品
          dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
        }
      }
    }
    printf("%d\n",dp[n][v]);
  }
  return 0;
}

1.3 优化空间空间复杂度

以上方法的时间和空间复杂度均为O(V*N),其中时间复杂已经不能再优化,但是空间复杂度可以优化到O(N)。

先考虑上面讲的基本思路是如何实现的,肯定是有一个主循环 i = 1....N,每次算出来二维数组f[i][0....v]的所有值;

如果只用一个数组f[0...V],能不能保证第i次循环结束后 f[v]中表示的就是我们定义的状态 f[i][v]呢?

f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推出来,能否保证在推f[i][[v]时(即在第i次主循环中推f[v]时)能够得到f[i-1][v]和

f[i-1][v-c[i]]的值呢?事实上,这要求在每次循环中我们以v=V......0的顺序推 f[v],这样才能保证推 f[v]时,f[v-c[i]]保存的是状态 f[i-1][v-c[i]] 的值。

#include
#include
#include
using namespace std;
const int maxn = 105;
int dp[maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
  int v;//背包容量
  int n;//N件物品
  while(scanf("%d %d",&v,&n) != EOF){
    for(int i = 0;i < n; i++){
      scanf("%d",&c[i]);
    }
    for(int i = 0;i < n; i++){
      scanf("%d",&w[i]);
    }

    //动态规划边界问题
    memset(dp,0,sizeof(dp));

    for(int i = 0;i < n; i++){//枚举物品
      for(int j = v;j >= c[i]; j--){//枚举背包容量
        //当前背包容量可以放下这件物品
          dp[j] = max(dp[j],dp[j - c[i]] + w[i]);
        }
      }

    printf("%d\n",dp[v]);
  }
  return 0;
}

1.4 初始化的细节问题

求最优解的背包问题中,有两种问法:

1.求恰好装满背包时的最优解。

初始化时,f[0]=0,f[1...v]为-INF(INF=0x3f3f3f3f),这样最终得到的f[N]是一种恰好装满背包的最优解

2.没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设 为0。

初始化的f数组事实上就是在没有任何物品可以放入背包时的合 法状态。

如果要求背包恰好装满,那么此时只有容量为0的背包,可能的价值为0,只能被nothing“恰好 装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−∞了。

如果 背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0, 所以初始时状态的值也就全部为0了。

二 完全背包问题

2.1题目

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i], 价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最 大。

2.2基本思路

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种 物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很 多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最 大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

f[i][v] = max{f[i−1][v−k×c[i]] + k×w[i]}     0 <= k×c[i] <= v

这跟01背包问题一样有O(VN)个状态需要求解,但求解每个状态的时间已经不是常数了,求解 状态f[i][v]的时间是Θ( v / c[i]),总的复杂度可以认为是Θ(V ×∑ V / c[i]),是比较大的。

#include
#include
using namespace std;
const int maxn = 1005;
int dp[maxn][maxn];
int c[maxn],w[maxn];
int main()
{
  int n,v;
  while(scanf("%d %d",&v,&n) != EOF){
    for(int i = 0;i < maxn; i++){
      dp[i][0] = dp[0][i] = c[i] = w[i] = 0;
    }
    for(int i = 1;i <= n; i++){
      scanf("%d",&c[i]);
    }
    for(int i = 1;i <= n; i++){
      scanf("%d",&w[i]);
    }
    for(int i = 1;i <= n; i++){
      for(int j = 1;j <= v; j++){
        dp[i][j] = dp[i - 1][j];
        if(c[i] <= j){
          dp[i][j] = max(dp[i][j],dp[i][j - c[i]] + w[i]);
        }
      }
    }
    printf("%d\n",dp[n][v]);
  }

  return 0;
}

一个简单有效的优化

完全背包问题有一个很简单有效的优化:若两件物品i、 j满 足c[i] <= c[j]且w[i] >= w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可 将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方 法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为 有可能特别设计的数据可以一件物品也去不掉。

首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同 的物品中价值最高的是哪个,可以Θ(V + N)地完成这个优化。

//完全背包
#include
using namespace std;
const int maxn=1e4+10;
int n;//n种物品
int v;//背包容量
int c[maxn];
int w[maxn];
int dp[maxn];
int main()
{
    while(scanf("%d%d",&v,&n)!=EOF){
        for(int i=0;i

三 多重背包问题

3.1 题目

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费 的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

3.2 基本算法

这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略 微一改即可。 因为对于第i种物品有Mi+1种策略:取0件,取1件……取Mi件。令F[i, v]表 示前i种物品恰放入一个容量为v的背包的最大价值,则有状态转移方程:

F[i,v] = max{ F[i − 1, v − k ∗ Ci ] + k ∗ Wi | 0 ≤ k ≤ Mi }       复杂度是O(V * ΣMi)。

3.3 转化为01背包问题

另一种好想好写的基本方法是转化为01背包求解:

方法一:O(V * ΣMi) 

把第i种物品换成Mi件01 背包中的物品,则得到了物品数为ΣMi的01背包问题。直接求解之,复杂度仍 然是O(V * ΣMi)。

#include
#include
using namespace std;
const int maxn = 5005;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
  int v;//背包容量
  int n;//N件物品
  int T;
  scanf("%d",&T);
  while(T--){
        scanf("%d %d",&v,&n);
    int p=1;
    for(int i = 1;i <= n; i++){
        int cost,value,num;
      scanf("%d%d%d",&cost,&value,&num);
    while(num--){
        c[p]=cost;
        w[p]=value;
        p++;
    }
}

    //动态规划边界问题
    for(int i = 0;i <= n; i++){
      dp[i][0] = dp[0][i] = 0;
    }

    for(int i = 1;i <= p; i++){//枚举p件物品
      for(int j = 1;j <= v; j++){//枚举背包容量
        if(j < c[i]){   //当前背包容量放不下这件物品
          dp[i][j] = dp[i - 1][j];
        }else{      //当前背包容量可以放下这件物品
          dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
        }
      }
    }
    printf("%d\n",dp[p-1][v]);
  }
  return 0;
}

方法二:O(V * ΣlogMi) 

但是我们期望将它转化为01背包问题之后,能够像完全背包一样降低复杂 度。 仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问 题中第i种物品可取的每种策略——取0 . . . Mi件——均能等价于取若干件代换 以后的物品。另外,取超过Mi件的策略必不能出现。

方法是:将第i种物品分成若干件01背包中的物品,其中每件物品有一个系 数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数 分别为1, 2, 2 ^2 . . . 2 ^ (k−1) , Mi − 2^ (k ) + 1,且k是满足Mi − 2 ^ k + 1 > 0的最大整数。

例 如,如果Mi为13,则相应的k = 3,这种最多取13件的物品应被分成系数分别 为1, 2, 4, 6的四件物品。 分成的这几件物品的系数和为Mi,表明不可能取多于Mi件的第i种物品。另 外这种方法也能保证对于0 . . . Mi间的每一个整数,均可以用若干个系数的和表 示。这里算法正确性的证明可以分0 . . . 2 ^ (k−1)和2 ^ k . . . Mi两段来分别讨论得出, 这样就将第i种物品分成了O(logMi)种物品,将原问题转化为了复杂度 为O(V * ΣlogMi) 的01背包问题,是很大的改进。

#include
#include
using namespace std;
const int maxn = 5005;
int dp[maxn][maxn];
int c[maxn];//第i件物品耗费的空间是Ci
int w[maxn];//得到的价值是Wi。
int main()
{
  int v;//背包容量
  int n;//N件物品
  int T;
  scanf("%d",&T);
  while(T--){
        scanf("%d %d",&v,&n);
    int p=1;
    for(int i = 1;i <= n; i++){
        int cost,value,num;
      scanf("%d%d%d",&cost,&value,&num);
      int k=1;
    while(num-(k*2)+1>0){
        c[p]=k*cost;
        w[p]=k*value;
        k*=2;
        p++;
    }
        k=(num-k+1);
        c[p]=k*cost;
        w[p]=k*value;
        p++;
}

    //动态规划边界问题
    for(int i = 0;i <= n; i++){
      dp[i][0] = dp[0][i] = 0;
    }

    for(int i = 1;i <= p; i++){//枚举p件物品
      for(int j = 1;j <= v; j++){//枚举背包容量
        if(j < c[i]){   //当前背包容量放不下这件物品
          dp[i][j] = dp[i - 1][j];
        }else{      //当前背包容量可以放下这件物品
          dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - c[i]] + w[i]);
        }
      }
    }
    printf("%d\n",dp[p-1][v]);
  }
  return 0;
}

 

你可能感兴趣的:(动态规划)