前幾天發了一篇文章《Java編程能力強化——狼羊過河問題》,有朋友指出了一些問題,這些問題有:1、沒有採用物件導向的思想,沒有定義自己的類,好像與Java無關,像是C語言的編程思維。2、沒有給出代碼的思路。3、對是否能夠提高Java編程能力表示懷疑。本文首先對第一個問題進行解釋,然後給出這一類問題的通用的解決方案,然後對之前的狼羊過河代碼進行分析,主要是對涉及的Java知識進行分析。
第一,編程式就是解決問題,解決問題才是硬道理
簡單理解,程式就是對使用者的輸入進行處理,然後輸出處理的結果。把人的需求表示成電腦可以理解的形式,然後由電腦進行處理,然後把計算的結果再轉換為使用者可以理解的形式。資訊的表示是資料庫結構的問題,資訊的處理是演算法要解決的問題,所以在電腦課程中資料結構和演算法是非常重要的兩門課。而資料結構和演算法的實現需要一門語言,所以你可能會看到資料結構(C語言)或者資料結構(Java語言),演算法通常採用虛擬碼。
對於狼羊過河問題,如果採用物件導向的分析,需要考慮描述中出現的對象,根據這些對象抽象出來類以及類層次,這裡涉及的對象包括3隻狼、3隻羊、一條船,以及狼羊共存的規則以及船的容量,但是這些對象基本上沒有屬性和行為,所以構造成類也並不合適,所在在給出的參考代碼中沒有進行物件導向的設計。
另外這個題目主要是考察如何解決問題,關心的是羊和狼的數量,如何在各種約束條件下把狼和羊運送到河對岸,所以參考答案的重點是演算法。
而在實現演算法的過程用到了String、StringBuffer、Integer、異常處理、數組、方法的遞迴調用等Java語言特性,所以不能說與Java無關。
個人覺得Java中需要掌握的有3個方面的知識:基本文法、常用類庫和物件導向特性,而本練習題主要是對前兩個方面的強化。
第二,搜尋解決方案類問題的通用解法
這類問題有下面的特點:
1、有初始狀態,例如3隻狼和3隻羊需要過河,意味著剛開始3隻狼和3隻羊都在河的一個岸邊。
2、有目標狀態,例如3隻狼和3隻羊要到達河的對岸,意味著最後3隻狼和3隻羊都要和的對岸。
3、通過一些規則可以使狀態發生變化,例如可以用船把2隻狼運送到和的對岸,這時候的狀態就是河的一邊有3隻羊和1隻狼,河的另一邊有2隻浪(當然可以根據一邊的數字推算出另一邊的數字,因為要麼在這邊要麼在那邊)。
人是如何來解決這類問題的呢?看下面的圖:
首先考慮3隻狼3隻羊在左邊,船隻能裝兩隻動物,所以有5種方案,然後看看每一種方案產生的結果是不是想要的結果,如果不是,繼續看有哪些情況,上面的圖展示了這個過程。圖中的右表示把動物運送到對岸,左表示船再回來。
程式如何來實現這個過程呢?
程式的實現過程是對人思考問題的類比。有兩種方式,一種方式按照層來找,初始狀態下,考慮5種可能,然後對5種可能進行分析,然後對每一種情況的可能變化情況再考慮,直到找到目標狀態,或者不能再變化,稱為廣度優先搜尋。另一種方式是先考慮5種情況中的一種,考慮完之後考慮這一種情況能夠轉移成哪些狀態,然後再考慮其中一個,稱為深度優先搜尋。之前給出的參考答案採用的是深度優先搜尋。
例如,如果按照廣度優先:則每個狀態處理的順序為:1(第一層) 2 3(第二層) 4 5 6(第三層) 7 8 9 10(第四層)
如果按照深度優先:則每個狀態的處理順序為:1 2 4 7 3 5 8 9 6 10,2下面的情況處理完再處理3下面的情況,同樣5下面的情況處理完才處理6下面的情況。
演算法的關鍵區段如下:
1、需要定義1個結構來表示狀態,包括初始狀態、中間狀態和目標狀態。我的參考代碼中使用了int state[]表示狀態。
2、要明確初始狀態和目標狀態。代碼中初始狀態為[3,3]表示岸上有3隻狼和3隻羊,目標狀態為[0,0]表示狼和羊都過河了。
3、要明確狀態轉換的規則,通常每個規則可以定義為方法,方法的輸入是一種狀態,而輸出是另一種狀態。代碼中move方法完成了這個功能,只是我把多個規則寫在了同一個方法中。
4、編寫主程式,從初始狀態開始,不斷地進行狀態轉換直到結束。有兩種處理方式,方法的遞迴調用,和迴圈。
可能存在的問題:
1、死迴圈,最直觀的例子,讓一隻狼到對岸,然後再回來,然後再過去,然後再回來...,永遠不會有結束的時候,對於這個問題需要進行控制,有效方式是記錄走過的狀態,如果發現重複,放棄這個方案。程式中有兩個地方進行了控制。
2、解的空間可能會非常大,如果不採取措施可能永遠也運行不完。例如下象棋,大家都知道誰想的遠,誰就可能贏,如果讓電腦把100步之後的情況都考慮到,我想人可能很難贏,但是讓電腦考慮100步需要太大的計算量,如果考慮每一步有20種走法,則需要考慮20的100次方,可以想象這個數字有多大。對於這個問題,通常會採用啟發學習法搜尋以及剪枝等。感興趣的同學可以參考相關書籍。
三、關鍵程式碼分析
完整代碼參考:Java編程能力強化——狼羊過河問題
關鍵點分析:
1、public void next(int state[],StringBuffer str)
遞迴調用的方法,處理指定的狀態下面的所有狀態。
2、 if(str==null){ //表示第一步
// 一隻狼一隻羊
newState = move(state,"-1-1");
next(newState,new StringBuffer("-1-1"));
// 兩隻狼過河
newState = move(state,"-2-0");
next(newState,new StringBuffer("-2-0"));
return;
}
對初始狀態進行處理,也可以合并在下面的代碼中,如果合并需要判斷str是否為空白。另外,這裡的return不能省略。
3、 if(state[0]==0 && state[1]==0){ // 全部轉移到右岸了
printResult(str);
return;
}
表示得到目標狀態,處理結束。
4、 // 兩隻狼
if(state[0]>=2 && !str.substring(0,4).equals("+2+0")){
newState = move(state,"-2-0");
if(check(newState)){
next(newState,new StringBuffer(str).insert(0,"-2-0"));
}
}
注意:new StringBuffer(str).insert(0,"-2-0")不能寫成str.insert(0,"-2-0")
另外!str.substring(0,4).equals("+2+0")的作用防止去和回船上的動物是一樣的。
5、 public int[] move(int state[],String info){
int lang = 0;
try{
lang = Integer.parseInt(info.substring(0,2));
}catch(Exception e){
lang = Integer.parseInt(info.substring(1,2));
}
int yang = 0;
try{
yang= Integer.parseInt(info.substring(2));
}catch(Exception e){
yang = Integer.parseInt(info.substring(3));
}
int[] result = new int[state.length];
result[0] = state[0]+lang;
result[1] = state[1]+yang;
return result;
}
注意:try語句的使用,因為+3轉換為數位時候出現異常
另外不能直接修改state的元素值然後把state返回。
6、public boolean check(int state[]){
判定羊的個數不能小於狼的個數。
7、public boolean hasExist(StringBuffer str){
判斷是否有死迴圈。
8、public void printResult(StringBuffer str){
輸出結果方案。
大家可以試著使用廣度優先的方式實現代碼,可以不用遞迴的方式進行處理(如果遞迴的層數比較多,會出現堆疊溢位問題)。