3

In how many ways can a group of 9 people work in 3 disjoint subgroups of 2, 3 and 4 persons? How can I generates all the possibilities via backtracking with javascript.

Example:

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]], ...

Note that I do not want permutations of the group members; i.e. [[aldo,beat],...] is the same solution as [[beat,aldo],...]. However, there's a difference between [[aldo,beat],[carla,david],...] and [[carla,david],[aldo,beat],...].

*No libraries please.

rafaelcastrocouto
  • 10,518
  • 1
  • 34
  • 58
  • You don't want to use a library, so you're basically looking for an algorithm? Have you tried anything? You can get some inspiration from http://stackoverflow.com/questions/127704/algorithm-to-return-all-combinations-of-k-elements-from-n or http://stackoverflow.com/questions/12991758/creating-all-possible-k-combinations-of-n-items-in-c (the second link is close to what you want, albeit in another language). – user247702 Jul 09 '13 at 12:06
  • 1
    I updated my answer and made the `group` function ten times faster. – Aadit M Shah Jul 11 '13 at 02:29

3 Answers3

4

If you only need the number of ways a group of 9 people can be divided into 3 subgroups of 2, 3 and 4 people each then that's easy to compute mathematically using C (the function to calculate the number of combinations).

  1. First you have 9 people out of which you need to select 2 people. Hence you do C(9, 2).
  2. Next you have 7 people out of which you need to select 3 people. Hence you do C(7, 3).
  3. Finally you have 4 people out of which you need to select 4 people. Hence you do C(4, 4). However C(n, n) is always 1.

Hence the number of ways to divide a group of 9 people into 3 subgroups of 2, 3 and 4 people is C(9, 2) * C(7, 3) * C(4, 4). This can be simplified to C(9, 2) * C(7, 3), which is 36 * 35 which is 1260.

We can write a function to compute this for us:

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;
}

To make this function work we need to define the function combinations:

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

Finally we need to define the function for factorial:

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

Then we compute the number of ways as follows:

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

You can see the demo here: http://jsfiddle.net/bHSuh/

Note that we didn't need to specify the last subgroup of 4 people because that is implied.


However I believe that you want to generate each possible way. This is the sort of thing that the amb operator is perfect for. So the first thing we'll do is write the amb operator in JavaScript:

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
}

Next we'll write a function which picks a given set of items from a list of indices:

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];
}

We also need a function which will generate a list of indices:

function getIndices(length) {
    var indices = [];

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

Finally we'll implement the group function recursively:

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;
    }
}

Now you can use it as follows:

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

Notice again that you didn't need to specify the last subgroup of 4 people since it's implied.

Now let's see whether the output is as we expected it to be:

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

There you go. There are exactly 1260 ways that a group of 9 people can be divided into 3 subgroups of 2, 3 and 4 people each.


Now I know that my group function looks a little daunting but it's actually really simple. Try to read it and understand what's going on.

Imagine that you're the boss of 9 people. How would you divide them into 3 subgroups of 2, 3 and 4 people? That's exactly the way my group function works.

If you still can't understand the logic after a while then I'll update my answer and explain the group function in detail. Best of luck.


BTW I just realized that for this problem you don't really need amb. You may simply use forEach instead. The resulting code would be faster because of the absence of try-catch blocks:

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--;
    }
}

Since we don't use amb anymore the execution time of the program has decreased tenfold. See the result for yourself: http://jsperf.com/amb-vs-foreach

Also I've finally created a demo fiddle of the above program: http://jsfiddle.net/Ug6Pb/

Aadit M Shah
  • 67,342
  • 26
  • 146
  • 271
1

i am sure there are faster formulas, but i was never that great at math, and this seems to work if i understand the problem correctly:

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 

i didn't bother to recursive-ize the function since you only need to go 4-in and a lispy invocation works, but if i had to go further, i'd want to write one additional outer wrapper to sandwich the calls...

dandavis
  • 14,821
  • 4
  • 34
  • 35
1

the core implementation of the backtracking algorithm is simple (see function doBacktrack below). Usually the complexity is in the details of the specific backtracking problem

the following is my implementation of a backtracking algorithm for your problem. it is based on the backtracking algorithm description in Steven Skiena's Algorithm Design Manual (or what I remember of it).

I've not added pruning to the algorithm (because it's already taken me longer than I thought it would do :) ) but if you want to improve its performance just add a reasonable implementation for the function done() to prevent continuing with the processing of candidates that can be inferred to not be viable solutions

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);
Francisco Meza
  • 845
  • 5
  • 8