diff --git a/README.md b/README.md index ed6c94b235..38bb5cf6bd 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ ### Algorithms by Paradigm * **Greedy** + * [Unbound Knapsack Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/knapsack-problem) * **Divide and Conquer** * [Euclidean Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/math/euclidean-algorithm) - calculate the Greatest Common Divisor (GCD) * [Permutations](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/permutations) (with and without repetitions) @@ -95,7 +96,7 @@ * [Longest Common Substring](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/longest-common-substring) * [Longest Increasing subsequence](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/longest-increasing-subsequence) * [Shortest Common Supersequence](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/shortest-common-supersequence) - * [Knapsack Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/knapsack-problem) + * [0/1 Knapsack Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/knapsack-problem) * Maximum subarray * Maximum sum path * Integer Partition diff --git a/src/algorithms/sets/knapsack-problem/Knapsack.js b/src/algorithms/sets/knapsack-problem/Knapsack.js index 112722c11d..d23e12e0fb 100644 --- a/src/algorithms/sets/knapsack-problem/Knapsack.js +++ b/src/algorithms/sets/knapsack-problem/Knapsack.js @@ -9,16 +9,9 @@ export default class Knapsack { 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 @@ -35,9 +28,6 @@ export default class Knapsack { } 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 @@ -53,8 +43,30 @@ export default class Knapsack { }).sort(this.possibleItems); } - // Solve 0/1 knapsack problem using dynamic programming. + sortPossibleItemsByValuePerWeightRatio() { + this.possibleItems = new MergeSort({ + /** + * @var KnapsackItem itemA + * @var KnapsackItem itemB + */ + compareCallback: (itemA, itemB) => { + if (itemA.valuePerWeightRatio === itemB.valuePerWeightRatio) { + return 0; + } + + return itemA.valuePerWeightRatio > itemB.valuePerWeightRatio ? -1 : 1; + }, + }).sort(this.possibleItems); + } + + // Solve 0/1 knapsack problem + // Dynamic Programming approach. solveZeroOneKnapsackProblem() { + // 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(); + this.selectedItems = []; // Create knapsack values matrix. @@ -138,6 +150,29 @@ export default class Knapsack { } } + + // Solve unbounded knapsack problem. + // Greedy approach. + solveUnboundedKnapsackProblem() { + this.sortPossibleItemsByValue(); + this.sortPossibleItemsByValuePerWeightRatio(); + + for (let itemIndex = 0; itemIndex < this.possibleItems.length; itemIndex += 1) { + if (this.totalWeight < this.weightLimit) { + const currentItem = this.possibleItems[itemIndex]; + + // Detect how much of current items we can push to knapsack. + const availableWeight = this.weightLimit - this.totalWeight; + const maxPossibleItemsCount = Math.floor(availableWeight / currentItem.weight); + + if (maxPossibleItemsCount) { + currentItem.quantity = maxPossibleItemsCount; + this.selectedItems.push(currentItem); + } + } + } + } + get totalValue() { /** @var {KnapsackItem} item */ return this.selectedItems.reduce((accumulator, item) => { diff --git a/src/algorithms/sets/knapsack-problem/KnapsackItem.js b/src/algorithms/sets/knapsack-problem/KnapsackItem.js index 79e3886389..4c84eeee6f 100644 --- a/src/algorithms/sets/knapsack-problem/KnapsackItem.js +++ b/src/algorithms/sets/knapsack-problem/KnapsackItem.js @@ -21,6 +21,12 @@ export default class KnapsackItem { return this.weight * this.quantity; } + // This coefficient shows how valuable the 1 unit of weight is + // for current item. + get valuePerWeightRatio() { + return this.value / this.weight; + } + 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 index d222e19aac..9569908f45 100644 --- a/src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js +++ b/src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js @@ -86,4 +86,27 @@ describe('Knapsack', () => { expect(knapsack.selectedItems[1].toString()).toBe('v5 w1 x 1'); expect(knapsack.selectedItems[2].toString()).toBe('v7 w1 x 1'); }); + + it('should solve unbound knapsack problem', () => { + const possibleKnapsackItems = [ + new KnapsackItem({ value: 84, weight: 7 }), // v/w ratio is 12 + new KnapsackItem({ value: 5, weight: 2 }), // v/w ratio is 2.5 + new KnapsackItem({ value: 12, weight: 3 }), // v/w ratio is 4 + new KnapsackItem({ value: 10, weight: 1 }), // v/w ratio is 10 + new KnapsackItem({ value: 20, weight: 2 }), // v/w ratio is 10 + ]; + + const maxKnapsackWeight = 17; + + const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight); + + knapsack.solveUnboundedKnapsackProblem(); + + expect(knapsack.totalValue).toBe(84 + 84 + 20 + 10); + expect(knapsack.totalWeight).toBe(17); + expect(knapsack.selectedItems.length).toBe(3); + expect(knapsack.selectedItems[0].toString()).toBe('v84 w7 x 2'); + expect(knapsack.selectedItems[1].toString()).toBe('v20 w2 x 1'); + expect(knapsack.selectedItems[2].toString()).toBe('v10 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 index 3a373796f1..d8ea8b8b39 100644 --- a/src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js +++ b/src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js @@ -2,31 +2,34 @@ 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 }); + const knapsackItem = 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); + expect(knapsackItem.value).toBe(3); + expect(knapsackItem.weight).toBe(2); + expect(knapsackItem.quantity).toBe(1); + expect(knapsackItem.valuePerWeightRatio).toBe(1.5); + expect(knapsackItem.toString()).toBe('v3 w2 x 1'); + expect(knapsackItem.totalValue).toBe(3); + expect(knapsackItem.totalWeight).toBe(2); - item1.quantity = 0; + knapsackItem.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); + expect(knapsackItem.value).toBe(3); + expect(knapsackItem.weight).toBe(2); + expect(knapsackItem.quantity).toBe(0); + expect(knapsackItem.valuePerWeightRatio).toBe(1.5); + expect(knapsackItem.toString()).toBe('v3 w2 x 0'); + expect(knapsackItem.totalValue).toBe(0); + expect(knapsackItem.totalWeight).toBe(0); - item1.quantity = 2; + knapsackItem.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); + expect(knapsackItem.value).toBe(3); + expect(knapsackItem.weight).toBe(2); + expect(knapsackItem.quantity).toBe(2); + expect(knapsackItem.valuePerWeightRatio).toBe(1.5); + expect(knapsackItem.toString()).toBe('v3 w2 x 2'); + expect(knapsackItem.totalValue).toBe(6); + expect(knapsackItem.totalWeight).toBe(4); }); });