勇幸|thinking (
---話說這道題還是三年前徑點公司來學院筆試中的一道題目,當時剛進入實驗室,師兄在帶著我們做新生培訓的時候做過這道題,最近回顧dp的一些基礎,翻找以前寫的程式,發現了這道題,就貼一下,給出兩種方法的**,並對比了它們在不同規模問題下的效率。
題目:20個桶,每個桶中有10條魚,用網從每個桶中抓魚,每次可以抓住的條數隨機,每個桶只能抓一次,問一共抓到180條的排列有多少種 (也可求概率)。
分析:我們要在20個桶中一共抓取180條魚,每個桶能抓到魚的條數為0-10,仔細想下,這個問題是可以分解成子問題的:假設我們在前i個桶中抓取了k(0<=k<=10*i)條魚,那麼抓取180條魚的排列種數就等於在剩下的(20-i)個桶中抓取(180-k)條魚的方法加上前i個桶中抓取k條魚的方法。
例如,在第乙個桶中抓取了2條魚,那麼總的排列數等於在剩下19個桶中抓取178條魚的排列種數;如果在第乙個桶中抓取了10條魚,那麼總的排列數等於在剩下19個桶中抓取170條魚的排列數,,,依次分解該問題,總的排列數就等於所有這些排列數的總和。有點dp的感覺。
換個思維,在實現上這個題目可以有更為簡潔的方法,我們看看這個問題的對偶問題,抓取了180條魚之後,20個桶中剩下了20條魚,不同的抓取的方法就對應著這些魚在20個桶中不同的分布,於是問題轉化為將20條魚分到20個桶中有多少中不同的排列方法(這個問題當然也等價於180條魚分到20個桶中有多少種不同的方法)?其中,每個桶最多放10條,最少放0條。這樣一轉化,無論是用搜尋還是dp,問題規模都縮小了很大一塊。
按照這個分析,最直接的方法就是用遞迴了,遞迴實現dp問題,自頂向下,為了防止重複計算子問題(例如求19個桶放12條魚的方法數時算了一遍子問題17個桶放10條魚的方法數,在算18個桶,17個桶時就不用再計算17個桶放10條魚的情況了),一般設定乙個備忘錄,記錄已經計算過的子問題,其實這個備忘錄就是在自底向上實現
dp時的狀態轉移矩陣。
遞迴實現,如果桶沒了,魚還有,說明這種排列不符合要求,應該結束並返回0;如果桶還有,魚沒了,說明這種排列也不符合要求;只有在桶沒了,魚也沒了的情況下才說明20條魚恰好分放到了20個桶。根據上面分析我們知道每個桶有11種情況,**如下:
#include using namespace std; /*
撈魚:將20條魚放在20個桶中,每個桶最多可以放10條
求得所有的排列方法
dp自頂向下遞迴 備忘錄*/
int dp[21][21]; /* 備忘錄,儲存子問題的解; 表示前i個桶放j條魚的方法數 */
int allocate(int bucketn, int fishn)
if(bucketn == 0 || fishn < 0)
/* 如果子問題沒有計算就計算,否則直接返回即可 */
if(dp[bucketn][fishn] == 0)
}return dp[bucketn][fishn];}
void main()
}
輸出:
結果如圖,先輸入乙個小資料驗證解是否正確,可以看出這個解是非常大的,最初實現的兩種情況都是等了好久都沒有出來結果,一種是沒有使用備忘錄,單純遞迴的搜尋,非常非常非常慢,等了兩分鐘都沒有結果;一種是沒有求對偶問題,而是求dp[20][180]也是相當的慢。
既然可以用dp,我們通常使用自底向上的方法,下面來看看非遞迴實現的方法。自底向上就需要考慮合法狀態的初始化問題,從小規模去考慮,20個桶太大,考慮零個桶,乙個桶,零個桶裝多少魚都是非法的,故就是0;乙個桶裝魚,裝0-10條魚都是合法的,其餘的就不合法了;dp[i][j]:前i個桶放j條魚的方法共分為11種情況:前i-1個桶放j-k(0<=k<=10)條魚的方法總和。我們可以得到狀態方程:
1
f(i,j) = sum
考慮到這,dp的程式就出來了,**如下:
#include using namespace std; /*
撈魚:將20條魚放在20個桶中,每個桶最多可以放10條
求得所有的排列方法
自底向上dp f(i,j) = sum
該方法中測試 20個桶 180條魚,與遞迴速度做對比*/
/* 實現1 */
int dp[21][200]; /* 前i個桶放j條魚的方法數 */
int i, j, k;
void main()
for(int i = 2; i <= bucketn; ++i) /* 從第二個桶開始 */}}
printf("%d\n",dp[bucketn][fishn]);}}
輸出:
當我們測試20個桶放180條魚的方法,結果立即就算出來了,而用遞迴則是等了半天沒反應,由此我們可以看出效率的差別有多大。同時,兩個對偶問題的答案是一樣的,說明我們的分析是沒錯的,:-)。
其實,**還可以更簡練,仔細想想,就是初始化狀態的方法;其實初始化合法狀態完全可以這樣想,問題始終都是分解成子問題的,根據遞迴的實現方法,只有分解到0個桶裝0條魚才是合法的,那麼我們就初始化這乙個狀態為合法即可,然後從第乙個桶開始向上計算,**如下:
/* 實現2 */
int dp[21][200];
int i, j, k;
void main()}}
printf("%d\n",dp[bucketn][fishn]);
}
從遞迴到非遞迴再到現在,乙個看似規模很大很複雜的問題只用簡單的幾行**就可以解決,關鍵在於怎麼思考,要好好修煉。
總結:(全文完)
C語言實現撈魚問題
撈魚問題 20個桶,每個桶中有10條魚,用網從每個桶中抓魚,每次可以抓住的條數隨機,每個桶只能抓一次,問一共抓到180條的排列有多少種。分析 看看這個問題的對偶問題,抓取了180條魚之後,20個桶中剩下了20條魚,不同的抓取的方法就對應著這些魚在20個桶中不同的分布,於是問題轉化為將20條魚分到20...
有趣的演算法
friday,july 22,2016 19 50 50 a b兩人分別在兩座島上。b生病了,a有b所需要的藥。c有一艘小船和乙個可以上鎖的箱子。c願意在a和b之間運東西,但東西只能放在箱子裡。只要箱子沒被上鎖,c都會偷走箱子裡的東西,不管箱子裡有什麼。如果a和b各自有一把鎖和只能開自己那把鎖的鑰匙...
兩個有趣的演算法問題
2019年2月19日註 這篇文章原先發在自己github那邊的部落格,時間是2017年2月5日 一共是兩道題,第一道是上學期的matlab考試的時候碰到的,另外一道是師弟發的一道數學題的學習筆記,於是找了個時間想了一下,結合網上找到的資料參考。用的是matlab語言。1.乙隻青蛙,每次可以選擇跳1級...