算法设计技巧: Rounding

本文介绍一种近似算法的设计技巧: Rounding. 具体来说, 它有两种实现思路:

  1. Rounding solution. 以整数规划为例, 可以先求其线性规划松弛解, 然后对解进行四舍五入(同时保证可行性), 从而得到整数解.
  2. Rounding instance. 修改原问题实例然后求解, 同时保证解的可行性(对原问题而言). 修改实例的目的是使得问题更容易求解.

背包问题

考虑 n n n个商品, 它们的大小为 w 1 , w 2 , … , w n ∈ N + w_1, w_2, \ldots, w_n \in \mathbb{N}^+ w1,w2,,wnN+, 价值为 p 1 , p 2 , … , p n ∈ N + p_1, p_2, \ldots, p_n \in \mathbb{N}^+ p1,p2,,pnN+. 给定一个大小为 W W W的背包, 我们需要找到一组商品装入背包中(商品总大小不超过 W W W)使得装入背包的商品总价值达到最大.

设决策变量 x i ∈ { 0 , 1 } x_i\in \{0,1\} xi{0,1}代表商品 i i i是否被装入背包中. 背包问题可以写成如下整数规划:
max ⁡ ∑ i p i x i s.t.  ∑ i = 1 n w i x i ≤ W x i ∈ { 0 , 1 } . \begin{aligned} \max & \sum_i p_ix_i \\ \text{s.t. } & \sum_{i=1}^n w_i x_i \leq W \\ & x_i \in \{0,1\}. \end{aligned} maxs.t. ipixii=1nwixiWxi{0,1}.

虽然背包问题可以用动态规划求得最优解, 但从时间复杂度上来看它是指数时间的算法. 当问题规模(encoding length of input)非常大时, 我们可以考虑对它进行近似地求解. 下面从Rounding的角度介绍几种近似求解的思路.

LP-Rounding

考虑上述规划的松弛问题(Linear programming relaxation):
max ⁡ ∑ i p i x i s.t.  ∑ i = 1 n w i x i ≤ W 0 ≤ x i ≤ 1. \begin{aligned} \max & \sum_i p_ix_i \\ \text{s.t. } & \sum_{i=1}^n w_i x_i \leq W \\ & 0 \leq x_i \leq 1. \end{aligned} maxs.t. ipixii=1nwixiW0xi1.

x = ( x 1 , x 2 , … , x n ) T x = (x_1, x_2, \ldots, x_n)^T x=(x1,x2,,xn)T代表松弛问题的最优解. 如果 x x x是整数解则直接输出, 否则需要把它Rounding成整数解.

Lemma. 存在松弛问题的最优解 x x x,它最多包含一个非整数的分量.

证明. 假设存在两个变量 x i , x j ∈ ( 0 , 1 ) x_i, x_j\in (0,1) xi,xj(0,1). 不失一般性, 不妨假设 p i / w i ≥ p j / w j p_i/w_i \geq p_j/w_j pi/wipj/wj. 增大 x i x_i xi的同时减小 x j x_j xj直到 x i = 1 x_i = 1 xi=1 x j x_j xj等于0.

因此, 我们得到如下算法:

  1. 计算松弛问题的最优解 x x x;
  2. 保证 x x x最多包含一个非整数分量 x k x_k xk(单纯形算法直接保证), 然后令 x k = 0 x_k=0 xk=0;
  3. W ′ = W − ∑ i w i W' = W - \sum_{i}w_i W=Wiwi代表背包剩余的空间, 从剩下的商品中挑选一个价值最大且大小不超过 W ′ W' W的商品加入背包.

Python实现

from ortools.linear_solver import pywraplp


class KnapsackLPRounding(object):
    """ 背包问题LP Rounding(近似)解法.
    """

    def __init__(self, w, p, W):
        """
        :param w: 物品大小, list
        :param p: 物品价值, list
        :param W: 背包大小, int
        """
        self._w = w
        self._p = p
        self._W = W
        self._n = len(self._w)
        self._result = None

    def _solve_lp(self):
        solver = pywraplp.Solver('MasterModel', pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        n = len(self._w)
        # 决策变量
        x = [solver.NumVar(0, 1, 'x[%d]' % i) for i in range(n)]
        # 约束
        ct = solver.Constraint(0, self._W)
        for i in range(n):
            ct.SetCoefficient(x[i], self._w[i])
        # 目标
        obj = solver.Objective()
        for i in range(n):
            obj.SetCoefficient(x[i], self._p[i])
        obj.SetMaximization()
        # 求解
        solver.Solve()
        # 得到计算结果
        return [x[i].solution_value() for i in range(n)]

    def _rounding(self, x):
        """ 对分数解x取整, 然后从剩余商品中挑一个价值最大的商品装入背包(如果可行)
        """
        sol = [i for i in range(len(x)) if abs(x[i] - 1) < 1e-6]
        # 背包剩余的空间
        available_space = self._W - sum([self._w[i] for i in sol])
        # 剩下的物品
        left_over = set(range(self._n)) - set(sol)
        # 剩下物品的价值
        left_over_profits = [self._p[i] for i in left_over]
        # 按从大到小排序
        left_over_items = list(sorted(zip(left_over, left_over_profits), key=lambda item: item[1], reverse=True))
        left_over = [item[0] for item in left_over_items if self._w[item[0]] <= available_space]

        return sol + [left_over[0]] if left_over else sol

    def solve(self):
        # 求解背包问题的松弛解
        x = self._solve_lp()
        # Rounding solution
        self._result = self._rounding(x)
        return self

完整代码

PTAS

PTAS的全称是Polynomial Time Approximation Scheme. 给定参数 ϵ > 0 \epsilon>0 ϵ>0, 它是多项式时间的 ( 1 + ϵ ) (1+\epsilon) (1+ϵ)近似算法, 即算法对应的目标函数值与最优目标函数值之差不超过最优值的 ϵ \epsilon ϵ倍.

算法-k

  1. 枚举所有不超过 k k k个商品的集合 S \mathcal{S} S, 共有 O ( k n k ) O(kn^k) O(knk)种情况.
  2. S \mathcal{S} S中选择价值最大的集合 S ∗ ∈ S S^*\in \mathcal{S} SS.
  3. 用贪心算法填充剩下的空间.

评估

设OPT代表最优解, ALG代表算法解. 可以证明1: OPT / ALG ≤ 1 + 1 / k \text{OPT} / \text{ALG} \leq 1+1/k OPT/ALG1+1/k. 时间复杂度是 O ( k n k + 1 ) O(kn^{k+1}) O(knk+1). 由于 k k k是常数, 该算法是多项式时间. 当参数 k k k越大, 算法解与最优解越相近, 但时间复杂度越高. 时间复杂度的增长是关于 k k k的指数函数.

Python实现

枚举算法的实现. 分成两个函数考虑

  • choose_exact(n, k): 从 n n n个物品里选 k k k个, 枚举所有情况.
  • choose_at_most(n, k): 从 n n n个物品里选至少 1 1 1个至多 k k k个, 枚举所有情况.
def choose_exact(n, k):
    """
    从n个物品[0, 1, ... n-1]里选择k个, 枚举所有情况. 例如:
    >>> choose_exact(4, 2)
    [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
    >>> choose_exact(3, 3)
    [[0, 1, 2]]
    """
    if k == 1:
        return [[i] for i in range(n)]
    res_mid = choose_exact(n, k - 1)
    result = []
    for p in res_mid:
        for i in range(max(p)+1, n):
            result.append(p + [i])
    return result


def choose_at_most(n, k):
    """ 从n个物品[0, 1, ..., n-1]里选择至少1个至多k个, 枚举所有情况.
    """
    result = []
    for i in range(1, k+1):
        result += choose_exact(n, i)
    return result

背包问题的PTAS.

class KnapsackPTAS(object):
    """
    背包问题的PTAS.
    近似比 = (1+1/k), 计算时间复杂度 = O(kn^{k+1}).
    """

    def __init__(self, w, p, W):
        """
        :param w: 物品大小, list
        :param p: 物品价值, list
        :param W: 背包大小, int
        """
        self._w = w
        self._p = p
        self._W = W
        self._n = len(self._w)
        self._result = None

    def solve(self, k):
        # step1. 枚举所有不超过k个物品的解, 取价值最大的可行解
        solutions = choose_at_most(self._n, k)
        max_sol = None
        max_p = 0
        for sol in solutions:
            if sum([self._w[i] for i in sol]) > self._W:
                continue
            p = sum([self._p[i] for i in sol])
            if p > max_p:
                max_p = p
                max_sol = sol
        # step2. 背包的剩余空间用贪心算法填充
        self._result = self._greedy(max_sol)
        return self

    def _greedy(self, sol):
        """
        给定可行解sol, 把背包剩余的空间用贪心算法填充.
        """
        
        # 背包剩余的空间
        available_space = self._W - sum([self._w[i] for i in sol])
        new_items = []
        # 剩下的物品
        left_over = set(range(self._n)) - set(sol)
        # 计算剩下物品的性价比
        left_over_values = [self._p[i] / self._w[i] for i in left_over]
        # 按性价比从大到小排序
        left_over_items = sorted(zip(left_over, left_over_values), key=lambda x: x[1], reverse=True)
        left_over = [item[0] for item in left_over_items]
        # 把剩余的物品依次装入背包(如果能装下)
        for i in left_over:
            if self._w[i] <= available_space:
                new_items.append(i)
                available_space -= self._w[i]

        return sol + new_items

完整代码

FPTAS

FPTAS的全称是Fully Polynomial Time Approximation Scheme. 给定 ϵ > 0 \epsilon>0 ϵ>0, 算法解ALG与最优解OPT的关系也满足 OPT / ALG ≤ 1 + ϵ \text{OPT}/\text{ALG} \leq 1+\epsilon OPT/ALG1+ϵ. 它与PTAS区别的地方在于算法的时间复杂度是关于 1 / ϵ 1/\epsilon 1/ϵ的多项式函数(PTAS是关于 1 / ϵ 1/\epsilon 1/ϵ的指数函数).

该算法基于动态规划: 设计原问题的一个动态规划算法,然后修改问题实例, 通过动态规划求解从而得到原问题的解. 修改之后的实例可以保证算法的计算复杂度是多项式时间的.

动态规划

f ( i , j ) f(i, j) f(i,j)代表背包中物品价值 等于 j j j所需要的最小的总体积(total weight), 其中物品来自集合 { 1 , 2 , … , i } \{1, 2, \ldots, i\} {1,2,,i}. 令 P = max ⁡ ( p i ) P=\max(p_i) P=max(pi), 因此 n P nP nP j j j的上限.

考虑 i = 1 , 2 , … , m i=1, 2, \ldots, m i=1,2,,m, j = 1 , 2 , … , n P j=1, 2, \ldots, nP j=1,2,,nP.

递归式

f ( i + 1 , j ) = { min ⁡ { f ( i , j ) , f ( i , j − p i + 1 ) + w i + 1 } ,  if  p i + 1 ≤ j f ( i , j )  otherwise f(i+1,j) = \begin{cases} \min\{f(i,j), f(i, j - p_{i+1}) + w_{i+1}\}, & \text{ if } p_{i+1}\leq j \\ f(i, j) & \text{ otherwise} \end{cases} f(i+1,j)={min{f(i,j),f(i,jpi+1)+wi+1},f(i,j) if pi+1j otherwise

初始条件
f ( 1 , p 1 ) = w 1 f ( 1 , 0 ) = 0 f ( i , j ) = ∞ , for the rest  i , j \begin{aligned} & f(1, p_1) = w_1 \\ & f(1, 0) = 0 \\ & f(i, j) = \infty, \quad \text{for the rest } i, j \end{aligned} f(1,p1)=w1f(1,0)=0f(i,j)=,for the rest i,j

Python实现

下面是上述动态规划的实现. 更多关于动态规划的标准实践可以参考 算法设计技巧: 动态规划 (Dynamic Programming).

import math


class KnapsackDP(object):
    """ 背包问题的动态规划算法.
    """

    def __init__(self, w, p, W):
        """
        :param w: 物品大小, list
        :param p: 物品价值, list
        :param W: 背包大小, int
        """
        self._w = w
        self._p = p
        self._W = W
        self._n = len(self._w)
        self._f = self._init_recurrence_formula()
        self._result = None

    def _init_recurrence_formula(self):
        n = len(self._w)
        f = [[]] * n
        max_p = max(self._p)
        for i in range(n):
            f[i] = [math.inf] * n * max_p
            f[0][0] = 0  # !
            f[0][self._p[0]] = self._w[0]
        return f

    def solve(self):
        n = len(self._w)
        max_p = max(self._p)
        # result_items保存计算的中间结果
        # key = profit, value = 达到此profit所包含的一个item
        result_items = {self._p[0]: 0}  # 初始化
        for i in range(n-1):
            for j in range(n * max_p):
                if self._p[i+1] <= j:
                    self._f[i+1][j] = min(self._f[i][j],
                                          self._f[i][j-self._p[i+1]] + self._w[i+1])
                    if self._f[i][j-self._p[i+1]] + self._w[i+1] < self._f[i][j]:
                        result_items[j] = i+1
                else:
                    self._f[i+1][j] = self._f[i][j]

            self._result = self._get_result(result_items, self._get_profit())
        return self

FPTAS

  1. 给定 ϵ > 0 \epsilon > 0 ϵ>0, 令 K = ϵ P / n K = \epsilon P / n K=ϵP/n.
  2. p i ′ = ⌊ p i / K ⌋ p'_i = \lfloor p_i/K\rfloor pi=pi/K, ∀ i = 1 , 2 , … , n \forall i=1,2,\ldots,n i=1,2,,n.
  3. 用上述动态规划求解新的实例(对应价值 p i ′ p'_i pi), 然后输出结果.

可以证明1: ALG ≥ ( 1 − ϵ ) OPT \text{ALG} \geq (1-\epsilon) \text{OPT} ALG(1ϵ)OPT, 且算法的时间复杂度为 O ( n 2 ⌊ n / ϵ ⌋ ) O(n^2\lfloor n/\epsilon\rfloor) O(n2n/ϵ).

Python实现

class KnapsackFPTAS(object):
    """ 动态规划FPTAS.
    近似比: ALG >= (1-epsilon)OPT, 时间复杂度 = O(n^2 * floor(n/epsilon))
    """
    def __init__(self, w, p, W):
        """
        :param w: 物品大小, list
        :param p: 物品价值, list
        :param W: 背包大小, int
        """
        self._w = w
        self._p = p
        self._W = W
        self._n = len(self._w)
        self._result = None

    def solve(self, epsilon):
        k = epsilon * max(self._p) / len(self._w)
        p1 = [int(x/k) for x in self._p]
        dp = KnapsackDP(self._w, p1, self._W).solve()
        self._result = dp.get_result()
        return self

完整代码

参考文献


  1. K. Lai and M. X. Goemans. The Knapsack Problem and Fully Polynomial Time Approximation Schemes (FPTAS), lecture notes, 2006. ↩︎ ↩︎

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: 这是Java中的一个异常,意思是需要进行四舍五入。通常是因为进行了除法运算,但结果不能被精确表示为一个整数或小数。解决方法是使用BigDecimal类进行精确计算,或者使用Math.round()方法进行四舍五入。 ### 回答2: java.lang.ArithmeticException: rounding necessary是Java中的一个异常。它表示在进行数值运算时,需要进行舍入操作。在Java中,当进行除法或四舍五入等运算时,如果结果不是一个整数,则需要进行舍入以获得最接近的整数或指定的小数位数。 这个异常通常发生在以下情况下: 1. 当进行除法运算时,如果除数不能整除被除数,则需要进行舍入才能得到结果。如果无法进行舍入操作,就会抛出该异常。 2. 当通过四舍五入方式将一个小数转换为整数或指定小数位数时,如果需要进行舍入操作才能得到结果,则会抛出该异常。 为了解决这个异常,我们可以采取以下措施: 1. 检查除法运算中的被除数和除数,确保能够整除,避免出现舍入错误。如果无法整除,则需要重新设计算法或找到其他解决方案。 2. 在进行四舍五入运算时,可以使用Java中的Math类提供的方法,如Math.round()来进行舍入操作。此外,还可以考虑使用BigDecimal类来处理精确的小数运算,它提供了很多可以进行舍入操作的方法。 总之,当我们在Java中遇到java.lang.ArithmeticException: rounding necessary异常时,表示在进行数值运算时需要进行舍入操作。要解决这个异常,我们需要检查除法运算中的被除数和除数,以及使用适当的方法进行舍入操作,保证结果的准确性。 ### 回答3: Java.lang.ArithmeticException: rounding necessary 是一个在Java中的异常类型。它表示在进行数值运算时需要进行四舍五入操作。 在进行数值运算时,可能会出现无法准确表示的结果,特别是涉及到浮点数和小数点的运算。由于计算机的内部表示方式有限,无法完美地表示所有的小数和浮点数。这就导致了某些运算结果的尾数部分可能无法精确表示,需要进行四舍五入操作。 当进行一个数值运算时,如果该运算需要进行四舍五入操作以确保结果的准确性,但是没有指示具体的四舍五入策略时,就会抛出 java.lang.ArithmeticException: rounding necessary 异常。 要解决这个异常,可以使用合适的四舍五入策略来确保数值运算的准确性。Java中提供了 BigDecimal 类来处理高精度的数值计算,它可以指定要使用的四舍五入策略。在进行数值运算时,可以使用 BigDecimal 类来处理数值,并指定需要使用的四舍五入策略,从而避免 java.lang.ArithmeticException: rounding necessary 异常的发生。 总之,解决 java.lang.ArithmeticException: rounding necessary 异常的方法是使用 BigDecimal 类来处理数值运算,并指定合适的四舍五入策略。这样可以确保数值运算的准确性,避免出现无法精确表示的结果导致的异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值