寻找数字的最佳可能子集组合以达到给定的总和或最接近给定的总和

时间:2021-03-22 21:06:24

标签: javascript algorithm math dynamic-programming subset-sum

所以,我有这个问题需要解决,显然这被称为子集求和问题,除了我不仅需要在精确到给定数字时获得子集,而且在如果没有达到给定数字的确切总和,它不应该超过参考数字,仅在下面,如果有两个以上可能的子集具有相同的结果,我想获得更好的子集分布,从数组中的最高到最低的数字作为首选,并限制每个子集不超过相同数字的10次,允许重复,例如:

这是具有预定义值的数组:

in_position = False

和给定的数字:

let num = [64.20, 107, 535, 1070];

一种可能的解决方案是:

let investment = 806.45

请注意,这个结果是指 nums 中的每个值允许达到总和的次数:

但更好的解决方案是:

[0, 2, 1, 0] // this sums up to 749 (since there is no way to get to 806.45 with the given array)

还有一个更好的解决方案(因为首先考虑较高的值而不是较低的值)

[4, 5, 0, 0] // this sums up to 791.80 (since there is no way to get to 806.45 with the given array)

另一个重要的限制是永远不应该给出负面结果。

到目前为止,我已经尝试过这样(在 VueJS 中):

[4, 0, 1, 0] // this sums up to 791.80 also but you can see it's taking a higher value when possible.

在我的方法中发生的事情是我尝试从每次乘法中获得所有可能的结果,然后我删除我用这种组合制作的数组之间匹配的那些,最终得到结果,但这并没有得到很好的优化,而且我相信这个问题可能有更好的解决方案。

无论如何忽略 vuejs 实现,那只是在 DOM 中设置值。

***ES6 解决方案会很棒。

CodeSandbox 玩:CODESANDBOX LINK

提前致谢。

2 个答案:

答案 0 :(得分:1)

让我们首先考虑 2 变量的情况。我们要找到两个数 x1, x2 使得

f(x1,x2) = 64.20 * x1 + 107 * x2 - 806.45

enter image description here

如果我们允许 x1, x2 是实数,那么这只是一条直线的方程。在问题中,我们只想要正整数解,即网格点。我已经很糟糕地绘制了网格点,线上方的网格为红色,线下方的网格点为蓝色。然后问题是找到最接近线的网格点。

注意有很多点我们从来不需要考虑,只有红色和蓝色网格点是可能的候选。

enter image description here

生成彩色网格点非常简单。我们可以从 0 到 13 循环遍历 x1 值,通过求解方程来计算实际的 x2 值

x2 =  (806.45 - 64.20 * x1)/107

并找到 floor(x2) 和 ceil(x2)。稍微好一点的是循环从 0 到 8 的 x2 求解 x1

x1 =  (806.45 - 107 * x2)/64.20.

另一种方法可能是某种零跟随例程。如果你在给定的网格点 (x1,x2) 计算 f(x1,x2) 如果它小于 0 我们需要考虑 (x1+1,x2) 或 (x1,x2+1),如果 f(x1 ,x2) 大于零考虑 (x1-1,x2) 或 (x1,x2-x1)。我认为这种方法的复杂性并没有带来任何好处。

如果我们现在转向 3D,我们有一个包含三个变量的方程,x1、x2、x3

f(x1,x2,x3) = 64.20 * x1 + 107 * x2 + 535 * x3 - 806.45

这定义了一个 3D 平面,要求所有变量都为非负,将其限制为平面的三角形部分。

enter image description here

为了在这里找到候选点,我们可以遍历可能的整数对 (x1,x2),然后找到确切的 x3 值

x3 = (806.45 - 64.20 * x1 - 107 * x2)/ 535

以及它的地板和天花板。候选 (x1,x2) 只在候选三角形中排成一行,因此我们可以使用以下程序。

// First find max possible x1

x1_max = ceil(806.45/64.20)
for(x1 = 0; x1< x1_max; ++x1) {
   // for a given x1 find the maximum x2
   x2_max = ceil((806.45 - 64.20*x1)/107)
   for(x2=0;x2<x2_max;++x2) {
       // for a given (x1,x2) find the exact x3
       x3 = (806.45 - 64.20 * x1 - 107 * x2)/ 535
       // and its floor and ceiling
       x3l = floor(x3); x3h = ceil(x3);
       add_to_candidates(x1,x2,x3l);
       add_to_candidates(x1,x2,x3h);
   }
}

一旦你可以候选人,只需选择绝对值最小的那个。

类似的想法会扩展到更多变量。

答案 1 :(得分:1)

这是一种方法。我没有仔细看你的,不知道这比它有什么优势。

这是一种蛮力方法,只需取每个类别的潜在单个值的叉积,求和总数,然后减少列表以找到最接近目标的所有选项。我最初的写法略有不同,试图捕捉最接近目标的值,无论是在目标之上还是之下。

这是一个带有几个辅助函数的实现:

const sum = (ns) =>
  ns .reduce ((a, b) => a + b, 0)

const crossproduct = (xss) => 
  xss.reduce((xs, ys) => xs.flatMap(x => ys.map(y => [...x, y])), [[]])

const range = (lo, hi) =>
  [...Array(hi - lo)] .map ((_, i) => lo + i)

const call = (fn, ...args) => 
  fn (...args)

const closestSums = (t, ns) => 
  call (
    (opts = crossproduct (ns .map (n => range (0, 1 + Math .ceil (t / n))))) => 
      opts .map (xs => [xs, sum (xs .map ((x, i) => x * ns [i]))])
      .reduce (
        ({best, opts}, [opt, tot]) => 
          call (
            (diff = t - tot) => 
              diff >= 0 && diff < best
                ? {best: diff, opts: [opt]}
              : diff >= 0 && diff == best
                ? {best, opts: [...opts, opt]}
              : {best, opts}
          ),
          {best: Infinity, opts: []}
        ) .opts
  )

const byHigher = (as, bs) =>
  as .reduceRight ((r, _, i) => r || (as[i] < bs[i] ? 1 : as[i] > bs[i] ? -1 : 0), 0)

const closestCounts = (t, ns) => 
  closestSums (t * 100, ns .map (n => 100 * n)) 
    .filter (cs => cs.every(c => c <= 10))
    .sort (byHigher)

console .log (
  closestCounts (806.45, [64.20, 107, 535, 1070]),
  closestCounts (791.8, [64.20, 107, 535, 1070])  // exact match
)
.as-console-wrapper {max-height: 100% !important; top: 0}

请注意,包装器将所有内容乘以 100 以消除小数和潜在的浮点舍入错误。如果那不行,那么为匹配添加一些容差就很容易了。

最后的排序很简单,假设您的值按数字升序排列。否则,分拣机就必须变得更加复杂。

正如我所说,这不太可能有效,并且它可能不会在您的版本上获得任何好处,但至少是一种不同的方法。