Мое приложение ссылается на объект базы данных, который действует как каталог. Это каталог предметов, которые можно создать, если у пользователя есть необходимые компоненты. Вот небольшой образец каталога:
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"
]
Я написал блок кода, который определяет, какие элементы возможны с элементами в myComponents
. Это довольно просто, хотя и не особенно лаконично или стильно.
С компонентами, перечисленными в myComponents
, возможны все элементы в этом образце itemCatalog
. Однако они невозможны одновременно. Причина этого, конечно же, в том, что на все предметы не хватает комплектующих.
Мне нужна логика, которая может определить, какие элементы возможны одновременно, учитывая компоненты в myComponents
при сравнении с itemCatalog
. На выходе должен быть массив массивов. Каждый внутренний массив будет списком одновременно возможных элементов каталога. В этом случае с компонентами, находящимися в настоящее время в myComponents
, это будет выглядеть так:
[
["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"]
]
Ниже моя текущая логика. Там много журналов, которые помогают просеять, но основная проблема с функцией buildSimultaneousItems()
заключается в том, что после того, как элемент проверяется на другой элемент во время итерации, эти два элемента не проверяются снова. Я не хочу слишком углубляться в это, так как не хочу отпугивать людей информационной перегрузкой. Все довольно просто, несмотря на свою безобразность. Главное, чтобы ожидаемый результат был выше. Пожалуйста, не стесняйтесь задавать вопросы.
// 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.info(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.info(JSON.stringify(possibleItems[i]), ": ", JSON.stringify(possibleItemsComponents[i]), "-----------------------")
simultaneousItemsComponents.push(possibleItemsComponents[i]);
//console.info(possibleItemsComponents[i][0])
for (let j = 0; j < possibleItems.length; j++) {
console.info("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.info("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.info(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/[email protected]/lodash.min.js"></script>
Сначала прекратите использовать lodash.
@ r3wt речь идет не о возможных комбинациях. itemCatalog
дает нам возможные комбинации. Я хочу знать, какие комбинации возможны одновременно, учитывая компоненты пользователя в myComponents
. Представьте, что каждый из этих компонентов потребляется при создании предмета. Ожидаемый результат с данными компонентами указан в вопросе.
@AkihitoKIRISAKI, не могли бы вы объяснить, почему?
@J.AdamConnor J.AdamConnor затрудняет чтение кода для тех, кто не знаком с lodash. это также обычно приводит к неэффективному коду. наконец, lodash
— это пережиток того времени, когда в javascript не было множества функций для массивов и объектов. большинство проблем, которые он может решить, в наши дни можно решить на чистом js.
Вы уже разместили этот вопрос здесь: stackoverflow.com/questions/65319596/…
@pilchard Формулировка этого вопроса была слишком общей и приводила к ответам, которые больше касались манипуляций со строками, чем анализа данных.
На самом деле я только что проверил свой ответ на ваших данных здесь, и он работает (хотя вы не указали, как обрабатывать дубликаты в списке).
@J.AdamConnor lodash слишком велик, чтобы управлять только структурой данных. Многие их методы могут быть реализованы стандартным API браузера.
@pilchard вы имеете в виду дубликаты в выводе или дубликаты в myComponents
?
Отвечает ли это на ваш вопрос? Создание массива уникальных комбинаций
Я поднял связанный с этим вопрос об алгоритме.
Я думаю, что для получения вывода требуется несколько функций.
а) Функция, которая говорит: при наличии необходимых компонентов (для предмета) существуют ли запасы для изготовления предмета?
// are required components (req) a subset of inventory (inv) ?
const canMakeItem = (req, inv) => req.every(r => inv.indexOf(r) > -1);
b) Функция «n select k» для возврата массива комбинаций элементов, которые следует рассматривать как «одновременно создаваемые». Я использовал вот этот:
// 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])
);
}
В вашем примере вывода мы видим пары элементов, потому что это то, что позволяют примеры входных данных каталога элементов и списка компонентов. Учитывая больший каталог элементов и больший список компонентов, вывод будет больше, чем «пары» - я попытаюсь решить это позже в ответе. Использование функции «n select k» в конечном итоге позволит протестировать комбинации из 3, 4 и т. д. элементов из каталога.
c) Тестируется функция, которая может принимать разницу двух массивов (сохраняя дубликаты) для отслеживания оставшихся компонентов, поскольку набор элементов «одновременно создается». Я использовал вот этот:
// 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() ));
}
г) Моя версия 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;
});
}
Начальная точка здесь, например.
[
[violin, guitar],
[guitar, trumpet],
[violin, trumpet]
]
В том, что все элементы массива должны быть проверены, но не все возможны, например. струны, используемые на скрипке и недоступные для гитары. Функция chooseCombos
должна устранять дубликаты, например. [trumpet, violin]
.
Для каждого массива предположим, что первый элемент можно создать, и удалите необходимые компоненты из инвентаря. Для остальных элементов повторите тест на возможность создания и удалите компоненты. Если в какой-то момент элемент не может быть создан, верните false
для этого массива, и он будет отфильтрован из результата.
д) Собираем все вместе:
// 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);
Массив createableSingleItems
в качестве начальной точки позже уменьшит избыточную обработку и позволит createableCombos
«узнать», что arr[0]
можно «создать».
Итак, выбираем пары из каталога предметов (где известно, что каждую пару можно создать отдельно):
// candidate pairs - pairs is n choose _2_
let candidatePairs = chooseCombos(createableSingleItems, 2, []);
let createablePairs = createableCombos (candidatePairs, myComponents)
Производит этот вывод:
[
["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"]
]
Для вывода name
d, например.
createablePairs.map(a => a.map(b => itemCatalog[b].name));
Дает:
[
["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"]
]
Выбор троек (n выберите 3) из каталога предметов дает []
, потому что у нас недостаточно инвентаря...
Максимальный выбор для n choose k
— k == n == max(catalog)
. Для очень большого каталога и очень большого запаса может потребоваться оптимизация для этого метода. Для очень большого каталога и относительно небольшого запаса может быть достаточно подготовительного шага createableSingleItems
.
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.info("Pairs:");
console.info(createablePairs );
console.info("Triples:");
console.info(createableTriples);
Привет! Спасибо вам за вашу работу! Он определенно производит правильный вывод, но меня немного смущает одна вещь. Количество одновременно создаваемых предметов зависит от количества компонентов в инвентаре. Например, если в инвентаре 4 компонента, 2 предмета всегда будут создаваться одновременно. Если компонентов 6, то всегда одновременно возможно 3 предмета и т.д. Таким образом, второй параметр в ChooseCombos() не нужен.
Один из способов заставить это производить правильный вывод без необходимости вручную вводить k — использовать Math.floor(myComponents.length /2)
во втором параметре для chooseCombos()
Привет. Да, непонятна связь между количеством комплектующих и количеством позиций в каталоге. Для примера данных есть 7 элементов, 5 компонентов, и выходные данные представлены парами. Если вывод должен состоять из троек, где количество компонентов равно 6 или 7, тогда да, это сработает. Честно говоря, я не был уверен, есть ли у вас каждый растущий инвентарь и все растущие предметы для изготовления и т. д. Похоже, у вас есть полулинейная зависимость между инвентарем и созданием предметов?
Да, поэтому для всех элементов каталога всегда требуется только два компонента. Поэтому количество одновременно создаваемых элементов всегда будет вдвое больше количества компонентов.
теперь правильнее писать acc.get(v) || 0
с помощью нулевого оператора объединения, acc.get(v) ?? 0
. это не влияет на эту конкретную программу, но someExpression || someDefault
сталкивается с некоторыми проблемами, когда someExpression
присутствует, но также ложно.
(Примечание: ниже приведена обновленная версия, отвечающая дополнительным требованиям.)
Вот еще один подход, основанный на простом рекурсивном алгоритме: мы смотрим на первый элемент в списке и, если мы можем это сделать, мы объединяем его с каждым из результатов, сформированных путем вызова функции с остатком целей и списком компонентов за вычетом тех, которые необходимы для изготовления этого предмета. Если мы не можем сделать первый элемент, мы просто повторяемся с остальными элементами и полным списком компонентов. Рекурсия достигает дна, когда список элементов пуст. Чтобы использовать это, мы сначала преобразуем ваш каталог в массив с помощью Object.values
, так как нам вообще не нужны ваши ключи объектов.
Как только мы нашли наши коллекции, мы удаляем те, которые являются строгими подмножествами других. Это потому, что помимо полных значений, которые вы хотите, функция collect
также собирает наборы, которые могут содержать другие. Например, с приведенными выше данными он собирает [["Bramble Vest", "Hextech Gunblade"], ["Bramble Vest", "Morellonomicon"], ["Bramble Vest", "Zeke's Herald"], ["Bramble Vest"], ...]
(с еще дюжиной элементов, многие из которых содержат отдельные компоненты). Обратите внимание, что четвертый элемент, ["Bramble Vest"]
, является строгим подмножеством каждого из трех предыдущих. Используя maximize
, мы удаляем такие подмножества из результата.
Эта разбивка полезна, потому что collect
сам по себе выражает полезный алгоритм. (Реализация по-прежнему привязана к вашей структуре, используя свойства components
и name
каждого элемента, но нетрудно сделать ее более общей.) Этот алгоритм использует items
, набор наборов компонентов, и components
, набор компонентов и возвращает список всех возможных коллекций items
, которые могут быть созданы с этим фиксированным списком компонентов. Наложение maximize
поверх этого дает нам и вашу цель, и этот несколько более общий алгоритм вместе. Это также более простой алгоритм, насколько я могу судить. Возможно, кто-нибудь может показать мне упрощение, которое делает эти два шага за один.
Вот реализация:
// 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}
Начнем с набора служебных функций:
dropFirst
удаляет первое вхождение значения в массиве значений. Например,
// v------------ First 'bar'
dropFirst ('bar', ['foo', 'bar', 'baz', 'qux', 'bar', 'bar', 'corge'])
//=> ["foo", "baz", "qux", "bar", "bar", "corge"]
// ^---------------------------- now missing
dropEvery
расширяет это, чтобы удалить каждое из списка значений из основного списка, используя dropFirst
. Например
// will all be removed -----------v------v--------------------v
dropEach (['bar', 'foo', 'bar'], ['foo', 'bar', 'baz', 'qux', 'bar', 'bar', 'corge'])
//=> ["baz", "qux", "bar", "corge"]
canMake
сообщает, можем ли мы составить список компонентов, учитывая имеющиеся компоненты. Например, используя примерный список компонентов,
canMake (['B.F. Sword', 'Chain Vest']) (myComponents) //=> true
canMake (['B.F. Sword', 'Chain Vest', 'B.F. Sword']) (myComponents) //=> false
Первый работает, потому что в наших компонентах есть и меч, и жилет. Второй терпит неудачу, потому что у нас есть только один меч.
Есть множество других методов, которые мы могли бы использовать для написания этой функции. Рекурсивная версия соответствует остальным этим функциям, но мы также могли бы сравнить количество соответствующих строк между компонентами элемента и нашими доступными компонентами.
(Примечание: эти первые три функции могли бы быть намного проще, если бы мы реализовали тип MultiSet/Bag как для компонентов предметов, так и для нашего общего списка компонентов. Я не буду пробовать это здесь, но, возможно, это стоит изучить.)
isSubset
просто сообщает, является ли один массив строк подмножеством другого. Здесь мы не заботимся о множественности, так как наши выходные данные не включают много копий любого из наших элементов.
maximize
обсуждалось выше. Он удаляет из списка коллекций те, которые являются подмножествами других в списке.
Затем у нас есть наша центральная функция,
collect
, который определяет, какие подмножества нашего списка предметов можно сделать с помощью наших компонентов. Алгоритм описан выше.И наша публичная функция-обертка,
simultaneousItems
, который вызывает Object.values
на вашем входе, чтобы преобразовать его в полезный формат для collect
, передает это и список компонентов в collect
, а затем вызывает maximize
по результатам. Эта функция дает ввод, который, я думаю, вам нужен.Это вывод из предоставленных данных:
[
["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"]
]
Если мы добавим второй «B.F. Sword» в наш список компонентов, мы получим этот список:
[
["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"]
]
Было бы интересно превратить collect
в более общую функцию, которую было бы легко использовать для определения makeSimultaneous
. Я также не удивлюсь, если эта общая проблема будет хорошо известной проблемой с некоторыми оптимизированными алгоритмами для нее. Мне также было бы любопытно узнать об алгоритмической производительности. Но все это в другой день.
Также есть разумный аргумент в пользу превращения вашего вывода в набор наборов, а не в массив массивов. Порядок массивов не имеет значения, и в любом случае Set является более логичной структурой данных. Я, вероятно, не стал бы этого делать, как бы это ни было логично, поскольку мне все еще легче работать с массивами. Но это стоит учитывать.
В комментарии от OP описано дополнительное требование, которое не соответствует вышеизложенному: предметы, которые мы собираем, могут встречаться несколько раз. Это может быть понятно тому, кто знаком с рассматриваемой игрой, но приведенный выше код не справляется с этим.
Более того, это не простое решение. Дизайн collect
выше заключался в том, чтобы выбрать, собирать ли первый предоставленный предмет (если возможно) или нет, а затем повторять оставшиеся предметы и компоненты, оставшиеся после использования необходимых для предмета. Я не видел простого способа изменить это, чтобы разрешить несколько копий.
Итак, вот переписывание collect
со смесью существующих вспомогательных функций и новых для его поддержки:
// 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 dropEachRepeatedly = (n, xs, ys) =>
n == 0 ? ys : dropEach(xs, dropEachRepeatedly(n - 1, xs, ys))
const canMake = ([c, ...cs], comps) =>
c == undefined ? true : comps .includes (c) ? canMake (cs, dropFirst (c, comps)) : false
const howMany = (xs, ys) =>
canMake (xs, ys)
? 1 + howMany (xs, dropEach(xs, ys))
: 0
const range = (lo, hi) => Array .from ({length: hi - lo + 1}, (_, i) => i + lo)
const count = (xs) =>
xs .reduce ((a, x) => ((a[x] = (a[x] || 0) + 1), a), {})
const isMultiSubset = (xs, ys, cx = count (xs), cy = count (ys)) =>
Object .keys (cx) .every (x => cx [x] <= (cy [x] || 0))
const maximize = (xs) =>
xs .filter (x => ! (xs .some (y => x !== y && isMultiSubset (x, y))))
// main function
const collect = ([x, ...xs], ys) =>
x == undefined
? [[]]
: range (0, howMany (x.components, ys)) .reverse() .flatMap(
(n) => collect(xs, dropEachRepeatedly(n, x.components, ys)) .map (
coll => [...Array(n).fill(x.name), ...coll]
)
)
// 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"]
const myComponents = ["B.F. Sword", "Chain Vest", "Giant's Belt", "Chain Vest", "Chain Vest", "Needlessly Large Rod", "Chain Vest"]
// demo
console .log (
simultaneousItems (itemCatalog, myComponents)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
Добавив в наши компоненты еще два «Кольчуги», мы получили следующий результат:
[
["Bramble Vest", "Bramble Vest", "Hextech Gunblade"],
["Bramble Vest", "Bramble Vest", "Morellonomicon"],
["Bramble Vest", "Bramble Vest", "Zeke's Herald"],
["Bramble Vest", "Guardian Angel", "Locket of the Iron Solari"],
["Bramble Vest", "Guardian Angel", "Morellonomicon"],
["Bramble Vest", "Guardian Angel", "Sunfire Cape"],
["Bramble Vest", "Hextech Gunblade", "Sunfire Cape"],
["Bramble Vest", "Locket of the Iron Solari", "Sunfire Cape"],
["Bramble Vest", "Locket of the Iron Solari", "Zeke's Herald"],
["Guardian Angel", "Locket of the Iron Solari", "Sunfire Cape"]
]
Как и прежде, collect
является нашей основной функцией, а simultaneousItems
представляет собой простую оболочку, которая массирует ввод перед вызовом collect
, а затем запускает maximize
результат.
Многие из вспомогательных функций одинаковы. Изменилось только maximize
. Теперь он зависит от isMultiSubset
вместо isSubset
(который нам больше не нужен). Но у нас также есть несколько дополнительных помощников:
dropEachRepeatedly
удаляет несколько копий одного списка (здесь компоненты предмета) из другого (наши доступные компоненты)
howMany
сообщает, сколько копий одного списка можно сделать из членов другого
range
просто генерирует диапазон целых чисел. Например
range (3, 12) //=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
count
подсчитывает количество вхождений каждого значения в списке. Например
count (['a', 'b', 'a', 'c', 'b', 'd', 'b'])
//=> {a: 2, b: 3, c: 1, d: 1}
isMultiSubset
сообщает, является ли одно мультимножество (здесь выражено в виде массива, но порядок не имеет значения) подмножеством другого. Например, ['a' , 'b' , 'a']
не является множественным подмножеством ['a', 'b', 'c', 'd']
, поскольку в первом есть два 'a'
, а во втором — только один. Но это множественное подмножество ['a', 'b', 'c', 'a']
, так как 'a'
s и 'b'
достаточно, чтобы их можно было использовать. Поскольку теперь мы допускаем наличие нескольких копий компонентов в каждой выходной конфигурации, нам необходимо использовать это при максимизации.
Наша основная функция collect
теперь работает следующим образом: если у нас нет элементов на входе, мы возвращаем массив, содержащий только пустой массив. Если мы это делаем, мы фокусируемся на первом компоненте, подсчитываем, сколько раз он вписывается в наш список компонентов, затем для каждого значения от этого числа до нуля мы выбираем включение такого количества копий элемента и повторяемся для оставшихся. элементы и компоненты, уменьшенные на это количество копий списка компонентов элемента. Мы просто возвращаем сглаженную версию этого результата.
Вполне вероятно, что этот код можно упростить. Я начал с того, что у нас уже было, и изменил оттуда. Часто это не приводит к таким хорошим результатам, как если бы мы планировали это с самого начала. Но часто у нас нет такой роскоши.
актуально: вы когда-нибудь играли в лигу легенд?
задача нахождения только максимальных подмножеств максимально интересна и максимально разочаровывает! собирается потратить больше времени на это. спасибо, что поделился всем этим, Скотт
@Спасибо: Нет, мое представление о видеоиграх заглохло вскоре после выхода Space Invaders и Asteroids. Если я играю в игру на компьютере, то это, скорее всего, онлайн-шахматы.
@Спасибо: моя maximum
реализация O (n^2)
, и мне интересно, есть ли более эффективная. Я не продумывал здесь выполнение основной функции. В то время как максимальная часть интересна, меня больше заинтриговали идеи в collect
. Я написал более общую версию, которая использует две дополнительные функции: одну для получения components
из элемента, другую для получения представления для сбора. Таким образом, приведенное выше будет const collect = genericCollect(x => x.components, x => x.name)
с очевидной реализацией. Это также решает связанный с этим вопрос, но не стоит публиковать.
Изначально я писал форму, похожую на collect
, но мне нужен был способ удалить дублирование итераций, вызванное if (canMake()) ...
. То есть canMake
выполняет сканирование с использованием .includes
, а затем dropEach
повторяет сканирование с использованием рекурсии. Это похоже на return myMap.has("someKey") ? myMap.get("someKey") : someDefault
. Меня всегда беспокоило, что has
+get
выполняет в два раза больше работы, чем необходимо. Нулевой оператор объединения, return myMap.get("someKey") ?? someDefault
, работает, но желателен лучший API для Map и Set. Я обхожу это, используя CPS в своем ответе, но это не идеально...
что касается maximize
, я боюсь, что мы не увидим улучшения без того более способного _Set
или MultiSet
, о котором мы говорили. Я уже могу сказать, что эта проблема будет преследовать меня в ближайшие недели.
@Спасибо: сейчас горяча другая проблема, но я с нетерпением жду вашего решения!
@Спасибо: да! спасибо за ваши подробные и вдумчивые ответы, ребята. Я копаюсь в обоих прямо сейчас.
Я принял это как ответ, так как он более полный, чем первоначально принятый ответ.
Я только что понял, что это решение не учитывает возможные дубликаты элементов: например, если у вас есть ["Chain Vest", "Chain Vest", "Chain Vest", "Chain Vest"]
, оно должно производить ["Bramble Vest", "Bramble Vest"]
. Но он производит только ["Bramble Vest"]
. Я только начал искать решение этой проблемы, но я подумал, что дам вам возможность исследовать себя.
@J.AdamConnor: Ааа, я не думал, что это требование. (Я ничего не знаю об игре.) Я не уверен, что есть простой патч для этой техники. Я постараюсь подумать об этом в ближайшее время.
@J.AdamConnor: Ну, это съело мой обеденный перерыв. Но у меня есть обновленная версия.
Спасибо. Я не думаю, что концепция здесь специфична для приложения. Учитывая исчерпывающий список ресурсов и «книгу рецептов», вполне возможно, что вы всегда сможете приготовить два или более одинаковых рецепта, если у вас достаточно ресурсов. Я даже удивлен, что не заметил этого раньше. Я предлагаю сделать обновленный раздел единственным ответом, поскольку я не верю, что он каким-либо образом специально адаптирован для моего конкретного варианта использования.
@J.AdamConnor: я думаю, что у обоих есть разумное применение. И есть еще одно обобщение, когда имеется ограниченное количество каждого предмета (в наличии достаточно унобтаниума только для изготовления двух Ежевичных жилетов, даже если у вас есть четырнадцать кольчужных жилетов, и в противном случае вы могли бы сделать семь из них). Исходный случай, который я решил, аналогичен : Вот список доступных предметов, по одному на Ежевичный жилет, Ангел-хранитель, ..., и ваш инвентарь для обмена: четыре кольчужных жилета, один лучший меч... Фактически, это третья общая версия (которую я сейчас не пытаюсь) можно использовать для замены любой версии выше.
принятие желаемого за действительное
Ваш вопрос привлек мое внимание, и я начал решать его с помощью одного из моих любимых приемов программирования. Принятие желаемого за действительное — это способ программирования, при котором вы пишете свою программу и желаете, чтобы она была правдой.
const result =
make
( Object.values(itemCatalog)
, myComponents
)
console.info(result)
// ...
Выше я хотел бы вызвать make
с items
сделать и доступным components
. Загаданное желание - это желание, исполненное... тобой! -
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))
По пути мы загадываем больше желаний. Сложность создания набора предметов make
отличается от сложности создания одного предмета make1
. Желание исполнено -
const make1 = (item, components, ifTrue, ifFalse) =>
pick
( components
, item.components
, (remainder, _) => ifTrue(remainder, item.name)
, ifFalse
)
Стань жадным сейчас. Пока мы на этом, я бы хотел pick
элементы из массива. Желание исполнено -
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
)
Проси метр, возьми парсек. О, вам нужно выбрать один элемент из массива? Желание исполнено pick1
-
const pick1 = (t, q, ifTrue, ifFalse) =>
search
( t
, v => v === q
, (v, i) => ifTrue(drop(t, i), v)
, ifFalse
)
Ты не один из тех хромых джиннов, которые ограничиваются исполнением трех желаний. Если бы я только мог search
массив...
Желание исполнено! -
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)
Исполняет желания на дни -
const drop = (t, i, n = 1) =>
t.slice(0, i).concat(t.slice(i + n))
Когда желания заканчиваются и желания исполняются, ваша программа завершена. Как по волшебству -
// 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" ]
, []
]
Вывод для make
здесь показывает все возможные варианты покупки. Написание максимальных подмножеств — настоящая головная боль. Сейчас я собираюсь изучить записи Скотта...
писать модули
Организация кода — важное гигиеническое упражнение. Объединение функций в модули способствует повторному использованию кода и дает нам управляемый барьер абстракции.
Многие из написанных нами функций являются универсальными и могут использоваться для любого массива. Давайте поместим их в модуль под названием 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 }
Мы объединим функции создания предметов в модуль под названием craft
. Обратите внимание, как этот модуль может зависеть от функций, предоставляемых другим модулем.
// 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 }
Наконец напишите свой модуль main
-
// main.js
import { make } from "./craft.js"
const itemCatalog = ...
const myComponents = ...
const result =
make
( Object.values(itemCatalog)
, myComponents
)
console.info(result)
// ...
Очень интересно. Обычно, когда вы публикуете ответ на вопрос, на который я также ответил, я предпочитаю ваше мнение. На этот раз не так много. CPS мощен, но его вирусная природа, кажется, усложняет простые идеи. Тем не менее, мне тоже нравится видеть этот подход. Есть ли простая очистка, которая не даст, скажем, и [ 'Bramble Vest', 'Hextech Gunblade' ]
, и `[ 'Hextech Gunblade', 'Bramble Vest' ]` в одном и том же выводе?
А мне очень нравится джинн подход!
я приветствую честный отзыв. формы cps, представленные здесь, являются попыткой и декларативным API вокруг общего const r = someFunc(); if (someCondition(r)) ifTrue(r) else ifFalse(r)
, где r требует промежуточного присваивания, невозможного с одним функциональным выражением — очень типичен случай const r = someArray.find(f) if (r === undefined) notFound() else somethingWith(r)
. теперь я думаю, что монада-продолжение могла бы очистить некоторые вирусные щупальца, которые вы описываете 😅
и вы правы, это решение не отличает [a, b]
от [b, a]
, еще одна проблема, которую я должен решить!
вам нужны все возможные комбинации, и если да, то каков желаемый результат?