最近在搗鼓演算法,所以寫一些關於演算法的文章
此係列為動態規劃相關文章。
系列歷史文章:
演算法系列-動態規劃(1):初識動態規劃
找零錢問題,湊數問題
最近老幣越來越值錢,是投資的一個好方向。
這不,八哥從某魚入手了幾張老幣。
這是一塊的:
這是五塊的:
這是十塊的:
不得不說,老幣還是挺好看的
看看這成色,過幾年一定很值錢,這就是我留給我孩子的財產。
但是不小心給羅拉看到了,然後就有了下面的對話....
對話記錄 |
---|
羅拉 八哥,這錢不錯,給幾張給我玩玩 |
八哥 姐姐,這是錢,我的投資,怎麼能隨便玩 |
羅拉 我就玩兩天,又不會弄壞 |
八哥 這有啥好玩?你又不是沒見過 |
羅拉 真小氣,玩下能少塊肉? |
八哥 話是這麼說沒錯,可是我還沒捂熱呢~ |
羅拉 怎麼?要我買?瞧你這出息... |
八哥 別激動,這哪能啊,談錢多傷感情 |
羅拉 行,沒問題,但是不能超出我能力範圍 |
八哥 這...,好吧 |
找零錢的方式
錢?她能力範圍?又不太簡單?動態規劃?八哥腦子一動,馬上就想到一個題目。
於是,虎軀一震,眉頭一舒,摸摸下巴,點點頭。
“有了,羅拉請聽題”
“你看,我這裡的舊幣有面值{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 - 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優化的原理
“算我輸了,你寶貝等下就還你,話說這個豈不是用數學方法更快?”羅拉賭品還是可以的。
“所以我說了對於這個問題是個樣啊,我只要稍微變化一下,公式就不好使了”
“是嗎?舉個例子看看” 羅拉來了興趣。
“行,看在你賭品不錯的份上,舉了例子”
走格子最短路徑
“從前有個公主,被魔王抓了,關在魔窟”
“一個勇敢王子準備前往魔窟營救公主,這個過程充滿危險,稍有不慎就會有生命危險。”
“魔王在王子的必經之路上佈滿了陷阱,每一個陷阱都會對王子造成傷害,地圖如下所示”
“王子開始在左上角,每次只能往左或往右走一步,由於魔王布了陷阱,每走一步都會失去部分生命值”
“王子有初始生命,請問王子能否成功救出公主”?
“這案例就沒法用排列組合來做了,應為不是每個格子都是一樣的數字了。”八哥不緊不慢的舉了個例子。
“好像是誒,排列組合有點難,感覺動態規劃挺好做的吧”羅拉想了一會,還是放棄用排列組合了。
“是的,你可以試試動態規劃怎麼做唄。”
“嗯,我看看,也做了好多題了,看看能不能獨立做出來,你別給我提示了,我先理一下” 看來羅拉幹勁十足啊。
“王子有初始血量,想要成功就出公主就不能半路給跪了”
“要救出公主,只要我失去的生命值小於初始生命值,就可以了”
“只要求出所有路徑所損失生命值的最小值和王子初始生命值做對比,就可以知道王子有沒有可能救出公主了”
“所以這個也是一個求最小值的問題”
羅拉顯然思路很清晰
“接下來就是分析一下動態規劃要怎麼做了”
“用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
“嗯,不錯,看來動態規劃你掌握的不錯了。”八哥看了看結果,點頭笑道。
“做多了幾道題,感覺就這麼回事,沒啥難度。”羅拉不免翹起了尾巴。
“別開心的太早,明天我找個經典案例給你試試?”八哥不懷好意道
“沒問題,今晚出去吃吧,難得這麼早下班。”
“好啊,等下,我先把寶貝放好先”。
歡迎關注【兔八哥雜談】,會持續分享更多內容.