編程之美學習之最長子序列的解法
實在愚鈍,雖然是以前看過的算法,今天也是折騰了一天才稍微弄懂了一些。特此記下筆記
第一次遇到這個問題的場景是猴子摘桃問題,原題如下:
小猴子下山,沿著下山的路有一排桃樹,每棵樹都結了一些桃子。小猴子想摘桃子,但是有一些條件需要遵守,小猴子只能沿著下山的方向走,
不能回頭,每顆樹最多摘一個,而且一旦摘了一棵樹的桃子,就不能再摘比這棵樹結的桃子少的樹上的桃子。那麽小猴子最多能摘到幾顆桃子呢?
舉例說明,比如有5棵樹,分別結了10,4,5,12,8顆桃子,那麽小猴子最多能摘3顆桃子,來自於結了4,5,8顆桃子的桃樹
首先講第一種解法:
要明白這題最關鍵的一步就是要清楚,不要直接拿數組中的元素一個個去比較,只需要知道,前 i 個元素中最長的遞增子序列(LIS)的長度,或者,以第 i 個元素結尾的LIS的長度 ,第1個元素結尾的LIS的只有他自己一個,所以result[0] = 1,以第2個元素結尾的LIS則之需要和第1個元素peach[0]比較即可,若大於第1個元素,則在result[0]++ 就是他的LIS ,若小於,則同樣result[1]=1 , 很明顯,第i個元素結尾的LIS起碼都是1。。 以後的任意i值,都可以依次類推獲得到結果。 以下是該方法的算法代碼:
/*小猴子下山,沿著下山的路有一排桃樹,每棵樹都結了一些桃子。小猴子想摘桃子,但是有一些條件需要遵守,小猴子只能沿著下山的方向走, 不能回頭,每顆樹最多摘一個,而且一旦摘了一棵樹的桃子,就不能再摘比這棵樹結的桃子少的樹上的桃子。那麽小猴子最多能摘到幾顆桃子呢? 舉例說明,比如有5棵樹,分別結了10,4,5,12,8顆桃子,那麽小猴子最多能摘3顆桃子,來自於結了4,5,8顆桃子的桃樹。*/ int peaches[] = null; @Test public void test222(){ Scanner in = new Scanner(System.in); System.out.print("請輸入數的顆樹:"); int trees = Integer.parseInt(in.nextLine().trim()); peaches = new int[trees]; for (int i = 0; i < peaches.length; i++) { peaches[i] = Integer.parseInt(in.nextLine().trim()); } System.out.println(pick(peaches)); System.out.println(findMax2(peaches)); } /* 5 2 3 156 15 6156 156 165 15 6*/ // 思路:求出以位置所有 以 peach[i](存入result[i]中)結尾的最長遞增子序列的長度--->根據每個 result[0~j], // 拿peach[i]和peach[j]比較,如果peach[i]>peach[j],且result[j]+1>result[i],很明顯,就該把result[i]值加1 // 所以,這種方法起碼能找出一個最長序列,有時候可以找出多條,但有個條件,結尾的peach[i] 一定不相同 int pick(int[] peaches) { int max = 1; int length = peaches.length; List<ArrayList<Integer>> sequences = new ArrayList<ArrayList<Integer>>(); // 記錄每個位置的最長遞增子序列的長度 int result[] = new int[length]; for (int i = 0; i < length; i++) { result[i] = 1; ArrayList list= new ArrayList<Integer>(); list.add(peaches[i]); for (int j = 0; j < i; j++) { //必須新new一個,不然下面的循環都會操作同一個對象 ArrayList newL = new ArrayList(); newL.add(peaches[i]); // 如果是i位置大於j位置,且j位置的最長遞增子序列的長度+1長於目前i位置的最長遞增子序列的長度,則更新i位置的最長遞增子序列 if (peaches[j] <= peaches[i] && result[j] + 1 > result[i]){ result[i] = result[j] + 1; newL.addAll(sequences.get(j)); list = newL; } } sequences.add(list); } for (int i : result) max = i > max ? i : max; /* for(ArrayList<Integer> l : sequences){ if(l.size() >= max){ System.out.println(l); } } */ return max; } // 方法二 private int findMax2(int [] peaches){ int [] maxV = new int[peaches.length]; maxV[1] = peaches[0]; maxV[0] = -1; int [] LIS = new int[peaches.length]; for(int i=0 ; i<LIS.length ; i++){ LIS[i] = 1; } int nMaxLIS = 1; for(int i=1 ; i<peaches.length ; i++){ int j; for(j = nMaxLIS ; j>=0 ; j--){ if(peaches[i] >maxV[j]){ LIS[i] = j+1; break; } } if(LIS[i] > nMaxLIS){ nMaxLIS = LIS[i]; maxV[LIS[i]] = peaches[i]; }else if( maxV[j]<peaches[i] &&peaches[i]<maxV[j+1] ){ maxV[LIS[i]] = peaches[i]; } } return nMaxLIS; }
以上多了我自己添加了打印序列的代碼,同學們也可以自己屏蔽掉(想看的話,是從後往前看哦,我懶得改了)。同時我也偷了個懶,順便把第二種發放貼了上去了。在這裏繼續介紹下去...
第二種方法的思路很簡單,即開辟一段新的空間存儲相應長度LCR中的最大元素的最小元素,舉個例子:1,2,5,4 ... 當掃描到5(i=2)的時候,maxV[3] = 5 , LIS[2] = 3 ,maxV[3] 即指的是 長度為3的LCR中最大元素的最小元素,所以當掃描到3的時候,maxV[3]就更新為4了 , 因為 5>4 ,且他們的LIS的長度都為3
這樣子,就可以直接比較當前的peach[i]和nMaxLIS就可以算出當前i的LIS
這樣做是有原因的! 其實現在這麽做的話,效率還是n2 , 其實還有第三種代碼,因為maxV中的記錄肯定是滿足maxV[1] < maxV[2] <maxV[3] ....細心的同學可以已經發現,這種規律可以用二分查找方法取代遍歷 這樣就可以把效率提高到 nlog2n (二分查找的效率是log2n)。依據單調遞增,將上面便利部分,做以下改動
最後將上面的代碼換成二分查找去匹配,效率應該會更有改善,有興趣的同學可以嘗試
以前純屬個人觀點,如有錯誤還請大佬們海涵,還請大佬們指點
編程之美學習之最長子序列的解法