一次由爬樓梯和零錢兌換II引起的DP子問題定義思考

在LeetCode上有兩道題目非常類似墅冷,分別是

如果我們把每次可走步數(shù)/零錢面額限制為[1,2], 把樓梯高度/總金額限制為3. 那么這兩道題目就可以抽象成"給定[1,2], 求組合成3的組合數(shù)和排列數(shù)"。

接下來引出本文的核心兩段代碼捅伤,雖然是Cpp寫的宁玫,但是都是最基本的語法主届,對于可能看不懂的地方浪秘,我加了注釋评架。

class Solution1 {
public:
    int change(int amount, vector<int>& coins) {
        int dp[amount+1];
        memset(dp, 0, sizeof(dp)); //初始化數(shù)組為0
        dp[0] = 1;
        for (int j = 1; j <= amount; j++){ //枚舉金額
            for (int coin : coins){ //枚舉硬幣
                if (j < coin) continue; // coin不能大于amount
                dp[j] += dp[j-coin];
            }
        }
        return dp[amount];
    }
};
class Solution2 {
public:
    int change(int amount, vector<int>& coins) {
        int dp[amount+1];
        memset(dp, 0, sizeof(dp)); //初始化數(shù)組為0
        dp[0] = 1;
        for (int coin : coins){ //枚舉硬幣
            for (int j = 1; j <= amount; j++){ //枚舉金額
                if (j < coin) continue; // coin不能大于amount
                dp[j] += dp[j-coin];
            }
        }
        return dp[amount];
    }
};

如果不仔細(xì)看漓帅,你會覺得這兩個Solution似乎是一模一樣的代碼锨亏,但細(xì)心一點(diǎn)你會發(fā)現(xiàn)他們在嵌套循環(huán)上存在了差異。這個差異使得一個求解結(jié)果是排列數(shù)忙干,一個求解結(jié)果是組合數(shù)器予。

因此在不看后面的分析之前,你能分辨出哪個Solution是得到排列捐迫,哪個Solution是得到組合嗎乾翔?


在揭曉答案之前,讓我們先分別用DP的方法解決爬樓梯和零錢兌換II的問題施戴。每個解題步驟都按照DP三部曲反浓,a.定義子問題,b. 定義狀態(tài)數(shù)組暇韧,c. 定義狀態(tài)轉(zhuǎn)移方程勾习。

70. 爬樓梯

問題描述如下:

假設(shè)你正在爬樓梯。需要 n 階你才能到達(dá)樓頂懈玻。

每次你可以爬 1 或 2 個臺階巧婶。你有多少種不同的方法可以爬到樓頂呢?

這道題目子問題是涂乌,problem(i) = sub(i-1) + sub(i-2), 即求解第i階樓梯等于求解第i-1階樓梯和第i-2階樓梯之和艺栈。

狀態(tài)數(shù)組是 DP[i], 狀態(tài)轉(zhuǎn)移方程是DP[i] = DP[i-1] = DP[i-2]

那么代碼也就可以寫出來了。

class Solution {
public:
    int climbStairs(int n) {
        int DP[n+1];
        memset(DP, 0, sizeof(DP));
        DP[0] = 1;
        DP[1] = 1;
        for (int i = 2; i <= n; i++){
            DP[i] = DP[i-1] + DP[i-2] ;
        }
        return DP[n];

    }
};

由于每次我們只關(guān)注DP[i-1]和DP[i-2]湾盒,所以代碼中能把數(shù)組替換成2個變量湿右,降低空間復(fù)雜度,可以認(rèn)為是將一維數(shù)組降維成點(diǎn)罚勾。

如果我們把問題泛化毅人,不再是固定的1吭狡,2,而是任意給定臺階數(shù)丈莺,例如1,2,5呢划煮?

我們只需要修改我們的DP方程DP[i] = DP[i-1] + DP[i-2] + DP[i-5], 也就是DP[i] = DP[i] + DP[i-j] ,j =1,2,5

在原來的基礎(chǔ)上,我們的代碼可以做這樣子修改

class Solution {
public:
    int climbStairs(int n) {
        int DP[n+1];
        memset(DP, 0, sizeof(DP));
        DP[0] = 1;
        int steps[2] = {1,2};
        for (int i = 1; i <= n; i++){
            for (int j = 0; j < 2; j++){
                int step = steps[j];
                if ( i < step ) continue;// 臺階少于跨越的步數(shù)
                DP[i] = DP[i] + DP[i-step];
            }
        }
        return DP[n];

    }
};

后續(xù)修改steps數(shù)組缔俄,就實(shí)現(xiàn)了原來問題的泛化弛秋。

那么這個代碼是不是看起來很眼熟呢?我們能不能交換內(nèi)外的循環(huán)呢俐载?也就是下面的代碼

for (int j = 0; j < 2; j++){
    int step = steps[j];
    for (int i = 1; i <= n; i++){
        if ( i < step ) continue;// 臺階少于跨越的步數(shù)
         DP[i] = DP[i] + DP[i-step];
    }
}

大家可以嘗試思考下這個問題蟹略,嵌套循環(huán)是否能夠調(diào)換,調(diào)換之后的DP方程的含義有沒有改變遏佣?

零錢兌換II

問題描述如下:

給定不同面額的硬幣和一個總金額挖炬。寫出函數(shù)來計(jì)算可以湊成總金額的硬幣組合數(shù)。假設(shè)每一種面額的硬幣有無限個状婶。

定義子問題: problem(i) = sum( problem(i-j) ), j =1,2,5. 含義為湊成總金額i的硬幣組合數(shù)等于湊成總金額硬幣i-1, i-2, i-5,...的子問題之和茅茂。

我們發(fā)現(xiàn)這個子問題定義居然和我們之前泛化的爬樓梯問題居然是一樣的,那后面的狀態(tài)數(shù)組和狀態(tài)轉(zhuǎn)移方程也是一樣的太抓,所以當(dāng)前問題的代碼可以在之前的泛化爬樓梯問題中進(jìn)行修改而得。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int dp[amount+1];
        memset(dp, 0, sizeof(dp)); //初始化數(shù)組為0
        dp[0] = 1;
        for (int j = 1; j <= amount; j++){ //枚舉金額
            for (int i = 0; i < coins.size(): i++){ 
                int coin = coins[i]; //枚舉硬幣
                if (j < coin) continue; // coin不能大于amount
                dp[j] += dp[j-coin];
            }
        }
        return dp[amount];
    }
};

這就是我們之前的Solution1代碼令杈。

但是當(dāng)你運(yùn)行之后走敌,卻發(fā)現(xiàn)這個代碼并不正確,得到的結(jié)果比預(yù)期的大逗噩。究其原因掉丽,該代碼計(jì)算的結(jié)果是排列數(shù),而不是組合數(shù)异雁,也就是代碼會把1,2和2,1當(dāng)做兩種情況捶障。更加根本的原因是我們子問題定義出現(xiàn)了錯誤。

正確的子問題定義應(yīng)該是纲刀,problem(k,i) = problem(k-1, i) + problem(k, i-k)

即前k個硬幣湊齊金額i的組合數(shù)等于k-1個硬幣湊齊金額i的組合數(shù)加上在原來i-k的基礎(chǔ)上使用硬幣的組合數(shù)项炼。說的更加直白一點(diǎn),那就是用前k的硬幣湊齊金額i示绊,要分為兩種情況開率锭部,一種是沒有用前k-1個硬幣就湊齊了,一種是前面已經(jīng)湊到了i-k面褐,現(xiàn)在就差第k個硬幣了拌禾。

狀態(tài)數(shù)組就是DP[k][i], 即前k個硬幣湊齊金額i的組合數(shù)。

這里不再是一維數(shù)組展哭,而是二維數(shù)組湃窍。第一個維度用于記錄當(dāng)前組合有沒有用到硬幣k闻蛀,第二個維度記錄現(xiàn)在湊的金額是多少?如果沒有第一個維度信息您市,當(dāng)我們湊到金額i的時候觉痛,我們不知道之前有沒有用到硬幣k。

因?yàn)檫@是個組合問題墨坚,我們不關(guān)心硬幣使用的順序秧饮,而是硬幣有沒有被用到。是否使用第k個硬幣受到之前情況的影響泽篮。

狀態(tài)轉(zhuǎn)移方程如下

if 金額數(shù)大于硬幣
    DP[k][i] = DP[k-1][i] + DP[k][i-k]
else
    DP[k][i] = DP[k-1][i]

因此正確代碼如下:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int K = coins.size() + 1;
        int I = amount + 1;
        int DP[K][I];
        //初始化數(shù)組
        for (int k = 0; k < K; k++){
            for (int i = 0; i < I; i++){
                DP[k][i] = 0;
            }
        }
        //初始化基本狀態(tài)
        for (int k = 0; k < coins.size() + 1; k++){
            DP[k][0] = 1;
        }
        for (int k = 1; k <= coins.size() ; k++){
            for (int i = 1; i <= amount; i++){  
                if ( i >= coins[k-1]) {
                    DP[k][i] = DP[k][i-coins[k-1]] + DP[k-1][i]; 
                } else{
                    DP[k][i] = DP[k-1][k];
                }
            }
        }
        return DP[coins.size()][amount];
    }
};

我們初始化的數(shù)組大小為coins.size()+1* (amount+1), 這是因?yàn)榈谝涣惺怯矌艦?的基本情況盗尸。

此時,交換這里面的循環(huán)不會影響最終的結(jié)果帽撑。也就是

for (int i = 1; i <= amount; i++){  
    for (int k = 1; k <= coins.size() ; k++){ 
        if ( i >= coins[k-1]) {
            DP[k][i] = DP[k][i-coins[k-1]] + DP[k-1][i]; 
         } else{
             DP[k][i] = DP[k-1][k];
         }
     }
}

之前爬樓梯問題中泼各,我們將一維數(shù)組降維成點(diǎn)。這里問題能不能也試著降低一個維度亏拉,只用一個數(shù)組進(jìn)行表示呢扣蜻?

這個時候,我們就需要重新定義我們的子問題了及塘。

此時的子問題是莽使,對于硬幣從0到k,我們必須使用第k個硬幣的時候笙僚,當(dāng)前金額的組合數(shù)芳肌。

此狀態(tài)數(shù)組DP[i]表示的是對于第k個硬幣能湊的組合數(shù)

狀態(tài)轉(zhuǎn)移方程如下

 DP[[i] = DP[i] + DP[i-k]

于是得到我們開頭的第二個Solution。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int dp[amount+1];
        memset(dp, 0, sizeof(dp)); //初始化數(shù)組為0
        dp[0] = 1;
        for (int coin : coins){ //枚舉硬幣
            for (int i = 1; i <= amount; i++){ //枚舉金額
                if (i < coin) continue; // coin不能大于amount
                dp[i] += dp[i-coin];
            }
        }
        return dp[amount];
    }
};

好了肋层,繼續(xù)之前的問題亿笤,這里的內(nèi)外循環(huán)能換嗎?

顯然不能栋猖,因?yàn)槲覀冞@里定義的子問題是净薛,必須選擇第k個硬幣時,湊成金額i的方案蒲拉。如果交換了肃拜,我們的子問題就變了,那就是對于金額i, 我們選擇硬幣的方案全陨。

同樣的爆班,我們回答之前爬樓梯的留下的問題,原循環(huán)結(jié)構(gòu)對應(yīng)的子問題是辱姨,對于樓梯數(shù)i, 我們的爬樓梯方案柿菩。第二種循環(huán)結(jié)構(gòu)則是,固定爬樓梯的順序雨涛,我們爬樓梯的方案枢舶。也就是第一種循環(huán)下懦胞,對于樓梯3,你可以先2再1凉泄,或者先1再2躏尉,但是對于第二種循環(huán)

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市后众,隨后出現(xiàn)的幾起案子胀糜,更是在濱河造成了極大的恐慌,老刑警劉巖蒂誉,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件教藻,死亡現(xiàn)場離奇詭異,居然都是意外死亡右锨,警方通過查閱死者的電腦和手機(jī)括堤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绍移,“玉大人悄窃,你說我怎么就攤上這事□褰眩” “怎么了轧抗?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞬测。 經(jīng)常有香客問我鸦致,道長,這世上最難降的妖魔是什么涣楷? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮抗碰,結(jié)果婚禮上狮斗,老公的妹妹穿的比我還像新娘。我一直安慰自己弧蝇,他們只是感情好碳褒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著看疗,像睡著了一般沙峻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上两芳,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天摔寨,我揣著相機(jī)與錄音,去河邊找鬼怖辆。 笑死是复,一個胖子當(dāng)著我的面吹牛删顶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播淑廊,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼逗余,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了季惩?” 一聲冷哼從身側(cè)響起录粱,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎画拾,沒想到半個月后啥繁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碾阁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年输虱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脂凶。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡宪睹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚕钦,到底是詐尸還是另有隱情亭病,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布嘶居,位于F島的核電站罪帖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邮屁。R本人自食惡果不足惜整袁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望佑吝。 院中可真熱鬧坐昙,春花似錦、人聲如沸芋忿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戈钢。三九已至痹仙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間殉了,已是汗流浹背开仰。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抖所。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓梨州,卻偏偏與公主長得像,于是被迫代替她去往敵國和親田轧。 傳聞我的和親對象是個殘疾皇子暴匠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

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