子集和數問題_回溯


有人說算法導論中沒有回溯和分支定界這兩種算法。我覺得這個算是導論中算法的應用吧,廢話不多說,走起。
回溯算法之子集和數問題。

這個算法要解決的問題:假定有N個不同的正數(通常稱為權),要求找出這些數中所有使得某和數為M的組合。

這種問題的解的形式:(1)問題的解是大小固定的N元組,解向量中的元素的個數就是正數的個數,每個元素為X(i),它的取值為0或者1,表示這個解是否包                                    含了相對應的正數W(i)。

                              (2)問題的解是大小不固定的K元組,這里不做討論。

這樣的整個的求解過程就構成了一棵樹,對於i級上的一個結點,其左兒子是對應於X(i)=1產生的狀態,右兒子是對應於X(i)=0產生的狀態(父節點到兒子節點的邊可以看成一種決策,這種決策就是選不選這個正數)。

但是為了防止這棵樹長得很大,我們可以引入限界函數,可以提前預知這個結點不可能產生最后的解,這樣我們就能提前的殺死這個結點,同時也能夠提前的殺死這個結點的所有的子樹,這樣就大大的減少了樹的節點數,加快了產生最終解的速度。

限界函數的產生:

(1)我們假設一個前提條件:這些正數是按照非降次序排列的。
(2)引入一個記號:B(X(1),...,X(k))表示是否可以把第K個正數加入進來,所以它的取指為true或者false。

那么當我們考慮是否要把第K個正數加入到解向量中的時候,我們就能找到兩個條件組成這個限界函數了:

(1)這個公式的含義是:當你考慮是否要把第K個正數加入到解向量的時候,不管你要加進來或者是不打算把它加進來,前K個解向量的和(包括第K個,當然X(k)可能是0或者1),加上后面所有的數的和一定要大於等於M,否則你把剩下的數都加了進來還比M小,這次的決策X(k)=0或者1肯定得不到滿足條件的解向量。所以也就沒有必要擴展這個結點的左兒子或者右兒子了。(說的明白點,如果X(k)=1不滿足上面的式子,那就沒有必要擴展第K-1個正數的左兒子了;右兒子同理;如果還不理解不要緊,看后面的例子)

(2)這個公式的含義是,當你考慮是否要把第K個正數加入到解向量的時候,不管你要加進來或者是不打算把它加進來,提前往后看一步,判斷如果把第K+1個正數算進來后的值大於M,就不把第K個正數加進來。也就是說不生成第K-1個節點的兒子。(說明白點,不管你的第K個結點是否加入到解向量中,如果X(k)=1不滿足上面的式子,那就沒有必要擴展第K-1個正數的左兒子了;右兒子同理;如果還不理解不要緊,看后面的例子)

注意:這個條件可能很難理解,首先這些數是非降序排列的,如果要考慮加入這個第K個數的話,分三種情況:

①加入進來剛好等於M,那么正好就得到了一個解向量。此時不要產生這個結點,因為提前向后看一步肯定會不滿足(2)的,所以這種情況下樹已經被界限函數殺死了,但是確實找到了正確的解向量,所以程序中的動作是輸出這個解向量。樹中的表現形式是,產生一個不同於普通結點的終結結點表示找到了一個正確的解向量。(后面的圖中有區分,方塊表示擴展了的結點,圓圈表示正確的解向量)
②加入進來的正數求和后小於M,往后看一步,看第K+1個節點,如果在加上第K+1個正數后的和大於M,則后面就不會再有滿足條件的解向量了,因為這些正數是非降序排列的。后面的每個數和前面的這K個數的和一定都大於M;同時前面的K-1個數的和小於M。也就是說不會產生解向量,這這個第K個結點就不會加入到樹中來,樹在這里被界限函數殺死。
③加入進來的正數求和后大於M,因為這些正數是非降序排列的,顯而易見不能產生解向量。

同時,如果決策是不加如這個第K個正數(產生右孩子),如果前面這K個數的和(包括第K個的X(k)=0)加上第K+1個數的和大於M,也不會產生解向量,同樣可以不用產生這個右孩子。

只有同時滿足(1)(2)兩個條件的時候,B(X(1),...,X(k))=true,也就是說可以產生第K個正數的結點,否則就要在他的上級結點殺死。

注意:其實回溯算法很簡單,但是重點和難點在於找到最后的界限函數,界限函數找的好就能提前殺死好多的節點,大大的提高算法的效率,如果界限函數找的不好就會是一個很爛的算法。

好了,界限函數也明確了,下面先看看偽代碼:

 1 procedure SUMOFSUB(s,k,r)
 2 //找W(1:n)中和數為M的所有子集。進入此過程時X(1),…,X(k-1)的值已確定。W(j)按非降次序排列。//
 3 //下面的變量解釋:s表示已經加進來的這個序列的和;r表示還沒有加入進來的所有的數的和;k表示級數//
 4 global integer M,n; 
 5 global real W(1:n); 
 6 global boolean X(1:n);
 7 real r, s; 
 8 integer k,j;
 9 //生成左兒子//
10 X(k)←111 if s+W(k)=M then
12     print(X(j),j←1 to k)
13 else
14 //這里指判斷了界限函數的一個條件:我們假設所有的數的和大於等於M,否則沒意義了,將一定無解;還假設第一個數小於等於M//
15     if s+W(k)+W(k+1) ≤ M then//B(k)=true//
16         call SUMOFSUB(S+W(k),k+1,r-W(k))
17     endif
18 endif
19 //生成右兒子和計算Bk的值//
20 if s+r-W(k) ≥ M and s+W(k+1) ≤ M//B(k)=true//
21     then X(k)←0
22         call SUMOFSUB(s,k+1,r-W(k))
23 endif
24 end SUMOFSUB

注解:為什么第二個if中只判斷了一個條件?因為我們一開始就假設所有的數的和大於等於M,所以在生成左孩子的時候,這個條件一定滿足,因為我們的做法是把這個數加進來。只要他的父結點滿足條件,它就滿足條件(父結點如果是根,我們有假設;父結點如果是爺爺結點的右孩子,那么父結點判斷界限函數了;父結點是爺爺結點的左孩子,那么往上遞推)。

可能到這里還有好多的不明白,那么我們來實際的跑一次:

設n=4個正數的集合,W={11,13,24,7},和M=31。求W的所有元素之和為M的子集。

解:

注意:(1)最后的解不是一個結點,而是在上一級就截斷了,用小圓圈表示這個解。還有A結點的打印輸出不是N元組。
        (2)一定要先對所有的正數排序
        (3)構建上面的樹的時候,產生一個結點,如果要接着構建其左孩子,那么讓他入棧,等到輪到他的時候再出棧構建其右孩子。

--------------------------------------------------------------------------------------------------

好了,鋪墊了那么久,終於該輪到代碼上場了,看看具體的實現(其實最終要的還是界限函數的選取,不要本末倒置):

  1 #include <stdio.h>
  2 #define M 31
  3 #define N 5
  4 
  5 int w[N] = {0,11,13,24,7};
  6 int x[N] = {0};
  7 int flag = 0;
  8 
  9 //回溯算法實現
 10 void sumOfSub(int s, int k, int r);
 11 //首先對這些正數排序
 12 void InsertionSort(int a[], int low, int high);
 13 //每產生一個解向量就打印出來,同時清零。准備下一個解向量
 14 void print();
 15 
 16 int main()
 17 {
 18     int sum = 0;
 19     //先判斷所有數的和是否小於M,如果小於M則不會有解向量
 20     for(int i=1; i<N; i++)
 21     {
 22         sum += w[i];
 23     }//for
 24 
 25     if(sum < M)
 26     {
 27         printf("沒有解向量滿足條件\n");
 28         return 0;
 29     }//if
 30 
 31     //如果要用回溯算法,首先對數據排序。因為數據的規模不大,用InsertionSort搞定
 32     InsertionSort(w, 1, N-1);
 33 
 34     if(w[0] > M)
 35     {
 36         printf("沒有解向量滿足條件\n");
 37         return 0;
 38     }//if
 39 
 40     //回溯算法的准備工作完畢,下面開始調用
 41     sumOfSub(0,1,sum);
 42 
 43     //通過flag的值判斷print()函數有沒有被調用過,從而確定是否存在解向量
 44     if(!flag)
 45     {
 46         printf("不存在滿足條件的序列\n");
 47     }
 48 
 49     return 0;
 50 }
 51 void sumOfSub(int s, int k, int r)
 52 {
 53     //生成左子樹
 54     x[k] = 1;
 55     if(s + w[k] == M)
 56     {
 57         print();
 58     }//if
 59     else
 60     {
 61         if(k < N - 1 && s + w[k] + w[k+1] <= M)
 62         {
 63             sumOfSub(s+w[k], k + 1, r - w[k]);
 64         }//if
 65 
 66     }//else
 67 
 68     //生成右子樹
 69     x[k] = 0;
 70     if(k < N - 1 && s + r - w[k] >= M && s + w[k+1] <= M)
 71     {
 72         sumOfSub(s, k + 1, r - w[k]);
 73     }//if
 74 
 75 }
 76 
 77 void print()
 78 {
 79     for(int i=1; i<N; i++)
 80     {
 81         printf("%d ", x[i]);
 82     }
 83     printf("\n");
 84     flag = 1;
 85 }
 86 
 87 void InsertionSort(int a[], int low, int high)
 88 {
 89     int unsort = low + 1;
 90     int j;
 91     for(;unsort <= high; ++unsort)
 92     {
 93         int temp = a[unsort];
 94         j = unsort - 1;
 95         while(j >= 0 && temp < a[j])
 96         {
 97             a[j+1] = a[j];
 98             j--;
 99         }//while
100 
101         a[j+1] = temp;
102     }//for
103 }

 

 測試:

(1)程序數據:{5,10,12,13,15,18}
       
程序結果:

(2)程序數據:{11,13,24,7}
       程序結果:

未完待續:

回溯算法的子集和數問題到此告一段落,有時間再追加時間復雜度。


免責聲明!

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



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