背包问题

来源:互联网 发布:福利软件下载 编辑:程序博客网 时间:2024/06/02 21:18

  • 概述
    • 问题抽象
    • 问题变形
    • 一般描述
  • 0-1背包问题
    • 案例
      • 数学符号化
      • 状态转移方程
      • 根据状态转移方程 进行编程
  • 适用于贪心算法的背包问题
    • 部分背包问题
    • 案例
    • 分析 及 解答
      • 为何适合使用贪心算法
      • 形式化表示
      • 核心代码实现

概述

背包问题(Knapsack problem)是一种组合优化问题。
背包问题十分经典,是因为很多实际的问题、典型的问题,都可以通过背包问题通俗的表达出来。也就是说,很多实际问题中你都能看到背包问题的影子。(背包问题,可以看做一类问题的模型)

问题抽象

在组合优化问题中,在有限的约束条件中寻找最优的目标

在背包问题中的约束条件:给定物品的特点、给定背包的限制。
背包问题的目标:最小的物品的数量、最大化物品的价值等等。

问题变形

前面我们把背包问题进行了切分,从大的角度来说,我们将背包问题切分成了约束条件和目标。而将约束条件和目标进行改变时,变可以得到很多背包问题的变形。
比如:

约束条件
给定物品的特点
可拆分的 & 不可拆分的
等等
宝贝的特点
有限的 & 无限的
等等

目标
背包装最多的重量。
背包装最多的价值。
等等

多样的问题变形
然后我们发现,把上面的约束条件与目标组合后,能够产生大量的背包问题的变形。(这也说明为何背包问题如此经典)

一般描述

问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高

需要说明

  • 一类问题】上面的一般表述,是较为抽象的,因为要概括这一类问题。为直观理解所以就抽象到该层次,如果继续抽象的话,“重量”和“价格”也可以抽象为一类事物,即“物品的属性”。
  • 目标:最优化】我们有一个目标(最大化或最小化),根据现有资源去决策,进而达到我们想要实现的目的。(需要说明的是,可行解可能有很多,但最优解一般是少数的,所以我们的目标可以表述为:在可行解空间中,找到最优解
  • 问题具体化】因为上面关于背包问题的描述是一般问题,因此我们通过添加一些限制条件,使得背包问题具体化。(后面问题需要和问题一般描述相联系,更易系统理解

0-1背包问题

在一般描述的基础上,添加限制条件

  • 唯一性】每个物品具有唯一性
  • 放入 & 不放入】每个物品或者整体放入背包中(0表示),或者不放入不放入背包中(1表示)。不存在只放入部分的情况(0< x < 1)。

    PS : 如果将物品换成几袋大米,再进行分配时,就打破了0,1的限制,因为大米是可以放进部分的)

案例

有编号为a、b、c、d、e的5件物品,他们的重量分别是2、2、6、5、4,他们的价值分别是6、3、5、4、6,现在给你一个承重为10的背包,如何让背包里装的物品拥有最大的价值??

数学符号化

n 代表东西的数量、c 代表包的最大承重、Wi 是第i件物品的重量、Pi 是第i件物品的价值、F[i,j] 是最大价值(i个物品放入承重j的包)。

说明

  • n , c , Wi , Pi 构成限制条件(约束)。
  • F[i, j] 中则记录在个阶段取得最大值。(i , j 表示状态,i表示物品的状态,j表示背包的状态)

状态转移方程

F[i][j]=max(F[i1][j],F[i1][jwi]+Pi)
说明:

  • 假设F[i][j]已经是最大的了。
  • 如果要求F[i,j]的最大值,则要看这两种 状态
    • 第一种情况(不带):F[i1,j]
    • 第二种情况(带):F[i1,jWi]+Pi(j>=Wi) (在背包中给第i个物品预留空间Wi,剩下的i-1件物品重新摆放,最后在加上第i个物品的价值Pi)

根据状态转移方程 进行编程

根据状态转移方程,我们很容易写出递归的解法。
方法1:(递归 + 未使用数组记忆)

Int  rec( int i , int j){ int res;    if(i==-1){          res=0;    //表明物品已经放完了。  }else if(j<w[i]){    res=rec(i-1,j);//此时包内的空间不够放下第i个物品。  }else{ // 进行比较,如果放入物品的价值比原来已经放入物品的总价值高,则替换原来的     res=max(rec(i-1,j),rec(i-1,j-w[i])+v[i]); }    dp[i][j]=res;  // TODO 可通过该数组记忆,空间换时间    return dp[i][j];    }

说明:(针对关键算法

  • 为何可使用一维数组?】将记录第i个物品的情况通过内循环的方式进行了替换
  • 外层循环】外层一次循环表示的是,考虑第 i 个物品后的,更新过去的未考虑i的情况(针对 j >= w[i] 的情况,内层循环
  • 为何内层循环倒序?】外层的循环,第 i 次循环依赖于 第 i-1 次循环的结果,而不应该 第 i 次 循环更新之后的结果,否则会出现重复加第i个物品的要求,这就不符合了0-1背包的问题。
    • 举个简单的例子,dp[j]=max(dp[jw[i]]+val[i],dp[j]),假如 dp[jw[i]]+val[i]>dp[i] (表示需要更新上一次循环造成的影响,替换上一次的结果,而这一次则将价值未val[i]的第i个物品加入了这一种情况 )。但是存在另一个问题,在dp[j]=dp[jw[i]]+val[i]中你不能确定是否dp[jw[i]]在这之前是否已经加入了第i个物品(如果这个dp[j - w[i]]先发生,那么可能就已经加入第i个物品)。
    • 所以针对上面的问题,通过逆序的方式(背包容量大的情况依赖于 背包容量小的情况,反之,则小不会依赖大,这个特点使得我们通过逆序的方式只加入一次)

参考算法来自:背包问题之01背包
方法2:(一维滚动数组)

#include <iostream>  using namespace std;  int w[5], val[5];  int dp[10]; //表示背包容量为i时的最大值,初始化为0.  int main()  {     int t, m, res=-1;     cin >> t >> m;     for(int i=1; i<=m; i++)     {         cin >> w[i] >> val[i];     }     //关键算法(滚动数组)     for(int i=1; i<=m; i++) //物品          for(int j=t; j>=0; j--) //容量,逆序         {             if(j >= w[i])                 dp[j] = max(dp[j-w[i]]+val[i], dp[j]);         }     cout << dp[t] << endl;     return 0; }

参考:
01背包问题-通俗易懂
【PPT】0-1背包问题之动态规划法

适用于贪心算法的背包问题

部分背包问题

与0-1背包问题类似,所不同的是在选择物品i装入背包时可以选择物品i的一部分,而不一定要全部装入背包。

案例

更改前一个0-1背包问题】有编号为a、b、c、d、e的5袋大米,他们的重量分别是2、2、6、5、4,他们的价值分别是6、3、5、4、6,现在给你一个承重为10的背包,如何让背包里装的物品拥有最大的价值??
每袋大米可部分装入背包之中

分析 及 解答

为何适合使用贪心算法?

背包的总容量是有限的(为10),假如说最后一定能够把背包装满,那么我们 可以得到如下公式:

=

=/



使

形式化表示

n 代表东西的数量、c 代表包的最大承重、(将物品按照 平均价值(W/P、)从大到小排序,第i个物品表示是平均价值第i大的),Wi 是第i件物品的重量、Pi 是第i件物品的价值、F(i) 是最大价值(前i个物品放入包中)。res(i)表示剩余的背包空间。
在此基础上可形式化为:

res(i1)>WiF(i)=F(i1)+Pi

res(i1)<Wi,F(i)=F(i1)+(Pi/Wi)res(i1)

核心代码实现

//w[5],p[5]已经排好了序,下标越小,平均价值值越大。(5表示物体数量)//res表示背包剩余的最大承重。(初始为10)double results = 0for(int i = 0;i < 5 ; i++){if(w[i] <= res){    results += p[i];    }else{    results += p[i] / w[i] * res    }    //results即为所求}

更新中…

原创粉丝点击