【问题标题】:Problem: Bob's Sale问题:鲍勃的销售
【发布时间】:2011-06-21 09:00:14
【问题描述】:

注意:这是对有关 SWF 文件中记录排序的现实问题的抽象改写。解决方案将帮助我改进开源应用程序。

Bob 有一家商店,他想进行销售。他的商店有许多产品,他的库存中每种产品都有一定的整数单位。他还有许多架在货架上的价格标签(与产品数量一样多),上面已经印有价格。他可以在任何产品上贴上任何价格标签(该产品的全部库存中一件商品的统一价格),但是有些产品有一个额外的限制 - 任何此类产品可能不会比某个其他产品便宜。

您必须找到如何安排价格标签,以使 Bob 的所有商品的总成本尽可能低。总成本是每个产品的指定价格标签乘以该产品的库存数量的总和。


给定:

  • N – 产品和价格标签的数量
  • Si, 0≤ii的产品的库存数量(整数)
  • Pj, 0≤jj的价格(整数)
  • K – 附加约束对的数量
  • Ak, Bk, 0≤k
  • 任何产品索引在B中最多出现一次。因此,这个邻接表形成的图实际上是一组有向树。

程序必须找到:

  • Mi, 0≤iMi 是产品的价格 i)

满足条件:

  1. PMAk ≤ PMBk,对于 0≤k
  2. Σ(Si × PMi) for 0≤ i

请注意,如果不是第一个条件,解决方案将是简单地按价格排序标签和按数量排序产品,并直接匹配两者。

输入的典型值为 N,K


以下是为什么大多数简单解决方案(包括拓扑排序)不起作用的一个示例:

您有 10 件数量为 1 到 10 的商品,以及 10 个价格为 1 到 10 美元的价格标签。有一个条件:数量为 10 的商品不得低于数量为 1 的商品。

最优解是:

Price, $   1  2  3  4  5  6  7  8  9 10
Qty        9  8  7  6  1 10  5  4  3  2

总费用为 249 美元。如果将 1,10 对放在任一极端附近,总成本会更高。

【问题讨论】:

  • 如果对项目进行拓扑排序,数量作为“决胜局”会发生什么。然后与价格相匹配。这是否提供了最佳/足够好的解决方案?
  • Bob 应该开一家价值 1 美元的商店。
  • 啊,好吧,我发现了一个基于拓扑排序的贪婪方法中断的情况。库存:Ax1、Bx10、Cx2。约束:A
  • @BlueRaja:为了升级吹毛求疵,上述问题不在 NP 中。询问是否存在总价格小于 X 的配置在 NP 中,这会导致原始问题的多项式等价解决方案。这可能是 NP 难的。几分钟的随意思考未能产生一个已知的 NP 完全问题,从而简化为这个问题。
  • 这是我在网站上看到的最好的算法问题之一。我正在向它添加赏金以尝试激发对答案的兴趣。

标签: algorithm


【解决方案1】:

因为我认为这个问题很有趣,所以我做了一个模型来使用约束编程寻找解决方案。该模型是用称为MiniZinc 的建模语言编写的。

include "globals.mzn";

%%% Data declaration
% Number of products
int: n;
% Quantity of stock
array[1..n] of int: stock;
% Number of distinct price labels
int: m;
% Labels
array[1..m] of int: labels;
constraint assert(forall(i,j in 1..m where i < j) (labels[i] < labels[j]),
              "All labels must be distinct and ordered");
% Quantity of each label
array[1..m] of int: num_labels;
% Number of precedence constraints
int: k;
% Precedence constraints
array[1..k, 1..2] of 1..n: precedences;

%%% Variables
% Price given to product i
array[1..n] of var min(labels)..max(labels): prices :: is_output;
% Objective to minimize
var int: objective :: is_output;

%%% Constraints
% Each label is used once
constraint global_cardinality_low_up_closed(prices, labels, num_labels, num_labels);

% Prices respect precedences
constraint forall(i in 1..k) (
            prices[precedences[i, 1]] <= prices[precedences[i, 2]]
       );

% Calculate the objective
constraint objective = sum(i in 1..n) (prices[i]*stock[i]);

%%% Find the minimal solution
solve minimize objective;

问题的数据在单独的文件中给出。

%%% Data definitions
n = 10;
stock = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
m = 10;
labels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
num_labels = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
k = 1;
precedences = [| 1, 10 |];

该模型相当幼稚且直截了当,没有花哨的东西。使用Gecode后端解决示例问题,生成如下输出(假设模型在model.mzn,数据在data.dzn)

$ mzn2fzn -I/usr/local/share/gecode/mznlib/ model.mzn data.dzn
$ fz -mode stat -threads 0 model.fzn 
objective = 265;
prices = array1d(1..10, [1, 10, 9, 8, 7, 6, 5, 4, 3, 2]);
----------
objective = 258;
prices = array1d(1..10, [2, 10, 9, 8, 7, 6, 5, 4, 1, 3]);
----------
objective = 253;
prices = array1d(1..10, [3, 10, 9, 8, 7, 6, 5, 2, 1, 4]);
----------
objective = 250;
prices = array1d(1..10, [4, 10, 9, 8, 7, 6, 3, 2, 1, 5]);
----------
objective = 249;
prices = array1d(1..10, [5, 10, 9, 8, 7, 4, 3, 2, 1, 6]);
----------
==========

%%  runtime:       0.027 (27.471000 ms)
%%  solvetime:     0.027 (27.166000 ms)
%%  solutions:     5
%%  variables:     11
%%  propagators:   3
%%  propagations:  136068
%%  nodes:         47341
%%  failures:      23666
%%  peak depth:    33
%%  peak memory:   237 KB

对于较大的问题,它当然要慢得多,但随着时间的推移,模​​型通常会生成连续更好的解决方案。

【讨论】:

    【解决方案2】:

    这是Gero's answer 的后续活动。这个想法是修改他的结构以显示强大的 NP 硬度。

    没有选择$t_i=(2b+1)^i$,而是选择了$t_i=i$。现在,您必须修改参数 $P=2b\sum_{1\leq i \leq k} t_i$ 的解意味着存在 3 分区。

    采取任意货架订单。按以下方式进行核算:将根产品的数量单位 $w_i-1$ 分配给其叶产品。那么每个产品都有数量 2。根据约束的定义,这不会转移到更高的价格。 在这种转变之后,价格将恰好是 $P$。 如果转移将一些数量转移到较低的奖金,则原始奖金严格大于 $P$。

    因此,只有当所有叶产品的奖品与其根产品相同时,才有可能获得所要求的奖品,这意味着存在一个 3 分区。

    引用SWAT 2010 paper这个参数的结果表明,即使对数字进行一元编码和$k$不同的价格标签,运行时间为$f(k)\cdot n^{O(1)}$将违反“标准复杂性假设”。 这使得运行时间为 $n^{O(k)}$ 的动态编程提示看起来还不错。


    这是从 cstheory 的同一 answer 交叉发布的。

    【讨论】:

      【解决方案3】:

      对于一般情况,问题是 NP 完全的。这可以通过减少 3 分区来显示(这仍然是一个强大的 NP 完全版本的装箱)。

      w1, ..., wn为3-partition实例的对象的权重,令 b 是 bin 大小,k = n/3 是允许填充的 bin 数量。因此,如果可以对对象进行分区,使得每个 bin 恰好有 3 个对象,则存在 3 分区。

      为了减少,我们设置 N=kb 并且每个 bin 由相同价格的 b 个价格标签表示(想想 Pi 增加每 b 个标签)。设ti,1≤ik,为对应的标签的价格第 i 个 bin。 对于每个 wi,我们有一个数量为 wi 的产品 Sj sub> + 1(我们称其为 wi 的根积)和另一个 wi - 1 数量为 1 的产品,要求比 Sj 便宜(称这些为休假产品)。

      对于ti = (2b + 1)i,1≤ik ,当且仅当 Bob 可以卖出 2bΣ1≤ik ti:

      • 如果有3-partition的解决方案,那么对象wiwb乘积sub>j, wl 分配到同一个 bin 中,可以在不违反限制的情况下标上相同的价格。 因此,解决方案的成本为 2bΣ1≤ikti(因为价格为 ti 的产品的总数量是 2b)。
      • 考虑 Bob's Sale 的最优解。 首先观察到,在任何解决方案中,超过 3 个根产品共享相同的价格标签,对于每个“太多”的根产品,都有一个更便宜的价格标签贴在少于 3 个根产品上。如果每个价格标签(如果存在)恰好有 3 个根产品,这比任何解决方案都要糟糕。
        现在仍然可以有一个 Bob's Sale 的解决方案,每个价格有 3 个根标签,但是他们的离开产品没有贴上相同的价格标签(垃圾箱有点溢出)。 假设最昂贵的价格标签标记了 wi 的根产品,该产品具有更便宜的标签休假产品。这意味着 3 个根标签 wiwjwl 加起来不等于 b。因此,标有此价格的产品的总成本至少为 2b+1
        因此,这样的解决方案具有成本 tk(2b+1) + 一些其他分配成本。由于现有 3 分区的最优成本是 2bΣ1≤ikt i ,我们必须证明刚刚考虑的情况更糟。如果 tk > 2b Σ1≤ik-1 sub>ti (注意现在总和是 k-1 )。设ti = (2b + 1)i, 1≤ik,就是这样。如果不是最昂贵的价格标签是“坏”的价格标签,那么这也成立,但任何其他价格标签都成立。

      所以,这是破坏性的部分 ;-) 但是,如果不同价格标签的数量是一个常数,您可以使用动态规划在多项式时间内求解。

      【讨论】:

        【解决方案4】:

        作为社区 wiki 发布一些想法,请随时编辑。

        如果您将附加约束考虑为必须以每个节点必须位于其父节点右侧的方式布置或重新排列一组自上而下的树,则此问题更容易可视化(左边的产品更便宜,右边的产品更贵)。

        假设两种产品冲突,如果第一种产品的库存比第二种产品多,但第一种产品不能比另一种便宜(因此它们被“拉”到不同方向的价格-明智的)。同样,冲突产品组是指至少有两种产品发生冲突,并且其产品均不与该组之外的任何产品发生冲突。

        我们可以做一些观察:

        1. 当“放置”(分配价格标签)两个冲突的产品时,它们将始终彼此相邻。
        2. 如果您按数量对所有产品进行排序而不考虑约束,然后对它们进行优化排列以使它们满足约束,那么冲突组中所有产品的最终位置将始终位于(包括)最左边和最右边的初始位置之间产品。
        3. 1234563您可以安全地将它们视为两个不同的约束树(或单个节点),而忘记它们之间存在依赖关系。 (simple example)
        算法思路:
        1. 首先,放置所有不受限制的产品。
        2. 对于每个约束树:
          1. 在所有指向右的边(非冲突产品之间的边)上将其拆分为子树。 我们现在有一组所有边都指向左侧的子树。
          2. 对于每个子树:
            1. 获取它的拓扑排序列表
            2. 尝试在每个位置插入此列表,从该子树中产品的最低到最高初始位置开始,确定总价最低的那个
          3. 对于步骤 2.1 中删除的每条边:
            1. 如果两个子树的新位置“冲突”:
              1. 将较高的列表与较低的列表连接(拓扑排序的特殊情况)
              2. 类似地尝试找到串联列表的最佳位置
              3. 为了将来的合并,将两个检查的子树视为一个子树

        这个算法的主要问题是如何处理已经放置的约束对的位移。我想简单地尝试通过迭代搜索替换移位的链可能会起作用,但算法看起来已经太复杂而无法正常工作。

        如果不同价格的数量很少,您可以为每个不同的价格使用一个双端队列(或双向链表),以保存分配给该价格的所有商品。双端队列从最低价格到最高价格排序。将一个项目插入双端队列会将最后一个项目移动到下一个双端队列的开头(用于下一个更高的不同价格),之后的所有双端队列以此类推。

        关于迭代 / 冒泡排序算法需要注意的一点:当您有一对相互冲突的产品时,仅仅在任一方向上贪婪地走一个位置直到下一个位置没有改进是不够的。这是我通过 playing around a bit 用 Mathematica 编写的test case generator 得到的一个测试用例:

        Price, $   1 2 7 9
        Qty        3 2 1 4
        

        限制是让 4 数量的项目位于 1 数量的项目的右侧。如上图,总价为 50 美元。如果您将货币对向左移动一个位置(所以它是3 1 4 2),总价值会上升到 51 美元,但如果再往前移动(1 4 3 2),它就会下降到 48 美元。

        【讨论】:

        • 当您说“每个节点都必须位于其父节点的右侧”时,我感到很困惑——这似乎消除了任何指向左边缘的可能性。 A成为B的父母意味着什么? (我假设它的意思是“产品 A 的价格必须低于产品 B”。)
        • 是的,没错。我的文章中提到具有左指边缘的树的部分暗示在它们被安排以满足约束之前(产品节点最初按数量排序)。
        【解决方案5】:

        这个问题类似于 CS 文献中考虑的许多调度问题。请允许我将其重述为一个。

        问题(“具有优先级、权重和一般延迟惩罚的非抢占式单机调度”)

        输入:

        • 工作 1,…,n

        • 作业上的“树状”优先关系(哈斯图是一片森林)

        • 权重 w1, ..., wn

        • 从 {1, ..., n} 到 Z+

        • 的非递减延迟惩罚函数 L(t)

        输出:

        • {1, …, n} 的排列 π 最小化 ∑j wj L(π(j)) 受到约束,即对于所有 i prec j我们有 π(i)

        通信:工作产品; i prec j i 的价格低于 j;重量 数量; L(t) tth 最低价

        当 L 是线性时,由于 Horn [1],存在一种有效的多项式时间算法。 文章在付费墙后面,但主要思想是

        1. 对于所有 j,找到仅包含 j 及其平均权重最大的后续作业的连接集。 例如,如果 n = 6 并且优先约束是 1 prec 2 和 2 prec 3 和 2 prec 4 和 4 prec 5,那么考虑的集合为 2 {2}, {2, 3}, {2, 4}, {2, 3, 4}, {2, 4, 5}, {2, 3, 4, 5}。 我们实际上只需要最大平均权重,可以通过动态规划自下而上计算。

        2. 按关联集的平均权重顺序贪婪地安排作业。

        在 Cyber​​Shadow 的示例中,我们有 n = 10 和 1 个 prec 10 和 wj = j 和 L(t) = t。 步骤 1 中计算的值是

        • 作业 1:5.5(1 和 10 的平均值)

        • 工作 2:2

        • 工作 3:3

        • 工作 4:4

        • 工作 5:5

        • 工作 6:6

        • 作业 7:7

        • 工作 8:8

        • 工作 9:9

        • 作业 10:10

        最佳顺序是 9、8、7、6、1、10、5、4、3、2。


        即使对于 L 的不同选择,该算法在实践中也可能运行良好,因为最优性证明使用局部改进。 或者,也许 CS Theory Stack Exchange 上的某个人会有一个想法。

        [1] W. A. 霍恩。 具有树状优先顺序和线性延迟惩罚的单机作业排序。暹罗应用数学杂志,卷。 23,第 2 期(1972 年 9 月),第 189-202 页。

        【讨论】:

        • 我承认这有点超出我的想象,但我不太确定如何将数量和价格转换为重量和延迟惩罚函数。
        • 我仍然不能说我完全理解这一点(我希望必须先深入研究相应的文献),但您是否说该算法应该能够应对 cme​​ts 中提出的问题和下面的帖子?一方面,术语“贪婪”和“局部最优”是否意味着如果下一个位置没有立即改善,它将停止尝试某个方向?我已经发布(并且刚刚更新)了一个简单的测试用例,这不起作用(仅针对那部分,但我想它可以向任一方向扩展)。
        • 另一件事是,我没有看到任何关于价格和数量之间的产品总和的明确检查。我必须假设迟到和体重相乘才能在第 2 步中获得整体适应度?
        • 当 L 不是线性时,没有理论上的保证。该算法是贪心的,但它不是本地搜索。
        • 没有乘法,因为 L 被假定为线性。
        【解决方案6】:

        解决问题的一种方法是使用0-1 linear programming 表达它并使用Balas 的加法算法解决它。以下是问题的编码方式:

        • 变量: N2 个二进制变量。为了清楚起见,我将用两个整数对它们进行索引:xij 当且仅当产品 i1 > 被分配标签 j
        • 目标函数:最小化SiPj的ij之和sub>xij(代表原始目标函数)。
        • 约束:对于 PjxAj 上的每个 k 总和kj – PjxBkj≤ 0(表示原始价格约束)。
        • 约束:对于 xijj 上的每个 i 总和为 1;对于 xij 的每个 j 总和超过 i1(表示 x 编码一个排列)。

        我不是线性规划方面的专家,可能存在更有效的编码。

        【讨论】:

        • @Bolo- 0-1 NP-hard 中的线性规划,对吧?所以这只是使用在最坏情况下不会在多项式时间内的求解器来解决问题的一种很好的优化方法?
        • @templatetypedef:线性规划问题的复杂性往往是悲观的。
        • @templatetypedef 对。如果这个问题在 P 中,那么相关的多项式算法肯定会优于 0-1 线性规划解决方案。但是,如果问题不在 P 中(我怀疑是这种情况),那么线性规划应该是一种合理的方法,因为它已经研究了半个多世纪,并且求解器通常很快在实践中.
        【解决方案7】:

        按字典顺序生成价格排列,并返回第一个符合约束条件的排列。

        假设产品和价格已经排序(分别从少到多,从高到低),

        1. 0 &lt;= k &lt; nl[n] = 0 设置l[k] = k+1。然后设置k = 1
        2. 设置p = 0, q = l[0]
        3. 设置M[k] = q。如果任何具体涉及P[k] 的价格约束失败,则执行5。否则,如果k = n,则返回M[1]...M[n]
        4. 设置u[k] = p, l[p] = l[q], k = k + 1并转到2。
        5. 设置p = q, q = l[p]。如果q != 0 转3。
        6. 设置k = k - 1,如果k = 0则终止。否则,设置p = u[k], q = M[k], l[p] = q 并转到5。

        这是来自 Knuth 的计算机编程艺术,第 4 卷,第 2 卷,第 7.2.1.2 节中的算法 X(稍作修改)。与 Knuth 的大多数算法一样,它使用基于 1 的索引。修改它以适应典型编程语言的从 0 开始的索引,我留给读者作为练习。

        编辑:

        不幸的是,事实证明这并不能保证非递减序列。我将不得不多考虑一下,看看这是否可以挽救。

        【讨论】:

        • 鉴于数量不完全相同,这能正常工作吗?
        • 应该的。如果您考虑一下,最好的解决方案是库存中数量最少的产品获得最高价格,然后依此类推,每个数量增加的产品价格较低。然后,按字典顺序排列价格映射将找到成本逐渐提高且没有逆转的解决方案。
        • 这将如何扩展到 N 件可能有很多价格限制的商品?
        【解决方案8】:

        您可以尝试首先解决更简单的情况,您只需按价格和产品数量对标签进行排序,并直接匹配两者,然后在第一个近似值上使用进化过程:生成有序产品列表的随机变化你有,将少量随机选择的项目在列表中向上或向下移动几个位置,计算列表中每个变化的总成本,保留最好的几个,并将它们作为你下一代的基础。我希望,在几代人中迭代这个过程最终应该可以为您的问题提供正确的答案,而这只是暴力破解解决方案所需时间的一小部分。

        【讨论】:

        • 通用算法会遇到局部最小值问题(请参阅我的社区 wiki 帖子)。
        猜你喜欢
        • 1970-01-01
        • 2012-11-24
        • 1970-01-01
        • 1970-01-01
        • 2018-03-24
        • 2021-10-11
        • 2014-06-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多