首先給出如下的半數集問題
給定一個自然數n,由n 開始可以依次產生半數集set(n)中的數如下。
(1) n∈set(n);
(2) 在n 的左邊加上一個自然數,但該自然數不能超過最近添加的數的一半;
(3) 按此規則進行處理,直到不能再添加自然數為止。
例如,set(6)={6,16,26,126,36,136}。半數集set(6)中有6 個元素。
注意半數集是多重集。
首先分析題目意思:已知一個數n,要整數k1∈[1,n/2],依次放到這個數的左邊(實際上就是放到高位),然后再進行類似操作,只不過再次操作時,添加到左邊的數k2的取值范圍為k2∈[1,k1/2],然后依次進行操作,直到不能操作為止。當時看到這個問題,第一印象就是用一個變長容器,找到一個元素就添加進去,最后統計元素個數。基於上述流程,很容易用遞歸的方法寫出程序。最初用C++的STL vector容器寫出的程序如下:
1 #include<iostream> 2 #include<vector> 3 using namespace std; 4 int getScale(int num); 5 int calcHalfSetNum(int startNum); 6 void addElementInHalfSet(int num, int lastNum, vector<int> &set); 7 int main() { 8 int n,i; 9 vector<int> source; 10 while (cin >> n) 11 source.push_back(n); 12 for (i = 0; i < source.size(); i++) 13 cout << calcHalfSetNum(source[i]) << endl; 14 return 0; 15 } 16 int getScale(int num) { 17 int result; 18 result = 1; 19 while (num > 0) { 20 result *= 10; 21 num /= 10; 22 } 23 return result; 24 } 25 int calcHalfSetNum(int startNum) { 26 vector<int> dynArray; 27 addElementInHalfSet(startNum, startNum, dynArray); 28 return dynArray.size(); 29 } 30 void addElementInHalfSet(int num,int lastNum, vector<int> &set) { 31 int i; 32 set.push_back(num); 33 for (i = 1; i <= lastNum / 2; i++) 34 addElementInHalfSet(num + i*getScale(num), i, set); 35 return; 36 }
本身題目不要求輸出目標集合中的所有元素,因此沒有必要用vector來保存集合中的所有元素。根據題意,很容易得出一個一般性的遞推式:h(n)=1+h(1)+...+h(k),其中k=n/2。得到這個遞推式,也很容易使用遞歸來寫出程序。但是,時間復雜度和剛才的程序一樣依然是指數階的,只是常數小一點(省卻了容器操作和動態內存分配的開銷)。但是基於這個遞推式,很容易發現會有很多重復計算,用動態規划的方法能夠避免這類重復計算。
在《算法導論》一書中,提及動態規划有兩種設計方法。一種方法是帶備忘的自頂向下的方法,另一種是自底向上的方法。兩種方法具有相同的復雜度,常數略有差別。對於這個問題,顯然對於子問題h(i)中i越小,子問題越小,即越基本。一半情況下,會選擇自頂向下的方法,在自頂向下的方法中,需要在主體函數外部維護一個額外的數組,來保存中間計算結果。算法導論書中使用的是用一個“引子函數”的方法,即用一個輔助函數,先分配一個數組(更一般的,進行動態數據結構的初始化),然后把這個數組作為參數傳遞給主體函數,主題函數在遞歸時,依然將這個數組傳遞下去,從而保證了在整個遞歸過程中,所有的子情況都能夠訪問這個數組。對於這個問題而言,為了簡單起見,可以設置一個全局數組。對於輔助數組,還需要做的一點工作就是,需要確認這個數組中的值,是不是我所需要的已經計算的子問題的結果。可以采取的方法很多,這里使用了不屬於子問題的解的集合的元素(0)作為標志,有些時候,當問題的解覆蓋了整個數據類型的取值范圍時,也可能需要更為復雜的方式。
基於這種自頂向下的思路,我的C++代碼實現如下:
1 #include<iostream> 2 #include<vector> 3 using namespace std; 4 int calcHalfSetNum(int startNum); 5 int a[10000]; 6 int main() { 7 int n; 8 unsigned i; 9 vector<int> source; 10 while (cin >> n) 11 source.push_back(n); 12 for (i = 0; i < source.size(); i++) 13 cout << calcHalfSetNum(source[i]) << endl; 14 return 0; 15 } 16 int calcHalfSetNum(int startNum) { 17 int result = 1; 18 int i; 19 if (a[startNum] > 0) 20 return a[startNum]; 21 for (i = 1; i <= startNum / 2; i++) 22 result += calcHalfSetNum(i); 23 a[startNum] = result; 24 return result; 25 } 26
對於另一種自底向上的方法,就是從h(1)開始計算,計算h(i)時,所計算過的結果都存放在數組的對應位置里,直接拿來用就可以了,直到i=n時,循環結束。這種方法就不需要在函數體外維護一個數組了,因為不需要遞歸調用,函數開頭聲明的數組在整個算法過程中都是可見的。這里是我一個哥們用C寫的代碼,以供參考:
1 #include<stdio.h> 2 int main() 3 { 4 int a[1000]={0},i,j,n; 5 a[0]=1;a[1]=1; 6 for(i=2;i<1000;i++) 7 for(j=0;j<=i/2;j++) 8 a[i]+=a[j]; 9 while(scanf("%d",&n)!=EOF) 10 printf("%d\n",a[n]); 11 return 0; 12 }
最后還要提及一下就是,如果要輸出所有元素時,因為每個元素都不一樣,動態規划的方法就不可行了。動態規划算法的核心之一要求問題必須有重復的子問題的解。
