算法思想之動(dòng)態(tài)規(guī)劃(三)——找零錢問題

前言

今天我們繼續(xù)討論經(jīng)典的動(dòng)態(tài)規(guī)劃問題之找零錢問題宰缤。

找零錢問題

問題描述

假設(shè)你是一名超市收銀員痊剖,現(xiàn)有n種不同面值的貨幣告丢,每種面值的貨幣可以使用任意張枪蘑。顧客結(jié)賬時(shí),你需要找給顧客aim元零錢岖免,你可以給出多少種方法岳颇。例如,有1颅湘、2话侧、3元三種面值的貨幣,你需要找零3元闯参,那么共有3種方法:1張1元+1張2元瞻鹏、3張1元、1張3元鹿寨。

問題分析

假設(shè)長度為n的一維數(shù)組penny新博,其中每個(gè)元素對(duì)應(yīng)每種貨幣的面值。找零錢問題可以抽象為使用penny中的元素可以有多少種方法組成數(shù)值aim脚草。
簡單的赫悄,我們可以遍歷數(shù)組,對(duì)下標(biāo)index的元素使用i(0 \leq i \leq aim / penny[index])次, 計(jì)算剩余的數(shù)組元素和剩余數(shù)值滿足要求的方法數(shù)玩讳。把每一次的方法數(shù)相加求和即為該問題的解。不難發(fā)現(xiàn)嚼贡,每一次要求解的都是和父問題具有同樣性質(zhì)的子問題熏纯,即使用penny[index:n-1]中的元素有多少種方法組成數(shù)值aim - i * penny[index]
由此粤策,很容易寫出該問題的暴力搜索(即遞歸)方法和記憶搜索方法樟澜。但是如果要直接寫出動(dòng)態(tài)規(guī)劃的狀態(tài)轉(zhuǎn)移方程可能需要費(fèi)點(diǎn)功夫。不過叮盘,我們可以按照算法思想之動(dòng)態(tài)規(guī)劃(一)討論的動(dòng)態(tài)規(guī)劃的一般步驟進(jìn)行思考秩贰。
(1)劃分階段:按照問題的時(shí)間或空間特征,把問題分為若干個(gè)階段柔吼。在劃分階段時(shí)毒费,注意劃分后的階段一定要是有序的或者是可排序的,否則問題就無法求解愈魏。
(2)確定狀態(tài)和狀態(tài)變量:將問題發(fā)展到各個(gè)階段時(shí)所處于的各種客觀情況用不同的狀態(tài)表示出來觅玻。當(dāng)然想际,狀態(tài)的選擇要滿足無后效性。
(3)確定決策并寫出狀態(tài)轉(zhuǎn)移方程:因?yàn)闆Q策和狀態(tài)轉(zhuǎn)移有著天然的聯(lián)系溪厘,狀態(tài)轉(zhuǎn)移就是根據(jù)上一階段的狀態(tài)和決策來導(dǎo)出本階段的狀態(tài)胡本。所以如果確定了決策,狀態(tài)轉(zhuǎn)移方程也就可寫出畸悬。但事實(shí)上常常是反過來做侧甫,根據(jù)相鄰兩個(gè)階段的狀態(tài)之間的關(guān)系來確定決策方法和狀態(tài)轉(zhuǎn)移方程。
(4)尋找邊界條件:給出的狀態(tài)轉(zhuǎn)移方程是一個(gè)遞推式蹋宦,需要一個(gè)遞推的終止條件或邊界條件披粟。

對(duì)于上面4條,我們一一來看妆档。
(1) 劃分階段
構(gòu)造有序或可排序的階段僻爽,要求我們計(jì)算的時(shí)候肯定當(dāng)前計(jì)算結(jié)果依賴于前面階段的結(jié)果。如果問題中的aim=3贾惦,penny = [1, 2, 3]胸梆,如何劃分階段呢?對(duì)于penny來說须板,可按照下標(biāo)進(jìn)行劃分碰镜,即1,2,3這3個(gè)階段;對(duì)于aim來說习瑰,由于每一階段下都要求aim是非負(fù)整數(shù)绪颖,那么我們可以劃分為0-aim,即0甜奄,1柠横,2,3這4個(gè)階段课兄。
(2) 確定狀態(tài)和狀態(tài)變量
由第(1)步牍氛,我們可以得到一個(gè)的n \times (aim+1)的矩陣dp,每個(gè)矩陣元素dp[i][j]代表的含義是使用數(shù)組pennyi個(gè)元素組成數(shù)值j的方法總數(shù)烟阐。
(3) 確定決策并寫出狀態(tài)轉(zhuǎn)移方程
由第(2)步總結(jié)可得出規(guī)律:dp[i][j] = dp[i-1][j] + dp[i-1][j-penny[i]] + ... + dp[i-1][j-k*penny[i]].例如:仍然以(1)中的問題為例搬俊,dp[1][2] = dp[0][2] + dp[0][2-1*1]),代表使用penny前2個(gè)元素(即1蜒茄,2)組成2的方法數(shù) = 不使用2組成2的方法數(shù) + 使用1個(gè)2組成2的方法數(shù)唉擂。
其實(shí),對(duì)于該狀態(tài)轉(zhuǎn)移方程還可以繼續(xù)優(yōu)化:令z = j-penny[i]由于dp[i][z] = dp[i-1][z] + dp[i-1][z - penney[i]] +... + dp[i-1][z-k' *penny[i]]檀葛,可以看出dp[i][z]dp[i][j]展開式中第2項(xiàng)之后的值是一樣的玩祟,因此狀態(tài)方程可化簡為:dp[i][j] = dp[i-1][j] + dp[i][j-penny[i]]
(4) 尋找邊界條件
由第(3)步,可得到化簡前的邊界條件為:j-k*penny[i] \geq 0屿聋,化簡后的邊界條件為:j-penny[i] \geq 0卵凑。
總結(jié)來看庆聘,只要解決問題的階段、狀態(tài)和狀態(tài)轉(zhuǎn)移決策確定了勺卢,就可以寫出狀態(tài)轉(zhuǎn)移方程(包括邊界條件)伙判。

代碼實(shí)現(xiàn)

public class Exchange {
    // 用于記憶搜索的計(jì)算過的方案
    public static HashMap<String, Integer> map = new HashMap<>();

    public static int countWays(int[] penny, int n, int aim) {
        if (0 == n || null == penny || aim <= 0) {
            return 0;
        }
        // return core1(penny, 0, aim);
        // return core2(penny, 0, aim);
        // return core3(penny, n, aim);
        return core4(penny, n, aim);
    }

    /**
     * 暴力搜索
     * @param penny
     * @param index
     * @param aim
     * @return
     */
    public static int core1(int[] penny, int index, int aim) {
        int result = 0;
        if (index == penny.length) {
            result = aim == 0 ? 1 : 0;
        } else {
            for (int i = 0; i * penny[index] <= aim; i++) {
                result += core1(penny, index + 1, aim - i * penny[index]);
            }
        }
        return result;
    }

    /**
     * 記憶搜索
     * @param penny
     * @param index
     * @param aim
     * @return
     */
    public static int core2(int[] penny, int index, int aim) {
        String key = String.valueOf(index) + "_" + String.valueOf(aim);
        if (map.containsKey(key)) {
            return map.get(key);
        }
        int result = 0;
        if (index == penny.length) {
            result = aim == 0 ? 1 : 0;
        } else {
            for (int i = 0; i * penny[index] <= aim; i++) {
                result += core2(penny, index + 1, aim - i * penny[index]);
            }
        }
        map.put(key, result);
        return result;
    }

    /**
     * 動(dòng)態(tài)規(guī)劃
     * @param penny
     * @param n
     * @param aim
     * @return
     */
    public static int core3(int[] penny, int n, int aim) {
        int[][] dp = new int[n][aim + 1];
        for (int i = 0; i < aim + 1; i++) {
            dp[0][i] = i % penny[0] == 0 ? 1 : 0;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < aim + 1; j++) {
                for (int m = 0; m * penny[i] <= j; m++) {
                    dp[i][j] += dp[i - 1][j - m * penny[i]];
                }
            }
        }
        return dp[n-1][aim];
    }

    /**
     * 動(dòng)態(tài)規(guī)劃優(yōu)化
     * @param penny
     * @param n
     * @param aim
     * @return
     */
    public static int core4(int[] penny, int n, int aim) {
        int[][] dp = new int[n][aim + 1];
        for (int i = 0; i < aim + 1; i++) {
            dp[0][i] = i % penny[0] == 0 ? 1 : 0;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < aim + 1; j++) {
                if (j < penny[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - penny[i]];
                }
            }
        }
        return dp[n - 1][aim];
    }

    public static void main(String[] args) {
        int[] penny = new int[]{3, 4, 7};
        int n = penny.length;
        int aim = 33;
        System.out.println(countWays(penny, n, aim));
    }
}

其他經(jīng)典問題

未來幾篇博文,我將繼續(xù)對(duì)經(jīng)典的動(dòng)態(tài)規(guī)劃問題進(jìn)行整理黑忱,敬請關(guān)注~
由于本人水平有限宴抚,文章難免有欠妥之處,歡迎大家多多批評(píng)指正甫煞!

寫在最后

歡迎大家關(guān)注我的個(gè)人博客復(fù)旦猿菇曲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市抚吠,隨后出現(xiàn)的幾起案子常潮,更是在濱河造成了極大的恐慌,老刑警劉巖楷力,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊式,死亡現(xiàn)場離奇詭異,居然都是意外死亡萧朝,警方通過查閱死者的電腦和手機(jī)岔留,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來检柬,“玉大人献联,你說我怎么就攤上這事『沃罚” “怎么了里逆?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長用爪。 經(jīng)常有香客問我原押,道長,這世上最難降的妖魔是什么项钮? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任班眯,我火速辦了婚禮希停,結(jié)果婚禮上烁巫,老公的妹妹穿的比我還像新娘。我一直安慰自己宠能,他們只是感情好亚隙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著违崇,像睡著了一般阿弃。 火紅的嫁衣襯著肌膚如雪诊霹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天渣淳,我揣著相機(jī)與錄音脾还,去河邊找鬼。 笑死入愧,一個(gè)胖子當(dāng)著我的面吹牛鄙漏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播棺蛛,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怔蚌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了旁赊?” 一聲冷哼從身側(cè)響起桦踊,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎终畅,沒想到半個(gè)月后籍胯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡声离,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年芒炼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片术徊。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡本刽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赠涮,到底是詐尸還是另有隱情子寓,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布笋除,位于F島的核電站斜友,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垃它。R本人自食惡果不足惜鲜屏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望国拇。 院中可真熱鬧洛史,春花似錦、人聲如沸酱吝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽务热。三九已至忆嗜,卻和暖如春己儒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捆毫。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來泰國打工闪湾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绩卤。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓响谓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親省艳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娘纷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

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