算法系列-動態規劃(3):找零錢、走方格問題

最近在搗鼓算法,因此寫一些關於算法的文章
此係列爲動態規劃相關文章。面試

系列歷史文章:
算法系列-動態規劃(1):初識動態規劃算法

算法系列-動態規劃(2):切割鋼材問題數組

算法系列-動態規劃(3):找零錢、走方格問題優化


找零錢問題,湊數問題

最近老幣愈來愈值錢,是投資的一個好方向。code

這不,八哥從某魚入手了幾張老幣。blog

這是一塊的:遊戲

一元

這是五塊的:get

五塊

這是十塊的:數學

十塊

不得不說,老幣仍是挺好看的it

看看這成色,過幾年必定很值錢,這就是我留給我孩子的財產。

可是不當心給羅拉看到了,而後就有了下面的對話....

對話記錄

羅拉

八哥,這錢不錯,給幾張給我玩玩

八哥

姐姐,這是錢,個人投資,怎麼能隨便玩

羅拉

我就玩兩天,又不會弄壞

八哥

這有啥好玩?你又不是沒見過

羅拉

真小氣,玩下能少塊肉?

八哥

話是這麼說沒錯,但是我還沒捂熱呢~
這樣吧,雖然個人也是你的,可是你總要付出點啥吧,否則我純虧

羅拉

怎麼?要我買?瞧你這出息...

八哥

別激動,這哪能啊,談錢多傷感情
我用這錢出道題,你答得出來,這錢歸你了
答不出來,就讓我再捂幾天,怎樣?

羅拉

行,沒問題,可是不能超出我能力範圍

八哥

這...,好吧
os:豈不是註定我血虧???


找零錢的方式

錢?她能力範圍?又不太簡單?動態規劃?八哥腦子一動,立刻就想到一個題目。

因而,虎軀一震,眉頭一舒,摸摸下巴,點點頭。

「有了,羅拉請聽題」

「你看,我這裏的舊幣有面值{1,5,10}的,假設我這裏每種幣值數量都無限,請問我若是要湊成10元有幾種方法?」

「就這?」羅拉聽罷,不屑道。

「別急,這只是最簡單問題,後面還有幾個呢,保證一系列問題。」八哥一副奸計得逞的嘴臉。

「行吧,這有何難,組成十元,有如下幾種。」羅拉自信滿滿。
「第一種:我用10張一元」;
「第二種:我用2張五元」;
「第三種:我用1張十元」;
「第四種:我用1張五元和5張一元」;
「一共就這四種,沒錯吧」。

「嘖嘖,厲害呀羅拉,直接列舉出來了,你數學必定是數學老師教的。」八哥一副死豬不怕開水燙的樣子。

「咦...,別陰陽怪氣的,趕忙後面的問題,說好了,和前面一系列的,別換題目」羅拉嫌棄地擺擺手。

「放心,絕對是一系列的,並且是親生兒砸,請聽題」,八哥正聲道。

「請問,用上述的紙幣分別湊成50元,100元,1000元分別有幾種方法?」。

「你丫存心的吧,這我要算到何時,你要是再來個10000,我直接認輸得了?」。羅拉這火爆脾氣可忍不了。

「誰讓你手算了,你能夠把這個當成面試題,實現一個算法試試?」八哥啞然失笑。

「算法?算法也許能實現,可是超出我如今能力範圍好吧,這個不符合要求。」羅拉忿忿道。

「不對啊,這怎麼超出你能力範圍呢,前兩天不是剛跟你說了那啥嗎?你難道忘了?」八哥瞪大眼睛一副不敢置信的樣子。

「前兩天?動態規劃?」羅拉恍然大悟。

「對啊,這貨長得不夠動態?以至你認不出來?算了不扯了,你按照動態規劃的思路先分析分析吧。」八哥無奈道。

接下來,羅拉一頓分析猛如虎:
「嗯,我試試」。
「首先,我有{1,5,10}三種幣值,若是湊出n的組合數量有f(n)」 ;
「那麼接下來我就得拆分f(n),將他分紅更小的子問題」;
「因爲個人幣值只有三種,因此只能拆出f(n-1),f(n-5),f(n-10)」;
「又由於,這三種都是能夠獲得f(n),因此他們之間的關係爲f(n) = f(n-1) + f(n-5) + f(n-10)
「最後得考慮邊界值,邊界的起始是n=1,此時可選的方案f(1)=1」。

「不對哦,你想一想起始真的是n=1嘛?」 羅拉分析得正深刻的的時候,八哥打斷了她的思路。

「不是嗎?1 是咱們能夠直接肯定的吧?」羅拉不解。

1 是能夠直接肯定沒錯,更準確地說是咱們可以一眼看出。若是我要求5,咱們很容易獲得五個1和一個5兩個方案吧,你把5代入你那個公式試試?」。

n=5?,f(5) = f(5-1) + f(5-5) = f(4) + f(0)
「咦,還有個f(0),也就是說f(1)=f(1-1)=f(0),這裏漏了,0應該也是一種選擇,因此初始狀態應該是湊0,而且只有1種選擇。」羅拉恍然大悟。

「是的,因此如今能夠寫出代碼了吧?」

「嗯,稍後,此次不講碼徳直接能夠寫個徹底版的了」羅拉自信道。

因而一頓鍵盤噼裏啪啦,代碼出爐。

public class Coin {
    public static void main(String[] args) {
        System.out.println("湊成10塊的方案有:"+change(10) + "種");
        System.out.println("湊成10000塊的方案有:"+change(10000) + "種");
    }

    public static int change(int target) {
        int[] coins = {1, 5, 10};
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int coin : coins)
            for (int x = coin; x <= target ; x++) {
                dp[x] += dp[x - coin];
            }
        return dp[target];
    }
}

//輸出結果
湊成10塊的方案有:4種
湊成10000塊的方案有:1002001種

八哥瞄了一眼
「不錯,挺熟練了,不過這個不算是本身想出來的吧,我赤裸裸的提示了吧?我換一個角度再問一下不過度吧?」

「額,能夠,你問吧」羅拉老臉一紅,自知理虧,只得答應八哥的要求。


找零錢的最佳方案

「好,如今的問題是,我要湊出n,至少要多少張紙幣?作出來,我這寶貝就給你捂幾天又何妨?」。八哥撩一撩頭髮,笑道。

「行,我想一想,大概知道怎麼作了,我分析下先」,羅拉不甘示弱。

「首先對於一個f(n),個人結果能夠來自f(n-1),f(n-5),f(n-10)這點和以前同樣。」
「不同的地方在於咱們如今不是求和而是求最小值。」
「因此,f(n) = min(f(n-1),f(n-5),f(n-10)) + 1
「最後再肯定一下邊界,初始值應該是0,f(0)=0」。

「嗯,分析的沒錯,show me your code。」八哥點點頭。

「等等,立刻。」羅拉一喜,立刻開始舞動鍵盤。

啪啪兩分鐘,代碼出爐。

public class Coin {
    static int[] coins = {1, 5, 10};

    public static void main(String[] args) {
        System.out.println("湊成55塊至少須要的紙幣爲:" + minCoinCnt(55) + "張");
        System.out.println("湊成999塊至少須要的紙幣爲:" + minCoinCnt(999) + "張");
        System.out.println("湊成1000塊至少須要的紙幣爲:" + minCoinCnt(1000) + "張");
    }

    public static int minCoinCnt(int target) {
        int[] dp = new int[target + 1];
        //湊成0元須要0張
        dp[0] = 0;
        for (int x = 1; x <= target; x++) {
            dp[x] = Integer.MAX_VALUE;
            for (int coin : coins) {
                //fn(n) = min(f(n-1),f(n-5),f(n-10)),注意f(n)的n要大於等於0,因此須要(x-coin>=0)
                //選擇紙幣叫小的方案
                if (x - coin >= 0) dp[x] = Math.min(dp[x], dp[x - coin] + 1);
            }
        }
        return dp[target];
    }

}

//輸出結果
湊成55塊至少須要的紙幣爲:6張
湊成999塊至少須要的紙幣爲:104張
湊成1000塊至少須要的紙幣爲:100張

「嗯,能夠,我還覺得你會按照以前的循環來寫呢,想不到沒入坑。」 八哥悻悻道。

「哼,我又不傻,公式我都寫出來,還怕寫不出代碼?哈哈,趕忙的,願賭服輸,把你寶貝給我捂幾天。」羅拉一副小人得志的樣子。

「諾,拿去,你可要好好保護它們啊。」在把錢交出的瞬間,八哥心如刀割。沒辦法,即便不打賭也得交出去。哎....


走方格

三天後,晚上六點,羅拉下班回到家了,略帶笑容,顯然心情不錯。

「咦,羅拉今天怎麼這麼早?有啥開心事,看你樂得。」八哥疑惑

「今天事情工做比較簡單,因此沒那麼忙,今天公司下午茶玩遊戲,贏了點零食。」羅拉想到開心的事情,不覺語氣歡快起來了。

「遊戲?啥遊戲?」

「走方格,從一個格子走到另外一個格子有多少種走法。我答得比較快。碾壓同事」羅拉一副快誇個人樣子。

「走方格?是否是從左上角到右下角,只能向下或向右的走法,像這樣的?」八哥好像想起了什麼,拿起紙筆隨手畫了一個圖。

走方格

「是的,你知道?要不咱們玩玩?」羅拉看了一眼,顯然對本身很自信。

「好啊,不過得來點彩頭吧。」

「喲,說的好像你已經贏了似的,你想要啥彩頭?」

「那啥,舊幣你把玩了三天了,是否是該讓我捂一下了?」

「原來你打的是這主意...」羅拉沒好氣地說道。

「不過也無所謂,我以爲我不會輸,這樣,咱們各寫一組數組(l1,l2)和(b1,b2),分別組成l1 * b1,l2 * b2 的格子,而後計算,看誰先算出兩個,一局定勝負,能夠吧?」。

「嗯,很公平,我沒問題,開始吧。」 八哥成竹在胸。


走方格(走法數量)

不一下子,兩人都把紙條寫好了。

攤開紙條

羅拉寫的是(3,6)

八哥寫的是(7,5)

「咱們如今要計算3 * 7 ,6 * 5的方格走法,即便開始」。羅拉說完,拿起紙筆,畫了起來,贏在了起跑線。

30秒後

「嘿嘿,分別爲 28 和 126」,不到一分鐘,八哥邊說出了答案。

「你瞎說的吧,我第一個都還沒算完呢,你兩個都完了?」

「山人自有妙計,你輸了」

「等我算完再說,誰知道你的對的仍是錯的?」

「但是你要是本身算錯了或算好久那不是浪費時間?」

「否則捏,我總得驗證結果吧?」羅拉忍不住翻白眼。

「看你畫了這麼多圖,挺辛苦的,動動腦子,我要是在你公司,今天這遊戲就通殺了?」

「咦,難道有規律?」羅拉自動忽略八哥的後半句話。

「你三天前怎麼贏得個人舊幣的?你想一想?」

「贏錢?打賭啊,不對,難道是動態規劃?」

「是啊,你怎麼每次都得提醒纔想得起來啊」八哥無奈道。

「誰知道你連這都埋個坑?行了,我知道接下來該分析分析了。」

「假設到最右下角的方式有f(n),因爲只能往左邊或下面走,因此f(n)=f(上邊)+f(左邊)
「嗯...其實用二維數組表示好像更好,應該表示爲dp[x][y]=dp[x-1][y]+dp[x][y-1]
「接下來就是子問題的計算,直到邊界」
「這裏的邊界,應該是有沿着牆邊走,由於只能向左或向右,因此dp[x][0]=0,dp[0][y]=0
「接下來代碼實現」

public class WalkGrid {
    public static void main(String[] args) {
        System.out.println("3*7方格走法共有:"+walk(3,7)+" 種");
        System.out.println("5*6方格走法共有:"+walk(5, 6)+" 種");
    }

    public static int walk(int n, int m) {
        int[][] dp = new int[n][m];
        //定義邊界
        for (int i = 0; i < n; i++) dp[i][0] = 1;
        for (int i = 0; i < m; i++) dp[0][i] = 1;
        //雙重循環,計算dp數組的值
        for (int i = 1; i < n; i++)
            for (int j = 1; j < m; j++)
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        return dp[n - 1][m - 1];
    }
}
//輸出結果
3*7方格走法共有:28 種
5*6方格走法共有:126 種

「咦你的答案沒錯誒。不對,你沒寫代碼,並且一分鐘都不到,這確定不是最快的。」羅拉忽然醒悟。

「對於這個題目,固然不是最快的,你想一下,對於n * m的格子,我一共要走多少步?向上多少,向下多少?」

「向下是n-1,向右是m-1,一共是m + n - 2,但是這個和你算得快沒啥關係吧?」羅拉不解

「誰說不要緊,一共m + n - 2,我只要肯定向下或向右走的,另外一個方向的是否是也肯定了?換言之,就是m + n - 2中選n - 1m - 1吧,你發現了什麼?」

「從總數裏面選出某些...吖,是排列組合的組合,這是一個數學問題」羅拉恍然大悟。

「是的,這裏能夠當作是組合問題,經過組合共識,10之內的分分鐘就算出來了不過度吧,你甚至能夠試着代碼實現」八哥得意說道

「行吧,我試試,你就是想我寫代碼吧,我想一下組合公式組合數計算方法,從N項中選出M項:f(n,m) = n! / ((n - m)! * m!)

「代碼就是這樣」

public class WalkGrid {
    public static void main(String[] args) {
        System.out.println("3*7方格走法共有:" + cal(3, 7) + " 種");
        System.out.println("5*6方格走法共有:" + cal(5, 6) + " 種");
    }


    public static int cal(int n, int m) {
        int tot = m + n - 2;
        int res = 1;
        int max = Math.max(m - 1, n - 1);
        //公式中tot!與max!部分能夠抵消max!部分,減小計算量
        for (int i = tot; i > max; i--) res *= i;
        for (int i = 1; i <= tot - max; i++) res /= i;
        return res;
    }
}
//輸出結果
3*7方格走法共有:28 種
5*6方格走法共有:126 種

公式中的f(n,m) = n! / ((n - m)! * m!)
能夠化簡爲f(n,m) = n*(n-1)*(n-2)...*(m+1) / (n - m)! 就是代碼中max優化的原理

「算我輸了,你寶貝等下就還你,話說這個豈不是用數學方法更快?」羅拉賭品仍是能夠的。

「因此我說了對於這個問題是個樣啊,我只要稍微變化一下,公式就很差使了」

「是嗎?舉個栗子看看」 羅拉來了興趣。

「行,看在你賭品不錯的份上,舉了例子」


走格子最短路徑

「從前有個公主,被魔王抓了,關在魔窟」

「一個勇敢王子準備前往魔窟營救公主,這個過程充滿危險,稍有不慎就會有生命危險。」

「魔王在王子的必經之路上佈滿了陷阱,每個陷阱都會對王子形成傷害,地圖以下所示」

迷宮

「王子開始在左上角,每次只能往左或往右走一步,因爲魔王布了陷阱,每走一步都會失去部分生命值」

「王子有初始生命,請問王子可否成功救出公主」?

「這案例就無法用排列組合來作了,應爲不是每一個格子都是同樣的數字了。」八哥不緊不慢的舉了個例子。

「好像是誒,排列組合有點難,感受動態規劃挺好作的吧」羅拉想了一會,仍是放棄用排列組合了。

「是的,你能夠試試動態規劃怎麼作唄。」

「嗯,我看看,也作了好多題了,看看能不能獨立作出來,你別給我提示了,我先理一下」 看來羅拉幹勁十足啊。

「王子有初始血量,想要成功就出公主就不能半路給跪了」
「要救出公主,只要我失去的生命值小於初始生命值,就能夠了」
「只要求出全部路徑所損失生命值的最小值和王子初始生命值作對比,就能夠知道王子有沒有可能救出公主了」
「因此這個也是一個求最小值的問題」

羅拉顯然思路很清晰

「接下來就是分析一下動態規劃要怎麼作了」
「用dp[x][y]記錄走到(x,y)時損失的生命值」
「因爲只能向左或向右,因此相關的子問題爲dp[x][y]=dp[x-1][y]+dp[x][y-1]
「接下來考慮邊界問題」
「向右只有一條路經,因此dp[x][0]=dp[x-1][0]+(x,0)
「向下也只有一條路dp[0][y]=dp[0][y-1]+(0,y)
「入口,也就是(0,0)應該不損失生命值,因此,dp[0][0]=0」

「而後就是編寫代碼了」

「完事,你看看」羅拉用力敲下最後一下鍵盤。

public class SavePrincess {
    //魔王宮殿
    static int palaces[][] = {
            {0, 6, 9, 10, 12, 15},
            {17, 33, 32, 8, 21, 20},
            {3, 44, 11, 20, 1, 0}};

    public static void main(String[] args) {
        int init = 50;//初始生命值
        int min = save();
        System.out.println("王子初始血量爲:" + init + ", " + (min - init >= 0 ? "不能" : "能") + "救出公主");

        init = 80;//初始生命值
        System.out.println("王子初始血量爲:" + init + ", " + (min - init >= 0 ? "不能" : "能") + "救出公主");

        System.out.println("就出公主的損失生命值得最小值爲:" + min);

    }

    /**
     * 拯救公主的最低損失生命值
     * @return
     */
    public static int save() {
        int n = palaces.length;
        int m = palaces[0].length;
        int[][] dp = new int[n][m];
        //起始位置爲0
        dp[0][0] = 0;
        //向下初始化
        for (int i = 1; i < n; i++) dp[i][0] = dp[i - 1][0] + palaces[i][0];
        //向右初始化
        for (int i = 1; i < m; i++) dp[0][i] = dp[0][i - 1] + palaces[0][i];
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + palaces[i][j];
            }
        }
        return dp[n - 1][m - 1];
    }
}
//輸出結果
王子初始血量爲:50, 不能救出公主
王子初始血量爲:80, 能救出公主
就出公主的損失生命值得最小值爲:54

「嗯,不錯,看來動態規劃你掌握的不錯了。」八哥看了看結果,點頭笑道。

「作多了幾道題,感受就這麼回事,沒啥難度。」羅拉難免翹起了尾巴。

「別開心的太早,明天我找個經典案例給你試試?」八哥不懷好意道

「沒問題,今晚出去吃吧,可貴這麼早下班。」

「好啊,等下,我先把寶貝放好先」。

歡迎關注【兔八哥雜談】,會持續分享更多內容.

相關文章
相關標籤/搜索