forked from trekhleb/javascript-algorithms
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}`; | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
src/algorithms/sets/knapsack-problem/__test__/Knapsack.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); |
32 changes: 32 additions & 0 deletions
32
src/algorithms/sets/knapsack-problem/__test__/KnapsackItem.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |