這道最大m子段問題我是在課本《計算機算法分析與設計》上看到,課本也給出了相應的算法,也有解這題的算法的邏輯。但是,看完之后,我知道這樣做可以解出正確答案,但是我如何能想到要這樣做呢? 課本和網上的某些答案都講得比較晦澀,有些關鍵的步驟不是一般人可以想得到的。不僅要知其然,還要知其所以然。否則以后我們遇到類似的問題還是不會解。
下面是我解這道題的思考過程。我按照自己的想法做,做到最后發現和課本的思想差不多,也有一點差別。如果對這道題有些不明白,可以仔細看看,相信看完之后你會豁然開朗。
問題: 給定n個整數(可能為負數)組成的序列 以及一個正整數m,要求確定序列
的m個不相交子段,使這m個子段的總和達到最大。
0 首先舉個例子方便理解題目 如果 = {1,-2,3,4,-5,-6,7,8,-9} m=2 明顯所求兩個子段為{3,4}{7,8} 最大m子段和為26。
1 先想如何求得最大子段和。
1.1最容易想到的方法是窮舉法。列出所有的子段組合,求出每個組合的子段和,所有組合中最大者即為所求。
仔細分析后發現:計算量巨大且難以實現。果斷放棄。
1.2 分析:用數組a[1…n]來表示一個序列,用二維數組SUM[n][m]表示由數組a的前n個數字組成的子序列的最大m子段。(可知 n>=m)
SUM[n][m]即為所求.
分析最后一個數字a[n]所有可能的情況
1) a[n] 不屬於構成最大m子段和的一部分, SUM [n][m] = SUM [n-1][m]
2) a[n] 屬於構成最大m子段和的一部分, 且a[n]單獨作為一個子段。
此時SUM[n][m] = SUM[n-1][m-1]+a[n];
3) a[n] 屬於構成最大m子段和的一部分, 且a[n]作為最后一個子段的一部分。
此時比較復雜, a[n]只是作為最后一個子段的一部分, 所以a[n-1]也一定在最后一個子段之中,否則a[n]便是一個單獨的子段,前后矛盾.
所以SUM[n][m] = (包含a[n-1]的由前n-1個數字組成的子序列的最大m子段和) + a[n]
若用 b[n][m] 表示包含a[n]的、由前n個數字組成的子序列 的最大m子段和。
則 SUM[n][m] = b[n-1][m] + a[n]
1.3 我們仔細觀察第三種情況里面定義的b[n][m]: 包含a[n]的、由前n個數字組成的子序列 的最大m子段和。
假設a[k] (k∈[m,n])是 原問題的解中的最后一個子段的最后一個元素, 則b[k][m]即為原問題的最大子段和。(若不明白再多看b的定義幾次~)
所以原問題所求的最大子段和SUM[n][m]為
1.4 現在就好辦了, 分析b[n][m]的值如何計算。
回顧我們剛剛對a[n]的分析的三種情況中的后面兩種
1) a[n]單獨作為一個子段,
則 b [n][m] = SUM[n-1][m-1] + a[n]
(而SUM[n-1][m-1]= )
2) a[n]作為末尾子段的一部分
則 b[n][m] = b[n-1][m]+a[n]
分別計算情況1) 和情況2) 下的b[n][m], 比較, 取較大者.
a) 特殊情況,
若m=n 則a[n]為單獨子段 按情況1)計算
若n=1 則SUM[n][m] = a[n]
1.5 到這里很明顯可以看出這是一個動態規划的問題,還不太懂動態規划也沒關系,你只要記得,要計算b[i][j], 需要有:SUM[i-1][j-1]、b[i-1][j] 。
而 SUM[i-1][j-1]由數組b算出。需要先算出 b[k][j-1] (j-1<=k <=i-1 )。參見前面SUM的推導.
所以我需要先知道 b[k][j-1] (j-1<=k <=i-1 ) 以及 b[i-1][j]
所以,數組b 如何填寫?不明白可以畫個表看看
比如上表:在求SUM[8][4]時,我們需要先求的為圖中黃色區域.
黑色部分不可求(無意義), 白色部分在求解的時候不需要用到.
可以看出 我們只需要求 當 1<=j<=m 且 j<=i<=n-m+j 部分的b[i][j]就可以得出解.(此處我用畫圖 有誰可以有更方便的方法來理解歡迎討論)
至此 我們大概知道此算法如何填表了,以下為框架.
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
1.6 開始寫算法(我用java 實現)
1 package com.cpc.dp; 2 3 public class NMSum { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n為數組中的個數 8 int[][] b = new int[n+1][m+1]; 9 int[][] SUM = new int[n+1][m+1]; 10 11 for(int p=0;p<=n;p++) { // 一個子段獲數字都不取時 // 12 b[p][0] = 0; 13 SUM[p][0] = 0; 14 } 15 // for(int p=0;p<=m;p++) { // 當p > 0 時 並無意義, 此部分不會被用到,注釋掉 16 // b[0][p] = 0; 17 // SUM[0][p] = 0; 18 // } 19 for(int j=1;j<=m;j++){ 20 for (int i = j;i<=n-m+j;i++){ 21 22 // n=1 m=1 此時最大1子段為 a[0] java 數組為從0開始的 需要注意 后面所有的第i個數為a[i-1]; 23 if(i==1){ 24 b[i][j] = a[i-1]; 25 SUM[i][j] = a[i-1]; 26 }else 27 { 28 //先假設 第i個數作為最后一個子段的一部分 29 b[i][j] = b[i-1][j] + a[i-1]; 30 31 // 若第i個數作為單獨子段時 b[i][j]更大 則把a[i-1] 作為單獨子段 32 // 考慮特殊情況 若第一個數字為負數 b[1][1]為負數 在求b[2][1] SUM[1][0]=0>b[1][1] 則舍去第一個數字 此處合理 33 if(SUM[i-1][j-1]+a[i-1] > b[i][j]) b[i][j] = SUM[i-1][j-1] + a[i-1]; 34 35 //填寫SUM[i][j]供以后使用 36 if(j<i){ // i 比j 大時 37 if(b[i][j]>SUM[i-1][j]){ // 用b[i][j] 與之前求的比較 40 SUM[i][j] = b[i][j]; 41 }else { 42 SUM[i][j] = SUM[i-1][j]; 43 } 44 }else // i = j 45 { 46 SUM[i][j] = SUM[i-1][j-1] + a[i-1]; 47 } 48 } 49 }//end for 50 }// end for 51 System.out.println(SUM[n][m]); // 輸出結果 52 }// end of method 53 54 public static void main(String[] args) { 55 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 56 Sum(a, 3); 57 } 58 }
output : 33
測試通過
/************** 4.22 更新***************************/
2 算法的優化
2.1 分析 算法的空間復雜度 為O(mn).我們觀察一下,在計算b[i][j]時 我們用到b[i-1][j] 和 SUM[i-1][j-1],也就是說,每次運算的時候 我們只需要用到數組b的這一行以及數組SUM的上一行.
我們觀察一下算法的框架
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
// 計算b[i][j] 需要 SUM[i-1][j-1] 和 b[i-1][j]
// 計算SUM[i][j] 需要 SUM[i-1][j] b[i][j] SUM[i-1][j-1]
假設在 j=m 時(即最外面的for循環計算到最后一輪時)
要計算b[*][j] *∈[m,n]
我只需要知道 SUM[*-1][j-1] b[*-1][j] (即需要上一輪計算的數組SUM以及這一輪計算的數組b)
而之前所求的數組SUM和數組b其他部分的信息已經無效,
我們只關心最后一輪計算的結果,而最后一輪計算需要倒數第二輪計算的結果.
倒數第二輪計算需要再倒數第三結果.以此循環
因此我們可以考慮重復利用空間,
在一個位置所存儲的信息已經無效的時候,可以覆蓋這個位置,讓它存儲新的信息.
舉個例子: 老師在黑板上推導某一個公式的時候, 黑板的面積有限,而有時候推導的過程十分長,很快黑板不夠用了,這個老師通常會擦掉前面推導的過程,留下推導下一步要用的一部分,在擦掉的地方繼續寫.
但是如何安全地覆蓋前面失效的內容而不會導致覆蓋掉仍然需要使用的內容呢?
分析后可以得知一下約束:
1) 求b[i][j] 需要用到SUM[i-1][j-1] 所以SUM[i-1][j-1]必須在b[i][j]的值求完后才可以被覆蓋
2) 求b[i][j] 需要用到 b[i-1][j] (j 相等)
3) 求SUM[i][j] 需要用到 SUM[i-1][j] (j 相等)
4) 求SUM[i][j] 需要用到 b[i][j] (j 相等)
5) 求SUM[i][j] 需要用到SUM[i-1][j-1] (i的位置錯開)
對於最外面的for循環
我們只關心最后一輪(也就是第(j=m)輪)的結果,所以考慮把兩個二維數組變成一維數組b[1...n] 、SUM[1..n]
假設在第j輪計算后:
b[i] 表示的意義與原來的 b[i][j]相同 ( 也就是原來的b[i][j]會覆蓋b[i][j-1] )
SUM[i] 表示什么呢
我們觀察約束1)知道,在第j 輪計算 b[i] (即原來的b[i][j])時,仍然會用到原來SUM[i-1][j-1],
也就是說 , 在計算b[i]時,SUM[i-1] 需要存儲的是原來的SUM[i-1][j-1]
對於里面的for 循環
由於計算 b[i]需要SUM[i-1]
所以在計算完b[i] 后才計算新的SUM[i-1]
即在b[i]計算完后,可以覆蓋掉SUM[i-1] 使之表示原來的SUM[i-1][j]
也就是說, 在第j輪計算完畢后, SUM[i] 表示的意義與原來的SUM[i][j]相同
2.2 分析得差不多了, 廢話少說,開始優化代碼
1 package com.cpc.dp; 2 3 public class NMSUM2 { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n為數組中的個數 8 int[] b = new int[n+1]; 9 int[] SUM = new int[n+1]; 10 11 b[0] = 0;// 一個子段獲或者數字都不取時 ,也可以不設置,因為 java默認int數組中元素的初始值為0 12 SUM[1] = a[0]; 13 14 for(int j=1;j<=m;j++){ 15 b[j] = b[j-1] + a[j-1]; // i=j 時 16 SUM[j-1] = -1; // 第j 輪 SUM[j-1]表示原來的 SUM[j-1][j] 無意義 設置為-1 17 int temp = b[j]; 18 for (int i = j+1;i<=n-m+j;i++){ 19 20 //先假設 第i個數作為最后一個子段的一部分 21 b[i] = b[i-1] + a[i-1]; 22 // 若第i個數作為單獨子段時 b[i][j]更大 則把a[i-1] 作為單獨子段 23 if(SUM[i-1]+a[i-1] > b[i]) b[i] = SUM[i-1] + a[i-1]; 24 25 //下面原來計算的是原來的SUM[i][j] ,但是現在要修改的應該是原來的SUM[i][j-1] ,如何把SUM[i][j]保存 下來? 26 // 可以在循環外面定義一個變量temp來暫存 等下一次循環再寫入 27 SUM[i-1] = temp; 28 if(b[i]>temp){ 29 temp = b[i]; //temp 記錄SUM[i][j] 30 } 31 }//end for 32 SUM[j+n-m] = temp; 33 }// end for 34 System.out.println(SUM[n]); // 輸出結果 35 }// end of method 36 37 public static void main(String[] args) { 38 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 39 Sum(a, 1); 40 } 41 }
output: 25
算法的空間復雜度變為o(n)~ 優化完畢!
3 如何記錄具體每個子段對應,下次再做。有誰做出了也可以直接評論。
可以轉載 注明出處 http://www.cnblogs.com/chuckcpc/
--by陳培城