2

I am trying to write an algorithm that will 'fill' a vehicle to it's capacity based on different input combinations. The problem is solved, but it is far too slow to be usable for a greater amount of combinations.

For example: I have a vehicle with a capacity of 10, I have different combinations of types(attributes) of seats that can fill the vehicle differently(ambulatory, or normal seat capacity of 1 is default). Also, each combination that is not equal to 10(greater than 10 is removed) will be filled in with ambulatory capacity, or just a normal seat. Like so:

const a = [ {
 name: 'wheelchair',
 capacity: 0,
 total: 3
}, {
  name: 'walker',
  capacity: 2,
  total: 5
  }, {
  name: 'service animal',
  capacity: 2,
  total: 5
}];

It is also worth noting in the example above that the wheelchair can only be added 3 times per combination because it has a total of 3(max). The 0 capacity for the wheelchair is an example of a designated spot for this particular attribute that doesn't take up any of the other ambulatory seats.

I have tried a few different approaches for this and my algorithm works fine for this particular combination, or even if I add a few more. But, if I add an attribute with a total of 10 and capacity of one, this will increase the total possibilities by an order of magnitude and slow down the algorithm immensely. In my approach I am finding the different permutations and then filtering out the duplicates to find the combinations, if there were a way to find only the combinations, perhaps it would reduce the calculation load, but I can't think of a way. I have a specific way the output needs to look, which is the output at the bottom however, I have control over the input and can change if necessary. Any ideas or help is greatly appreciated.

This code was modified from this answer https://stackoverflow.com/a/21640840/6025994


  // the power set of [] is [[]]
  if(arr.length === 0) {
      return [[]];
  }

  // remove and remember the last element of the array
  var lastElement = arr.pop();

  // take the powerset of the rest of the array
  var restPowerset = powerSet(arr);


  // for each set in the power set of arr minus its last element,
  // include that set in the powerset of arr both with and without
  // the last element of arr
  var powerset = [];
  for(var i = 0, len = restPowerset.length; i < len; i++) {

      var set = restPowerset[i];

      // without last element
      powerset.push(set);

      // with last element
      set = set.slice(); // create a new array that's a copy of set
      set.push(lastElement);
      powerset.push(set);
  }

  return powerset;
};

var subsetsLessThan = function (arr, number) {
  // all subsets of arr
  var powerset = powerSet(arr);

  // subsets summing less than or equal to number
  var subsets = new Set();
  for(var i = 0, len = powerset.length; i < len; i++) {

      var subset = powerset[i];

      var sum = 0;
      const newObject = {};
      for(var j = 0, len2 = subset.length; j < len2; j++) {
          if (newObject[subset[j].name]) {
            newObject[subset[j].name]++;
          } else {
            newObject[subset[j].name] = 1;
          }
          sum += subset[j].seat;
      }
      const difference = number - sum;

      newObject.ambulatory = difference;

      if(sum <= number) {
          subsets.add(JSON.stringify(newObject));
      }
  }

  return [...subsets].map(subset => JSON.parse(subset));
};

const a = [{
  name: 'grocery',
  capacity: 2,
  total: 5
}, {
  name: 'wheelchair',
  capacity: 0,
  total: 3
}];
const hrStart = process.hrtime();
const array = [];

for (let i = 0, len = a.length; i < len; i++) {
  for (let tot = 0, len2 = a[i].total; tot < len2; tot++) {
    array.push({
      name: a[i].name,
      seat: a[i].capacity
    });
  }
}
const combinations = subsetsLessThan(array, 10);
const hrEnd = process.hrtime(hrStart);
// for (const combination of combinations) {
//   console.log(combination);
// }

console.info('Execution time (hr): %ds %dms', hrEnd[0], hrEnd[1] / 1000000)

Expected results are all combinations of results passed in to be less than vehicle capacity, so it is a combination less than sum algorithm essentially. For instance, the expected result of the code I have posted would be -->

[{"ambulatory":10},{"wheelchair":1,"ambulatory":10},{"wheelchair":2,"ambulatory":10},{"wheelchair":3,"ambulatory":10},{"grocery":1,"ambulatory":8},{"grocery":1,"wheelchair":1,"ambulatory":8},{"grocery":1,"wheelchair":2,"ambulatory":8},{"grocery":1,"wheelchair":3,"ambulatory":8},{"grocery":2,"ambulatory":6},{"grocery":2,"wheelchair":1,"ambulatory":6},{"grocery":2,"wheelchair":2,"ambulatory":6},{"grocery":2,"wheelchair":3,"ambulatory":6},{"grocery":3,"ambulatory":4},{"grocery":3,"wheelchair":1,"ambulatory":4},{"grocery":3,"wheelchair":2,"ambulatory":4},{"grocery":3,"wheelchair":3,"ambulatory":4},{"grocery":4,"ambulatory":2},{"grocery":4,"wheelchair":1,"ambulatory":2},{"grocery":4,"wheelchair":2,"ambulatory":2},{"grocery":4,"wheelchair":3,"ambulatory":2},{"grocery":5,"ambulatory":0},{"grocery":5,"wheelchair":1,"ambulatory":0},{"grocery":5,"wheelchair":2,"ambulatory":0},{"grocery":5,"wheelchair":3,"ambulatory":0}]

ntraylor
  • 53
  • 6
  • Why is the capacity of a wheelchair 0 ? Shouldn't it be 1 ? – Jonas Wilms Jun 21 '19 at 12:43
  • That particular input is an example of a designated wheelchair area, on a bus for instance. when the capacity is 0 it doesn't take up any other ambulatory seats. – ntraylor Jun 21 '19 at 12:50
  • But then an unlimited amount of them could be added as `0 * Infinity < 10` ? – Jonas Wilms Jun 21 '19 at 12:52
  • That's fair, I forgot to add it in my question, but the total of the object makes that impossible. For example the total wheelchairs in this example is 3, so there can only ever be 3. I've updated the question to reflect that scenario. – ntraylor Jun 21 '19 at 12:54
  • Ah okay, but you remove that `total` before doing the calculations. I'll edit my answer to reflect that. – Jonas Wilms Jun 21 '19 at 12:55
  • The only interesting question today (under the js tag) and yet it only received an upvote ... – Jonas Wilms Jun 21 '19 at 12:59

1 Answers1

2

There is one trick you can use to improve your algorithm, called backtracking: If you reach an impossible path, e.g. 5 -> 6, then you don't have to continue searching there, as 5 + 6 is already greater 10. Through that you can eliminate a lot of combinations.

   function* combineMax([current, ...rest], max, previous = {}) {
     // Base Case:  if there are no items left to place, end the recursion here
     if(!current) { 
       // If the maximum is reached exactly, then this a valid solution, yield it up
       if(!max) yield previous; 
       return;
     }

     // if the "max" left is e.g. 8, then the grocery with "seat" being 2 can only fit in 4 times at max, therefore loop from 0 to 4
     for(let amount = 0; (!current.seat || amount <= max / current.seat) && amount <= current.total; amount++) {
       // The recursive call
       yield* combineMax(
        rest, // exclude the current item as that was  used already
        max - amount * current.seat, // e.g. max was 10, we take "seat: 2" 3 times, then the max left is "10 - 2 * 3"
        { ...previous, [current.name]: amount } // add the current amount
       );
     }
   }

   const result = [...combineMax([
    { name: 'grocery', seat: 2, total: Infinity }, 
    { name: 'wheelchair', seat: 0, total: 3 },
    { name: 'ambulatory equipment', seat: 1, total: Infinity },
     //...
   ], 10)];
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • This is brilliant! Thank you. Although, the result seems to be missing a few combinations. Namely: ```{ "grocery": 5, "ambulatory": 0 }, { "grocery": 5, "wheelchair": 1, "ambulatory": 0 }, { "grocery": 5, "wheelchair": 2, "ambulatory": 0 }, { "grocery": 5, "wheelchair": 3, "ambulatory": 0 }``` – ntraylor Jun 21 '19 at 13:15
  • results are perfect now. – ntraylor Jun 21 '19 at 13:26