14

我想要一种算法(没有特定语言)从一组整数中找到一个子集,使它们的总和在一定范围内。

例如,如果我有一群人,他们的权重如下。

var people:{
   jane:126,
   julia:112,
   charles:98,
   john:182,
   bob:213,
   edgar: 237,
   jay: 223,
   dan: 191,
   alex: 210,
   david: 196
}

现在,从这些人中,我想找到一个总重量在 818-822 磅之间的子集(如果你想算一下……别打扰,这些数字超出了我的想象,并且我什至不知道这个数据集是否有解决方案)。组中的人数无关紧要,只是较大集合中的一组。真的,任何组都可以(尽管在我的情况下随机更好)。

请注意,这只是一个简单的示例……实际上会有数百人,并且可能没有适合此标准的组合。因为实际数字会比这大得多,所以我担心一个^n 问题并运行数千次迭代,即使我需要它运行得非常快。

也许那天我在计算机科学课上睡着了,但是除了蛮力方法之外,我什么都想不出来。

我将其标记为 javascript,仅仅是因为它最接近我的实际实现(而且它更容易阅读)。对其他解决方案持开放态度,只要它们不以某处的某些 Cthulhu 功能为基础。

我知道这是一个奇怪的问题,但这里的任何帮助都将不胜感激。


好吧,我难住了。23 小时内发布赏金,我可以在代码方面理解 - 我的背景肯定不在这个领域,我什至很难辨别用于描述问题的符号,更不用说解决方案了。

有人想帮助我,给我一些可以修改到最终项目的示例 javascript 代码吗?我会尽可能增加 250pt 的赏金......但如果有一个体面的解决方案,我会在时机成熟时分发它。

4

4 回答 4

10

这类似于0-1 背包问题子集和问题

如果权重不是非常大的整数,则动态规划解决方案应该是有效的。


这是动态编程算法的javascript实现。如果您想要随机组,只需在应用此算法之前随机打乱人员列表。

var p = {
   jane:126,
   julia:112,
...
};

function subset(people, min, max)
{
  var subsets = [];
  subsets[0] = '';

  for (var person in people)
  {
    for (var s = min-1; s >= 0; --s)
    {
      if (s in subsets)
      {
        var sum = s + people[person];

        if (!(sum in subsets))
        {
          subsets[sum] = subsets[s] + ' ' + person;

          if (sum >= min && sum <= max)
          {
            return subsets[sum];
          }
        }
      }
    }
  }

  return 'Not found';
}

print(subset(p, 818, 822));
于 2012-10-09T20:32:23.020 回答
4

它看起来像是“二元背包问题”的一种变体,增加了一个界限,即如果最佳拟合仍在可接受的范围之外,则答案将被拒绝。

您可能想研究“多项式时间近似”。

一种方法是按重量对集合进行排序。然后你开始从中间向下和向上看:你得到 dan、john、david、alex,你在 779。你加上 jane,发现自己在 905,到 87 太多了;所以你检查下面的名字,朱莉娅,那是 112,看看差异之间最接近的匹配。与 Julia (210-112) 交换 Alex 将失去 98,与 Julia 交换 David 将失去 84。泡沫,冲洗,重复。

该算法是 O(n log n) 进行排序,然后取决于集合大小。它有几个缺点(不能保证会聚,群体往往是连续的,会在起点附近聚集人等),但如果你只想要“一个群体”,那可能就足够了。

您还可以递归地实现该算法。最坏的情况是 O(n^3 log n),但如果你真的在与人类一起工作(权重聚集在相当小的范围内,曲线平滑),收敛可能会很快。

于 2012-10-09T20:32:57.553 回答
3

这就是我所说的“排序和括号”问题。解决方法是对数据进行排序,然后将目标值或目标范围括起来。

例如,在这种情况下,排序顺序为:

98
112
126
182
191
196
210
213
223
237

现在你对列表进行平均:178.8。因此起始括号是 (126,182)。从这个平均值开始移动:sum(126,182,112,191,98) = 709,太小了。把98删掉,换成另一边的值:196,即sum(126,182,112,191,196) = 807,还是太小了。转到高端的下一个值,sum(126,182,112,191,210)=821。好的,找到一个匹配项。通过继续此过程,您可以找到每个匹配项。基本上,括号的作用是帮助您仅搜索所有可能组合的子集,因此您不必检查每个组合。您是从平均值向外生成组合,而不是从一端或另一端生成组合。

每当您的总和超过/低于该范围时,您就会终止高/低端的组合生成并切换到另一个。这是问题的最佳解决方案。

实现方法:要实现这个算法,你需要一个按“字典”顺序工作的组合生成器。然后,您从 n(例如 5 个)项目开始,并确定中值组合,如上所示。然后你得到下一个较低的组合,如果你是低的,你切换到下一个更高的组合,依此类推。

-------------- 附录 -------

在考虑了这一点之后,最好为此使用普通的更改类型算法,而不是字典组合器。这种类型的算法将生成所有组合,但仅在给定时间切换任意 2 个元素。基本上,您修改此算法以在超出范围(超出范围或低于范围)时切换方向。

于 2012-10-09T21:11:05.180 回答
1

这是对类似问题的回答Find combination(s) sum of element(s) in array which sum equal to a given number

<?php

$limit = 12;
$array = array(6,1,10,4,1,3,11,2,15,5,12,10,17);

echo implode(', ',$array).'<br>';

// remove items 15 and 17 because they are great then $limit = 12
$array = array_filter($array, function($var) use ($limit) {
  return ($var <= $limit);
});

rsort($array);
echo implode(', ',$array);

// the algorithm is usable if the number of elements is less than 20 (because set_time_limit)
$num = count($array); 

//The total number of possible combinations 
$total = pow(2, $num);

$out = array();

// algorithm from http://r.je/php-find-every-combination.html
// loop through each possible combination  
for ($i = 0; $i < $total; $i++) {  

    $comb = array();

    // for each combination check if each bit is set 
    for ($j = 0; $j < $num; $j++) { 
       // is bit $j set in $i? 
        if (pow(2, $j) & $i){
          $comb[] = $array[$j];
        }      
    } 

    if (array_sum($comb) == $limit)
    {
      $out[] = $comb;
    }
}

array_multisort(array_map('count', $out), SORT_ASC, $out);

$out = array_unique($out, SORT_REGULAR);

foreach($out as $result) echo implode(', ', $result).'<br>';

此代码的输出是总和仅为 $limit(12) 的组合列表...

12
10, 2
11, 1
5, 4, 3
6, 4, 2
6, 5, 1
10, 1, 1
5, 4, 2, 1
6, 3, 2, 1
6, 4, 1, 1
5, 3, 2, 1, 1
于 2014-01-15T14:33:51.610 回答