第一次寫這方面的blog.自己也是初次接觸相關知識,寫的有不妥的地方十分歡迎大家指正~
這是浙大PAT上的一道算法題(據說是浙大04年研究生復試題),題目是這樣的:
[PAT] Maximum Subsequence Sum
Given a sequence of KK integers { N_1N1, N_2N2, ..., N_KNK }. A continuous subsequence is defined to be { N_iNi, N_{i+1}Ni+1, ..., N_jNj } where 1 \le i \le j \le K1≤i≤j≤K. The Maximum Subsequence is the continuous subsequence which has the largest sum of its elements. For example, given sequence { -2, 11, -4, 13, -5, -2 }, its maximum subsequence is { 11, -4, 13 } with the largest sum being 20.
Now you are supposed to find the largest sum, together with the first and the last numbers of the maximum subsequence.
Input Specification:
Each input file contains one test case. Each case occupies two lines. The first line contains a positive integer KK (\le 10000≤10000). The second line containsKK numbers, separated by a space.
Output Specification:
For each test case, output in one line the largest sum, together with the first and the last numbers of the maximum subsequence. The numbers must be separated by one space, but there must be no extra space at the end of a line. In case that the maximum subsequence is not unique, output the one with the smallest indices ii and jj (as shown by the sample case). If all the KK numbers are negative, then its maximum sum is defined to be 0, and you are supposed to output the first and the last numbers of the whole sequence.
Sample Input:
10 -10 1 2 3 4 -5 -23 3 7 -21
Sample Output:
10 1 4
- 時間限制:200ms
- 內存限制:64MB
- 代碼長度限制:16kB
翻譯成中文就是,在程序第一行輸入一個正整數N,第二行輸入一個含有N個整數的序列,從中取連續的任意n個數(當然不超過N),讓這n個數的和最大.接着要輸出這個和的值,以及這個片段的首個和末尾數字.如果有多個連續片段的和一樣大,則取下標最小的.如果全為負數,則定義其最大和為0,首個和末尾數分別是序列的首個和末尾數;
這個只是一個數學問題,不需要特別高深的編程技巧,所以我只說一下我的思路(文尾也會有源碼).
既然是求連續最大和,我們首先可以肯定的說:
1.我不要負數(但不能說不要0!后面會解釋).
2.我要盡可能多的連續正數.
根據題目要求,我們要輸出頭尾的數,顯然我們需要一個表來保存我們輸入的數.僅僅是數嗎?當然不是!我們還要判斷下標的大小,所以還需要保存他們的標號.於是你可能就想到了數組這種東西:直接int sequence[N]然后一個一個塞進去,簡單!但這樣的話,寫出來的程序恐怕就是好幾個for循環套來套去,然后出現O(n3)甚至O(n4)這種比較尷尬的事情--別忘了題目還有時間限制(200ms),我們要在各個方面都盡量加快程序的效率.首先要用指針操作代替使用數組索引(具體原因參見這里),malloc或許是一個不錯的選擇,但malloc出來的空間沒有清零(我這個算法不清零的話,算到最后會gg..),所以改成calloc.這里還要構造一個結構體:
1 typedef struct
2 { 3 int right_indices; //右下標
4 int left_indices; //左下標
5 int sum; //當前的數
6 } cluster;
這個結構體除了放輸入的數,還能放一個左下標,一個右下標,說白了就是題目中的那個邊界.
接下來,我們看一看上面的sample:
-10 1 2 3 4 -5 -23 3 7 -21
由上面提出的兩點我們可以知道,第一個 -10 肯定是不要的, 1 到 4 肯定是"綁定"的,也就是說只要你取了第5個元素4,為了使和最大,你肯定會一直往前取到第二個元素1.這個原理同樣適用於后面的 3 和 7,但是我們有沒有必要去跨越中間的 -5 和 -23(這很虧啊)去接受后面的 3 和 7 呢?這里其實包含了另一個關鍵思路:我們之所以會取中間的負數是因為后面(或者前面)的正數能夠彌補中間那些連續負數帶來的損失.概括一下前面的意思就是:先合並相鄰的同號再進行比較.以上sample便可轉化為
10 -28 10 -21
很好,一目了然知道最大和是10了~(注意,所有進行過處理而形成的序列都用cluster的結構體來保存!!!!)接下來的問題是如何再程序里面實現這個功能.先請看這個部分的代碼
1 int main(void) 2 { 3 int flag=2;//1:positive; -1:negative; 0:undefined
4 int K=0,i=0,temp=0,combine_count=0,n_count=0,original_count=0; 5 int current_max=0; 6 cluster* numbers=NULL; 7 int* original=NULL; 8 int final_left,final_right; 9 scanf("%d",&K); 10 original=(int *)calloc(K+2,sizeof(int)); 11 numbers=(cluster *)calloc(K+2,sizeof(cluster));
解釋一下代碼的意思:flag是一個標志變量,保存上一個數的狀態,其值有 2/1/0/-1;其中2是初始狀態,1為正,0為0,-1為負.temp是一個交換變量,存放當前讀入的數.K是要讀入的數的數量,i我就不說了.original_count為原始數據計數器.combine_count是合並后的計數器.original是一個用於保存原始數據的int數組.
首先我們從緩沖區一個一個讀數進original:
12 for (i=0;i<K;i++) 13 { 14 scanf("%d",&temp); 15 *(original+(++original_count))=temp;
用++original_count是為了讓用*(numbers+i)直接訪問第i個數,以免出錯.
16 switch (flag) 17 { 18 default:if(temp > 0) //default = 2;
19 { 20 numbers->sum=temp; 21 numbers->left_indices=original_count;; 22 flag = 1; 23 } 24 else if (temp < 0) n_count++; 25 else if (temp == 0) 26 { 27 numbers->left_indices=original_count; 28 flag = 0; 29 } 30 break;
每次讀入一個數據后會接着進行判斷,只有在遇到第一個正數時才會寫入cluster,並把標志更新為1(正數)然后更新最小下標,遇到負數會讓負數計數器n_count加1,遇到0會將標志變為0,也會更新最小下標!這個很關鍵,因為假設輸入了"0 0 0 1 2 3",那么最小下標應從第一個0算起而不是1算起(雖然他們的和都是6,但題目要求輸出最小的下標!)
31 case 1:if(temp>0) 32 { 33 ((numbers+combine_count)->sum)+=temp; 34 (numbers+combine_count)->right_indices=original_count; 35 } 36 else if (temp<0) 37 { 38 n_count++; 39 (numbers+combine_count)->right_indices=original_count-1; 40 (numbers+(++combine_count))->sum=temp; 41 flag = -1; 42 } 43 else if ( temp == 0) 44 { 45 (numbers+combine_count)->right_indices=original_count-1; 46 flag = 0; 47 } 48 break; 49 case -1:if(temp < 0) 50 { 51 (numbers+combine_count)->sum+=temp; 52 n_count++; 53 } 54 else if(temp == 0) 55 { 56 (numbers+combine_count)->left_indices=original_count; 57 } 58 else if (temp > 0) 59 { 60 ((numbers+(++combine_count))->sum)=temp; 61 (numbers+combine_count)->left_indices=original_count; 62 flag=1; 63 } 64 break; 65 case 0:if(temp > 0) 66 { 67 ((numbers+combine_count)->sum)+=temp; 68 (numbers+combine_count)->right_indices=original_count; 69 flag=1; 70 } 71 else if (temp < 0) 72 { 73 n_count++; 74 } 75 else if ( temp == 0) ; 76 break;
終於在cluster中寫入第一個數了(它絕對是正數),此后的事情是這樣的:
1.如果讀入的數和上一個數同號(當然不考慮0啦),就把新的數加到舊的數上,如果遇到符號相反的,就讓numbers指針加一,再把這個數寫下去.這里又分幾種情況:
a)正變負,會順便更新最大下標(最后那個正數的序號寫到right_indices)
b)負變正,會更新最小下標(正在輸入的那個數的序號寫到left_indices)
c)正變0,不更新下標!!!
d)負變0,更新下一個cluster的最小下標!!!(雖然等下可能又是負數)
2.如果上一個數是0.
e)0變正:不更新最小下標,但是要把正數寫到新的cluster中
f)0變負:負數計數器加一
此時已經可以做第一次判斷,如果負數計數器(n_count)等於輸入的數的數量,則直接輸出"0 0":
if (n_count==K) { printf("0 %d %d",*(original+1),*(original+original_count)); return 0; }
以上便是在讀入數據過程中對它們的第一步操作,這是一個動態處理過程,輸入完成則處理也完成了.最后numbers結構數組中會是如下的結構(original則原封不動地保留了每一個數.):
"+ - + - + - + - + ..."(+代表正數,+代表負數,每個正數都含有兩個下標,負數儲存下標的空間是空的)
nice!看起來更有規律了不是嗎~接下來進行下一步的處理:
i=0; while (i<=combine_count) { temp=((numbers+i)->sum)+((numbers+i+1)->sum); if (temp>=0) { { if ((numbers+i+2)->sum != 0) { ((numbers+i+2)->sum)+=temp; ((numbers+i+2)->left_indices)=((numbers+i)->left_indices); } else if ((numbers+i+2)->sum == 0) { ((numbers+i+2)->sum)+=temp; ((numbers+i+2)->left_indices)=((numbers+i)->left_indices); ((numbers+i+2)->right_indices)=((numbers+i)->right_indices); } } if (current_max<((numbers+i+2)->sum)) { current_max=((numbers+i+2)->sum); final_left=((numbers+i+2)->left_indices); final_right=((numbers+i+2)->right_indices); } } else if (temp == 0) { ; } i+=2; }
我們把 "+ - + - + - + - + ..."分組為:"[+ - ][+ - ][+ - ][+ - ][+ ..."有落單也沒關系,然后把每組內的正數加到負數上,正數的下標都是偶數,每次循環結尾讓i加2就可以使指針對准正數.因為是正負間隔,所以一旦[+ -]中的和大於0,那么就可以將這個和再加到下一組的正數上!這樣保證了跨越負數能夠得到補償!畫個圖就是這樣:
[+1 -1 ][+2 -2 ][+3 -3 ][+4 -4 ] -> [+1 -1][+s -2][+3 -3 ][+4 -4 ] 其中+s=(+1 ) + (-1 )+ (+2 )
如果某組內的和小於0,則將之前累積的那個正數暫時存入current_max中,然后再下一組中繼續上面的累加形式,遇到小於0就拿出來與current_max比,如果大就更新current_max.當然,這個過程中會更新下標,具體的更新規則看上面的代碼.這樣以后,我們只遍歷了一次序列,就找到了和最大且下標和最小的一串連續的數,至於他們的下標,在遍歷的時候每次更新current_max就會把其對應的下標寫入final_left和final_right.(取最后一次累加時下一組的正數的最大下標,取前一組和中的最小下標).
做到這里,基本就結束了,但是某些特殊情況會導致bug,如果輸入的數據過少,則會導致即使程序遍歷結束,current_max還是為0,此時就要手動將numbers第一個正數賦給current_max,此外還有一種情況會下標顛倒(映像中是最大和只有一個數的時候...?有條件的做個測試然后告訴我吧 T T).
if (current_max == 0 ) { current_max=(numbers->sum); final_left=((numbers)->left_indices); final_right=((numbers)->right_indices); } if (final_left>final_right) final_right=final_left;
之所以要單獨處理這種情況另一方面也是我寫的判斷邏輯不夠完善以至需要加兩個收尾的操作...最后直接打印結果即可,有任何建議歡迎提出~(完)
P.S 下方附PTA運行結果