Add knapsack problem.

This commit is contained in:
Oleksii Trekhleb 2018-04-30 12:37:12 +03:00
parent 1c3cecf318
commit d20d0c8d4f
4 changed files with 302 additions and 0 deletions

View 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);
}
}

View 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}`;
}
}

View File

@ -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');
});
});

View File

@ -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);
});
});