Dynamic Programming
0-1 Knapsack Problem
Recursion:
- When
weight[i] > w
, we havedp[i][w] = dp[i - 1][w]
. - When
weight[i] <= w
, we havedp[i][w] = max{dp[i - 1][w - weight[i]] + values[i], dp[i - 1][w]}
function solve(n, W, weights, values) {
let dp = [];
// initialize
for (let i = 0; i <= n; ++i) {
dp[i] = [0];
}
for (let w = 0; w <= W; ++w) {
dp[0][w] = 0;
}
// complete dp
for (let i = 1; i <= n; ++i) {
for (let w = 0; w <= W; ++w) {
if (w < weights[i]) {
dp[i][w] = dp[i - 1][w];
} else {
dp[i][w] = Math.max(dp[i - 1][w - weights[i]] + values[i], dp[i - 1][w]);
}
}
}
return dp[n][W];
}
// For convenience, we use values[0] and weights[0] as placeholders.
console.log(solve(8, 200, [-1, 79, 58, 86, 11, 28, 62, 15, 68], [-1, 83, 14, 54, 79, 72, 52, 48, 62]));
We notice that when we compute dp[i][j]
, at most we need to know dp[i - 1][w]
and dp[i - 1][w - weights[i]]
, which are on the left and upper-left side relative to dp[i][w]
in the matrix. Since that it's obvious that we can switch the order of the two-layers loop in our code.
function solve(n, W, weights, values) {
let dp = [];
// initialize
for (let i = 0; i <= n; ++i) {
dp[i] = [0];
}
for (let w = 0; w <= W; ++w) {
dp[0][w] = 0;
}
// complete dp
for (let w = 0; w <= W; ++w) {
for (let i = 1; i <= n; ++i) {
if (w < weights[i]) {
dp[i][w] = dp[i - 1][w];
} else {
dp[i][w] = Math.max(dp[i - 1][w - weights[i]] + values[i], dp[i - 1][w]);
}
}
}
return dp[n][W];
}
// For convenience, we use values[0] and weights[0] as placeholders.
console.log(solve(8, 200, [-1, 79, 58, 86, 11, 28, 62, 15, 68], [-1, 83, 14, 54, 79, 72, 52, 48, 62]));
Longest common subsequence
Recursion:
- When
i=0
orj=0
,we havedp[i][j]=0
- When
i>0
,j>0
,str1[i]==str2[j]
, we havedp[i][j]=dp[i-1][j-1]+1
- When
i>0
,j>0
,str1[i]!=str2[j]
, we havedp[i][j]=max{dp[i-1][j], dp[i][j-1]}
Find a answer
function solve(str1, str2) {
let dp = [];
let status = [];
// initialize
for (let i = 0; i <= str1.length; ++i) {
dp[i] = [0];
status[i] = [];
}
for (let j = 0; j <= str2.length; ++j) {
dp[0][j] = 0;
}
// complete dp and status
for (let i = 1; i <= str1.length; ++i) {
for (let j = 1; j <= str2.length; ++j) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
status[i][j] = '↖';
}
else if (dp[i - 1][j] >= dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
status[i][j] = '←';
}
else {
dp[i][j] = dp[i][j - 1];
status[i][j] = '↑';
}
}
}
// rebuild the lcs
let i = str1.length;
let j = str2.length;
let ans = [];
while (i > 0 && j > 0) {
switch (status[i][j]) {
case '↖':
ans.push(str1[i - 1]);
--i, --j;
break;
case '←':
--i;
break;
case '↑':
--j;
break;
}
}
return ans.reverse().join('');
}
console.log(solve("ABCBDAB", "BDCABA"));
Find all answers
function rebuild(str1, i, j, status) {
if (i <= 0 || j <= 0) {
return [''];
}
switch (status[i][j]) {
case '↖':
/*
For example, now str1[i-1]='d',
and we get ['ab', 'ba', 'ac'] after call `rebuild(str1, i - 1, j - 1, status)`.
Then we want to return ['abd', 'bad', 'acd'] here.
*/
return rebuild(str1, i - 1, j - 1, status).map(s => s + str1[i - 1]);
case '←':
return rebuild(str1, i - 1, j, status);
case '↑':
return rebuild(str1, i, j - 1, status);
case '×':
/*
For example, now we get ['ab'] after call `rebuild(str1, i - 1, j, status)`,
and ['ad', 'bc'] after call `rebuild(str1, i, j - 1, status)`.
Then we want to return ['ab', 'ad', 'bc'] here.
*/
return [...rebuild(str1, i - 1, j, status), ...rebuild(str1, i, j - 1, status)];
}
}
function solve(str1, str2) {
let dp = [];
let status = [];
// initialize
for (let i = 0; i <= str1.length; ++i) {
dp[i] = [0];
status[i] = [];
}
for (let j = 0; j <= str2.length; ++j) {
dp[0][j] = 0;
}
// complete dp and status
for (let i = 1; i <= str1.length; ++i) {
for (let j = 1; j <= str2.length; ++j) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
status[i][j] = '↖';
}
else if (dp[i - 1][j] == dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
status[i][j] = '×';
}
else if (dp[i - 1][j] > dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
status[i][j] = '←';
}
else {
dp[i][j] = dp[i][j - 1];
status[i][j] = '↑';
}
}
}
// rebuild the lcs
return rebuild(str1, str1.length, str2.length, status);
}
console.log(solve("ABCBDAB", "BDCABA"));
Matrix-chain multiplication
We use dp[i][j]
where 1<=i<=j<=n
to store the optimal cost when multiplying matrices . And we hope we will get the final answer in dp[1][n]
.
We use p[i]
where 0<=i<=n
to store the size information of matrices. For example, the size of matrix is p[0] * p[1]
, the size of matrix is p[1] * p[2]
, ..., the size of matrix is p[i - 1] * p[i]
.
Since that, we know the cost to compute (i.e. the number of times to multiply) is p[i - 1] * p[i] * p[i + 1]
. Further, if we have already know the optimal cost of multiplying matrices sequence as dp[i][k]
and as dp[k+1][j]
, we can know the optimal cost of multiplying as dp[i][j]
is dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j]
.
Recursion for 1 <= i < j <= n
:
- When
i == j
, we havedp[i][j] = 0
since there is no matrices were multiplied here. - When
i < j
, we havedp[i][j] = min{ dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j] }
fori <= k < j
.
function rebuild(status, i, j) {
if (i == j) return `A${i}`;
let k = status[i][j];
return `(` + rebuild(status, i, k) + rebuild(status, k + 1, j) + ')';
}
function solve(p) {
let dp = [];
let status = [];
let n = p.length - 1;
// initalize
for (let i = 0; i <= n; ++i) {
dp[i] = [];
dp[i][i] = 0;
status[i] = [];
}
// complete dp & status
for (let l = 2; l <= n; ++l) {
for (let i = 1; i <= n - l + 1; ++i) {
let j = l + i - 1;
dp[i][j] = Infinity;
for (let k = i; k < j; ++k) {
let temp = dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j];
if (temp < dp[i][j]) {
dp[i][j] = temp;
status[i][j] = k;
}
}
}
}
// rebuild
return rebuild(status, 1, n);
}
console.log(solve([30, 35, 15, 5, 10, 20, 25]));
Greedy Algorithms
Single-Source Shortest Paths
Dijkstra’s algorithm
function dijkstra(n, edges, start) {
let graph = new Array(n).fill(0).map(_ => Array(n).fill(0))
edges.forEach(edge => {
let u = edge[0].charCodeAt(0) - 65;
let v = edge[1].charCodeAt(0) - 65;
graph[u][v] = edge[2];
});
start = start.charCodeAt(0) - 65;
let dist = Array(n).fill(Infinity);
let parent = Array(n).fill(-1);
let vertex = Array(n).fill(0).map((_, i) => i);
dist[start] = 0; // the shorest path from start to start is zero.
while (vertex.length > 0) {
vertex.sort((u, v) => dist[u] - dist[v]);
let nearest = vertex.shift(); // select the vertex has the shortest distance with start currently.
for (let next = 0; next < n; ++next) {
let weight = graph[nearest][next];
// relax all the vertices adjacent with nearest vertex.
if (weight > 0 && dist[nearest] + weight < dist[next]) {
dist[next] = dist[nearest] + weight;
parent[next] = nearest;
}
}
}
for (let u = 0; u < n; ++u) {
if (u === start) continue;
let path = [u];
let p = parent[u];
while (p != -1) {
path.unshift(p);
p = parent[p];
}
console.log(path.map(u => String.fromCharCode(u + 65)).join('->'), `cost=${dist[u]}`);
}
}
dijkstra(5, [
['A', 'B', 10],
['A', 'D', 5],
['B', 'D', 2],
['D', 'B', 3],
['B', 'C', 1],
['D', 'C', 9],
['D', 'E', 2],
['C', 'E', 4],
['E', 'C', 6],
['E', 'A', 7]
], 'A');
The Bellman-Ford algorithm
function bellman_ford(n, edges, start) {
// initialize
let graph = new Array(n).fill(0).map(_ => Array(n).fill(0))
edges.forEach(edge => {
let u = edge[0] = edge[0].charCodeAt(0) - 65;
let v = edge[1] = edge[1].charCodeAt(0) - 65;
graph[u][v] = edge[2];
});
start = start.charCodeAt(0) - 65;
let dist = Array(n).fill(Infinity);
let parent = Array(n).fill(-1);
dist[start] = 0; // the shorest path from start to start is zero.
// relax all the edges n times.
for (let i = 0; i < n; ++i) {
for (let [u, v] of edges) {
let weight = graph[u][v];
if (weight > 0 && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
parent[v] = u;
}
}
}
// check if negative-weight cycles exist.
for (let [u, v] of edges) {
if (dist[u] + graph[u][v] < dist[v]) {
console.error("The graph contains negative-weight cycle(s).");
}
}
// output
for (let u = 0; u < n; ++u) {
if (u === start) continue;
let path = [u];
let p = parent[u];
while (p != -1) {
path.unshift(p);
p = parent[p];
}
console.log(path.map(u => String.fromCharCode(u + 65)).join('->'), `cost=${dist[u]}`);
}
}
bellman_ford(5, [
['A', 'B', 10],
['A', 'D', 5],
['B', 'D', 2],
['D', 'B', 3],
['B', 'C', 1],
['D', 'C', 9],
['D', 'E', 2],
['C', 'E', 4],
['E', 'C', 6],
['E', 'A', 7]
], 'A');
Minimum Spanning Trees
Prim
function solve(n, edges) {
// initialize the graph
let graph = Array(n).fill(0).map(_ => Array(n).fill(0));
let base = 'a'.charCodeAt(0);
for (let edge of edges) {
let u = edge[0].charCodeAt(0) - base;
let v = edge[1].charCodeAt(0) - base;
graph[u][v] = graph[v][u] = edge[2];
}
// initialize
let parent = Array(n);
let dist = Array(n).fill(Infinity);
let vertices = []; // to store the vertices haven't be "dragged" to MST yet.
for (let i = 0; i < n; ++i) vertices.push(i);
dist[0] = 0;
// generate the MST
while (vertices.length > 0) {
let nearest_vertex;
// since this is a simple version of Prim,
// we just use sorting instead of a min heap.
vertices.sort((u, v) => dist[u] - dist[v]);
nearest_vertex = vertices.shift();
for (let adj_vertex = 0; adj_vertex < n; ++adj_vertex) {
let new_dist = graph[nearest_vertex][adj_vertex];
if (new_dist != 0 && new_dist < dist[adj_vertex] && vertices.includes(adj_vertex)) {
dist[adj_vertex] = new_dist;
parent[adj_vertex] = nearest_vertex;
}
}
}
// output
let sum_cost = 0;
for (let u = 1; u < n; ++u) {
console.log(`${String.fromCharCode(parent[u] + base)} ${String.fromCharCode(u + base)}`);
sum_cost += dist[u];
}
console.log(`The sum cost of MST is ${sum_cost}.`);
}
let edges = [
['b', 'c', 8],
['c', 'd', 7],
['a', 'b', 4],
['i', 'c', 2],
['e', 'd', 9],
['h', 'a', 8],
['h', 'b', 11],
['h', 'i', 7],
['h', 'g', 1],
['g', 'i', 6],
['g', 'f', 2],
['f', 'c', 4],
['f', 'd', 14],
['f', 'e', 10],
];
solve(9, edges);
Kruskal
function InitSet(n) {
let set = [];
for (let i = 0; i < n; ++i) set.push(i);
return set;
}
function FindSet(set, i) {
if (set[i] === i) {
return i;
} else {
return (set[i] = FindSet(set, set[i]));
}
}
function UnionSet(set, i, j) {
set[i] = set[j];
}
function solve(n, edges) {
// pre-process edges
let base = 'a'.charCodeAt(0);
for (let edge of edges) {
edge[0] = edge[0].charCodeAt(0) - base;
edge[1] = edge[1].charCodeAt(0) - base;
}
/* generate the MST */
// here we use a disjoint set to record the connect-components which our edges belong to.
let set = InitSet(n);
let edges_mst = [];
edges.sort((e1, e2) => e1[2] - e2[2]);
while (edges.length > 0) {
let cur_edge = edges.shift();
let [u, v, weight] = cur_edge;
if (FindSet(set, u) !== FindSet(set, v)) {
edges_mst.push(cur_edge);
// now the vertex u and v are in the same connect-components,
// so we need to union their original connect-components.
UnionSet(set, u, v);
}
}
// output
let sum_cost = 0;
for (let [u, v, weight] of edges_mst) {
console.log(`${String.fromCharCode(u + base)} ${String.fromCharCode(v + base)}`);
sum_cost += weight;
}
console.log(`The sum cost of MST is ${sum_cost}.`);
}
let edges = [
['b', 'c', 8],
['c', 'd', 7],
['a', 'b', 4],
['i', 'c', 2],
['e', 'd', 9],
['h', 'a', 8],
['h', 'b', 11],
['h', 'i', 7],
['h', 'g', 1],
['g', 'i', 6],
['g', 'f', 2],
['f', 'c', 4],
['f', 'd', 14],
['f', 'e', 10],
];
solve(9, edges);
An activity-selection problem
function solve(n, starts, ends) {
let activity = [];
for (let i = 0; i < n; ++i) activity.push(i);
activity.sort((i, j) => ends[i] - ends[j])
let ans = [activity[0]];
let last_activity_end_time = ends[activity[0]];
for (let i = 1; i < n; ++i) {
let cur = activity[i];
if (starts[cur] >= last_activity_end_time) {
ans.push(cur);
last_activity_end_time = ends[cur];
}
}
return ans;
}
console.log(solve(11, [1,3,0,5,3,5,6,8,8,2,12], [4,5,6,7,9,9,10,11,12,14,16]));