子集和数问题_回溯

有人说算法导论中没有回溯和分支定界这两种算法。我觉得这个算是导论中算法的应用吧,废话不多说,走起。
回溯算法之子集和数问题。

这个算法要解决的问题:假定有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张图片

注意:(1)最后的解不是一个结点,而是在上一级就截断了,用小圆圈表示这个解。还有A结点的打印输出不是N元组。
        (2)一定要先对所有的正数排序
        (3)构建上面的树的时候,产生一个结点,如果要接着构建其左孩子,那么让他入栈,等到轮到他的时候再出栈构建其右孩子。

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

好了,铺垫了那么久,终于该轮到代码上场了,看看具体的实现(其实最终要的还是界限函数的选取,不要本末倒置):

  1 #include 
  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)
 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)
 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张图片

(2)程序数据:{11,13,24,7}
       程序结果:

未完待续:

回溯算法的子集和数问题到此告一段落,有时间再追加时间复杂度。

转载于:https://www.cnblogs.com/stemon/p/3402822.html

你可能感兴趣的:(子集和数问题_回溯)