半數集問題與動態規划


首先給出如下的半數集問題
給定一個自然數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 }

最后還要提及一下就是,如果要輸出所有元素時,因為每個元素都不一樣,動態規划的方法就不可行了。動態規划算法的核心之一要求問題必須有重復的子問題的解。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM