《數(shù)據(jù)結(jié)構(gòu)與算法之美》27——初識動態(tài)規(guī)劃

前言

今天開始學(xué)習(xí)動態(tài)規(guī)劃拟逮,一共有三節(jié)撬统,分別是:初識動態(tài)規(guī)劃、動態(tài)規(guī)劃理論敦迄、動態(tài)規(guī)劃實戰(zhàn)恋追。今天這一節(jié)就是初識動態(tài)規(guī)劃。

動態(tài)規(guī)劃比較適合用來求解最優(yōu)問題罚屋,比如最大值苦囱、最小值等等。它可以非常顯著地降低時間復(fù)雜度脾猛,提高代碼的執(zhí)行效率撕彤。

下面會通過兩個非常經(jīng)典的動態(tài)規(guī)劃問題模型來展示為什么需要動態(tài)規(guī)劃,以及動態(tài)規(guī)劃解題方法是如何演化出來的猛拴。

0-1背包問題

對于一組不同重量羹铅、不同分割的物品蚀狰,我們需要選擇一些裝入背包,在滿足背包最大重量限制的前提下职员,背包中物品總重量的最大值是多少呢麻蹋?

這問題可以通過回溯算法來解決,代碼如下:

public class Soluction {
    private int maxW = int.MinValue; // 結(jié)果
    private int[] weight = new int[] { 2, 2, 4, 6, 3 }; // 物品重量
    private int n = 5; // 物品個數(shù)
    private int w = 9; // 背包最大承受

    public void F (int i, int cw) {
        if (cw == w || i == n) { // cw==w表示背包滿了廉邑,i==n表示物品都考察完了
            if (cw > maxW) maxW = cw;
            return;
        }

        F (i + 1, cw); // 選擇不裝第i個物品
        if (cw + weight[i] <= w) {
            F (i + 1, cw + weight[i]); // 選擇裝第i個物品
        }
    }

    public int GetResult () {
        return maxW;
    }
}

回溯算法可以通過遞歸樹來分析哥蔚,如下圖:

回溯算法遞歸樹

通過遞歸樹可計算回溯算法的時間復(fù)雜度,時間復(fù)雜度是指數(shù)級的O(2^n)蛛蒙,非常高糙箍。

通過遞歸樹可知,回溯算法中存在大量的重復(fù)計算牵祟,這個可以通過“備忘錄”的方法來優(yōu)化深夯,優(yōu)化后的代碼如下:

// 回溯算法優(yōu)化,增加“備忘錄”
public class Soluction2 {
    private int maxW = int.MinValue; // 結(jié)果
    private int[] weight = new int[] { 2, 2, 4, 6, 3 }; // 物品重量
    private int n = 5; // 物品個數(shù)
    private int w = 9; // 背包最大承受
    private bool[][] mem = new bool[5][]; // 備忘錄诺苹,默認(rèn)值false

    public Soluction2 () {
        for (int i = 0; i < 5; i++) {
            mem[i] = new bool[10];
        }
    }

    public void F (int i, int cw) {
        if (cw == w || i == n) { // cw==w表示背包滿了咕晋,i==n表示物品都考察完了
            if (cw > maxW) maxW = cw;
            return;
        }
        if (mem[i][cw]) return; // 重復(fù)狀態(tài)
        F (i + 1, cw); // 選擇不裝第i個物品
        if (cw + weight[i] <= w) {
            F (i + 1, cw + weight[i]); // 選擇裝第i個物品
        }
    }

    public int GetResult () {
        return maxW;
    }
}

下面我們來看一下動態(tài)規(guī)劃是如何解決這個問題的。

  1. 把整個求解過程分為n個階段收奔,每個階段會決策一個物品是否放到背包中掌呜。
  2. 把每一層重復(fù)的狀態(tài)合并,只記錄不同的狀態(tài)坪哄,然后基于上一層的狀態(tài)集合來推導(dǎo)下一層的狀態(tài)集合质蕉。
  3. 以此類推,直到考察完所有物品翩肌。只需要在最后一層模暗,找一個值為true的最接近w的值,就是背包中物品總重量的最大值念祭。

整個過程如下圖:

動態(tài)規(guī)劃求解過程

翻譯成代碼如下:

public int Knapsack (int[] weight, int n, int w) {
    bool[][] states = new bool[n][]; // 默認(rèn)值false
    for (int i = 0; i < n; i++) states[i] = new bool[w + 1];

    states[0][0] = true; // 第一行的數(shù)據(jù)要特殊處理兑宇,可以利用哨兵優(yōu)化
    if (weight[0] <= w) states[0][weight[0]] = true;

    for (int i = 1; i < n; i++) // 動態(tài)規(guī)劃狀態(tài)轉(zhuǎn)移
    {
        for (int j = 0; j <= w; j++) // 不把第i個物品放入背包,則把上一輪的狀態(tài)移下來
            if (states[i - 1][j]) states[i][j] = states[i - 1][j];

        for (int j = 0; j <= w - weight[i]; j++) // 放第i個物品粱坤,在不超出背包最大承重w的范圍內(nèi)隶糕,再判斷上一輪的狀態(tài),來設(shè)置
            if (states[i - 1][j]) states[i][j + weight[i]] = true;
    }

    for (int i = w; i >= 0; i--) // 從最后一行(下標(biāo)n-1)找到值為true的位置站玄,即為結(jié)果
        if (states[n - 1][i]) return i;

    return 0;
}

這個代碼的時間復(fù)雜度是O(n * w)枚驻,其中n表示物品個數(shù),w表示背包可以承載的總重量蜒什〔饨眨空間復(fù)雜度也是O(n * w),使用了個二維數(shù)組來保存狀態(tài)。

實際上空間復(fù)雜度可以進(jìn)一步代碼霎冯,只需要w + 1的空間即可铃拇。代碼優(yōu)化如下:

public int Knapsack2 (int[] weight, int n, int w) {
    bool[] states = new bool[w + 1];
    states[0] = true;
    if (weight[0] <= w) states[weight[0]] = true;

    // 對比上面的代碼,主要是不再保存每一輪的狀態(tài)沈撞,而是合并保存狀態(tài)慷荔。
    for (int i = 1; i < n; i++) {
        for (int j = w - weight[i]; j >= 0; --j) // 對比上面的代碼,這里是從大到小遍歷缠俺,目的是為了避免狀態(tài)的修改显晶,影響后續(xù)的結(jié)果。
            if (states[j]) states[j + weight[i]] = true; 
    }

    for (int i = w; i >= 0; --i)  
        if (states[i]) return i;

    return 0;
}

對比上面的代碼壹士,上面代碼是保存每一輪的狀態(tài)磷雇,而實際上這個問題只需要最后一輪的狀態(tài)即可。但是要注意第8行代碼躏救,j是從大到小遍歷的唯笙,這里主要是因為只有一輪狀態(tài)保存,為了不影響其他狀態(tài)盒使。

0-1背包問題升級版

剛剛的背包只涉及背包重量和物品重量崩掘,我們改造一下剛剛的背包問題,引入物品價值這一變量少办。

對于一組不同重量苞慢、不同價值、不可分割的物品英妓,選擇將某些物品裝入背包挽放,在滿足背包最大重量的限制的前提下,背包中可裝入物品的總價值最大是多少鞋拟?

這個問題依舊可以使用回溯算法來解決骂维,但是就沒辦法通過備忘錄來優(yōu)化了惹资。

我們來看一下動態(tài)規(guī)劃如何解決這個問題贺纲,直接上代碼:

public int Knapsack3 (int[] weight, int[] value, int n, int w) {
    int[][] states = new int[n][];
    for (int i = 0; i < n; i++) states[i] = new int[w + 1];
    states[0][0] = 0;
    if (weight[0] <= w) states[0][weight[0]] = value[0];

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= w; j++) // 不放第i個物品
            if (states[i - 1][j] > 0) states[i][j] = states[i - 1][j];

        for (int j = 0; j <= w - weight[i]; j++) // 放第i個物品
            if (states[i - 1][j] > 0) states[i][j + weight[i]] = states[i - 1][j] + value[i];
    }

    for (int i = w; i >= 0; i--) if (states[n - 1][i] > 0) return states[n - 1][i];

    return 0;
}

時間復(fù)雜度和空間復(fù)雜度一樣是O(n * w)。同理褪测,一樣可以優(yōu)化空間猴誊,代碼如下:

public int Knapsack4 (int[] weight, int[] value, int n, int w) {
    int[] states = new int[w + 1];
    states[0] = 0;
    if (weight[0] <= w) states[weight[0]] = value[0];

    for (int i = 1; i < n; i++) {
        for (int j = w - weight[i]; j >= 0; j--) // 放第i個物品
            if (states[j] > 0) states[j + weight[i]] = states[j] + value[i];
    }

    for (int i = w; i >= 0; i--) 
        if (states[i] > 0) return states[i];

    return 0;
}

案例:如何巧妙解決“雙十一”購物時的湊單問題

淘寶的“雙十一”購物節(jié)有各種促銷活動,比如“滿200減50”侮措。假設(shè)你女朋友的購物車中有n個(n>100)想買的商品懈叹,她希望從里面選幾個,在湊夠滿減條件的前提下分扎,讓選出來的商品價格總和最大程度地接近滿減條件(200元)澄成。

直接上代碼:

public class Soluction4 {
    // items商品價格,n商品個數(shù), w表示滿減條件,比如200
    public void Double11advance (int[] items, int n, int w) {
        int[][] states = new int[n][];
        for (int i = 0; i < n; i++) {
            states[i] = new int[3 * w + 1];
        }
        states[0][0] = 0;
        if (items[0] <= 3 * w) states[0][items[0]] = items[0];

        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= 3 * w; j++) { // 不放第i個物品
                if (states[i - 1][j] > 0) states[i][j] = states[i - 1][j];
            }

            for (int j = 0; j <= 3 * w - items[i]; j++) { // 放第i個物品
                if (states[i - 1][j] > 0) states[i][j + items[i]] = states[i - 1][j] + items[i];
            }
        }

        int maxW = 0; // 找出大于等于w的最小值
        for (maxW = w; maxW < 3 * w + 1; ++maxW) {
            if (states[n - 1][maxW] > 0) break;
        }

        if (maxW == 3 * w + 1) return; // 沒有可行解

        Console.WriteLine ($"w的最小值:{maxW}");
        Console.Write ("購買組合:");
        for (int i = n - 1; i >= 1; --i) { // i表示二維數(shù)組中的行墨状,j表示列
            if (maxW - items[i] >= 0 && states[i - 1][maxW - items[i]] > 0) { // states[i][j]有值卫漫,要么是不購買i,
                Console.Write (items[i] + " "); // 購買這個商品
                maxW = maxW - items[i];
            }
        } // else 沒有購買這個商品,j不變肾砂。
        if (maxW != 0) Console.Write (items[0]);
    }
}

class Program {
    static void Main (string[] args) {
        Soluction4 soluction = new Soluction4 ();
        soluction.Double11advance (new int[] { 15, 93, 23, 843, 74, 82, 39, 46, 5, 26, 32, 37 }, 12, 200);
    }
}

輸出結(jié)果是:37 32 26 5 46 39 15

上面的代碼有一點要注意的列赎,如何根據(jù)狀態(tài)集合來判斷是否購買此商品。

狀態(tài)(i, j)只有可能從(i - 1, j)或者(i - 1, j - value[i])兩個狀態(tài)推導(dǎo)出來镐确。所以就檢查這兩個狀態(tài)是否可達(dá)的包吝,也就是states[i - 1][j]或者states[i - 1][j - value[i]]是否為true。

如果states[i - 1][j]可達(dá)源葫,就說明我們沒有選擇購買第i個商品诗越,如果states[i - 1][j - value[i]]可達(dá),那就說明我們選擇了購買第i個商品息堂。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市储矩,隨后出現(xiàn)的幾起案子感耙,更是在濱河造成了極大的恐慌,老刑警劉巖持隧,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件即硼,死亡現(xiàn)場離奇詭異,居然都是意外死亡屡拨,警方通過查閱死者的電腦和手機(jī)只酥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呀狼,“玉大人裂允,你說我怎么就攤上這事「缤В” “怎么了绝编?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長貌踏。 經(jīng)常有香客問我十饥,道長,這世上最難降的妖魔是什么祖乳? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任逗堵,我火速辦了婚禮,結(jié)果婚禮上眷昆,老公的妹妹穿的比我還像新娘蜒秤。我一直安慰自己汁咏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布作媚。 她就那樣靜靜地躺著梆暖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掂骏。 梳的紋絲不亂的頭發(fā)上轰驳,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機(jī)與錄音弟灼,去河邊找鬼级解。 笑死,一個胖子當(dāng)著我的面吹牛田绑,可吹牛的內(nèi)容都是我干的勤哗。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼掩驱,長吁一口氣:“原來是場噩夢啊……” “哼芒划!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起欧穴,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤民逼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涮帘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拼苍,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年调缨,在試婚紗的時候發(fā)現(xiàn)自己被綠了疮鲫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡弦叶,死狀恐怖俊犯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伤哺,我是刑警寧澤燕侠,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站默责,受9級特大地震影響贬循,放射性物質(zhì)發(fā)生泄漏咸包。R本人自食惡果不足惜桃序,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烂瘫。 院中可真熱鬧媒熊,春花似錦奇适、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柠衅,卻和暖如春皮仁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菲宴。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工贷祈, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喝峦。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓势誊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谣蠢。 傳聞我的和親對象是個殘疾皇子粟耻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容