Выходной массив одновременно возможных уникальных комбинаций элементов

Мое приложение ссылается на объект базы данных, который действует как каталог. Это каталог предметов, которые можно создать, если у пользователя есть необходимые компоненты. Вот небольшой образец каталога:

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>

вам нужны все возможные комбинации, и если да, то каков желаемый результат?

r3wt 21.12.2020 23:33

Сначала прекратите использовать lodash.

Akihito KIRISAKI 21.12.2020 23:33

@ r3wt речь идет не о возможных комбинациях. itemCatalog дает нам возможные комбинации. Я хочу знать, какие комбинации возможны одновременно, учитывая компоненты пользователя в myComponents. Представьте, что каждый из этих компонентов потребляется при создании предмета. Ожидаемый результат с данными компонентами указан в вопросе.

J. Adam Connor 21.12.2020 23:37

@AkihitoKIRISAKI, не могли бы вы объяснить, почему?

J. Adam Connor 21.12.2020 23:38

@J.AdamConnor J.AdamConnor затрудняет чтение кода для тех, кто не знаком с lodash. это также обычно приводит к неэффективному коду. наконец, lodash — это пережиток того времени, когда в javascript не было множества функций для массивов и объектов. большинство проблем, которые он может решить, в наши дни можно решить на чистом js.

r3wt 21.12.2020 23:40

Вы уже разместили этот вопрос здесь: stackoverflow.com/questions/65319596/…

pilchard 21.12.2020 23:40

@pilchard Формулировка этого вопроса была слишком общей и приводила к ответам, которые больше касались манипуляций со строками, чем анализа данных.

J. Adam Connor 21.12.2020 23:42

На самом деле я только что проверил свой ответ на ваших данных здесь, и он работает (хотя вы не указали, как обрабатывать дубликаты в списке).

pilchard 21.12.2020 23:45

@J.AdamConnor lodash слишком велик, чтобы управлять только структурой данных. Многие их методы могут быть реализованы стандартным API браузера.

Akihito KIRISAKI 21.12.2020 23:45

@pilchard вы имеете в виду дубликаты в выводе или дубликаты в myComponents?

J. Adam Connor 21.12.2020 23:47

Отвечает ли это на ваш вопрос? Создание массива уникальных комбинаций

pilchard 21.12.2020 23:53

Я поднял связанный с этим вопрос об алгоритме.

Scott Sauyet 22.12.2020 22:37
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
12
126
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Я думаю, что для получения вывода требуется несколько функций.

а) Функция, которая говорит: при наличии необходимых компонентов (для предмета) существуют ли запасы для изготовления предмета?

// 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"]
]

Для вывода named, например.

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 kk == 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() не нужен.

J. Adam Connor 22.12.2020 07:02

Один из способов заставить это производить правильный вывод без необходимости вручную вводить k — использовать Math.floor(myComponents.length /2) во втором параметре для chooseCombos()

J. Adam Connor 22.12.2020 07:31

Привет. Да, непонятна связь между количеством комплектующих и количеством позиций в каталоге. Для примера данных есть 7 элементов, 5 компонентов, и выходные данные представлены парами. Если вывод должен состоять из троек, где количество компонентов равно 6 или 7, тогда да, это сработает. Честно говоря, я не был уверен, есть ли у вас каждый растущий инвентарь и все растущие предметы для изготовления и т. д. Похоже, у вас есть полулинейная зависимость между инвентарем и созданием предметов?

Robin Mackenzie 22.12.2020 09:28

Да, поэтому для всех элементов каталога всегда требуется только два компонента. Поэтому количество одновременно создаваемых элементов всегда будет вдвое больше количества компонентов.

J. Adam Connor 22.12.2020 10:29

теперь правильнее писать acc.get(v) || 0 с помощью нулевого оператора объединения, acc.get(v) ?? 0. это не влияет на эту конкретную программу, но someExpression || someDefault сталкивается с некоторыми проблемами, когда someExpression присутствует, но также ложно.

Mulan 23.12.2020 17:22
Ответ принят как подходящий

(Примечание: ниже приведена обновленная версия, отвечающая дополнительным требованиям.)

Вот еще один подход, основанный на простом рекурсивном алгоритме: мы смотрим на первый элемент в списке и, если мы можем это сделать, мы объединяем его с каждым из результатов, сформированных путем вызова функции с остатком целей и списком компонентов за вычетом тех, которые необходимы для изготовления этого предмета. Если мы не можем сделать первый элемент, мы просто повторяемся с остальными элементами и полным списком компонентов. Рекурсия достигает дна, когда список элементов пуст. Чтобы использовать это, мы сначала преобразуем ваш каталог в массив с помощью 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 теперь работает следующим образом: если у нас нет элементов на входе, мы возвращаем массив, содержащий только пустой массив. Если мы это делаем, мы фокусируемся на первом компоненте, подсчитываем, сколько раз он вписывается в наш список компонентов, затем для каждого значения от этого числа до нуля мы выбираем включение такого количества копий элемента и повторяемся для оставшихся. элементы и компоненты, уменьшенные на это количество копий списка компонентов элемента. Мы просто возвращаем сглаженную версию этого результата.

Вполне вероятно, что этот код можно упростить. Я начал с того, что у нас уже было, и изменил оттуда. Часто это не приводит к таким хорошим результатам, как если бы мы планировали это с самого начала. Но часто у нас нет такой роскоши.

актуально: вы когда-нибудь играли в лигу легенд?

Mulan 23.12.2020 16:50

задача нахождения только максимальных подмножеств максимально интересна и максимально разочаровывает! собирается потратить больше времени на это. спасибо, что поделился всем этим, Скотт

Mulan 23.12.2020 16:51

@Спасибо: Нет, мое представление о видеоиграх заглохло вскоре после выхода Space Invaders и Asteroids. Если я играю в игру на компьютере, то это, скорее всего, онлайн-шахматы.

Scott Sauyet 23.12.2020 16:55

@Спасибо: моя maximum реализация O (n^2), и мне интересно, есть ли более эффективная. Я не продумывал здесь выполнение основной функции. В то время как максимальная часть интересна, меня больше заинтриговали идеи в collect. Я написал более общую версию, которая использует две дополнительные функции: одну для получения components из элемента, другую для получения представления для сбора. Таким образом, приведенное выше будет const collect = genericCollect(x => x.components, x => x.name) с очевидной реализацией. Это также решает связанный с этим вопрос, но не стоит публиковать.

Scott Sauyet 23.12.2020 17:01

Изначально я писал форму, похожую на 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 в своем ответе, но это не идеально...

Mulan 23.12.2020 17:17

что касается maximize, я боюсь, что мы не увидим улучшения без того более способного _Set или MultiSet, о котором мы говорили. Я уже могу сказать, что эта проблема будет преследовать меня в ближайшие недели.

Mulan 23.12.2020 17:19

@Спасибо: сейчас горяча другая проблема, но я с нетерпением жду вашего решения!

Scott Sauyet 23.12.2020 17:41

@Спасибо: да! спасибо за ваши подробные и вдумчивые ответы, ребята. Я копаюсь в обоих прямо сейчас.

J. Adam Connor 24.12.2020 02:56

Я принял это как ответ, так как он более полный, чем первоначально принятый ответ.

J. Adam Connor 25.12.2020 07:19

Я только что понял, что это решение не учитывает возможные дубликаты элементов: например, если у вас есть ["Chain Vest", "Chain Vest", "Chain Vest", "Chain Vest"], оно должно производить ["Bramble Vest", "Bramble Vest"]. Но он производит только ["Bramble Vest"]. Я только начал искать решение этой проблемы, но я подумал, что дам вам возможность исследовать себя.

J. Adam Connor 04.01.2021 10:13

@J.AdamConnor: Ааа, я не думал, что это требование. (Я ничего не знаю об игре.) Я не уверен, что есть простой патч для этой техники. Я постараюсь подумать об этом в ближайшее время.

Scott Sauyet 04.01.2021 15:11

@J.AdamConnor: Ну, это съело мой обеденный перерыв. Но у меня есть обновленная версия.

Scott Sauyet 04.01.2021 19:26

Спасибо. Я не думаю, что концепция здесь специфична для приложения. Учитывая исчерпывающий список ресурсов и «книгу рецептов», вполне возможно, что вы всегда сможете приготовить два или более одинаковых рецепта, если у вас достаточно ресурсов. Я даже удивлен, что не заметил этого раньше. Я предлагаю сделать обновленный раздел единственным ответом, поскольку я не верю, что он каким-либо образом специально адаптирован для моего конкретного варианта использования.

J. Adam Connor 06.01.2021 17:04

@J.AdamConnor: я думаю, что у обоих есть разумное применение. И есть еще одно обобщение, когда имеется ограниченное количество каждого предмета (в наличии достаточно унобтаниума только для изготовления двух Ежевичных жилетов, даже если у вас есть четырнадцать кольчужных жилетов, и в противном случае вы могли бы сделать семь из них). Исходный случай, который я решил, аналогичен : Вот список доступных предметов, по одному на Ежевичный жилет, Ангел-хранитель, ..., и ваш инвентарь для обмена: четыре кольчужных жилета, один лучший меч... Фактически, это третья общая версия (которую я сейчас не пытаюсь) можно использовать для замены любой версии выше.

Scott Sauyet 06.01.2021 17:20

принятие желаемого за действительное

Ваш вопрос привлек мое внимание, и я начал решать его с помощью одного из моих любимых приемов программирования. Принятие желаемого за действительное — это способ программирования, при котором вы пишете свою программу и желаете, чтобы она была правдой.

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' ]` в одном и том же выводе?

Scott Sauyet 23.12.2020 23:56

А мне очень нравится джинн подход!

Scott Sauyet 23.12.2020 23:56

я приветствую честный отзыв. формы 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). теперь я думаю, что монада-продолжение могла бы очистить некоторые вирусные щупальца, которые вы описываете 😅

Mulan 24.12.2020 00:21

и вы правы, это решение не отличает [a, b] от [b, a], еще одна проблема, которую я должен решить!

Mulan 24.12.2020 00:22

Другие вопросы по теме