js算法初窥05(算法模式02-动态规划与贪心算法)

  在前面的文章中(js算法初窥02(排序算法02-归并、快速以及堆排)咱们学习了如何用分治法来实现归并排序,那么动态规划跟分治法有点相似,可是分治法是把问题分解成互相独立的子问题,最后组合它们的结果,而动态规划则是把问题分解成互相依赖的子问题。html

  那么我还有一个疑问,前面讲了递归,那么递归呢?分治法和动态规划像是一种手段或者方法,而递归则是具体的作操做的工具或执行者。不管是分治法仍是动态规划或者其余什么有趣的方法,均可以使用递归这种工具来“执行”代码。算法

  用动态规划来解决问题主要分为三个步骤:一、定义子问题,二、实现要反复执行来解决子问题的部分(好比用递归来“反复”),三、识别并求解出边界条件。这么说有点懵逼....那么咱们试试用动态规划来解决一些经典的问题。数组

1、最少硬币找零问题缓存

  最少硬币找零问题是硬币找零问题的一个变种。硬币找零问题是给出要找零的钱数,以及可用的硬币面额以及对应的数量,找出有多少种找零的方法。最少硬币找零问题则是要找出其中所需最少数量的硬币。好比咱们有1,5,10,25面额的硬币,若是要找36面额的钱,要如何找零呢?答案是一个25,一个10,一个1。这就是答案。那么如何把上面的问题转换成算法来解决呢?毕竟有了计算机很快速简单的就能够获得结果,不用咱们再费力地用人脑去解决问题了,下面咱们就来看一下代码:函数

//最少硬币找零
function MinCoinChange(coins) {
    // coins是有多少种面额的钱币。
    // 这里咱们直接把构造函数传进来的参数用私有变量存储一下。
    var coins = coins;
    // 缓存结果集的变量对象
    var cache = {};
    // 定义一个构造函数的私有方法,
    this.makeChange = function (amount) {
        // 这里的this指向的就是this.makeChange私有函数自己,把它赋值给一个变量是为了避免用在每次调用的时候都要计算(我的看法)
        var me = this;
        // amount就是咱们要找零的钱数,若是为非正数,直接返回空数组,由于你找零的钱数不该该为负数。
        if(!amount) {
            return [];
        };
        
        // cache[amount]的判断是为了在重复计算前面已经计算过的结果时能够直接返回结果
        // 避免重复计算所形成的时间浪费
        if(cache[amount]) {
            return cache[amount];
        };
        // min用来存储最终结果的数组,newMin和newAmount分别是在逻辑的执行过程当中,用于存储当前的符合条件的找零数组和找零钱数的。
        var min = [],newMin,newAmount;
        // 咱们循环coins的长度。经过循环,咱们为每个conis数组中的面额都进行下面的逻辑操做。(主要是为当前coin作递归)
        for(var i = 0; i < coins.length; i++) {
            // 选择coins中的当前面额。
            var coin = coins[i];
            // 咱们用要找零的钱数减去当前要找零的面额。并存储为newAmount变量。
            newAmount = amount - coin;
            // 在当前循环的递归中,若是newAmount是不小于0的值,也就是合法的找零的钱数,咱们一样为该数调用找零方法。
            // 这里就是有点相似分而治之的那种策略了,递归求解。
            if(newAmount >= 0) {
                newMin = me.makeChange(newAmount);
            };
            // 在前面符合条件的newAmount递归后会进入下一个值得逻辑执行,而后就会到这里的逻辑判断
            // 下面的if判断主要是判断是不是当前的最优解,若是是,那么就放入咱们最终的数组内。
            console.log(!min.length,min.length)
            if(newAmount >= 0 && (newMin.length < min.length - 1 || !min.length) && (newMin.length || !newAmount)) {
                min = [coin].concat(newMin);
                //console.log('new Min' + min + 'for' + amount);
            }
        };
        //cache存储了1到amount之间的全部结果
        //console.log(cache)
        return (cache[amount] = min);
    };
};

var minCoinChange = new MinCoinChange([1,5,10,25]);
console.log(minCoinChange.makeChange(36))

  这是用动态规划的方法来解决最少硬币找零问题,那么咱们再来看看如何用贪心算法求解最少硬币找零的问题。那么什么是贪心算法呢?贪心算法在有最优子结构的问题中尤其有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题可以分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。贪心算法与动态规划的不一样在于它对每一个子问题的解决方案都作出选择,不能回退。动态规划则会保存之前的运算结果,并根据之前的结果对当前进行选择,有回退功能。工具

  咱们仍是来看下代码:post

function MinCoinChange(coins) {
    var coins = coins;
    this.makeChange = function (amount) {
        var change = [],total = 0;
        for(var i = coins.length; i >= 0; i--) {
            var coin = coins[i];
            while(total + coin <= amount) {
                change.push(coin);
                total += coin;
            }
        }
        return change;
    };
}

var minCoinChange = new MinCoinChange([1,5,10,25]);
console.log(minCoinChange.makeChange(36))

  咱们看上面的代码,主要逻辑跟动态规划十分类似,只是代码自己要简单了很多。贪心算法从咱们的硬币中最大的开始拿,直到拿不了了再去拿下一个,直到返回最终结果。那么咱们看看两种解决方法有什么不经过。动态规划会经过cache来缓存以前的计算结果,在当前的计算结果中与以前的对比,选择二者之间的最优解。而贪心算法则只是选择了当前的最优解,不会回退,也不会去存储记录以前的解决方案。学习

 

2、背包问题优化

  背包问题实际上是一个组合优化问题,问题是这样的,给定一个固定大小,能携带重量为W的背包,以及一组有价值和重量的物品,找出一个最佳解决方案,使得装入背包的物品总重量不超过W,且总价值是最大的。这个问题有两个版本,一个是0-1背包问题,该版本只容许背包里装入完整的物品,不能拆分。还有另一个是能够装入分数物品。咱们后面会用贪心算法来解决分数背包问题。this

  咱们来看代码:

//背包问题
function knapSack(capacity,weights,values,n) {
    var i,w,a,b,kS = [];

    for (var i = 0; i <= n; i++) {
        kS[i] = [];
    }

    for(i = 0; i <= n; i++) {
        for(w = 0; w <= capacity; w++) {
            if(i == 0 || w == 0) {
                kS[i][w] = 0;
            } else if(weights[i - 1] <= w) {
                a = values[i - 1] + kS[i - 1][w - weights[i - 1]];
                b = kS[i - 1][w];
                kS[i][w] = (a > b) ? a : b;
            } else {
                kS[i][w] = kS[i - 1][w];
            }
        }
    }
    findValues(n,capacity,kS,weights,values);
    return kS[n][capacity];
};

function findValues(n,capacity,kS,weights,values) {
    var i = n,k = capacity;
    console.log('解决方案包括如下物品:');
    while(i > 0 && k > 0) {
        if(kS[i][k] !== kS[i - 1][k]) {
            console.log('物品' + i + ',重量:' + weights[i- 1] + ',价值:' + values[i - 1]);
            i--;
            k = k - kS[i][k];
        } else {
            i--;
        }
    }
}

var values = [3,4,5],weights = [2,3,4],capacity = 5,n = values.length;
console.log(knapSack(capacity,weights,values,n))

  上面的代码中,咱们最开始初始化一个矩阵,用来存放各类解决方案,并且要注意装入背包的物品i必须小于capacity,也就是小于背包可容纳的重量,才能够成为装入背包的一部分,否则你一个物品就超过了背包可容纳的重量,这是不容许的。而且当有两个物品重量相同的时候,咱们选择价值较大的哪个。

  其实上面的算法还能够继续优化,这里不作多讲,你们有兴趣能够深刻学习。

贪心算法的分数背包问题:

  分数背包问题和0-1背包问题相似,只是咱们能够在分数背包中加入部分的物品。代码并不难,你们本身写一下就明白了。

function knapSack(capacity,values,weights) {
    var n = values.length,load = 0,i = 0,val = 0;

    for(i = 0; i < n && load < capacity; i++) {
        if(weights[i] <= (capacity - load)) {
            val += values[i];
            load += weights[i];
        } else {
            var r = (capacity - load) / weights[i];
            val += r * values[i];
            load += weights[i];
        }
    }
    return val;
}
var values = [3,4,5],weights = [2,3,4],capacity = 6;

console.log(knapSack(capacity,values,weights))

3、最长公共子序列问题

  该问题是这样的,找出两个字符串序列中的最长子序列的长度。最长子序列是指,在两个字符串序列中以相同的顺序出现,但不要求必定是连续的字符串序列。

//最长公共子序列LCS
function lcs(wordX,wordY) {
    var m = wordX.length,n = wordY.length,l = [],i,j,a,b;
    var solution = [];

    for (i = 0; i <= m; ++i) {
        l[i] = [];
        solution[i] = [];
        for(j = 0; j <= n; ++j) {
            l[i][j] = 0;
            solution[i][j] = '0';
        }
    }

    for(i = 0; i <= m; i++) {
        for(j = 0; j <= n; j++) {
            if(i == 0 || j == 0) {
                l[i][j] = 0;
            } else if(wordX[i - 1] == wordY[j - 1]) {
                l[i][j] = l[i - 1][j - 1] + 1;
                solution[i][j] = 'diagonal';
            } else {
                a = l[i - 1][j];
                b = l[i][j - 1];
                l[i][j] = (a > b) ? a : b;
                solution[i][j] = (l[i][j] == l[i - 1][j]) ? 'top' : 'left';
            }
        }
    }
    printSolution(solution,l,wordX,wordY,m,n);
    return l[m][n];
}

function printSolution(solution,l,wordX,wordY,m,n) {
    var a = m,b = n,i,j,
    x = solution[a][b],
    answer = '';

    while(x !== '0') {
        if(solution[a][b] === 'diagonal') {
            answer = wordX[a - 1] + answer;
            a--;
            b--;
        } else if(solution[a][b] === 'left') {
            b--;
        } else if(solution[a][b] === 'top') {
            a--;
        }
        x = solution[a][b];
    }
    console.log('lcs:' + answer);
}

lcs("acbaed","abcadf");

   

4、矩阵链相乘

  该问题是要找出一组矩阵相乘的最佳方式(顺序),在开始以前,有必要给你们简单讲解一下矩阵相乘,简单来讲就是,加入一个n行m列的矩阵A和m行p列的矩阵B相乘,会获得一个n行p列的矩阵C。要注意,只有一个矩阵的行与另外一个矩阵的列相同两个矩阵才能够想乘。

  那么若是我想有A,B,C,D四个矩阵相乘,因为乘法知足结合律(小学数学知识点)。因此咱们能够这样(A(B(CD))),或者这样((AB)(CD))等五种相乘的方法,可是要注意的是,每种相乘的顺序不同,咱们的计算量也是不同的。因此,咱们来构建一个函数,找出计算量最少的相乘方法。这就是矩阵链相乘问题了。

//矩阵链相乘
function matrixChainOrder(p,n) {
    var i,j,k,l,q,m = [];


    //辅助矩阵s
    var s = [];
    for(i = 0; i <= n; i++) {
        s[i] = [];
        for(j = 0; j <= n; j++) {
            s[i][j] = 0;
        }
    }

    for(i = 0; i <= n; i++) {
        m[i] = [];
        m[i][i] = 0;
    };

    for(l = 2; l < n; l++) {
        for(i = 1; i <= n - l + 1; i++) {
            j = i + l - 1;
            m[i][j] = Number.MAX_SAFE_INTEGER;
            for(k = i; k <= j - 1; k++) {
                q = m[i][k] + m[k + 1][j] + p[i - 1]*p[k]*p[j];
                if(q < m[i][j]) {
                    m[i][j] = q;
                    s[i][j] = k;//辅助矩阵
                }
            }
        }
    }
    printOptimalParenthesis(s,1,n - 1);
    return m[1][n - 1];
}

function printOptimalParenthesis(s,i,j) {
    if(i == j) {
        console.log("A[" + i + "]");
    } else {
        console.log("(");
        printOptimalParenthesis(s,i,s[i][j]);
        printOptimalParenthesis(s,s[i][j] + 1,j);
        console.log(")");
    }
}

var p = [10,100,5,50,1,100];
n = p.length;
console.log(matrixChainOrder(p,n));

 

  最后,因为本人水平有限,能力与大神仍相差甚远,如有错误或不明之处,还望你们不吝赐教指正。很是感谢!

相关文章
相关标签/搜索