问题
My application references a database object that acts as a catalog. It's a catalog of items that can be crafted if the user has the necessary components. Here is a small sample of the catalog:
const itemCatalog = {
"bramble_vest" : {
"components" : [ "Chain Vest", "Chain Vest" ],
"name" : "Bramble Vest"
},
"guardian_angel" : {
"components" : [ "B.F. Sword", "Chain Vest" ],
"name" : "Guardian Angel"
},
"hextech_gunblade" : {
"components" : [ "B.F. Sword", "Needlessly Large Rod" ],
"name" : "Hextech Gunblade"
},
"locket_of_the_iron_solari" : {
"components" : [ "Chain Vest", "Needlessly Large Rod" ],
"name" : "Locket of the Iron Solari"
},
"morellonomicon" : {
"components" : [ "Giant's Belt", "Needlessly Large Rod" ],
"name" : "Morellonomicon"
},
"sunfire_cape" : {
"components" : [ "Chain Vest", "Giant's Belt" ],
"name" : "Sunfire Cape"
},
"zekes_herald" : {
"components" : [ "B.F. Sword", "Giant's Belt" ],
"name" : "Zeke's Herald"
}
}
When the user has the necessary components for any given item, the user can assemble that item. The user is awarded components arbitrarily and randomly, but how the user receives the components is not relevant to my question. Suffice to say that the user's components are put into an array on the client, which is then used to determine which items the user can assemble:
let myComponents = [
"B.F. Sword",
"Chain Vest",
"Giant's Belt",
"Chain Vest",
"Needlessly Large Rod"
]
I have written a block of code that determines which items are possible with the elements in myComponents
. That's fairly straightforward, even though it isn't particularly concise or stylish.
With the components listed in myComponents
all of the items in this sample of itemCatalog
are possible. However, they are not simultanesouly possible. The reason for this, of course, is that there are not enough components for all the items.
I need logic that can determine which items are simultaneously possible, given the components in myComponents
when referenced against itemCatalog
. The output should be an array of arrays. Each inner array would be a list of simultaneously possible catalog items. In this case, with the components currently in myComponents
it would look like this:
[
["Bramble Vest", "Hextech Gunblade"],
["Bramble Vest", "Morellonomicon"],
["Bramble Vest", "Zeke's Herald"],
["Guardian Angel", "Locket of the Iron Solari"],
["Guardian Angel", "Morellonomicon"],
["Guardian Angel", "Sunfire Cape"],
["Hextech Gunblade", "Sunfire Cape"],
["Locket of the Iron Solari", "Sunfire Cape"],
["Locket of the Iron Solari","Zeke's Herald"]
]
Below is my current logic. There's a lot of logging there to help sift through, but the main issue with the function buildSimultaneousItems()
is that once an item is checked against another item during iteration, those two items aren't checked again. I don't want to get into it too much, as I don't want to scare people away with information overload. It's all pretty straightforward, despite its ugliness. The main thing is that the expected output is above. Please feel free to ask questions.
// A catalog of items that can be assembled using components.
// The app uses this as a reference. This catalog is larger in the app, with many more items.
const itemCatalog = {
"bramble_vest" : {
"components" : [ "Chain Vest", "Chain Vest" ],
"name" : "Bramble Vest"
},
"guardian_angel" : {
"components" : [ "B.F. Sword", "Chain Vest" ],
"name" : "Guardian Angel"
},
"hextech_gunblade" : {
"components" : [ "B.F. Sword", "Needlessly Large Rod" ],
"name" : "Hextech Gunblade"
},
"locket_of_the_iron_solari" : {
"components" : [ "Chain Vest", "Needlessly Large Rod" ],
"name" : "Locket of the Iron Solari"
},
"morellonomicon" : {
"components" : [ "Giant's Belt", "Needlessly Large Rod" ],
"name" : "Morellonomicon"
},
"sunfire_cape" : {
"components" : [ "Chain Vest", "Giant's Belt" ],
"name" : "Sunfire Cape"
},
"zekes_herald" : {
"components" : [ "B.F. Sword", "Giant's Belt" ],
"name" : "Zeke's Herald"
}
}
// Components the user currently has
let myComponents = [
"B.F. Sword",
"Chain Vest",
"Giant's Belt",
"Chain Vest",
"Needlessly Large Rod"
]
// Returns array of possible items with provided component combinations (myComponents)
getPossibleItems = (arr) => {
let possibleItems = [];
for (const possItem in arr) {
if (doArraysMatch(arr[possItem].components, myComponents) == true) {
possibleItems.push(arr[possItem].name);
}
}
return possibleItems;
}
// Returns array of components at corresponding indices that correspond to the array returned in the above function
getPossItemsComponents = (arrA, arrB) => {
let possItemsComponents = []
for (const item in arrA) {
for (const combItem in arrB) {
console.log(arrB[combItem].name, ": ",arrB[combItem].components);
if (arrA[item] == arrB[combItem].name) {
possItemsComponents.push(arrB[combItem].components);
}
}
}
return possItemsComponents;
}
// Attempts to return an array of arrays. Each inner array is a list of items that can be
// assembled SIMULTANEOUSLY with the provided components (myComponents)
buildSimultaneousItems = () => {
let terms = [];
possibleItems = getPossibleItems(itemCatalog);
possibleItemsComponents = getPossItemsComponents(possibleItems, itemCatalog);
for (let i = 0; i < possibleItems.length; i++) {
let simultaneousItems = [];
let simultaneousItemsComponents = [];
simultaneousItems.push(possibleItems[i]);
console.log(JSON.stringify(possibleItems[i]), ": ", JSON.stringify(possibleItemsComponents[i]), "-----------------------")
simultaneousItemsComponents.push(possibleItemsComponents[i]);
//console.log(possibleItemsComponents[i][0])
for (let j = 0; j < possibleItems.length; j++) {
console.log("Does myItems", JSON.stringify(myComponents), " contain ",JSON.stringify(simultaneousItemsComponents[0].concat(possibleItemsComponents[j])), " for ", JSON.stringify(possibleItems[j]),this.containsAllItems(myComponents, simultaneousItemsComponents[0].concat(possibleItemsComponents[j])))
while (containsAllItems(myComponents, simultaneousItemsComponents[0].concat(possibleItemsComponents[j]))) {
simultaneousItems.push(possibleItems[j]);
console.log("Add ", JSON.stringify(possibleItemsComponents[j]), " to ", JSON.stringify(simultaneousItemsComponents[0]))
simultaneousItemsComponents[0].push(possibleItemsComponents[j][0]);
simultaneousItemsComponents[0].push(possibleItemsComponents[j][1]);
}
}
terms.push(simultaneousItems);
}
console.log(terms)
}
// Utility functions for comparing arrays -------------------------- //
doArraysMatch = (subset, superset) => {
const subsetCount = _.countBy(subset);
const supersetCount = _.countBy(superset);
return _.every(subsetCount, (count, value) => supersetCount[value] >= count);
}
containsAllItems = (arrA, arrB) => {
arrA.forEach(elA => {
if (arrB.includes(elA)) {
arrB.splice(arrB.indexOf(elA), 1);
}
})
if (arrB.length == 0) {
return true;
} else {
return false;
}
}
buildSimultaneousItems()
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.10/lodash.min.js"></script>
回答1:
Here's another approach, based on a simple recursive algorithm: We look at the first item in the list and if we can make it, we combine it with each of the results formed by calling the function with the remainder of the targets and the list of components less the ones required to make this item. If we can't make the first item, we just recur with the remainder of the items and the full list of components. The recursion bottoms out when the list of items is empty. To use this, we first convert your catalog to an array with Object.values
, since we don't need your object keys at all.
Once we've found our collections, we remove those that are strict subsets of another. That is because as well as the full values you want, the collect
function also gathers sets that could still contain others. With your above data, for instance, it collects [["Bramble Vest", "Hextech Gunblade"], ["Bramble Vest", "Morellonomicon"], ["Bramble Vest", "Zeke's Herald"], ["Bramble Vest"], ...]
(with a dozen more items, many containing single components.) Note that the fourth item, ["Bramble Vest"]
, is a strict subset of each of the three earlier ones. Using maximize
, we remove such subsets from the result.
This breakdown is useful because collect
expresses a useful algorithm on its own. (The implementation is still tied to your structure, using the components
and name
properties of each item, but it would not be difficult to make more generic.) That algorithm takes items
, a collection of collections of components, and components
, a collection of components, and returns a list of all possible collections of items
that could be made with that fixed list of components. Layering maximize
atop this gives us both your goal and this somewhat more general algorithm together. It's also a simpler algorithm, as far as I can tell. Perhaps someone can show me a simplification that does these two steps in one.
Here's an implementation:
// utility functions
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))))
// main function
const collect = ([x, ...xs], ys) =>
x == undefined
? [[]]
: canMake (x.components, ys)
? [
... collect (xs, dropEach (x .components, ys)) .map (coll => [x .name, ... coll]),
... collect (xs, ys)
]
: collect (xs, ys)
// public function
const simultaneousItems = (catalog, components) =>
maximize (collect (Object.values(catalog), components))
// sample data
const itemCatalog = { bramble_vest: {components : [ "Chain Vest", "Chain Vest" ], name : "Bramble Vest"}, guardian_angel: {components : [ "B.F. Sword", "Chain Vest" ], name : "Guardian Angel"}, hextech_gunblade: {components : [ "B.F. Sword", "Needlessly Large Rod" ], name : "Hextech Gunblade"}, locket_of_the_iron_solari: {components : [ "Chain Vest", "Needlessly Large Rod" ], name : "Locket of the Iron Solari"}, morellonomicon: {components : [ "Giant's Belt", "Needlessly Large Rod" ], name : "Morellonomicon"}, sunfire_cape: {components : [ "Chain Vest", "Giant's Belt" ], name : "Sunfire Cape"}, zekes_herald: {components : [ "B.F. Sword", "Giant's Belt" ], name : "Zeke's Herald"}}
const myComponents = ["B.F. Sword", "Chain Vest", "Giant's Belt", "Chain Vest", "Needlessly Large Rod"]
// demo
console .log (
simultaneousItems(itemCatalog, myComponents)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with a collection of utility functions:
dropFirst
removes the first occurrence of a value in an array of values. For instance,// v------------ First 'bar' dropFirst ('bar', ['foo', 'bar', 'baz', 'qux', 'bar', 'bar', 'corge']) //=> ["foo", "baz", "qux", "bar", "bar", "corge"] // ^---------------------------- now missing
dropEvery
extends this to remove each of a list of values from the main list, usingdropFirst
. For instance// will all be removed -----------v------v--------------------v dropEach (['bar', 'foo', 'bar'], ['foo', 'bar', 'baz', 'qux', 'bar', 'bar', 'corge']) //=> ["baz", "qux", "bar", "corge"]
canMake
reports whether we can make a list of components given the components at hand. For instance, using your sample list of components,canMake (['B.F. Sword', 'Chain Vest']) (myComponents) //=> true canMake (['B.F. Sword', 'Chain Vest', 'B.F. Sword']) (myComponents) //=> false
The first works because we have both the sword and the vest in our components. The second fails because we have only one sword.
There are numerous other techniques we could use to write this function. The recursive version fit with the rest of these functions, but we could also have compared counts of the relevant strings between the item's components and our available components.
(Note: these first three functions might have been much easier if we implemented a MultiSet/Bag type for both the items' components and our overall list of components. I won't try that here, but it might be worth investigating.)
isSubset
simply reports if one array of strings is a subset of another. Here we don't care about multiplicities, as our outputs don't include many copies of any one of our items.maximize
is discussed above. It removes from a list of collections those that are subsets of another in the list.
Then we have our central function,
collect
, which determines which subsets of our list of items can be made with our components. The algorithm is described above.
And our public wrapper function,
simultaneousItems
, which callsObject.values
on your input to put it into a useful format forcollect
, passes that and the list of components tocollect
, and then callsmaximize
on the results. This function yields the input I think you want.
This is the output from the data supplied:
[
["Bramble Vest", "Hextech Gunblade"],
["Bramble Vest", "Morellonomicon"],
["Bramble Vest", "Zeke's Herald"],
["Guardian Angel", "Locket of the Iron Solari"],
["Guardian Angel", "Morellonomicon"],
["Guardian Angel", "Sunfire Cape"],
["Hextech Gunblade", "Sunfire Cape"],
["Locket of the Iron Solari", "Sunfire Cape"],
["Locket of the Iron Solari", "Zeke's Herald"]
]
If we add a second "B.F. Sword" to our list of components, we get this list:
[
["Bramble Vest", "Hextech Gunblade", "Zeke's Herald"],
["Bramble Vest", "Morellonomicon"],
["Guardian Angel", "Hextech Gunblade", "Sunfire Cape"],
["Guardian Angel", "Locket of the Iron Solari", "Zeke's Herald"],
["Guardian Angel", "Morellonomicon"],
["Locket of the Iron Solari", "Sunfire Cape"]
]
It would be an interesting exercise to turn collect
into a more generic function that was still easy to use to define makeSimultaneous
. I would also not be surprised if that generic problem was a well-known problem with some optimized algorithms for it. I'd be curious about the algorithmic performance as well. But all that's for another day.
There is also a reasonable argument to be made for turning your output into a Set of Sets rather than an array of arrays. The ordering of the arrays is irrelevant, and in any such case, a Set is a more logical data structure. I probably wouldn't do this, as logical as it is, as I still find arrays easier to work with. But it's worth considering.
回答2:
wishful thinking
Your question caught my attention and I started hacking away at it with one of my favourite programming techniques. Wishful thinking is a way of programming where write your program and wish it were true -
const result =
make
( Object.values(itemCatalog)
, myComponents
)
console.log(result)
// ...
Above I wish I could call make
with items
to make and the available components
. A wish made is a wish granted... by you! -
const make = (items, components, i = 0, r = []) =>
i >= items.length
? [ r ]
: make1
( items[i]
, components
, (remainder, item) =>
make
( drop(items, i)
, remainder
, 0
, [ ...r, item ]
)
, _ => []
)
.concat(make(items, components, i + 1, r))
Along the way we make more wishes. The complexity of making a collection of items, make
, is separate from the complexity of making a single item, make1
. Wish granted -
const make1 = (item, components, ifTrue, ifFalse) =>
pick
( components
, item.components
, (remainder, _) => ifTrue(remainder, item.name)
, ifFalse
)
Get greedy now. While we're at it, I wish I could pick
elements from an array. Wish granted -
const pick = (t1, t2, ifTrue, ifFalse, i = 0) =>
i >= t2.length
? ifTrue(t1, t2)
: pick1
( t1
, t2[i]
, (r, _) =>
pick(r, t2, ifTrue, ifFalse, i + 1)
, ifFalse
)
Ask for a meter, take a parsec. Oh you need to pick a single item from an array? Wish granted by pick1
-
const pick1 = (t, q, ifTrue, ifFalse) =>
search
( t
, v => v === q
, (v, i) => ifTrue(drop(t, i), v)
, ifFalse
)
You're not one of those lame genies limited to fulfilling three wishes. If only I could search
the array...
Wish granted! -
const search = (t, f, ifTrue, ifFalse, i = 0) =>
i >= t.length
? ifFalse()
: Boolean(f(t[i]))
? ifTrue(t[i], i)
: search(t, f, ifTrue, ifFalse, i + 1)
Grants wishes for days -
const drop = (t, i, n = 1) =>
t.slice(0, i).concat(t.slice(i + n))
When the wishing ends and the wishes are fulfilled your program is complete. As if by magic -
// output
[ [ 'Bramble Vest', 'Hextech Gunblade' ]
, [ 'Bramble Vest', 'Morellonomicon' ]
, [ 'Bramble Vest', "Zeke's Herald" ]
, [ 'Bramble Vest' ]
, [ 'Guardian Angel', 'Locket of the Iron Solari' ]
, [ 'Guardian Angel', 'Morellonomicon' ]
, [ 'Guardian Angel', 'Sunfire Cape' ]
, [ 'Guardian Angel' ]
, [ 'Hextech Gunblade', 'Bramble Vest' ]
, [ 'Hextech Gunblade', 'Sunfire Cape' ]
, [ 'Hextech Gunblade' ]
, [ 'Locket of the Iron Solari', 'Guardian Angel' ]
, [ 'Locket of the Iron Solari', 'Sunfire Cape' ]
, [ 'Locket of the Iron Solari', "Zeke's Herald" ]
, [ 'Locket of the Iron Solari' ]
, [ 'Morellonomicon', 'Bramble Vest' ]
, [ 'Morellonomicon', 'Guardian Angel' ]
, [ 'Morellonomicon' ]
, [ 'Sunfire Cape', 'Guardian Angel' ]
, [ 'Sunfire Cape', 'Hextech Gunblade' ]
, [ 'Sunfire Cape', 'Locket of the Iron Solari' ]
, [ 'Sunfire Cape' ]
, [ "Zeke's Herald", 'Bramble Vest' ]
, [ "Zeke's Herald", 'Locket of the Iron Solari' ]
, [ "Zeke's Herald" ]
, []
]
The output for make
here shows all possible buying options. Writing maximal subsets is real head-scratcher. I'm going to pore over Scott's notes now...
write modules
Organising your code is an important hygienic exercise. Bundling function in modules promotes code reuse and gives us a manageable barrier of abstraction.
Many of the functions we wrote are generic and can be used on any array. Let's put them in a module called arr
-
// arr.js
const drop = (t, i, n = 1) =>
t.slice(0, i).concat(t.slice(i + n))
const pick1 = (t, q, ifTrue, ifFalse) =>
search
( t
, v => v === q
, (v, i) => ifTrue(drop(t, i), v)
, ifFalse
)
const pick = (t1, t2, ifTrue, ifFalse, i = 0) =>
i >= t2.length
? ifTrue(t1, t2)
: pick1
( t1
, t2[i]
, (r, _) =>
pick(r, t2, ifTrue, ifFalse, i + 1)
, ifFalse
)
const search = (t, f, ifTrue, ifFalse, i = 0) =>
i >= t.length
? ifFalse()
: Boolean(f(t[i]))
? ifTrue(t[i], i)
: search(t, f, ifTrue, ifFalse, i + 1)
export { drop, pick, pick1, search }
We'll bundle the item crafting functions into a module called craft
. Notice how this module can depend on functionality provided by another module -
// craft.js
import { drop, pick } from "./arr.js"
const make1 = (item, components, ifTrue, ifFalse) =>
arr.pick
( components
, item.components
, (remainder, _) => ifTrue(remainder, item.name)
, ifFalse
)
const make = (items, components, i = 0, r = []) =>
i >= items.length
? [ r ]
: make1
( items[i]
, components
, (remainder, item) =>
make
( arr.drop(items, i)
, remainder
, 0
, [ ...r, item ]
)
, _ => []
)
.concat(make(items, components, i + 1, r))
export { make, make1 }
Finally write your main
module -
// main.js
import { make } from "./craft.js"
const itemCatalog = ...
const myComponents = ...
const result =
make
( Object.values(itemCatalog)
, myComponents
)
console.log(result)
// ...
回答3:
I think several functions are required to get the output.
a) A function that says: given required components (for an item), does inventory exists to make the item ?
// are required components (req) a subset of inventory (inv) ?
const canMakeItem = (req, inv) => req.every(r => inv.indexOf(r) > -1);
b) An 'n choose k' function to return an array of item combinations that are to be considered as 'simultaneously createable'. I used this one:
// choose k items from array
const chooseCombos = (arr, k, prefix=[]) => {
if (k == 0) return [prefix];
return arr.flatMap((v, i) =>
chooseCombos(arr.slice(i+1), k-1, [...prefix, v])
);
}
In your example output, we see pairs of items because this is what the example inputs of the item catalog and component list allow. Given a bigger item catalog and bigger component list then the output would be more than 'pairs' - I try and address that later on in the answer. Using an 'n choose k' function will ultimately allow for testing of combinations of 3, 4, more etc items from the catalog.
c) A function that can take the difference of two arrays (preserving duplicates) to keep track of remaining components as a set of items 'simultaneous creatability' is tested. I used this one:
// array difference with dupes
const remainingComponents = (a, b) => {
return a.filter(function(v) {
return !this.get(v) || !this.set(v, this.get(v) - 1);
}, b.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map() ));
}
d) My version of buildSimultaneousItems
:
// eliminate combos (arrs[n]) with insufficient inventory (inv) for all items in combo
const createableCombos = (arrs, inv) => {
return arrs.filter(arr => {
// we know arr[0] is possible so remove components
let currentComponents = remainingComponents(myComponents, itemCatalog[arr[0]].components);
// check subsequent array items
for (let i=1; i<arr.length; i++) {
let requiredComponents = itemCatalog[arr[i]].components;
if (!canMakeItem(requiredComponents, currentComponents)) {
// this combo cannot be made from available components
return false
} else {
// remove components from inventory for this item
currentComponents = remainingComponents(currentComponents, requiredComponents);
}
}
// we can make all the items in this combo!
return true;
});
}
The start point here is e.g.
[
[violin, guitar],
[guitar, trumpet],
[violin, trumpet]
]
In that all items of the array are to be tested, but not all are possible e.g. strings used on violin and not available for guitar. The chooseCombos
function should eliminate the duplicates e.g. [trumpet, violin]
.
For each array, assume the first item is createable and remove the required components from inventory. For the rest of the items, repeat the createability test and remove components. If at any point an item cannot be created, then return false
for that array and it will be filtered out from the result.
e) Putting it all together:
// eliminate single items not able to be made
// in the example all items can be created
let createableSingleItems = Object.keys(itemCatalog)
.filter(k => canMakeItem(itemCatalog[k].components, myComponents) );
// candidate pairs - pairs is n choose _2_
let candidatePairs = chooseCombos(createableSingleItems, 2, []);
let createablePairs = createableCombos (candidatePairs, myComponents)
// candidate triples - n choose _3_
let candidateTriples = chooseCombos(createableSingleItems, 3, []);
let createableTriples = createableCombos (candidateTriples, myComponents);
The array of createableSingleItems
as a start point will reduce redundant processing later and enable createableCombos
to 'know' that arr[0]
is 'createable'.
So choosing pairs from the item catalog (where each pair is known to be createable individually):
// candidate pairs - pairs is n choose _2_
let candidatePairs = chooseCombos(createableSingleItems, 2, []);
let createablePairs = createableCombos (candidatePairs, myComponents)
Produces this output:
[
["bramble_vest", "hextech_gunblade"],
["bramble_vest", "morellonomicon"],
["bramble_vest", "zekes_herald"],
["guardian_angel", "locket_of_the_iron_solari"],
["guardian_angel", "morellonomicon"],
["guardian_angel", "sunfire_cape"],
["hextech_gunblade", "sunfire_cape"],
["locket_of_the_iron_solari", "sunfire_cape"],
["locket_of_the_iron_solari", "zekes_herald"]
]
For the name
d output e.g.
createablePairs.map(a => a.map(b => itemCatalog[b].name));
Gives:
[
["Bramble Vest", "Hextech Gunblade"],
["Bramble Vest", "Morellonomicon"],
["Bramble Vest", "Zeke's Herald"],
["Guardian Angel", "Locket of the Iron Solari"],
["Guardian Angel", "Morellonomicon"],
["Guardian Angel", "Sunfire Cape"],
["Hextech Gunblade", "Sunfire Cape"],
["Locket of the Iron Solari", "Sunfire Cape"],
["Locket of the Iron Solari", "Zeke's Herald"]
]
Choosing triples (n choose 3) from the item catalog gives []
because we don't have enough inventory...
The maximum choice for n choose k
is k == n == max(catalog)
. For a very large catalog and a very large inventory, it may be optimisation is required for this method. For a very large catalog and a relatively small inventory, the preparatory step of createableSingleItems
may suffice.
const itemCatalog = {
"bramble_vest" : {
"components" : [ "Chain Vest", "Chain Vest" ],
"name" : "Bramble Vest"
},
"guardian_angel" : {
"components" : [ "B.F. Sword", "Chain Vest" ],
"name" : "Guardian Angel"
},
"hextech_gunblade" : {
"components" : [ "B.F. Sword", "Needlessly Large Rod" ],
"name" : "Hextech Gunblade"
},
"locket_of_the_iron_solari" : {
"components" : [ "Chain Vest", "Needlessly Large Rod" ],
"name" : "Locket of the Iron Solari"
},
"morellonomicon" : {
"components" : [ "Giant's Belt", "Needlessly Large Rod" ],
"name" : "Morellonomicon"
},
"sunfire_cape" : {
"components" : [ "Chain Vest", "Giant's Belt" ],
"name" : "Sunfire Cape"
},
"zekes_herald" : {
"components" : [ "B.F. Sword", "Giant's Belt" ],
"name" : "Zeke's Herald"
}
}
let myComponents = [
"B.F. Sword",
"Chain Vest",
"Giant's Belt",
"Chain Vest",
"Needlessly Large Rod"
]
// are required components (req) a subset of inventory (inv) ?
const canMakeItem = (req, inv) => req.every(r => inv.indexOf(r) > -1);
// https://stackoverflow.com/questions/64414816/can-you-return-n-choose-k-combinations-in-javascript-using-array-flatmap
// choose k items from array
const chooseCombos = (arr, k, prefix=[]) => {
if (k == 0) return [prefix];
return arr.flatMap((v, i) =>
chooseCombos(arr.slice(i+1), k-1, [...prefix, v])
);
}
// https://stackoverflow.com/questions/39810641/get-difference-between-two-arrays-including-duplicates
// array difference with dupes
const remainingComponents = (a, b) => {
return a.filter(function(v) {
return !this.get(v) || !this.set(v, this.get(v) - 1);
}, b.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map() ));
}
// eliminate combos (arrs[n]) with insufficient inventory (inv) for all items in combo
const createableCombos = (arrs, inv) => {
return arrs.filter(arr => {
// we know arr[0] is possible so remove components
let currentComponents = remainingComponents(myComponents, itemCatalog[arr[0]].components);
// check subsequent array items
for (let i=1; i<arr.length; i++) {
let requiredComponents = itemCatalog[arr[i]].components;
if (!canMakeItem(requiredComponents, currentComponents)) {
// this combo cannot be made from available components
return false
} else {
// remove components from inventory for this item
currentComponents = remainingComponents(currentComponents, requiredComponents);
}
}
// we can make all the items in this combo!
return true;
});
}
// eliminate single items not able to be made
// in the example all items can be created
let createableSingleItems = Object.keys(itemCatalog)
.filter(k => canMakeItem(itemCatalog[k].components, myComponents) );
// candidate pairs - pairs is n choose _2_
let candidatePairs = chooseCombos(createableSingleItems, 2, []);
let createablePairs = createableCombos (candidatePairs, myComponents)
// candidate triples - n choose _3_
let candidateTriples = chooseCombos(createableSingleItems, 3, []);
let createableTriples = createableCombos (candidateTriples, myComponents);
// test
console.log("Pairs:");
console.log(createablePairs );
console.log("Triples:");
console.log(createableTriples);
来源:https://stackoverflow.com/questions/65400824/output-array-of-simultaneously-possible-unique-element-combinations