Java算法學習:藍橋杯——地宮尋寶(DFS✖記憶型遞歸)
題目:
標題:地宮取寶
X 國王有一個地宮寶庫。是 n x m 個格子的矩陣。每個格子放一件寶貝。每個寶貝貼着價值標籤。
地宮的入口在左上角,出口在右下角。
小明被帶到地宮的入口,國王要求他只能向右或向下行走。
走過某個格子時,如果那個格子中的寶貝價值比小明手中任意寶貝價值都大,小明就可以拿起它(當然,也可以不拿)。
當小明走到出口時,如果他手中的寶貝恰好是k件,則這些寶貝就可以送給小明。
請你幫小明算一算,在給定的局面下,他有多少種不同的行動方案能獲得這k件寶貝。
【數據格式】
輸入一行3個整數,用空格分開:n m k (1<=n,m<=50, 1<=k<=12)
接下來有 n 行數據,每行有 m 個整數 Ci (0<=Ci<=12)代表這個格子上的寶物的價值
要求輸出一個整數,表示正好取k個寶貝的行動方案數。該數字可能很大,輸出它對 1000000007 取模的結果。
例如,輸入:
2 2 2
1 2
2 1
程序應該輸出:
2
再例如,輸入:
2 3 2
1 2 3
2 1 5
程序應該輸出:
14
資源約定:
峯值內存消耗(含虛擬機) < 256M
CPU消耗 < 2000ms
請嚴格按要求輸出,不要畫蛇添足地打印類似:“請您輸入...” 的多餘內容。
所有代碼放在同一個源文件中,調試通過後,拷貝提交該源碼。
注意:不要使用package語句。不要使用jdk1.7及以上版本的特性。
注意:主類的名字必須是:Main,否則按無效代碼處理。
思路:
首先看到這個矩陣型+只能向右/向下的搜索,首先想到類似“2013年振興中華”這一題的dfs解法,那麼就按照這一個思路去解決這個問題。
首先要有一個主函數,和一個dfs函數,dfs函數的參數很重要,分別設定爲。x座標,y座標,max當前最大值,和cnt作爲計算寶物數量的計數器變量,用來判斷是否拿滿了k件寶物。
當小明走到某一步的時候,如果當前取出的寶物比k大,就不在搜索這條分支,return 0;
dfs裏面要注意的是,一定要有:
- 當前格子的價值比max的值大然後向右走/向下走並且拿起寶物
- 當前格子的價值比max的值 大或者小但是向右走/向下走但是不拿起寶物
這樣四種情況的遞歸調用。
當走到最右下角的時候,這時候有兩種情況,一個是取滿了k件寶物,可以return了,另外一種是還差一件取滿k件寶物,並且最後一格上面放着的寶物比當前已有的max還大,就再取出一件寶物來。
代碼實現具體:
private static final int MOD = 1000000007;
private static int n;
private static int m;
private static int k;
private static long ans = 0;
//存儲每次遞歸的結果
private static long[][][][] cache = new long[51][51][14][14];
private static int[][] table = {
{1,2,3},{2,1,5}
};
public static void main(String[] args) {
//行
n=2;
//列
m=3;
//需要的寶物數量
k=2;
//緩存數組初始化爲-1
ans = dfs(0,0,-1,0);
System.out.println(ans);
}
/**
* @param x
* @param y
* @param max
* @param cnt
* @return
*/
private static long dfs(int x,int y,int max,int cnt){
//溢出防禦,遞歸出口
if(x==n||y==m||cnt>k){
return 0;
}
int value = table[x][y];
long ans = 0;
//走到最後一個格子前,遞歸出口
if(x==n-1&&y==m-1){
//一種情況是已經取滿了k件寶物 另外一種情況是還差一件,但是最後一件的價值正好大於max
if(cnt==k||(cnt==k-1&&value>max)){
return 1;
}
return ans;
}
//如果當前格子的價值比最大值大,取出的情況
if(value>max){
//取出這個物品
ans+=dfs(x,y+1,value,cnt+1);
ans+=dfs(x+1,y,value,cnt+1);
}
//價值小或者價值大,但是不取出的情況
ans+=dfs(x,y+1,max,cnt);
ans+=dfs(x+1,y,max,cnt);
return ans%MOD;
}
上述代碼都依照dfs深度搜索的基本題型進行書寫即可,難點就在一跳出條件很複雜,遞歸的情況分支也很複雜。
並且這樣做,遞歸的效率很低。當數據量比較大的時候,run程序的時間就比較長,這樣給分就不高。
改進:
針對上面說的這種問題,我們觀察題目,由於這個問題中,DFS函數的參數是有可能相同的。
比如中間的任意一個格子,可以是來自多個方向,並且從每個方向過來的小明,身上攜帶的寶物最大價值和寶物數量都有可能一樣。
上面這種情況就是所謂的存在着重複子問題。這些具有相同參數的dfs函數在之前的代碼裏就需要反覆的去計算,這對計算機的內存和運行時間都是極大的浪費。因此採用動態規劃記憶型遞歸的方法來的方式來解決。
所謂記憶型遞歸就是吧相同參數的dfs函數的結果用一個長度爲(i*j*n*m)的數組保存,ijnm分別是dfs參數可能取到的範圍,比如題目中就有提示,這個記憶數組的長度就是51*51*14*14。當我們在這個數組中保存結果的時候,每一次調用dfs都從中查找相應的單元是否存在着之前記錄下來的答案。如果有,就返回,不用多次的去調用求解。這樣就節省了cpu資源和時間。
改進後的代碼實現:
public class _地宮取寶記憶型遞歸 {
private static final int MOD = 1000000007;
private static int n;
private static int m;
private static int k;
private static long ans = 0;
//存儲每次遞歸的結果
private static long[][][][] cache = new long[51][51][14][14];
private static int[][] table = {
{1,2,3},{2,1,5}
};
public static void main(String[] args) {
//行
n=2;
//列
m=3;
//需要的寶物數量
k=2;
//緩存數組初始化爲-1
for (int i = 0; i <51 ; i++) {
for (int j = 0; j < 51; j++) {
for (int l = 0; l < 14; l++) {
for (int o = 0; o < 14; o++) {
cache[i][j][l][o] = -1;
}
}
}
}
ans = dfs(0,0,-1,0);
System.out.println(ans);
}
/**
* @param x
* @param y
* @param max
* @param cnt
* @return
*/
private static long dfs(int x,int y,int max,int cnt){
//查詢緩存是否有記錄,若有,返回記錄
if(cache[x][y][max+1][cnt]!=-1){
return cache[x][y][max+1][cnt];
}
//溢出防禦,遞歸出口
if(x==n||y==m||cnt>k){
return 0;
}
int value = table[x][y];
long ans = 0;
//走到最後一個格子前,遞歸出口
if(x==n-1&&y==m-1){
//一種情況是已經取滿了k件寶物 另外一種情況是還差一件,但是最後一件的價值正好大於max
if(cnt==k||(cnt==k-1&&value>max)){
return 1;
}
return ans;
}
//如果當前格子的價值比最大值大,取出的情況
if(value>max){
//取出這個物品
ans+=dfs(x,y+1,value,cnt+1);
ans+=dfs(x+1,y,value,cnt+1);
}
//價值小或者價值大,但是不取出的情況
ans+=dfs(x,y+1,max,cnt);
ans+=dfs(x+1,y,max,cnt);
//寫緩存
cache[x][y][value][cnt] = ans%MOD;
return ans%MOD;
}
}
總結:第一次接觸到DP,以後會好好學習和理解動態規劃這個知識點。