動態(tài)規(guī)劃

1所袁、 什么樣的問題適合用動態(tài)規(guī)劃來求解

1.1. 問題具有最優(yōu)子結(jié)構(gòu);就是說問題的最優(yōu)解所包含的子問題的解也是最優(yōu)的旅赢;比如數(shù)字三角形問題中對于第i行j列的元素來說宛蚓,其到i+1行的最優(yōu)路徑是確定的即:max(dp[i+1][j], dp[i+1][j+1])激捏。
1.2. 狀態(tài)具有無后效性;即當(dāng)前的若干個(gè)狀態(tài)值一旦確定凄吏,則此后過程的演變就只和這若干個(gè)狀態(tài)的值有關(guān)远舅,和之前是采取哪種路徑演變到當(dāng)前的這若干個(gè)狀態(tài),沒有關(guān)系竞思。

2表谊、 動態(tài)規(guī)劃問題解題的一般思路

1、將原問題分解為子問題

  • 把原問題分解為若干個(gè)子問題盖喷,子問題和原問題形式類似爆办,只是規(guī)模變小,子問題都解決了的話课梳,原問題也就解決了距辆;
  • 子問題的解一旦求出就會被保存,所以每個(gè)子問題只用求解一次暮刃;
  • 比如數(shù)字三角形問題中某個(gè)元素對應(yīng)的最優(yōu)路徑的最大和就是一個(gè)子問題跨算;

2、確定狀態(tài)

  • 將和子問題相關(guān)的各變量的一組取值椭懊,稱為一個(gè)“狀態(tài)”诸蚕;一個(gè)狀態(tài)對應(yīng)于一個(gè)或多個(gè)子問題,所謂某個(gè)狀態(tài)下的值氧猬,就是這個(gè)狀態(tài)所對應(yīng)的子問題的解背犯;所有狀態(tài)的集合就構(gòu)成了問題的狀態(tài)空間,狀態(tài)空間的大小盅抚,與動規(guī)解決問題的時(shí)間復(fù)雜度相關(guān)漠魏;
  • 比如數(shù)字三角形問題中每個(gè)元素對應(yīng)的最優(yōu)路徑的最大和就是一個(gè)狀態(tài),總共有N*(N+1)/2個(gè)狀態(tài)妄均,乘以每個(gè)狀態(tài)計(jì)算所需的時(shí)間就是整個(gè)問題的求解時(shí)間柱锹;

3哪自、確定邊界狀態(tài)值

  • 比如數(shù)字三角形問題中邊界值就是三角形底邊的值;

4禁熏、確定狀態(tài)轉(zhuǎn)移方程

  • 如何從一個(gè)或多個(gè)值已知的狀態(tài)壤巷,求出另一個(gè)狀態(tài)的值,(“人人為我”遞推型)匹层。狀態(tài)的轉(zhuǎn)移可以用遞推公式表示隙笆,此公式就叫狀態(tài)轉(zhuǎn)移方程;
  • 比如數(shù)字三角形問題中的狀態(tài)轉(zhuǎn)移方程:
if(start_raw == n-1)//邊界條件 
   max_sum[start_raw][start_col] = nums[start_raw][start_col];
else
    int max_1 = maxSum(nums, start_raw + 1, start_col, n);
    int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n);
    int max = max_1 >= max_2 ? max_1 : max_2;
    max_sum[start_raw][start_col] = max + nums[start_raw][start_col];

3 實(shí)例講解

例題一升筏、 數(shù)字三角形

題目描述:

如下圖所示的數(shù)字三角形圖,尋找從頂至底的某處的一條路徑瘸爽,使該路徑所經(jīng)過的數(shù)字之和最大您访。(1)每一步只能沿左斜線向下或右斜線向下,只需要給出最大和就可以了剪决,不必給出具體路徑(2)1 < 三角形行數(shù) < 101(3)三角形數(shù)字為0灵汪,1,…99


image.png

解法方法分析:

  • 首先構(gòu)建輸入:該數(shù)據(jù)是三角形柑潦,屬于二維圖形享言,適合使用二維向量或是數(shù)組來表示;
    可以使用如下代碼來實(shí)現(xiàn)

#include<iostream>
#include<vector>
using namespace std;

void getInput(vector<vector<int> >& dp_test)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
        }
        dp_test.push_back(temp_vector);
        
    }
    cout << "The raw num is: " << raw_n << endl;
    cout << "The input nums are: " << endl;
    for(size_t i = 0; i < dp_test.size(); I++)
    {
        for(size_t j = 0; j < dp_test[i].size(); j++)
        {
            cout << dp_test[i][j] << " ";
        }
        cout << endl;
    }
}

得到的數(shù)組是這樣的

The raw num is: 5
The input nums are: 
7 
3 8 
8 1 0 
2 7 4 4 
4 5 2 6 5 

這里元素dp[i][j], ij從0開始渗鬼,每次向下移動的路徑就是dp[i+1][j]或者dp[i+1][j+1];maxSum(i,j)表示從dp[i][j]到底邊的各條路徑中览露,最佳路徑的數(shù)字和;

  • 解法一譬胎、 該問題是典型的遞歸問題
    dp[i][j]出發(fā)差牛,下一步只能走dp[i+1][j]或者dp[i+1][j+1],即只要知道maxSum(i+1,j)maxSum(i+1,j+1)就能得到maxSum(i,j) = max(maxSum(i+1,j),maxSum(i+1,j+1)) + dp[i][j]堰乔;邊界就是maxSum(n-1,*) = nums[n-1][*]
int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n)
{
    if(start_raw == n-1)//邊界條件 
        return nums[start_raw][start_col];
    int max_1 = maxSum(nums, start_raw + 1, start_col, n);
    int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n);
    int max = max_1 >= max_2 ? max_1 : max_2;
    return max + nums[start_raw][start_col];
}

int main()
{
    vector<vector<int>> input_vec;
    getInput(input_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size()) << endl;
    return 0;
}

分析遞歸算法的時(shí)間復(fù)雜度偏化,該遞歸方法是從上到下一層一層的遞歸求解,那么存在大量的重復(fù)計(jì)算镐侯,每個(gè)元素上面重復(fù)計(jì)算的次數(shù)如下入所示:


image.png

根據(jù)等比數(shù)列求和可該算法的時(shí)間復(fù)雜度是O(2^n),對于n=100行侦讨,時(shí)間肯定是超時(shí)的;

  • 解法二苟翻、 記憶遞歸型動規(guī)程序韵卤,采用遞歸的方式,時(shí)間復(fù)雜度太大主要是因?yàn)閮?yōu)太多的重復(fù)計(jì)算袜瞬,改進(jìn)的方法是將計(jì)算出的每一個(gè)maxSum值都存起來怜俐,下次用到的時(shí)候直接取用,避免重復(fù)計(jì)算邓尤;那么這時(shí)的時(shí)間復(fù)雜度根據(jù)等差數(shù)列求和可以知道是O(n^2);
#include<iostream>
#include<vector>
using namespace std;

void getInput(vector<vector<int>>& dp_test, vector<vector<int>>& dp_max)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        vector<int> max_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
            max_vector.push_back(-1);
            
        }
        dp_test.push_back(temp_vector);
        dp_max.push_back(max_vector);
    }
}

int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n, vector<vector<int>> max_sums)
{
    if(max_sums[start_raw][start_col] != -1)
        return max_sums[start_raw][start_raw];
    if(start_raw == n-1)//邊界條件 
    {
        max_sums[start_raw][start_col] = nums[start_raw][start_raw];
    }
    else
    {
        int max_1 = maxSum(nums, start_raw + 1, start_col, n, max_sums);
        int max_2 = maxSum(nums, start_raw + 1, start_col + 1, n, max_sums);
        int max = max_1 >= max_2 ? max_1 : max_2;
        max_sums[start_raw][start_col] = max + nums[start_raw][start_col];
    }
}

int main()
{
    vector<vector<int>> input_vec;
    vector<vector<int>> max_vec;
    getInput(input_vec,max_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size(), max_vec) << endl;
    return 0;
}
  • 解法三拍鲤、遞推(“人人為我”遞推型動歸程序)
    遞歸會有調(diào)用棧溢出的風(fēng)險(xiǎn)贴谎;可以使用遞推的方式,即從最后一行開始季稳,逐層向上求出每個(gè)元素位置的最大值擅这,這樣既可以每個(gè)元素只計(jì)算一次,時(shí)間復(fù)雜度為O(2^n),又不使用遞歸景鼠,不會有調(diào)用棧溢出的風(fēng)險(xiǎn)仲翎;采用遞推時(shí),最后一行的最大值就是自己铛漓,倒數(shù)第二行的最大值時(shí)元素本身加上最后一行兩個(gè)數(shù)的最大值溯香,上面的行以此類推,結(jié)果如下:
    輸入數(shù)據(jù):
    7
    3 8
    8 1 0
    2 7 4 4
    4 5 2 6 5

遞推后的結(jié)果:

30
23 21
20 13 10
7 12 10 10
4 5 2 6 5

上面的表格過程可以看出來浓恶,整個(gè)過程會從底向上求出每一個(gè)節(jié)點(diǎn)在以該節(jié)點(diǎn)開始到最下面的最優(yōu)路徑的解玫坛;這個(gè)值只是該節(jié)點(diǎn)的最優(yōu)解,并不是該層的最優(yōu)解包晰,但是當(dāng)我們以此方式求到最上面的節(jié)點(diǎn)即頂點(diǎn)時(shí)湿镀,頂點(diǎn)的最優(yōu)解就是我門需要的最優(yōu)解;這里不需要記錄路徑伐憾;

#include<iostream>
#include<vector>
using namespace std;

/*
@dp_max:的元素對于每一行都是復(fù)用的
*/
void getInput(vector<vector<int>>& dp_test, vector<int>& dp_max)
{
    vector<vector<int> > dp_test;
    cout << "enter raw num: " << endl;
    int raw_n;
    cin >> raw_n;
    int input;
    for(int raw = 0; raw < raw_n; raw++)
    {
        vector<int> temp_vector;
        for(int col = 0; col < raw+1; col++)
        {
            cout << "enter the numbers" << endl;
            cin >> input;
            temp_vector.push_back(input);
            
        }
        dp_test.push_back(temp_vector);
        for(auto iter : dp_test[raw_n - 1])
            dp_max.push_back(iter);
    }
}

int maxSum(vector<vector<int>> nums, int start_raw, int start_col, int n, vector<int> max_sums)
{
    for(int raw = n - 1; raw > start_raw - 1; raw--)
    {
        for(int col = 0; col < n; col++)
        {
        int max = nums[raw + 1][col] >= nums[raw + 1][col+1] ? nums[raw + 1][col] : nums[raw + 1][col + 1];
            max_sums[col] = max + nums[raw][col];
        }
    }
    return max_sums[start_col];
}

int main()
{
    vector<vector<int>> input_vec;
    vector<int> max_vec;
    getInput(input_vec,max_vec);
    cout << maxSum(input_vec, 0, 0, input_vec.size(), max_vec) << endl;
    return 0;
}
  • 遞歸到動規(guī)程序的一般轉(zhuǎn)換方法
    • 遞歸函數(shù)有n個(gè)參數(shù)勉痴,就定義一個(gè)n維的數(shù)組,數(shù)組的下標(biāo)是遞歸函數(shù)參數(shù)的取值范圍树肃,數(shù)組元素的值時(shí)遞歸函數(shù)的返回值蒸矛,這樣就可以從邊界開始,逐步填充數(shù)組扫外,這就相當(dāng)于計(jì)算遞歸函數(shù)值的逆過程莉钙。

例題二、 最長公共子序列

題目描述:

給出兩個(gè)字符串筛谚,求出這樣的一 個(gè)最長的公共子序列的長度:子序列 中的每個(gè)字符都能在兩個(gè)原串中找到磁玉, 而且每個(gè)字符的先后順序和原串中的 先后順序一致。

解法方法分析:

首先拿到題目驾讲,是求最優(yōu)解的題目蚊伞,一般用dp來解,但是dp需要有無后效性的最優(yōu)子問題吮铭,這個(gè)需要好好設(shè)計(jì)dp的狀態(tài)时迫,設(shè)計(jì)不出來就不能用dp;
首先想到的是對于長度為m和n的兩個(gè)字符串S1和S2谓晌,其

  • 狀態(tài):最長公共子序列用dp(m,n)表示掠拳;
  • 那么顯然邊界條件:
dp(i,0) = 0; (i = 0...m)
dp(0,j) = 0; (j = 0...n)
  • 狀態(tài)轉(zhuǎn)移方程
當(dāng)S1[i-1] == S2[j-1]時(shí)這時(shí)顯然的:dp(i, j) = dp(i-1, j-1) + 1; 
當(dāng)S1[i-1] != S2[j-1]時(shí)有:dp(i, j) = max(dp(i-1,j), dp(i, j-1))
屏幕快照 2019-11-19 上午12.34.52的副本.png
  • 顯然dp(i, j)不會比dp(i-1,j)和dp(i, j-1)小纸肉;
  • 那么會不會比兩個(gè)都大呢溺欧?假設(shè)dp(i, j)比dp(i-1,j)大喊熟,因?yàn)槎呶ㄒ徊煌木褪荢1[i-1],那么S1[i-1]一定是S1和S2的最長公共子序列的最后一個(gè)元素姐刁;同理芥牌,假設(shè)dp(i, j)比dp(i,j-1)大,那么S2[j-1]一定是S1和S2的最長公共子序列的最后一個(gè)元素聂使;那么必然S1[i-1] == S2[j-1]壁拉,而這個(gè)于假設(shè)沖突,所以可以得到上面的狀態(tài)轉(zhuǎn)移方程柏靶;
  • 采用遞推的方式實(shí)現(xiàn)


    屏幕快照 2019-11-19 上午12.40.12.png

    image.png
int longestCommonSubsequence(string text1, string text2) {
        if(text1.size() == 0 || text2.size() == 0){
            return 0;
        }
        int len1 = text1.size();
        int len2 = text2.size();
        vector<vector<int>> lcs;
        lcs.resize(len1 + 1);

        for(size_t i = 0; i <= len1; i++){
            lcs[i].push_back(0);
            lcs[i].resize(len2 + 1);
        }
        for(size_t j = 1; j <= len2; j++){
            lcs[0][j] = 0;
        }
        for(size_t i = 1; i <= len1; i++){
            for(size_t j = 1; j <= len2; j++){
                if(text1[i-1] == text2[j-1]){
                    lcs[i][j] = lcs[i-1][j-1] + 1;
                } else {
                    lcs[i][j] = lcs[i-1][j] > lcs[i][j-1] ? lcs[i-1][j] : lcs[i][j-1];
                }
            }
        }
        return lcs[len1][len2];
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弃理,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宿礁,更是在濱河造成了極大的恐慌案铺,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梆靖,死亡現(xiàn)場離奇詭異,居然都是意外死亡笔诵,警方通過查閱死者的電腦和手機(jī)返吻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乎婿,“玉大人测僵,你說我怎么就攤上這事⌒霍幔” “怎么了捍靠?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長森逮。 經(jīng)常有香客問我榨婆,道長,這世上最難降的妖魔是什么褒侧? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任良风,我火速辦了婚禮,結(jié)果婚禮上闷供,老公的妹妹穿的比我還像新娘烟央。我一直安慰自己,他們只是感情好歪脏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布疑俭。 她就那樣靜靜地躺著,像睡著了一般婿失。 火紅的嫁衣襯著肌膚如雪钞艇。 梳的紋絲不亂的頭發(fā)上啄寡,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機(jī)與錄音香璃,去河邊找鬼这难。 笑死,一個(gè)胖子當(dāng)著我的面吹牛葡秒,可吹牛的內(nèi)容都是我干的姻乓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼眯牧,長吁一口氣:“原來是場噩夢啊……” “哼蹋岩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起学少,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剪个,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后版确,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扣囊,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年绒疗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侵歇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,745評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吓蘑,死狀恐怖惕虑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情磨镶,我是刑警寧澤溃蔫,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站琳猫,受9級特大地震影響伟叛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沸移,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一痪伦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雹锣,春花似錦网沾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春醋旦,著一層夾襖步出監(jiān)牢的瞬間恒水,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工饲齐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钉凌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓捂人,卻偏偏與公主長得像御雕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子滥搭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評論 2 354

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