Skip to content

Commit d20d0c8

Browse files
committed
Add knapsack problem.
1 parent 1c3cecf commit d20d0c8

File tree

4 files changed

+302
-0
lines changed

4 files changed

+302
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import MergeSort from '../../sorting/merge-sort/MergeSort';
2+
3+
export default class Knapsack {
4+
/**
5+
* @param {KnapsackItem[]} possibleItems
6+
* @param {number} weightLimit
7+
*/
8+
constructor(possibleItems, weightLimit) {
9+
this.selectedItems = [];
10+
this.weightLimit = weightLimit;
11+
this.possibleItems = possibleItems;
12+
// We do two sorts because in case of equal weights but different values
13+
// we need to take the most valuable items first.
14+
this.sortPossibleItemsByValue();
15+
this.sortPossibleItemsByWeight();
16+
}
17+
18+
sortPossibleItemsByWeight() {
19+
// Sort possible items by their weight.
20+
// We need them to be sorted in order to solve knapsack problem using
21+
// Dynamic Programming approach.
22+
this.possibleItems = new MergeSort({
23+
/**
24+
* @var KnapsackItem itemA
25+
* @var KnapsackItem itemB
26+
*/
27+
compareCallback: (itemA, itemB) => {
28+
if (itemA.weight === itemB.weight) {
29+
return 0;
30+
}
31+
32+
return itemA.weight < itemB.weight ? -1 : 1;
33+
},
34+
}).sort(this.possibleItems);
35+
}
36+
37+
sortPossibleItemsByValue() {
38+
// Sort possible items by their weight.
39+
// We need them to be sorted in order to solve knapsack problem using
40+
// Dynamic Programming approach.
41+
this.possibleItems = new MergeSort({
42+
/**
43+
* @var KnapsackItem itemA
44+
* @var KnapsackItem itemB
45+
*/
46+
compareCallback: (itemA, itemB) => {
47+
if (itemA.value === itemB.value) {
48+
return 0;
49+
}
50+
51+
return itemA.value > itemB.value ? -1 : 1;
52+
},
53+
}).sort(this.possibleItems);
54+
}
55+
56+
// Solve 0/1 knapsack problem using dynamic programming.
57+
solveZeroOneKnapsackProblem() {
58+
this.selectedItems = [];
59+
60+
// Create knapsack values matrix.
61+
const numberOfRows = this.possibleItems.length;
62+
const numberOfColumns = this.weightLimit;
63+
const knapsackMatrix = Array(numberOfRows).fill(null).map(() => {
64+
return Array(numberOfColumns + 1).fill(null);
65+
});
66+
67+
// Fill the first column with zeros since it would mean that there is
68+
// no items we can add to knapsack in case if weight limitation is zero.
69+
for (let itemIndex = 0; itemIndex < this.possibleItems.length; itemIndex += 1) {
70+
knapsackMatrix[itemIndex][0] = 0;
71+
}
72+
73+
// Fill the first row with max possible values we would get by just adding
74+
// or not adding the first item to the knapsack.
75+
for (let weightIndex = 1; weightIndex <= this.weightLimit; weightIndex += 1) {
76+
const itemIndex = 0;
77+
const itemWeight = this.possibleItems[itemIndex].weight;
78+
const itemValue = this.possibleItems[itemIndex].value;
79+
knapsackMatrix[itemIndex][weightIndex] = itemWeight <= weightIndex ? itemValue : 0;
80+
}
81+
82+
// Go through combinations of how we may add items to knapsack and
83+
// define what weight/value we would receive using Dynamic Programming
84+
// approach.
85+
for (let itemIndex = 1; itemIndex < this.possibleItems.length; itemIndex += 1) {
86+
for (let weightIndex = 1; weightIndex <= this.weightLimit; weightIndex += 1) {
87+
const currentItemWeight = this.possibleItems[itemIndex].weight;
88+
const currentItemValue = this.possibleItems[itemIndex].value;
89+
90+
if (currentItemWeight > weightIndex) {
91+
// In case if item's weight is bigger then currently allowed weight
92+
// then we can't add it to knapsack and the max possible value we can
93+
// gain at the moment is the max value we got for previous item.
94+
knapsackMatrix[itemIndex][weightIndex] = knapsackMatrix[itemIndex - 1][weightIndex];
95+
} else {
96+
// Else we need to consider the max value we can gain at this point by adding
97+
// current value or just by keeping the previous item for current weight.
98+
knapsackMatrix[itemIndex][weightIndex] = Math.max(
99+
currentItemValue + knapsackMatrix[itemIndex - 1][weightIndex - currentItemWeight],
100+
knapsackMatrix[itemIndex - 1][weightIndex],
101+
);
102+
}
103+
}
104+
}
105+
106+
// Now let's trace back the knapsack matrix to see what items we're going to add
107+
// to the knapsack.
108+
let itemIndex = this.possibleItems.length - 1;
109+
let weightIndex = this.weightLimit;
110+
111+
while (itemIndex > 0) {
112+
const currentItem = this.possibleItems[itemIndex];
113+
const prevItem = this.possibleItems[itemIndex - 1];
114+
115+
// Check if matrix value came from top (from previous item).
116+
// In this case this would mean that we need to include previous item
117+
// to the list of selected items.
118+
if (
119+
knapsackMatrix[itemIndex][weightIndex] &&
120+
knapsackMatrix[itemIndex][weightIndex] === knapsackMatrix[itemIndex - 1][weightIndex]
121+
) {
122+
// Check if there are several items with the same weight but with the different values.
123+
// We need to add highest item in the matrix that is possible to get the highest value.
124+
const prevSumValue = knapsackMatrix[itemIndex - 1][weightIndex];
125+
const prevPrevSumValue = knapsackMatrix[itemIndex - 2][weightIndex];
126+
if (
127+
!prevSumValue ||
128+
(prevSumValue && prevPrevSumValue !== prevSumValue)
129+
) {
130+
this.selectedItems.push(prevItem);
131+
}
132+
} else if (knapsackMatrix[itemIndex - 1][weightIndex - currentItem.weight]) {
133+
this.selectedItems.push(prevItem);
134+
weightIndex -= currentItem.weight;
135+
}
136+
137+
itemIndex -= 1;
138+
}
139+
}
140+
141+
get totalValue() {
142+
/** @var {KnapsackItem} item */
143+
return this.selectedItems.reduce((accumulator, item) => {
144+
return accumulator + item.totalValue;
145+
}, 0);
146+
}
147+
148+
get totalWeight() {
149+
/** @var {KnapsackItem} item */
150+
return this.selectedItems.reduce((accumulator, item) => {
151+
return accumulator + item.totalWeight;
152+
}, 0);
153+
}
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export default class KnapsackItem {
2+
/**
3+
* @param {Object} itemSettings - knapsack item settings,
4+
* @param {number} itemSettings.value - value of the item.
5+
* @param {number} itemSettings.weight - weight of the item.
6+
* @param {number} itemSettings.itemsInStock - how many items are available to be added.
7+
*/
8+
constructor({ value, weight, itemsInStock = 1 }) {
9+
this.value = value;
10+
this.weight = weight;
11+
this.itemsInStock = itemsInStock;
12+
// Actual number of items that is going to be added to knapsack.
13+
this.quantity = 1;
14+
}
15+
16+
get totalValue() {
17+
return this.value * this.quantity;
18+
}
19+
20+
get totalWeight() {
21+
return this.weight * this.quantity;
22+
}
23+
24+
toString() {
25+
return `v${this.value} w${this.weight} x ${this.quantity}`;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Knapsack from '../Knapsack';
2+
import KnapsackItem from '../KnapsackItem';
3+
4+
describe('Knapsack', () => {
5+
it('should solve 0/1 knapsack problem', () => {
6+
const possibleKnapsackItems = [
7+
new KnapsackItem({ value: 1, weight: 1 }),
8+
new KnapsackItem({ value: 4, weight: 3 }),
9+
new KnapsackItem({ value: 5, weight: 4 }),
10+
new KnapsackItem({ value: 7, weight: 5 }),
11+
];
12+
13+
const maxKnapsackWeight = 7;
14+
15+
const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight);
16+
17+
knapsack.solveZeroOneKnapsackProblem();
18+
19+
expect(knapsack.totalValue).toBe(9);
20+
expect(knapsack.totalWeight).toBe(7);
21+
expect(knapsack.selectedItems.length).toBe(2);
22+
expect(knapsack.selectedItems[0].toString()).toBe('v5 w4 x 1');
23+
expect(knapsack.selectedItems[1].toString()).toBe('v4 w3 x 1');
24+
});
25+
26+
it('should solve 0/1 knapsack problem regardless of items order', () => {
27+
const possibleKnapsackItems = [
28+
new KnapsackItem({ value: 5, weight: 4 }),
29+
new KnapsackItem({ value: 1, weight: 1 }),
30+
new KnapsackItem({ value: 7, weight: 5 }),
31+
new KnapsackItem({ value: 4, weight: 3 }),
32+
];
33+
34+
const maxKnapsackWeight = 7;
35+
36+
const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight);
37+
38+
knapsack.solveZeroOneKnapsackProblem();
39+
40+
expect(knapsack.totalValue).toBe(9);
41+
expect(knapsack.totalWeight).toBe(7);
42+
expect(knapsack.selectedItems.length).toBe(2);
43+
expect(knapsack.selectedItems[0].toString()).toBe('v5 w4 x 1');
44+
expect(knapsack.selectedItems[1].toString()).toBe('v4 w3 x 1');
45+
});
46+
47+
it('should solve 0/1 knapsack problem with impossible items set', () => {
48+
const possibleKnapsackItems = [
49+
new KnapsackItem({ value: 5, weight: 40 }),
50+
new KnapsackItem({ value: 1, weight: 10 }),
51+
new KnapsackItem({ value: 7, weight: 50 }),
52+
new KnapsackItem({ value: 4, weight: 30 }),
53+
];
54+
55+
const maxKnapsackWeight = 7;
56+
57+
const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight);
58+
59+
knapsack.solveZeroOneKnapsackProblem();
60+
61+
expect(knapsack.totalValue).toBe(0);
62+
expect(knapsack.totalWeight).toBe(0);
63+
expect(knapsack.selectedItems.length).toBe(0);
64+
});
65+
66+
it('should solve 0/1 knapsack problem with all equal weights', () => {
67+
const possibleKnapsackItems = [
68+
new KnapsackItem({ value: 5, weight: 1 }),
69+
new KnapsackItem({ value: 1, weight: 1 }),
70+
new KnapsackItem({ value: 7, weight: 1 }),
71+
new KnapsackItem({ value: 4, weight: 1 }),
72+
new KnapsackItem({ value: 4, weight: 1 }),
73+
new KnapsackItem({ value: 4, weight: 1 }),
74+
];
75+
76+
const maxKnapsackWeight = 3;
77+
78+
const knapsack = new Knapsack(possibleKnapsackItems, maxKnapsackWeight);
79+
80+
knapsack.solveZeroOneKnapsackProblem();
81+
82+
expect(knapsack.totalValue).toBe(16);
83+
expect(knapsack.totalWeight).toBe(3);
84+
expect(knapsack.selectedItems.length).toBe(3);
85+
expect(knapsack.selectedItems[0].toString()).toBe('v4 w1 x 1');
86+
expect(knapsack.selectedItems[1].toString()).toBe('v5 w1 x 1');
87+
expect(knapsack.selectedItems[2].toString()).toBe('v7 w1 x 1');
88+
});
89+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import KnapsackItem from '../KnapsackItem';
2+
3+
describe('KnapsackItem', () => {
4+
it('should create knapsack item and count its total weight and value', () => {
5+
const item1 = new KnapsackItem({ value: 3, weight: 2 });
6+
7+
expect(item1.value).toBe(3);
8+
expect(item1.weight).toBe(2);
9+
expect(item1.quantity).toBe(1);
10+
expect(item1.toString()).toBe('v3 w2 x 1');
11+
expect(item1.totalValue).toBe(3);
12+
expect(item1.totalWeight).toBe(2);
13+
14+
item1.quantity = 0;
15+
16+
expect(item1.value).toBe(3);
17+
expect(item1.weight).toBe(2);
18+
expect(item1.quantity).toBe(0);
19+
expect(item1.toString()).toBe('v3 w2 x 0');
20+
expect(item1.totalValue).toBe(0);
21+
expect(item1.totalWeight).toBe(0);
22+
23+
item1.quantity = 2;
24+
25+
expect(item1.value).toBe(3);
26+
expect(item1.weight).toBe(2);
27+
expect(item1.quantity).toBe(2);
28+
expect(item1.toString()).toBe('v3 w2 x 2');
29+
expect(item1.totalValue).toBe(6);
30+
expect(item1.totalWeight).toBe(4);
31+
});
32+
});

0 commit comments

Comments
 (0)