From d20d0c8d4f4914a90e0d212dc38fb8370442d343 Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Mon, 30 Apr 2018 12:37:12 +0300 Subject: [PATCH] Add knapsack problem. --- .../sets/knapsack-problem/Knapsack.js | 154 ++++++++++++++++++ .../sets/knapsack-problem/KnapsackItem.js | 27 +++ .../__test__/Knapsack.test.js | 89 ++++++++++ .../__test__/KnapsackItem.test.js | 32 ++++ 4 files changed, 302 insertions(+) create mode 100644 src/algorithms/sets/knapsack-problem/Knapsack.js create mode 100644 src/algorithms/sets/knapsack-problem/KnapsackItem.js create mode 100644 src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js create mode 100644 src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js diff --git a/src/algorithms/sets/knapsack-problem/Knapsack.js b/src/algorithms/sets/knapsack-problem/Knapsack.js new file mode 100644 index 00000000..112722c1 --- /dev/null +++ b/src/algorithms/sets/knapsack-problem/Knapsack.js @@ -0,0 +1,154 @@ +import MergeSort from '../../sorting/merge-sort/MergeSort'; + +export default class Knapsack { + /** + * @param {KnapsackItem[]} possibleItems + * @param {number} weightLimit + */ + constructor(possibleItems, weightLimit) { + this.selectedItems = []; + this.weightLimit = weightLimit; + this.possibleItems = possibleItems; + // We do two sorts because in case of equal weights but different values + // we need to take the most valuable items first. + this.sortPossibleItemsByValue(); + this.sortPossibleItemsByWeight(); + } + + sortPossibleItemsByWeight() { + // Sort possible items by their weight. + // We need them to be sorted in order to solve knapsack problem using + // Dynamic Programming approach. + this.possibleItems = new MergeSort({ + /** + * @var KnapsackItem itemA + * @var KnapsackItem itemB + */ + compareCallback: (itemA, itemB) => { + if (itemA.weight === itemB.weight) { + return 0; + } + + return itemA.weight < itemB.weight ? -1 : 1; + }, + }).sort(this.possibleItems); + } + + sortPossibleItemsByValue() { + // Sort possible items by their weight. + // We need them to be sorted in order to solve knapsack problem using + // Dynamic Programming approach. + this.possibleItems = new MergeSort({ + /** + * @var KnapsackItem itemA + * @var KnapsackItem itemB + */ + compareCallback: (itemA, itemB) => { + if (itemA.value === itemB.value) { + return 0; + } + + return itemA.value > itemB.value ? -1 : 1; + }, + }).sort(this.possibleItems); + } + + // Solve 0/1 knapsack problem using dynamic programming. + solveZeroOneKnapsackProblem() { + this.selectedItems = []; + + // Create knapsack values matrix. + const numberOfRows = this.possibleItems.length; + const numberOfColumns = this.weightLimit; + const knapsackMatrix = Array(numberOfRows).fill(null).map(() => { + return Array(numberOfColumns + 1).fill(null); + }); + + // Fill the first column with zeros since it would mean that there is + // no items we can add to knapsack in case if weight limitation is zero. + for (let itemIndex = 0; itemIndex < this.possibleItems.length; itemIndex += 1) { + knapsackMatrix[itemIndex][0] = 0; + } + + // Fill the first row with max possible values we would get by just adding + // or not adding the first item to the knapsack. + for (let weightIndex = 1; weightIndex <= this.weightLimit; weightIndex += 1) { + const itemIndex = 0; + const itemWeight = this.possibleItems[itemIndex].weight; + const itemValue = this.possibleItems[itemIndex].value; + knapsackMatrix[itemIndex][weightIndex] = itemWeight <= weightIndex ? itemValue : 0; + } + + // Go through combinations of how we may add items to knapsack and + // define what weight/value we would receive using Dynamic Programming + // approach. + for (let itemIndex = 1; itemIndex < this.possibleItems.length; itemIndex += 1) { + for (let weightIndex = 1; weightIndex <= this.weightLimit; weightIndex += 1) { + const currentItemWeight = this.possibleItems[itemIndex].weight; + const currentItemValue = this.possibleItems[itemIndex].value; + + if (currentItemWeight > weightIndex) { + // In case if item's weight is bigger then currently allowed weight + // then we can't add it to knapsack and the max possible value we can + // gain at the moment is the max value we got for previous item. + knapsackMatrix[itemIndex][weightIndex] = knapsackMatrix[itemIndex - 1][weightIndex]; + } else { + // Else we need to consider the max value we can gain at this point by adding + // current value or just by keeping the previous item for current weight. + knapsackMatrix[itemIndex][weightIndex] = Math.max( + currentItemValue + knapsackMatrix[itemIndex - 1][weightIndex - currentItemWeight], + knapsackMatrix[itemIndex - 1][weightIndex], + ); + } + } + } + + // Now let's trace back the knapsack matrix to see what items we're going to add + // to the knapsack. + let itemIndex = this.possibleItems.length - 1; + let weightIndex = this.weightLimit; + + while (itemIndex > 0) { + const currentItem = this.possibleItems[itemIndex]; + const prevItem = this.possibleItems[itemIndex - 1]; + + // Check if matrix value came from top (from previous item). + // In this case this would mean that we need to include previous item + // to the list of selected items. + if ( + knapsackMatrix[itemIndex][weightIndex] && + knapsackMatrix[itemIndex][weightIndex] === knapsackMatrix[itemIndex - 1][weightIndex] + ) { + // Check if there are several items with the same weight but with the different values. + // We need to add highest item in the matrix that is possible to get the highest value. + const prevSumValue = knapsackMatrix[itemIndex - 1][weightIndex]; + const prevPrevSumValue = knapsackMatrix[itemIndex - 2][weightIndex]; + if ( + !prevSumValue || + (prevSumValue && prevPrevSumValue !== prevSumValue) + ) { + this.selectedItems.push(prevItem); + } + } else if (knapsackMatrix[itemIndex - 1][weightIndex - currentItem.weight]) { + this.selectedItems.push(prevItem); + weightIndex -= currentItem.weight; + } + + itemIndex -= 1; + } + } + + get totalValue() { + /** @var {KnapsackItem} item */ + return this.selectedItems.reduce((accumulator, item) => { + return accumulator + item.totalValue; + }, 0); + } + + get totalWeight() { + /** @var {KnapsackItem} item */ + return this.selectedItems.reduce((accumulator, item) => { + return accumulator + item.totalWeight; + }, 0); + } +} diff --git a/src/algorithms/sets/knapsack-problem/KnapsackItem.js b/src/algorithms/sets/knapsack-problem/KnapsackItem.js new file mode 100644 index 00000000..79e38863 --- /dev/null +++ b/src/algorithms/sets/knapsack-problem/KnapsackItem.js @@ -0,0 +1,27 @@ +export default class KnapsackItem { + /** + * @param {Object} itemSettings - knapsack item settings, + * @param {number} itemSettings.value - value of the item. + * @param {number} itemSettings.weight - weight of the item. + * @param {number} itemSettings.itemsInStock - how many items are available to be added. + */ + constructor({ value, weight, itemsInStock = 1 }) { + this.value = value; + this.weight = weight; + this.itemsInStock = itemsInStock; + // Actual number of items that is going to be added to knapsack. + this.quantity = 1; + } + + get totalValue() { + return this.value * this.quantity; + } + + get totalWeight() { + return this.weight * this.quantity; + } + + toString() { + return `v${this.value} w${this.weight} x ${this.quantity}`; + } +} diff --git a/src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js b/src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js new file mode 100644 index 00000000..d222e19a --- /dev/null +++ b/src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js @@ -0,0 +1,89 @@ +import Knapsack from '../Knapsack'; +import KnapsackItem from '../KnapsackItem'; + +describe('Knapsack', () => { + it('should solve 0/1 knapsack problem', () => { + const possibleKnapsackItems = [ + new KnapsackItem({ value: 1, weight: 1 }), + new KnapsackItem({ value: 4, weight: 3 }), + new KnapsackItem({ value: 5, weight: 4 }), + new KnapsackItem({ value: 7, weight: 5 }), + ]; + + const maxKnapsackWeight = 7; + + const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight); + + knapsack.solveZeroOneKnapsackProblem(); + + expect(knapsack.totalValue).toBe(9); + expect(knapsack.totalWeight).toBe(7); + expect(knapsack.selectedItems.length).toBe(2); + expect(knapsack.selectedItems[0].toString()).toBe('v5 w4 x 1'); + expect(knapsack.selectedItems[1].toString()).toBe('v4 w3 x 1'); + }); + + it('should solve 0/1 knapsack problem regardless of items order', () => { + const possibleKnapsackItems = [ + new KnapsackItem({ value: 5, weight: 4 }), + new KnapsackItem({ value: 1, weight: 1 }), + new KnapsackItem({ value: 7, weight: 5 }), + new KnapsackItem({ value: 4, weight: 3 }), + ]; + + const maxKnapsackWeight = 7; + + const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight); + + knapsack.solveZeroOneKnapsackProblem(); + + expect(knapsack.totalValue).toBe(9); + expect(knapsack.totalWeight).toBe(7); + expect(knapsack.selectedItems.length).toBe(2); + expect(knapsack.selectedItems[0].toString()).toBe('v5 w4 x 1'); + expect(knapsack.selectedItems[1].toString()).toBe('v4 w3 x 1'); + }); + + it('should solve 0/1 knapsack problem with impossible items set', () => { + const possibleKnapsackItems = [ + new KnapsackItem({ value: 5, weight: 40 }), + new KnapsackItem({ value: 1, weight: 10 }), + new KnapsackItem({ value: 7, weight: 50 }), + new KnapsackItem({ value: 4, weight: 30 }), + ]; + + const maxKnapsackWeight = 7; + + const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight); + + knapsack.solveZeroOneKnapsackProblem(); + + expect(knapsack.totalValue).toBe(0); + expect(knapsack.totalWeight).toBe(0); + expect(knapsack.selectedItems.length).toBe(0); + }); + + it('should solve 0/1 knapsack problem with all equal weights', () => { + const possibleKnapsackItems = [ + new KnapsackItem({ value: 5, weight: 1 }), + new KnapsackItem({ value: 1, weight: 1 }), + new KnapsackItem({ value: 7, weight: 1 }), + new KnapsackItem({ value: 4, weight: 1 }), + new KnapsackItem({ value: 4, weight: 1 }), + new KnapsackItem({ value: 4, weight: 1 }), + ]; + + const maxKnapsackWeight = 3; + + const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight); + + knapsack.solveZeroOneKnapsackProblem(); + + expect(knapsack.totalValue).toBe(16); + expect(knapsack.totalWeight).toBe(3); + expect(knapsack.selectedItems.length).toBe(3); + expect(knapsack.selectedItems[0].toString()).toBe('v4 w1 x 1'); + expect(knapsack.selectedItems[1].toString()).toBe('v5 w1 x 1'); + expect(knapsack.selectedItems[2].toString()).toBe('v7 w1 x 1'); + }); +}); diff --git a/src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js b/src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js new file mode 100644 index 00000000..3a373796 --- /dev/null +++ b/src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js @@ -0,0 +1,32 @@ +import KnapsackItem from '../KnapsackItem'; + +describe('KnapsackItem', () => { + it('should create knapsack item and count its total weight and value', () => { + const item1 = new KnapsackItem({ value: 3, weight: 2 }); + + expect(item1.value).toBe(3); + expect(item1.weight).toBe(2); + expect(item1.quantity).toBe(1); + expect(item1.toString()).toBe('v3 w2 x 1'); + expect(item1.totalValue).toBe(3); + expect(item1.totalWeight).toBe(2); + + item1.quantity = 0; + + expect(item1.value).toBe(3); + expect(item1.weight).toBe(2); + expect(item1.quantity).toBe(0); + expect(item1.toString()).toBe('v3 w2 x 0'); + expect(item1.totalValue).toBe(0); + expect(item1.totalWeight).toBe(0); + + item1.quantity = 2; + + expect(item1.value).toBe(3); + expect(item1.weight).toBe(2); + expect(item1.quantity).toBe(2); + expect(item1.toString()).toBe('v3 w2 x 2'); + expect(item1.totalValue).toBe(6); + expect(item1.totalWeight).toBe(4); + }); +});