mirror of
https://github.moeyy.xyz/https://github.com/trekhleb/javascript-algorithms.git
synced 2024-12-26 23:21:18 +08:00
Add knapsack problem.
This commit is contained in:
parent
1c3cecf318
commit
d20d0c8d4f
154
src/algorithms/sets/knapsack-problem/Knapsack.js
Normal file
154
src/algorithms/sets/knapsack-problem/Knapsack.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
27
src/algorithms/sets/knapsack-problem/KnapsackItem.js
Normal file
27
src/algorithms/sets/knapsack-problem/KnapsackItem.js
Normal file
@ -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}`;
|
||||
}
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user