【专业课学习】Dynamic Programming与 Greedy Algorithms例题整理

43 阅读6分钟

47139274_p0.jpg

Dynamic Programming

0-1 Knapsack Problem

Recursion:

  1. When weight[i] > w, we have dp[i][w] = dp[i - 1][w].
  2. When weight[i] <= w, we have dp[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:

  1. When i=0or j=0,we have dp[i][j]=0
  2. Wheni>0,j>0,str1[i]==str2[j], we have dp[i][j]=dp[i-1][j-1]+1
  3. Wheni>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 AiAi+1Aj1AjA_{i} A_{i+1} \dots A_{j-1} A_{j}. 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 A1A_{1} is p[0] * p[1], the size of matrix A2A_{2} is p[1] * p[2], ..., the size of matrix AiA_{i} is p[i - 1] * p[i].

Since that, we know the cost to compute AiAi+1A_{i} A_{i+1} (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 AiAi+1Ak1AkA_{i}A_{i+1} \dots A_{k-1} A_{k} as dp[i][k] and Ak+1Aj1AjA_{k+1} \dots A_{j-1} A_{j} as dp[k+1][j], we can know the optimal cost of multiplying AiAjA_{i} \dots A_{j} 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:

  1. When i == j, we have dp[i][j] = 0 since there is no matrices were multiplied here.
  2. When i < j, we have dp[i][j] = min{ dp[i][k] + dp[k + 1][j] + p[i - 1] * p[k] * p[j] } for i <= 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');

image.png

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]));