1. 程式人生 > >算法系列-動態規劃(3):找零錢、走方格問題

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

最近在搗鼓演算法,所以寫一些關於演算法的文章 此係列為動態規劃相關文章。 系列歷史文章: [算法系列-動態規劃(1):初識動態規劃](https://mp.weixin.qq.com/s/YhbOi2_LInQ7EXP3WDgodA) [算法系列-動態規劃(2):切割鋼材問題](https://mp.weixin.qq.com/s/TsX5dkKEmJn9Qup0__4AAg) 算法系列-動態規劃(3):找零錢、走方格問題 ---- ### 找零錢問題,湊數問題 最近老幣越來越值錢,是投資的一個好方向。 這不,八哥從某魚入手了幾張老幣。 這是一塊的: ![一元](https://img2020.cnblogs.com/blog/1293390/202012/1293390-20201217174834753-1531716336.png) 這是五塊的: ![五塊](https://img2020.cnblogs.com/blog/1293390/202012/1293390-20201217174900217-1645542979.png) 這是十塊的: ![十塊](https://img2020.cnblogs.com/blog/1293390/202012/1293390-20201217174915715-864304051.png) 不得不說,老幣還是挺好看的 看看這成色,過幾年一定很值錢,這就是我留給我孩子的財產。 但是不小心給羅拉看到了,然後就有了下面的對話.... 對話記錄 | ---|

**羅拉**

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

**八哥**

姐姐,這是錢,我的投資,怎麼能隨便玩

| | |

**羅拉**

我就玩兩天,又不會弄壞|

**八哥**

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

| | |

**羅拉**

真小氣,玩下能少塊肉?|

**八哥**

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

|

**羅拉**

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

**八哥**

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

| | |

**羅拉**

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

**八哥**

這...,好吧
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張 ``` “嗯,可以,我還以為你會按照之前的迴圈來寫呢,想不到沒入坑。” 八哥悻悻道。 “哼,我又不傻,公式我都寫出來,還怕寫不出程式碼?哈哈,趕緊的,願賭服輸,把你寶貝給我捂幾天。”羅拉一副小人得志的樣子。 “諾,拿去,你可要好好保護它們啊。”在把錢交出的瞬間,八哥心如刀割。沒辦法,即使不打賭也得交出去。哎.... ---- ### 走方格 三天後,晚上六點,羅拉下班回到家了,略帶笑容,顯然心情不錯。 “咦,羅拉今天怎麼這麼早?有啥開心事,看你樂得。”八哥疑惑 “今天事情工作比較簡單,所以沒那麼忙,今天公司下午茶玩遊戲,贏了點零食。”羅拉想到開心的事情,不覺語氣歡快起來了。 “遊戲?啥遊戲?” “走方格,從一個格子走到另一個格子有多少種走法。我答得比較快。碾壓同事”羅拉一副快誇我的樣子。 “走方格?是不是從左上角到右下角,只能向下或向右的走法,像這樣的?”八哥好像想起了什麼,拿起紙筆隨手花了一個圖。 ![走方格](https://img2020.cnblogs.com/blog/1293390/202012/1293390-20201217174944744-99128115.png) “是的,你知道?要不我們玩玩?”羅拉看了一眼,羅拉顯然對自己很自信。 “好啊,不過得來點彩頭吧。” “喲,說的好像你已經贏了似的,你想要啥彩頭?” “那啥,舊幣你把玩了三天了,是不是該讓我捂一下了?” “原來你打的是這主意...”羅拉沒好氣地說道。 “不過也無所謂,我覺得我不會輸,這樣,我們各寫一組陣列``(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 - 1`` 或 ``m - 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優化的原理 “算我輸了,你寶貝等下就還你,話說這個豈不是用數學方法更快?”羅拉賭品還是可以的。 “所以我說了對於這個問題是個樣啊,我只要稍微變化一下,公式就不好使了” “是嗎?舉個栗子看看” 羅拉來了興趣。 “行,看在你賭品不錯的份上,舉了例子” --- #### 走格子最短路徑 “從前有個公主,被魔王抓了,關在魔窟” “一個勇敢王子準備前往魔窟營救公主,這個過程充滿危險,稍有不慎就會有生命危險。” “魔王在王子的必經之路上佈滿了陷阱,每一個陷阱都會對王子造成傷害,地圖如下所示” ![迷宮](https://img2020.cnblogs.com/blog/1293390/202012/1293390-20201217175013729-1309908399.png) “王子開始在左上角,每次只能往左或往右走一步,由於魔王布了陷阱,每走一步都會失去部分生命值” “王子有初始生命,請問王子能否成功救出公主”? “這案例就沒法用排列組合來做了,應為不是每個格子都是一樣的數字了。”八哥不緊不慢的舉了個例子。 “好像是誒,排列組合有點難,感覺動態規劃挺好做的吧”羅拉想了一會,還是放棄用排列組合了。 “是的,你可以試試動態規劃怎麼做唄。” “嗯,我看看,也做了好多題了,看看能不能獨立做出來,你別給我提示了,我先理一下” 看來羅拉幹勁十足啊。 “王子有初始血量,想要成功就出公主就不能半路給跪了” “要救出公主,只要我失去的生命值小於初始生命值,就可以了” “只要求出所有路徑算損失生命值的最小值和王子初始生命值做對比,就可以知道王子有沒有可能救出公主了” “所以這個也是一個求最小值得問題” 羅拉顯然思路很清晰 “接下來就是分析一下動態規劃要怎麼做了” “用``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 ``` “嗯,不錯,看來動態規劃你掌握的不錯了。”八哥看了看結果,點頭笑道。 “做多了幾道題,感覺就這麼回事,沒啥難度。”羅拉不免翹起了尾巴。 “別開心的太早,明天我找個經典案例給你試試?”八哥不懷好意道 “沒問題,今晚出去吃吧,難得這麼早下班。” “好啊,等下,我先把寶貝放好先”。 歡迎關注【兔八哥雜談】,會持續分享更多內容.