背包问题详解

目录

1. 问题引入
---> 1.1 找零钱问题
---> 1.2 动态规划
2. 背包问题描述及详解
---> 2.1 01背包及代码实现
---> 2.2 分数背包(Fractional Knapsack)及代码实现

问题引入

在介绍背包问题之前,我们先来看一个小问题:找零钱问题。

找零钱问题

背包问题的一个浅显版本是找零钱问题。描述如下[1]:
如果某人欠了我们43.68美元,结果他还给我们了一张100美元,你手里的钱从100美元到1美分数量不等,这时候你要怎么找给他?怎么拿纸币给他比较合适?

实现代码(Python 3)

#!/usr/bin/env pypy3
# -*- coding: utf-8 -*-

_all = [10000, 5000, 2000, 1000, 500, 200, 100, 50, 25, 10, 5, 1] # 这里以美分为单位。1美元 = 100美分
owed = 10000 - 4368 # 转换成分单位之后就行
payed = list() # 在payed数组中存储换币的单位
for i in _all: # 从最大的币值开始遍历
    while (owed >= i): 
        owed -= i # 从大到小找钱
        payed.append(i) # 找的钱放到payed数组中

if __name__ == "__main__":
    print(payed)
    print(sum(payed))

C++实现

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

int main(void){
    int _all [] =  {10000, 5000, 2000, 1000, 500, 200, 100, 50, 25, 10, 5, 1}; // 这里以美分为单位
    int owed = 10000 - 4368; // = 5632
    vector<int> payed; // 初始化一个空向量
    int sum_; // 定义后面的和
    for(auto it: _all){ // 遍历数组
        while (owed >= it){
            owed -= it;
            payed.push_back(it)
        }
    }
    for (int i = 0; i <size(payed); i++){
        printf("%d", payed[i]);
        sum_ += payed[i];
    }
    printf("\n");
    printf("%d", sum_);
    return 0;
}

动态规划

在正式讲背包问题前,先要熟悉一下动态规划的概念。

由于递归会导致大量的重复计算,所以是否可以有一种方式对其进行优化呢?答案就是动态规划。动态规划的要旨是不要重复计算自己,换句话说,将已经计算的结果存储起来,以日后需要的时候不用再重复调用。

这样一来,算法复杂度便从指数级降到了线性级

下面以Fibonicci数列的两种解法为例:

法一:递归

Python实现:

#!/usr/bin/env pypy3
# -*- coding: utf-8 -*-

def fib(n):
    if (n <= 1):
        return n
    else:
        return fib(n-2) + fib (n-1)

C/C++实现

#include <cstdio>
using namespace std;

int fib(int n){
    if (n <= 1){
        return n;
    }
    else{
        f = fib(n - 2) + fib(n - 1);
        return f;
    }
}

法二:DP

Python实现:

#!/usr/bin/env pypy3
# -*- coding: utf-8 -*-

def fib(n):
    f = [0 for i in range(n+1)] 
    # 由于是用Python写代码,无法像C/C++那样预先指定数组大小
    # 所以先造一个全为0,大小为n的数组f存每次计算后的结果
    # 造出数组后就不需要重复计算了
    f[0] = 0
    f[1] = 1
    for i in range(n+1):
        f[i] = f[i - 1] + f[i - 2]
    return f[n]

C/C++实现

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

int f[100000]; // 指定一个足够大的数组

int fib(int n){
    f[0] = 0;
    f[1] = 1;
    for (int i = 2; i <= n; i++){
        f[i] = f[i - 2] + f[i - 1];
    }
    return f[n];
}

背包问题描述及详解

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

这是一个经典的动态规划问题。一般将背包问题分为这么几种:01背包问题分数背包问题完全背包问题多重背包问题。下面分别讨论。

01背包

01背包问题是背包问题中的基础。它的特点是:每种物品只有一件,可以选择放与不放。用比较抽象的语言表示是,有n件物品,每件物品的重量是w,价值为v,背包的容量为W。

根据如上信息,我们可以得到需要的变量如下:

变量名 类型 大小 作用
n int 依实际情况而定 物品数
c int 依实际情况而定 背包容量
w int array n 每个物品的重量
v int array n 每个物品的价值

根据动态规划的思想,我们可以把问题拆成一个一个子集来解决。试想,是不是可以考虑拆成背包重量为[1, W]的子集呢(思路1)?换成大白话说,就是是不是可以考虑为如果背包重量为1时,能达到的最大价值是多少?如果背包重量为2时,能达到的最大价值是多少?如果背包重量为3呢?

有了上面的考虑方法后,我们还应该想想是不是可以从另一个角度再考虑一下?

另一个考虑问题的角度是,“将前i件物品放入容量为v的背包中”(思路2)。如果只考虑前i件物品,那么,根据动态规划又可以转化为“将前i - 1件物品放入背包中”。第i件物品的策略可以是放或不放。如果不放,问题则转化为“将前i - 1件物品放入容量为v的背包中”;如果放,问题转化为“前i-1件物品放入剩下的容量为v-w[i]的背包中”。写成状态转移方程,则是:

m[i][j] = max(m[i - 1][j], m[i - 1][j - w(i)] + v[i])
"""
说明:
- i : 思路2中第i个物品
- j : 思路1中背包重量
- m: Memoisation集合
"""

上式的意思是:前i个东西放进容量为j的背包中,能获得的最大价值是多少?是不放第i个物品呢,还是放第二个物品呢?

有了如上思路后,我们可以写代码如下(用c++实现):

//#include <bits/stdc++.h> //万能头文件
#include <array>
#include <algorighm>
#include <cstdio>
#include <vector>
using namespace std;

int m[1005][1005]; // 定义一个足够大的全局二维数组,用于动态规划

/* 参数说明:
- vector<int> w: 该向量用于存储每个物品的重量
- vector<int> v: 该向量用于存储每个物品的价值
- int c: 背包的容量
*/
int knapsack01(vector<int> w, vector<int> v, int c){
    int n; // 由于每样物品只有一个
    n = w.size(); // 所以题目中所需的n可以通过求任意一个向量的长度而获得
    for(int i = 0; i < n; i++){ // 动态规划开始。i表示第i件物品
        for(int j = 0; j <= w; j++){ // j表示思路1中背包重量的子集
            if (i == 0 || j == 0){ // 第0个或背包重量为0时,直接赋值为0
                m[i][j] = 0;
            }
            else if(w[i - 1] <= j){ // 第i个物品如果可以放到背包中的话
                m[i][j] = max(m[i-1][j], (m[i-1][j-w[i - 1]] + v[i - 1]));
            } // 最大值在放与不放中挑选
            else{ // 如果物品过重,不放
                m[i][j] = m[i - 1][j];
            }
        }
    }
    return m[n][c];
}
#!/usr/bin/env python3
# encoding: utf-8

def knapsack01(w, v, c):
    """
    :param w: 每种物品重量的数组
    :param v: 每种物品价值的数组
    :param c: 背包的重量
    :return: 答案
    """
    n = len(w) # n可以直接通过求任一数组长度求得
    m = [[0 for i in range(c + 2)] for j in range (n + 2)] # 构造一个二维数组用于存储动态规划的结果
    for i in range(n + 1): # 动态规划开始,i代表第i个物品
        for j in range(c + 1): # j 代表思路1中的重量子集
            if (i == 0) or (j == 0): #第0个直接给0
                m[i][j] = 0
            elif (w[i - 1] <= j): # 如果能放进去
                m[i][j] = max(m[i-1][j], (m[i - 1][j - w[i - 1]]) + v[i - 1]) #结果从放与不放中挑选
            else: #如果放不进去
                m[i][j] = m[i - 1][j] #就不要放
    
    return m[n][c] #返回我们要求的答案

分数背包

预设条件与01背包相同。与01背包的不同点在于,它可以将物品拆开放。比如,放一半的物品A,2/3的物品B。

在分数背包问题中,我们要打破01背包时“放”与“不放”二分选择的思维惯性,而应这样思考:

暴力解法是将所有可能性比较一遍,然后取最大。但是这样做的复杂度极高,至少是指数级O(n 2 )。

那么是否有更优解?答案是有的。

大家购物时都会考虑性价比,是不是?那么,分数背包问题也可以从这个考虑。我们可以把所有东西的性价比算出来,然后将物品依性价比排序,由高到低,一个一个地放。如果物品放不进去过重放不进去,那么可以放一部分进去,直到放满为止

那么,复杂度便降到了O(n*logn).

简单吗?对!用贪心算法就可以了!

代码实现如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

class Items:
    def __init__(self, w, v):
        self.weight = w
        self.value = v
        self.ppr = w / v # 'ppr' means Price-Performance Ratio

def getAnswer(items, wk):
    '''
    :param items: a list which contains some Items
    :param wk: the weight of a knapsack
    return: the biggest value
    '''
    items.sort(key = lambda a: a.ppr, reverse = True)
    n = len(items) # We can gain "n" through the length of the list "items".
    bv = 0.0 # Define the biggest value.
    cw = 0 # Define the "current weight".
    for i in range(n):
        if ((cw + items[i].weight) <= wk):
            bv += items[i].value
        else:
            bv += items[i].value * ((k - cw) / items[i].weight)
    return bv

if __name__ == '__main__':
    a = int(input("需要几个背包?"))
    for i in range(a):
        items = list() # items用于表示物品
        k = eval(input("第{}个背包的重量是:".format(i + 1)))
        n = int(input("请输入物品个数:"))
        print("先输重量,再输价值")
        for j in range(n):
            w, v = map(int, input().split()) # w:重量,v价值
            an_item = Items(w, v)
            items.append(an_item)
        # J循环结束后就可以开始获取答案了
        val = getAnswer(items, k)
        print(val, "\n")

References

  1. Hetland M L. Python Algorithms: mastering basic algorithms in the Python Language[M]. Apress, 2014.
  2. 背包问题九讲
  3. 动态规划(geeksforgeeks)
  4. 01背包问题吐血详解
  5. 背包问题详解:01背包、完全背包、多重背包
  6. 0-1 Knapsack Problem | DP-10
  7. Fractional Knapsack Problem
  8. 01背包和部分背包问题分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容