2

I have an array of components:

let components = ["a", "b", "c"];

It is possible to combine those components to make products; i.e., "a" + "b" = "ab".

I have a catalog of possible products:

let catalog = ["ab", "ac", "bc"];

Using something like lodash, we can create an array of possible products. Output array would look like ["ab", "ac", "bc"].

But the problem is that not all of those products could be built because once the component for one is used, it is no longer available for use in other products that require it.

I need an output that only shows possible outcomes. When there are only 3 components, and every catalog product requires 2, obviously it's not possible to create more than one product at a time. So an output that would express that each is either-or would look like [["ab"],["ac"],["bc"]]. But if you have 4 components it is possible to create more than one product at a time.

let components = ["a", "b", "c", "d"];

Possible catalog products with these components should look like [["ad", "cb"], ["ac", "bd"], ["ab", "cd"]].

I need some help with the logic for a function that outputs an array like the above.

Below is an example of a function that outputs possible catalog products with the provided components. But it doesn't achieve the requirements stated above.

let components = ["a", "b", "c", "e"];

let catalog = [["a", "b"], ["a", "c"], ["b", "c"], ["c", "d"]];

// Check if subset is included in superset (respecting duplicates).
const isSubset = (subset, superset) => {
  const subsetCount = _.countBy(subset)
  const supersetCount = _.countBy(superset)

  return _.every(subsetCount, (count, value) => supersetCount[value] >= count)
}

catalog.forEach(el => {
if (isSubset(catalog, components) ==  true) console.log(el)
});

// Filter all catalog items, that could be build by components
const matches = _.pickBy(catalog, (catalog, thing) => isSubset(catalog, components))

console.log(matches);
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>

EDIT: The result should be an array of possible catalog products whose components do not overlap/conflict with other catalog products in their respective arrays. So for example, if we have...

let components = ["a", "b", "c", "d", "e"];

let catalog = ["ab", "ac", "ad", "ae", "bc", "bd", "be", "cd", "ce", "de"];

... the output should be something like:

// [ ["ab", "cd"], ["ab", "de"], ["ac", "bd"], ["ac", "be"], ["ad", "ce"], ["ad, "bc"], ["ae", "bc"], ["ae", "cd"] ]

It's possible I've missed some there, but the point is that the output should be an array that expresses the either-or relationship of its inner arrays. The products' combination order doesn't matter. For example, "cd" == "dc". No element in the inner arrays should share a component. For example, we should not have ["ab", "ac"].

J. Adam Connor
  • 1,694
  • 3
  • 18
  • 35
  • do you have two arrays as input? what should be the result of it? if you have only one, what result do you expect from it? can you supply a lager array with the wanted result? – Nina Scholz Dec 16 '20 at 09:14
  • Hi, I've updated the post to clarify for you. See the edit. Thanks. – J. Adam Connor Dec 16 '20 at 09:32
  • why do you have `components`, if you got `catalog`? for what is it necessary? – Nina Scholz Dec 16 '20 at 09:36
  • The catalog exists on the database and one of of its elements would look more like: `”magic_wand” : { “required_components” : [ “Stick”, “Magic Stone” ], “name” : “Magic Wand” }`. It exists because the user completes an item by collecting required components, and it is referenced for that purpose. – J. Adam Connor Dec 16 '20 at 14:09
  • Does this answer your question? [Javascript - Generating all combinations of elements in a single array (in pairs)](https://stackoverflow.com/questions/43241174/javascript-generating-all-combinations-of-elements-in-a-single-array-in-pairs) (just generate all paired arrays and then `filter()`) – pilchard Dec 16 '20 at 14:22
  • I raised a [related question](https://stackoverflow.com/q/65415963/1243641) about the algorithm – Scott Sauyet Dec 22 '20 at 21:37

4 Answers4

1

Edited to accommodate the needs in your comment. This example uses a fairly straight forward recursive function to generate all unique combinations with the option to limit the maximum combination size (used to generate the catalog with only combinations of size 2).

The possibleProducts are a filtered array of all the unique combinations generated by passing the catalog array to the same function with no limits.

// generates unique combinations recursively
function combineRec(array, size, limit = false) {
  array = array.map(e => [e]);
  // max size of combinations.
  size = size || array.length;
 
  const acc =[];
  
  const spread = (s, arr) => arr.forEach((e, i, a) => {
    let 
      seed = [...s, ...e],
      segment = a.slice(i + 1);
    acc.push([...s, ...e]);
    if (s.length < size - 1 && segment.length > 0) {
      spread(seed, segment);
    }
  });

  array.forEach((e, i, a) => (
    spread(e, a.slice(i + 1))
  ))

  // if limit is true return only combinations of specified size.
  return r = limit ? acc.filter(({ length }) => length === size) : acc;
}

// filters out combinations with duplicate elements
const filterByElements = (array) => array.filter(arr =>
  !arr.some((a, i) => a.some(e => arr.slice(i + 1).flat().includes(e)))
);

const components = ['a', 'b', 'c', 'd', 'e', 'f'];

// generate catalog (return only combinations of 2)
let catalog = combineRec(components, 2, true);

// generate all possible catalog combinations (returns all)
let catalogCombinations = combineRec(catalog);

// filter to exlude duplicates and map combination arrays to strings.
let possibleProducts = filterByElements(catalogCombinations)
  .sort((a, b) => a.length - b.length)
  .map(p => p.map(c => c.join('')));

console.log('catalog: ', JSON.stringify(catalog));
console.log('possibleProducts: ', JSON.stringify(possibleProducts));
.as-console-wrapper { max-height: 100% !important; top: 0; }
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • This solution does not account for larger collections of components. For example, if you have `let components = ["a", "b", "c", "d", "e", "f"]`, the output's inner arrays would be greater than 2 elements in length. For example, you could have as an output `[ ["ab", "cd", "ef"], ["ac", "bf", "de"], ... ]`. Each inner array should show all *simulatneously*-possible catalog products. – J. Adam Connor Dec 16 '20 at 20:31
  • This answer produces the following output when we put the correct components in `myComponents` (replace Zeke's Herald with Chain Vest) and assemble the components in each inner array: `[ ["Zeke's Herald", "Locket of the Iron Solari"], ["Zeke's Herald", "Bramble Vest"], ["Zeke's Herald", "Locket of the Iron Solari"], ["Guardian Angel", "Locket of the Iron Solari"], ["Guardian Angel", "Morellonomicon"], ["Hextech Gunblade", "Sunfire Cloak"], ["Hextech Gunblade", "Sunfire Cloak"], ["Hextech Gunblade", "Bramble Vest"], ["Guardian Angel", "Morellonomicon"], ["Morellonomicon", "Bramble Vest"] ]` – J. Adam Connor Dec 21 '20 at 23:15
  • There are duplicate items in that array, and it's missing items. I appreciate your work, but I'm afraid you misunderstand the question. – J. Adam Connor Dec 21 '20 at 23:17
  • The goal here isn't simply to rearrange the strings in various ways. It is to find out which items can be assembled at the same time, given the unique components in the user's possession. My question here is more specific and might allow you to better understand the goal: https://stackoverflow.com/questions/65400824/output-array-of-simultaneously-possible-unique-element-combinations?noredirect=1#comment115626153_65400824 – J. Adam Connor Dec 21 '20 at 23:19
  • It is *only* about rearranging strings in various ways. – pilchard Dec 21 '20 at 23:40
  • I suppose you're right about that, but it has to be done according to the goal of assembling the items in the catalog and only assembling those that can be simultaneously assembled. This approach doesn't achieve that. – J. Adam Connor Dec 21 '20 at 23:43
1

Is below code satisfy you requirement?

  1. Shuffle the components array as many as posible;
  2. Use array's splice function to splice with length of requires;

let components = ["a", "b", "c", "d", "e", "f"];

function getCatalogProducts(components: string[], requires: number) {
  if (components.length < requires) return [];

  if (components.length === requires) return [components];

  let results: string[] = [];
  let cloneComponents = [...components];
  while (cloneComponents.length) {
    let cur = cloneComponents.splice(0, requires);
    let curStr = cur.sort().join('');
    results.push(curStr);
  }

  return results;
}

let shuffleComponentsArray = permute(components);

console.clear();
let results: any = [];
for (let i = 0; i < shuffleComponentsArray.length; i++) {
  let posibleCatalogProducts = getCatalogProducts(shuffleComponentsArray[i], 2);
  if (!results.some((item: any) => posibleCatalogProducts.sort().join('') === item.sort().join(''))) {
    results.push(posibleCatalogProducts);
  }
}

console.log(results);


// Below 3 functions are shuffle array related

function swap(arr: any, a: number, b: number) {
  var temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

function factorial(n: number) {
  var val = 1;
  for (var i = 1; i < n; i++) {
    val *= i;
  }
  return val;
}

function permute(perm: any) {
  let results = [];
  var total = factorial(perm.length);

  for (var j = 0, i = 0, inc = 1; j < total; j++, inc *= -1, i += inc) {

    for (; i < perm.length - 1 && i >= 0; i += inc) {
      results.push([...perm]);
      swap(perm, i, i + 1);
    }

    results.push([...perm]);

    if (inc === 1) {
      swap(perm, 0, 1);
    } else {
      swap(perm, perm.length - 1, perm.length - 2);
    }
  }

  return results;
}
Kevin Zhang
  • 1,012
  • 6
  • 14
0

In addition to the other solution, with this one, you can adjust the size of the products. (Assuming that COMPONENTS % SIZE_OF_EACH_PRODUCT === 0, in this case).

const COMPONENTS = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
const SIZE_OF_EACH_PRODUCT = 3;

const getCombinations = (inputArr, r) => {
  const res = [];
  const f = (inputArr, tmpArr, r, inputIndex, tmpIndex) => {
    if (tmpIndex == r) {
      let s = [];
      for (let x = 0; x < r; x++) s.push(tmpArr[x]);
      res.push(s);
      return;
    }
    if (inputIndex >= inputArr.length) return;
    tmpArr[tmpIndex] = inputArr[inputIndex];

    f(inputArr, tmpArr, r, inputIndex + 1, tmpIndex + 1);
    f(inputArr, tmpArr, r, inputIndex + 1, tmpIndex);
  };
  f(inputArr, [], r, 0, 0);
  return res;
};

const catalog = getCombinations(COMPONENTS, SIZE_OF_EACH_PRODUCT);

const uniqueCombinations = getCombinations(catalog, COMPONENTS.length / SIZE_OF_EACH_PRODUCT).filter((arrays) => {
  for (let i = 0; i < arrays.length; i++)
    for (let u = 0; u < arrays.length; u++) 
      if (!(i === u))
        if (arrays[i].some((char) => arrays[u].includes(char))) return false;
  return true;
});

console.log(uniqueCombinations);
Max Luchterhand
  • 448
  • 3
  • 10
  • See my comment to the answer below. Also, specifying the size of each product isn't necessary in my specific case, as all products will require exactly two components. – J. Adam Connor Dec 16 '20 at 20:33
0

Based on your other recent related question, I think I understand that what you want would be for something like this:

const catalog= ["ad", "aab", "ace", "cd", "ba", "bd", "adf", "def"]
const components = ["a", "b", "a", "c", "d", "e"]

simultaneousItems (catalog, components) 

to yield

[["ad", "ace"], ["ad", "ba"], ["aab", "cd"], ["ace", "ba"], ["ace", "bd"], ["cd", "ba"]]

where every array in the result is a maximal subset of catalog items that can be made together out of the components supplied. So, for instance, none of the output arrays contains "adf" or "def", since we have no "f" in the components, and none of them contain both "ad" and "aab" since we only have two "a"s in the component list.

If this is correct, then the following snippet would seem to do. The code explanation in that answer serves for this, with two modifications:

  • The collect function is simplified here to a more logical core

  • The wrapper simultaneousItems is altered to split the input strings into arrays and joint back the output ones. And it no longer has to handle extracting the values from your itemCatalog format.

const dropFirst = (x, xs, i = xs .indexOf (x)) =>
  i < 0 ? [... xs] : [... xs .slice (0, i), ... xs .slice (i + 1)]

const dropEach = ([x, ...xs], ys) => 
  x == undefined ? ys : dropEach (xs, dropFirst (x, ys))

const canMake = ([c, ...cs], comps) => 
  c == undefined ? true : comps .includes (c) ? canMake (cs, dropFirst (c, comps)) : false

const isSubset = (xs, ys) =>
  xs .every (x => ys .includes (x))

const maximize = (xs) => 
  xs .filter (x => ! (xs .some (y => x !== y && isSubset (x, y))))

const collect = ([x, ...xs], ys) => 
  x == undefined
    ? [[]]
  : canMake (x, ys)
    ? [
        ... collect (xs, dropEach (x, ys)) .map (coll => [x, ... coll]), 
        ... collect (xs, ys)
      ]
    : collect (xs, ys)

const simultaneousItems = (catalog, components) => 
  maximize (collect (catalog .map (s => s .split ('')), components)) 
    .map (g => g .map (ss => ss .join ('')))


const catalog = ["ad", "aab", "ace", "cd", "ba", "bd", "adf", "def"]

const components = ["a", "b", "a", "c", "d", "e"]

console .log (
  simultaneousItems (catalog, components)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103