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

 

  最後,因爲本人水平有限,能力與大神仍相差甚遠,如有錯誤或不明之處,還望你們不吝賜教指正。很是感謝!

相關文章
相關標籤/搜索