CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/knapsack.ts
Views: 687
1
/*
2
This is based on
3
https://gist.github.com/frobnitzem/28707410b81870e88925097fdfe1f85b
4
but I converted it to modern typescript, added unit tests,
5
proper style conventions, etc.
6
7
Yes, I looked all over npm and couldn't find anything good.
8
9
There's no license but the code is very short and it's
10
just passed around implementing a very standard algorithm.
11
12
Let's just call it BSD 3-clause going forward.
13
TODO: make this an NPM package.
14
15
Solve the knapsack problem using recursive descent.
16
This wraps the actual solver below with a nice interface.
17
It also handles non-integer cost, but then the complexity
18
scales with the precision of the cost.
19
20
items is an object of named {cost, benefit} objects, e.g.
21
{ banana: {cost:5, benefit:33},
22
apple: {cost:1, benefit:12},
23
kiwi: {cost:1, benefit:7}
24
}
25
26
maxCost is the maximum cost allowed in the knapsack.
27
*/
28
29
export type Items = { [key: string]: { cost: number; benefit: number } };
30
31
export default function knapsack(
32
items: Items,
33
maxCost: number
34
): {
35
items: string[]; // names of the items to include
36
cost: number; // total cost of this choice
37
benefit: number; // total benefit of this choice
38
} {
39
// c = cost; b = benefit
40
const arr: { c: number; b: number; n: string }[] = [];
41
for (const key in items) {
42
arr.push({ c: items[key].cost, b: items[key].benefit, n: key });
43
}
44
// Sort descending for a 'greedy' initial search order.
45
arr.sort((x, y) => {
46
if (x.b == y.b) {
47
// lower cost breaks tie
48
return x.c - y.c;
49
}
50
return y.b - x.b; // Consider highest value first.
51
});
52
const memo = {};
53
let ret = knap(arr, maxCost, 0, memo);
54
// console.log(Object.keys(memo).length + " calls.");
55
for (let i = 0; i < ret[0].length; i++) {
56
// Name the winners.
57
ret[0][i] = arr[ret[0][i]].n;
58
}
59
60
return { items: ret[0], cost: ret[1], benefit: ret[2] };
61
}
62
63
//Knapsack algorithm
64
//==================
65
// wikipedia: [Knapsack (0/1)](http://en.wikipedia.org/wiki/Knapsack_problem#0.2F1_Knapsack_Problem)
66
// Given a set `[{cost:Number, benefit:Number}]`,
67
// find the maximum benefit possible provided that the cost
68
// must remain below or equal to the capacity, "maxCost",
69
// AND the benefit must be at or equal to "minBen".
70
// **params**:
71
// `items` : [{w:Number, b:Number}],
72
// `maxCost` : Number,
73
// `minBen` : Number
74
// **returns**:
75
// An object containing `maxValue` and `set`
76
77
// Solve a knapsack problem. This routine is meant
78
// to be called by knapsack (above).
79
// For efficiency, items should be sorted by
80
// descending benefit.
81
//
82
// returns [items], cost, benefit
83
//
84
// Note:
85
// This method uses dead-end elimination to avoid solving
86
// extra sub-problems.
87
// It uses a cache of calls at each (#items, maxCost)
88
// to avoid solving similar sub-problems multiple times.
89
// { (#items,maxCost) : [minBen_req, items, maxCost_found, minBen_found] }
90
function knap(items, maxCost, minBen, memo) {
91
let best: number[] = [];
92
let cost = 0;
93
let remain_ben = 0;
94
if (minBen < 0) minBen = 0;
95
let minBen_inp = minBen; // save the lower bound we started with for caching purposes
96
97
let note = items.length + " " + maxCost; // attempt to lookup the answer
98
if (note in memo) {
99
if (memo[note][0] <= minBen || memo[note][3] >= minBen) {
100
//console.log("re-used: " + note);
101
return memo[note].slice(1);
102
}
103
}
104
105
for (let i = 0; i < items.length; i++) {
106
// determine remaining possible benefit
107
if (items[i].c <= maxCost) remain_ben += items[i].b;
108
}
109
110
for (let i = 0; i < items.length; i++) {
111
if (items[i].c > maxCost) {
112
continue;
113
} // Can't include.
114
115
if (remain_ben < minBen) {
116
// Early termination check.
117
break;
118
}
119
remain_ben -= items[i].b;
120
121
let ret = knap(
122
items.slice(i + 1),
123
maxCost - items[i].c,
124
minBen - items[i].b,
125
memo
126
);
127
if (ret[2] + items[i].b > minBen) {
128
// Found a better subproblem solution.
129
best = ret[0].map(function (j) {
130
return i + j + 1;
131
});
132
best.push(i);
133
cost = ret[1] + items[i].c;
134
minBen = ret[2] + items[i].b; // up the ante
135
}
136
}
137
138
if (best.length == 0) {
139
memo[note] = [minBen_inp, [], 0, 0];
140
} else {
141
memo[note] = [minBen_inp, best, cost, minBen];
142
}
143
144
//console.log(note, memo[note]);
145
return memo[note].slice(1);
146
}
147
148