如何在javascript中将集合的元素分组为不相交的子集?

Posted

技术标签:

【中文标题】如何在javascript中将集合的元素分组为不相交的子集?【英文标题】:How can I group the elements of a set into disjoint subsets in javascript? 【发布时间】:2013-07-03 13:14:27 【问题描述】:

一个 9 人的小组在 2、3 和 4 人的 3 个不相交的小组中可以有多少种工作方式?如何通过 javascript 回溯生成所有可能性。

例子:

Gs = group([aldo,beat,carla,david,evi,flip,gary,hugo,ida],[2,2,5]);

console.log(Gs); // [[aldo,beat],[carla,david],[evi,flip,gary,hugo,ida]], ...

请注意,我不想要组成员的排列;即 [[aldo,beat],...] 与 [[beat,aldo],...] 的解决方案相同。但是,[[aldo,beat],[carla,david],...] 和 [[carla,david],[aldo,beat],...] 之间是有区别的。

*请不要使用库。

【问题讨论】:

您不想使用库,所以您基本上是在寻找算法?你试过什么吗?你可以从***.com/questions/127704/… 或***.com/questions/12991758/… 获得一些灵感(第二个链接很接近你想要的,尽管是另一种语言)。 我更新了我的答案并使group 函数快了十倍。 【参考方案1】:

如果您只需要一组 9 人可以分为 2、3 和 4 人的 3 个子组,那么这很容易使用 C 进行数学计算(计算组合数的函数) .

    首先您有 9 个人,您需要从中选择 2 个人。因此,您可以使用C(9, 2)。 接下来您有 7 个人,您需要从中选择 3 个人。因此,您可以使用C(7, 3)。 最后你有4个人,你需要从中选出4个人。因此你做C(4, 4)。但是C(n, n) 始终为 1。

因此,将一组 9 人分成 2、3 和 4 人的 3 个子组的方法数为 C(9, 2) * C(7, 3) * C(4, 4)。这可以简化为C(9, 2) * C(7, 3),即36 * 35,即1260

我们可以编写一个函数来为我们计算:

function ways(n) 
    var l = arguments.length, w = 1;

    for (var i = 1; i < l; i++) 
        var m = arguments[i];
        w *= combinations(n, m);
        n -= m;
    

    return w;

为了使这个函数工作,我们需要定义函数combinations

function combinations(n, k) 
    return factorial(n) / factorial(n - k) / factorial(k);

最后我们需要为factorial定义函数:

function factorial(n) 
    var f = n;
    while (--n) f *= n;
    return f;

然后我们计算方式的数量如下:

alert(ways(9, 2, 3)); // 1260

您可以在此处查看演示:http://jsfiddle.net/bHSuh/

请注意,我们不需要指定最后一个由 4 人组成的子组,因为这是隐含的。


但是,我相信您希望生成每一种可能的方式。这是 amb 运算符非常适合的事情。所以我们要做的第一件事就是在 JavaScript 中编写 amb 运算符:

function amb(options, callback) 
    var length = options.length;

    for (var i = 0; i < length; i++) 
        try 
            callback(options[i]);                       // try the next option
            return;                                     // no problem, quit
         catch (e) 
            continue;                                   // problem, next
        
    

    throw new Error("amb tree exhausted");              // throw a tantrum

接下来我们将编写一个函数,从索引列表中挑选一组给定的项目:

function pick(list, items) 
    var length = list.length, selected = [], rest = [];

    for (var i = 0; i < length; i++) 
        if (items.indexOf(i) < 0) rest.push(list[i]);
        else selected.push(list[i]);
    

    return [selected, rest];

我们还需要一个生成索引列表的函数:

function getIndices(length) 
    var indices = [];

    for (var i = 0; i < length; i++)
        indices.push(i);
    return indices;

最后我们将递归实现group函数:

function group(options, divisions) 
    var subgroup = [], groups = [], n = 0;
    var indices = getIndices(options.length);
    var division = divisions.shift(), remaining = divisions.length;

    try 
        amb(indices, select);
     catch (e) 
        return groups;
    

    function select(index) 
        subgroup.push(index);

        if (++n < division) 
            try  amb(indices.slice(index + 1), select); 
            catch (e)  /* we want to continue processing */ 
         else 
            var subgroups = pick(options, subgroup);

            if (remaining) 
                var children = group(subgroups.pop(), divisions.slice());
                var length = children.length;
                for (var i = 0; i < length; i++)
                    groups.push(subgroups.concat(children[i]));
             else groups.push(subgroups);
        

        n--;
        subgroup.pop();
        throw new Error;
    

现在你可以按如下方式使用它:

var groups = group([
    "aldo", "beat", "carla",
    "david", "evi", "flip",
    "gary", "hugo", "ida"
], [2, 3]);

再次注意,您不需要指定最后一个 4 人的子组,因为它是隐含的。

现在让我们看看输出是否如我们预期的那样:

console.log(groups.length === ways(9, 2, 3)); // true

给你。正好有 1260 种方法可以将一组 9 人分为 2、3 和 4 人的 3 个子组。


现在我知道我的group 函数看起来有点吓人,但实际上非常简单。尝试阅读并了解发生了什么。

假设你是 9 个人的老板。您如何将他们分成 2、3 和 4 人的 3 个子组?这正是我的group 函数的工作方式。

如果一段时间后您仍然无法理解逻辑,那么我将更新我的答案并详细解释group函数。祝你好运。


顺便说一句,我刚刚意识到对于这个问题,您并不需要amb。您可以简单地使用 forEach 代替。由于没有 try-catch 块,生成的代码会更快:

function group(options, divisions) 
    var subgroup = [], groups = [], n = 0;
    var indices = getIndices(options.length);
    var division = divisions.shift(), remaining = divisions.length;
    indices.forEach(select);
    return groups;

    function select(index) 
        subgroup.push(index);

        if (++n < division) indices.slice(index + 1).forEach(select);
        else 
            var subgroups = pick(options, subgroup);

            if (remaining) 
                var children = group(subgroups.pop(), divisions.slice());
                var length = children.length;
                for (var i = 0; i < length; i++)
                    groups.push(subgroups.concat(children[i]));
             else groups.push(subgroups);
        

        subgroup.pop();
        n--;
    

由于我们不再使用amb,程序的执行时间减少了十倍。自己查看结果:http://jsperf.com/amb-vs-foreach

我也终于创建了上述程序的演示小提琴:http://jsfiddle.net/Ug6Pb/

【讨论】:

除了有点慢,这似乎是正确的答案。 +1 @BenjaminGruenbaum 我已经修复了程序,让它快了十倍。 很好的答案,谢谢!感谢所有试图提供帮助的人。【参考方案2】:

我确信有更快的公式,但我从来没有那么擅长数学,如果我正确理解问题,这似乎可行:

function combo(r, ops)
     function unq(r)return r.filter(function(a,b,c)return !this[a] && (this[a]=1);,);  

     var combos=, pairs=[];

      r.forEach(function(a,b,c)
         combos[a]=r.filter(function not(a)return a!=this && !combos[a], a);
      );


      Object.keys(combos).forEach(function(k)
         combos[k].forEach(function(a)  
              pairs.push([k, a]+'');
         );     
      );

      return unq(unq( 
            pairs.map(function(a) 
                   return unq(a.split(",")).sort(); 
              )).map(function(a)
                   return a.length==ops && a;
              ).filter(Boolean))
            .sort();

//end combo



var r="aldo,beat,carla,david,evi,flip,gary,hugo,ida".split(",");

// find groups of different lengths:

combo(r, 2) // 2 folks ==  36 combos
combo( combo(r, 2), 3) // 3 folks == 84 combos
combo( combo( combo(r, 2), 3), 4) // 4 folks == 126 combos 

我没有费心对函数进行递归化,因为您只需要进行 4-in 和 lispy 调用即可,但如果我不得不更进一步,我想编写一个额外的外包装来将打电话...

【讨论】:

【参考方案3】:

回溯算法的核心实现很简单(参见下面的函数 doBacktrack)。通常复杂性在于具体回溯问题的细节

以下是我针对您的问题实施的回溯算法。它基于 Steven Skiena 的算法设计手册中的回溯算法描述(或者我记得的)。

我没有在算法中添加修剪(因为它花费的时间已经比我想象的要长:))但是如果你想提高它的性能,只需为函数 done() 添加一个合理的实现以防止继续处理可以推断为不可行的解决方案的候选人

function backtrack() 
  var people =
      ['aldo','beat','carla','david','evi','flip','gary','hugo','ida'];
  var initial_state =
      [[], [], []];
  var groups =
      [2, 3, 4];
  var data =
      groups: groups, people: people, people_idx_for_name: ;
  people.forEach(function(e, i) 
    data['people_idx_for_name'][e] = i;
  );
  var solutions = [];

  doBacktrack(initial_state, solutions, data);

  return solutions;


function doBacktrack(candidate, solutions, data) 
//  console.log('processing: ' + candidate);
  if (isSolution(candidate, data)) 
      processSolution(candidate, solutions);
  
  if (done(candidate, solutions, data)) 
    return;
  
  var new_candidates = calculateNewCandidates(candidate, data);

  for (var i=0; i<new_candidates.length; i++) 
    doBacktrack(new_candidates[i], solutions, data);
  


function calculateNewCandidates(candidate, data) 
  var groups = data['groups'];
  var i = 0;
  while (i<groups.length && candidate[i].length == groups[i])  i++; 
  if (i < groups.length) 
    //determine list of not yet selected people
    var not_yet_selected = determineNotYetSelectedPeople(candidate, data, i);

    var results = [];
    for (var j=0; j<not_yet_selected.length; j++) 
      var candidate_copy = candidate.slice(0);
      for (var k=0; k<candidate_copy.length; k++) 
        candidate_copy[k] = candidate_copy[k].slice(0);
      
      candidate_copy[i].push(not_yet_selected[j])
      results.push(candidate_copy);
    
    return results;

   else 
    return [];
  


function determineNotYetSelectedPeople(candidate, data, group) 
  var people = data['people'];
  var people_idx_for_name = data['people_idx_for_name'];
  var selected_people = ;
  var results = [];
  var max = -Number.MAX_VALUE;
  candidate.forEach(function(candidate_group, i) 
    candidate_group.forEach(function(already_selected_person_name) 
      var already_selected_person_idx = people_idx_for_name[already_selected_person_name];
      if (max < already_selected_person_idx && i==group)  max = already_selected_person_idx; 
      selected_people[already_selected_person_name] = true;
    );
  );
  for (var i=0; i<people.length; i++) 
    if (!selected_people[people[i]] && i > max)  results.push(people[i]); 
  
  return results;


function isSolution(candidate, data) 
  var groups = data['groups'];
  for (var i=0; i<groups.length; i++) 
    if (candidate[i].length != groups[i]) return false;
  
  return true;


function processSolution(candidate, solutions) 
  var solution = [];
  candidate.forEach(function(e) 
    var l = [];
    solution.push(l);
    e.forEach(function(f) 
      l.push(f);
    );
  );
  solutions.push(solution);


//use this to improve performance with prunning if possible
function done() 
  return false;


var solutions = backtrack();
console.log(solutions);
console.log(solutions.length);

【讨论】:

以上是关于如何在javascript中将集合的元素分组为不相交的子集?的主要内容,如果未能解决你的问题,请参考以下文章

请教matlab中分组排列问题

如何在javascript中将集合转换为数组

如何在 JavaScript 中将 File 对象添加到 FileList 集合中?

Java ArrayList<String>中有10个元素,怎么在遍历这个集合时把里面的10个元素赋值给10个变量?

如何在java中将方法标记为不鼓励?

如何在 Win32 控制台应用程序中将 argv[] 设置为不区分大小写?