“档次法”——用于物品体积分布不均匀的01背包问题的求解方法

问题起因

背包问题是什么,就不在此论述了,一般都用动态规划的方法来求解。

我在网上搜了一圈,发现那些材料讲的算法都好像有点问题,先贴出来链接:

  1. https://www.bilibili.com/video/BV1K4411X766?from=search&seid=13075630596412642079
  2. https://www.cnblogs.com/mfrank/p/10533701.html
  3. https://baike.baidu.com/item/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98

其它的我就不列举了,就是百度一搜就能搜到的方法,他们都是一样的,核心都是一个矩阵。

原题举例

我们就拿第一个B站链接的例子来说事,原题是这样:

  • 背包容积为8
  • 编号体积价值
    123
    234
    345
    456

然后画了个5*9的矩阵:

问题变形

这样做确实是对的,没问题,OK,我们给题目变换一下:

  • 背包容积为80
  • 编号体积价值
    1203
    2304
    3405
    4506

那现在,难到你要画一个5*81的矩阵吗?

OK,或许有人会说,那容积、体积都除以一个公约数就好了,那我再变形一下:

  • 背包容积为80
  • 编号体积价值
    1203
    2304
    3415
    4506

(手动滑稽😆)现在没有公约数了吧!

其实目前,还是能算出来的,只是慢一些,如果体积和容积的取值不再是离散的整数,而是在整个实数域上取值,这方法不就彻底失效了吗?

我的方法——“档次法”

我的方法原理其实非常简单,想必跟着我提问思路走过来的读者肯定都能想到。

我们会发现,优化的核心就是,发现最优策略并不是随着背包容积连续变化的,而是阶段性、跳跃变化的,呈现出一种“档次”的感觉。

代价(容积)收益(价值)方案
00/
203拿1
304拿2
507拿1,2
…(中间的省略)
14118全拿

所以,我们求解的时候,不是去直接去找最优组合这样一个最优解,而是反过来,先列一列这么些个东西,能搞出多少个档次,然后,对于任意给定的容积值V,只要找比V小的最大档次对应的方案,就是最优方案了。

档次表的前两列,其实就对应一个分段常值函数,我们就叫他“档次函数”吧!档次函数是一个从代价到收益的函数 f : R → R f:R \rightarrow R f:RR,比如,在上面那个档次表里面:

  • f ( − 1 ) = 0 f(-1) = 0 f(1)=0
  • f ( 20 ) = 3 f(20) = 3 f(20)=3
  • f ( 20.5 ) = 3 f(20.5) = 3 f(20.5)=3
  • f ( 50 ) = 7 f(50) = 7 f(50)=7

具体流程

算法的核心就是构造这样一个档次表,其实非常简单。

档次表初始只有一项:

代价(容积)收益(价值)方案
00/

然后,每来一个新的物品 i i i,设它体积为 x i x_i xi、价值为 y i y_i yi,我们只要看看这个新物品会带来多少个新的档次就行了。

什么是“新的档次”呢?当一个组合的代价 x x x和其对应的收益 y y y满足 f ( x ) < y f(x)<y f(x)<y就是一个新的档次了。如果 f ( x ) ≥ y f(x) \ge y f(x)y,那就意味着这个组合不比原来的好,如果 f ( x ) > y f(x) > y f(x)>y而且 x x x还不是区间端端点,那就意味这个新组合比原来还差了,谁会花更多的钱买更差的东西呢?

如果此时 x x x正好是 f f f那段区间的开始端点,那这种新方案与原方案是一样好的,视实际情况(要不要保留多个最优方案),自行处理。

那每来一个新物品的时候,会产生哪些可能的新选项呢?先说结论:如果现在的档次表为:

代价(容积)收益(价值)方案
0 0 0 0 0 0/
x 1 x_1 x1 y 1 y_1 y1
x 2 x_2 x2 y 2 y_2 y2
x k x_k xk y k y_k yk
x n x_n xn y n y_n yn

方案我就不列了,打公式挺累的。

那么就会产生下面这些可能的新档次:

代价(容积)收益(价值)方案
0 + x 0 + x 0+x 0 + y 0 + y 0+y/
x 1 + x x_1 + x x1+x y 1 + y y_1 + y y1+y
x 2 + x x_2 + x x2+x y 2 + y y_2 + y y2+y
x k + x x_k + x xk+x y k + y y_k + y yk+y
x n + x x_n + x xn+x y n + y y_n + y yn+y

很简单吧!对这里面每个条目判断一下,然后把符合条件的填进表里面就行了。

注意会有这样的情况: x k + x x_k+x xk+x正好等于原表的某一项且 y k + y y_k+y yk+y也更大,这时是要替换掉原来的档次:不仅要加,还要把原来的条目删掉。

不过要想证明这么做是对的要稍微麻烦一点。我们就来一步步捋一下这个算法的操作流程吧!

首先刚开始只有一项:

代价(容积)收益(价值)方案
0 0 0 0 0 0/

加入 i = 1 i=1 i=1后:

代价(容积)收益(价值)方案
0 0 0 0 0 0/
x 1 x_1 x1 y 1 y_1 y1

现在,又来个 i = 2 i=2 i=2的新项,它有以下可能的新档次:

代价(容积)收益(价值)方案
x 2 x_2 x2 y 2 y_2 y2
x 1 + x 2 x_1 + x_2 x1+x2 y 1 + y 2 y_1 + y_2 y1+y2

(补充一句,我们这里就假设所有项的代价和收益都是正数)

x 1 + x 2 x_1 + x_2 x1+x2肯定是大于 x 1 x_1 x1的嘛!它一定是一个新档次,加进去!

但是对于 x 2 x_2 x2,我们只知道它在区间 ( 0 , x 1 + x 2 ) (0, x_1 + x_2) (0,x1+x2)里面,和 x 1 x_1 x1到底谁大是不知道的。如果 x 2 > x 1 x_2 > x_1 x2>x1,那它也不一定是新档次,但是如果我们能先验知道数列 { x i } \{x_i\} {xi} { y i } \{y_i\} {yi}都是严格单调递增的话,那么 x 2 x_2 x2一定也是一个新档次了。

我们可通过一个排序预处理的步骤来保证严格单调递增

接下来,我们假设,很不巧地, x 2 x_2 x2档次性价比不行,没有被添加进表中,那么我们就得到了新档次表:

代价(容积)收益(价值)方案
0 0 0 0 0 0/
x 1 x_1 x1 y 1 y_1 y1
x 1 + x 2 x_1 + x_2 x1+x2 y 1 + y 2 y_1 + y_2 y1+y2

现在,又来了 x 3 x_3 x3,我们继续以上操作,它可能带来新档次:

代价(容积)收益(价值)方案
x 3 x_3 x3 y 3 y_3 y3
x 1 + x 3 x_1 + x_3 x1+x3 y 1 + y 3 y_1 + y_3 y1+y3
x 1 + x 2 + x 3 x_1 + x_2 + x_3 x1+x2+x3 y 1 + y 2 + y 3 y_1 + y_2 + y_3 y1+y2+y3

x 1 + x 2 + x 3 x_1 + x_2 + x_3 x1+x2+x3毫无疑问地,又是一个新档次,如果数列严格单调递增,那么 x 1 + x 3 x_1 + x_3 x1+x3也毫无疑问是一个新档次(我就不详细解释了,对照新档次的那条要求就行了)。

注意!这里面没有 x 2 + x 3 x_2 + x_3 x2+x3这一项,因为 x 2 x_2 x2这一项在我们之前构造的时候因为不满足要求没加进表,记得吗?那么问题来了:这种省略会不会导致错误?

当然是不会啦(我都想过了)!因为 y 2 + y 3 y_2 + y_3 y2+y3一定小于 y 1 + y 3 y_1 + y_3 y1+y3 x 2 + x 3 x_2 + x_3 x2+x3一定大于等于 x 1 + x 3 x_1 + x_3 x1+x3。( y 2 < y 1 ∧ x 2 ≥ y 1 y_2 < y_1 \wedge x_2 \ge y_1 y2<y1x2y1正是我们没把 y 2 y_2 y2添加进去的原因)

所以上面那么做就是对的了,算法的操作就讲完了。

复杂度分析

我这里就分析一下最坏情况吧!最坏情况就是档次最多的情况(也许这是实际中的“最好”情况)。

空间上,每加入 i = k i=k i=k的新项,就会多出 2 k − 1 2^{k-1} 2k1个新条目,做个累加:

O ( ∑ k = 1 n 2 k − 1 ) = O ( 2 n ) O(\sum_{k=1}^{n}2^{k-1}) = O(2^n) O(k=1n2k1)=O(2n)

时间上,每个新条目,我们都要判断它是不是能加进档次表,这一步判断的复杂度取决于我们用个什么数据结构存储档次表,我们就用最适合这个任务的红黑树(或者其它的平衡二叉搜索树)来存储档次表,每加入 i = k i=k i=k的新项时,对每一项的判断,都要花费 O ( lg ⁡ 2 k − 1 ) = O ( k ) O(\lg{2^{k-1}}) = O(k) O(lg2k1)=O(k)的代价,那么总代价就是:

O ( ∑ k = 1 n lg ⁡ 2 k − 1 ∗ 2 k − 1 ) = O ( ∑ k = 1 n k ∗ 2 k − 1 ) = ? O(\sum_{k=1}^{n}\lg{2^{k-1}}*2^{k-1}) = O(\sum_{k=1}^{n}k*2^{k-1}) = ? O(k=1nlg2k12k1)=O(k=1nk2k1)=?

后面我懒得化简了,有大佬路过帮我求下……

上限约束

上一节算出的复杂度好大呀,好像比原算法low上好多?要注意上面我们得出的档次表只是针对物品列表得出的,没有考虑我们的背包大小。这样做的好处是,我们得到的是一个通用的档次表。

如果对于一个特定的问题,背包大小已经确定了,那么我们在构造档次表的时候,就不必求出超出背包大小的档次了。在加入 i i i项的时候,如果算到 x k + x i x_k + x_i xk+xi时就发现已经大于背包大小了,从 x k + 1 x_{k+1} xk+1往后的就统统不用考虑了,因为预算表是严格递增的。

毫无疑问,这样做之后,算法真正的开销不会比最开始介绍的传统方法更高。

代码实现

下面是我用python实现的例子。

数据结构是不可能自己写的啦,用的现成库。

这里实现的是一个比较老的版本。刚开始设计这个算法时我以为必须要先排个序保证数列是严格单调递增的,然后筛出反常点作为次等选项,搞个二阶档次表,后来写这篇博客时发现不这么搞也能直接输出总的档次表……

不过这么做的确可以通过减小单个计算中的 n n n来减少时间开销的作用,具体效果要看数列的分布了(总时间复杂度应该不会变)

"""档次算法
"""
from math import inf
from typing import List, Tuple

from sortedcontainers import SortedDict


class GradeTable:
    def __init__(self, path0=[]):
        self.table = SortedDict()
        self.table[0] = 0, path0
        return

    def decide(self, m):
        i = self.table.bisect_right(m) - 1
        return self.table.items()[i]

    def add_grade(self, cost, gain, path=[]):
        i = self.table.bisect_right(cost) - 1
        g, p = self.table.values()[i]
        if g >= gain:
            return False  # 现有的比新档次还好
        self.table[cost] = gain, path
        return True

    def __repr__(self):
        return repr(self.table)


Options_t = List[Tuple[int, int]]  # 成本, 收益


def filter_inferior(options: Options_t):
    options.sort(key=lambda x: (x[0], -x[1]))
    superior = []
    inferior = []
    max_x = 0
    max_y = 0
    for x, y in options:
        if x > max_x:
            max_x = x
            if y > max_y:
                max_y = y
                superior.append((x, y))
            else:
                inferior.append((x, y))
        else:
            if y == max_y:
                superior.append((x, y))
            else:
                inferior.append((x, y))
    return superior, inferior


def build_grade_table(superior: Options_t, budget=inf):
    gt = GradeTable()
    for x, y in superior:
        buf = []
        for x_, (y_, p_) in gt.table.items():
            cost = x + x_
            if cost > budget:
                break
            buf.append((cost, y + y_, p_ + [x]))
        for i in buf:
            gt.add_grade(*i)
    return gt


def build_multi_grade_table(options: Options_t, budget=inf):
    superior, inferior = filter_inferior(options)
    gt = build_grade_table(superior, budget)
    ret = [gt]
    while inferior:
        superior, inferior = filter_inferior(inferior)
        gt = build_grade_table(superior, budget)
        ret.append(gt)
    return ret


ret = build_multi_grade_table([
    [10, 1],
    [10, 1],
    [10, 1],
    [20, 3],
    [20, 2],
    [20, 1],
    [30, 3],
    [30, 4],
    [40, 5],
    [40, 6],
    [70, 9],
    [70, 9],
    [80, 10],
    [100, 12],
    [90, 10],
    [50, 8],
])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值