问题:鲍勃的销售

时间:2011-02-04 13:31:15

标签: algorithm

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

鲍勃有一家商店,想要出售。他的商店里有很多产品,每种产品都有一定数量的单位库存。他还有许多货架价格标签(与产品数量一样多),价格已经印在他们身上。他可以在任何产品上放置任何价格标签(该产品的整个库存的一个项目的单一价格),但是有些产品有额外的限制 - 任何此类产品可能不比某个其他产品便宜。

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


假设:

  • N - 产品和价格标签的数量
  • S i ,0≤ i < N - 索引 i 的产品库存数量(整数)
  • P j ,0≤ j < N - 价格标签上的价格,其索引为 j (整数)
  • K - 附加约束对的数量
  • A k ,B k ,0≤ k < K - 附加约束的产品索引
    • 任何产品索引最多只能在B中出现一次。因此,此邻接列表形成的图形实际上是一组有向树。

该计划必须找到:

  • M i ,0≤ i < N - 从产品索引到价格标签索引的映射(P M i 是产品的价格 i

满足条件:

  1. P M A k ≤P M B k ,0≤ k < K
  2. 对于0≤,
  3. Σ(S i ×P M i i < N is minimal

  4. 请注意,如果不是第一个条件,解决方案就是按价格和产品按数量对标签进行排序,并直接匹配。

    输入的典型值为N,K <10000。在现实生活中,只有几个不同的价格标签(1,2,3,4)。


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

    您有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对放在任何一个极端附近,总成本会更高。

8 个答案:

答案 0 :(得分:16)

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

w 1 ,...,w n 为3分区实例的对象权重,让 b 是bin大小, k = n / 3 允许填充的bin数。因此,如果可以对对象进行分区,则每个bin只有3个对象,就有一个3分区。

对于减少,我们设置N = kb 并且每个bin由相同价格的 b 价格标签表示(想想P i 增加每个 b 标签)。设 t i ,1≤ i k ,是对应的标签的价格我是。 对于每个 w i ,我们有一个产品S j ,数量 w i + 1 (让我们称之为 w i 的根产品)和另一个 w i - 1 < / em>数量为1的产品需要比S j 便宜(称为休假产品)。

对于 t i =(2b + 1) i ,1≤ i k ,当且仅当Bob可以出售 2b Σ1≤ i k 时才有3分区 t i

  • 如果有3分区的解决方案,则所有 b 产品对应于对象 w i w <分配给同一个bin的sub> j w l 可以用相同的价格标记而不违反限制。 因此,该解决方案的成本 2b Σ1≤ i k t i < / sub> (因为价格 t i 的产品总数为 2b )。
  • 考虑Bob's Sale的最佳解决方案。 首先要注意的是,在任何解决方案中,超过3个根产品共享相同的价格标签,对于每个“太多”的根产品,有一个更便宜的价格标签,它坚持少于3个根产品。这比任何解决方案都要糟糕,因为每个价格标签(如果存在)正好有3个根产品 现在仍然可以找到Bob's Sale的解决方案,每个价格有3个根标签,但他们的休假产品不会使用相同的价格标签(垃圾箱流过)。 假设最昂贵的价格标签标记 w i 的根产品,其具有更便宜的标记假产品。这意味着3个根标签 w i w j w l <以最昂贵的价格标记的/ sub> 不会加起来 b 。因此,标有此价格的产品的总成本至少为 2b + 1 因此,这样的解决方案具有成本 t k (2b + 1) +一些其他分配成本。由于现有3分区的最优成本是 2b Σ1≤ i k t i ,我们必须表明刚才考虑的情况更糟。如果 t k &gt;则会出现这种情况。 2b Σ1≤ i k-1 t i (注意它现在是总和的 k-1 )。设置 t i =(2b + 1) i ,1≤ i k ,情况就是这样。如果不是最昂贵的价格标签是“坏”价格标签,那也是如此,但任何其他价格标签。

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

答案 1 :(得分:9)

这个问题类似于CS文献中考虑的许多调度问题。请允许我重申它。

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

输入:

  • 工作1,...,n

  • 工作中的“树状”优先关系预测(Hasse图是森林)

  • 权重w 1 ,...,w n

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

  • 的非减少的迟后罚函数L(t)

输出:

  • {1,...,n}的置换π最小化Σ j w j L(π(j))受制于所有i prec j的约束我们有π(i)&lt; π(j)的

通讯:工作&lt; =&gt;产品;我预先j&lt; =&gt;我的价格低于j;重量&lt; =&gt;数量; L(t)&lt; =&gt; t th 最低价格

当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. 按照相关集合的平均权重顺序安排工作。

  3. 在Cyber​​Shadow的例子中,我们有n = 10和1 prec 10和w j = 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. Horn。 具有树状优先顺序排序和线性延迟惩罚的单机作业排序。 SIAM应用数学杂志,Vol。 23,No。2(1972年9月),第189-202页。

答案 2 :(得分:4)

由于我认为问题很有趣,我做了一个使用约束编程寻找解决方案的模型。该模型使用名为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

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

答案 3 :(得分:3)

作为社区维基发布一些想法,随时可以编辑。

如果您考虑额外的约束条件,必须按照每个节点必须位于其父节点右侧的方式布局或重新排列一组自上而下的树,这个问题更容易可视化(左边的产品更便宜,而右边的产品更贵)。

假设两个产品冲突,如果第一个产品的库存多于第二个产品,那么第一个产品的价格必须不比另一个产品便宜(因此它们被“拉”到不同方向的价格-明智的)。同样,冲突的产品组是至少有两种产品存在冲突的产品,其产品不会与产品组外的任何产品发生冲突。

我们可以做一些观察:

  1. 当两个相互冲突的产品“放置”(分配价格标签)时,它们将始终彼此相邻。
  2. 如果按数量对所有产品进行排序而忽略约束,然后以最佳方式对它们进行排序以满足约束条件,那么冲突组中所有产品的最终位置将始终位于(包含)最左侧和最右侧的初始位置之间。产品
  3. 因此,如果您可以通过从树中删除单个右指向边缘将约束树拆分为两个,以使产品从底部子树的初始位置范围和树根的路径不重叠,您可以安全地将它们视为两个不同的约束树(或单个节点),并忘记它们之间存在依赖关系。 (simple example
  4. 算法思路:
    1. 首先,放置所有不受限制约束的产品。
    2. 对于每个约束树:
      1. 在所有右指向边缘(非冲突产品之间的边缘)上将其拆分为子树。 我们现在有一组子树,所有边都指向左边。
      2. 对于每个子树:
        1. 获取拓扑排序列表
        2. 尝试在此子树中产品的最低到最高初始位置的每个位置插入此列表,确定产生最低总价的那个
      3. 对于步骤2.1中删除的每个边缘:
        1. 如果两个子树的新位置“冲突”:
          1. 将较高的列表与较低的列表(拓扑排序的特殊情况)连接起来
          2. 同样尝试找到连接列表的最佳位置
          3. 对于将来的合并,请将两个检查的子树视为一个子树
    3. 该算法的主要问题是如何处理已经放置的约束对的位移。我想,只是试图通过迭代搜索重新放置移位链可能会起作用,但算法看起来已经过于复杂而无法正常工作。

      如果不同价格的数量较少,您可以为每个不同的价格使用双端队列(或双向链接列表),保留分配给它们的所有项目。这些deques的价格从最低到最高。将项目插入双端队列会将最后一个项目移动到下一个双端队列的开头(对于下一个更高的不同价格),等等之后的所有deques。

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

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

      约束是将1-qty项目右侧的4-qty项目。如上所示,总价为50美元。如果你将一对位置向左移动(所以它是3 1 4 2),则总计最高可达51美元,但如果再往前走一次(1 4 3 2)则会降至48美元。

答案 4 :(得分:3)

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

选择$ t_i = i $,而不是选择$ t_i =(2b + 1)^ 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交叉发布的。

答案 5 :(得分:1)

您可以尝试首先解决更简单的情况,您只需要按价格和产品按数量对标签进行排序,并直接匹配,然后在第一个近似值上使用进化过程:生成有序产品列表的随机变体你有,在列表中向上或向下移动少量随机选择的项目,计算列表中每个变体的总成本,保留最好的几个,并使其成为下一代的基础。我希望,最终迭代这个过程最终会给你正确的答案,只需要一小部分时间来强制解决问题。

答案 6 :(得分:1)

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

  • 变量: N 2 二进制变量。为了清楚起见,我将用两个整数对它们进行索引: x ij 1 当且仅当产品 i 被分配标签 j
  • 目标函数:最小化 S i的 i j 的总和 P j x ij (代表原始目标函数)。
  • 总和超过 j P j x A的 约束 k j - P j x B k j ≤ 0 (代表原始价格限制)。
  • 对于 x ij j 的每个 i 总和,
  • 约束 1 ;对于每个 j 总和 x ij 1 (说< strong> x 编码排列。)

我不是线性编程专家,可能存在更高效的编码。

答案 7 :(得分:0)

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

假设产品和价格已经分类(分别为最少,最高,最高),

  1. l[k] = k+10 <= k < n设置l[n] = 0。然后设置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.
  7. 这是Knuth的计算机编程艺术,第4卷,分册2,第7.2.1.2节中的算法X(稍作修改)。与Knuth的大多数算法一样,它使用基于1的索引。将其隐藏为适合您对典型编程语言的基于0的索引,我将其作为练习留给读者。

    修改

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